Replace glass effects with plain backdrop blur and refine shell layout

This commit is contained in:
Samuel Onoja committed 2026-09-26 09:12:51 +01:00
1 parent af32c29a49
commit 64c95f0b97
13 files changed
+793 -1006

No files matched your search

+15 -22
View File
@@ -34,8 +34,7 @@ decorations). Leaving fullscreen restores the previous maximized/tiled state.
Set `FERESE_TRACE_PERFORMANCE=1` to emit five-second per-output summaries under
the `ferese::render` tracing target. Summaries include rendered frames, damaged
pixels, average and longest render time, DRM missed-deadline totals, blur-active
frames, blur-region count, and sampled blur pixels.
pixels, average and longest render time, and DRM missed-deadline totals.
The native-client soak runner and its nested/DRM procedure are documented in
[docs/native-soak-testing.md](docs/native-soak-testing.md).
@@ -48,7 +47,7 @@ cargo build -p ferese-effects-probe
cargo run -p ferese -- --grant-effects -- target/debug/ferese-effects-probe
```
Add `--preview` to keep a glass popover visible in the nested compositor:
Add `--preview` to keep a solid popover visible in the nested compositor:
```bash
cargo build -p ferese-effects-probe
@@ -57,8 +56,7 @@ cargo run -p ferese -- --backend nested --grant-effects -- \
```
Press `Super+Enter` in the Ferese window to open `foot`, then move or update the
terminal beneath the popover to see backdrop capture, blur, tint, and damage
invalidation. Stop the preview with `Ctrl+C` in the host terminal.
terminal beneath the popover to inspect stacking, rounded clipping, and shadows. Stop the preview with `Ctrl+C` in the host terminal.
To verify that the private global is absent from the public Wayland socket:
@@ -108,29 +106,25 @@ blur = 18.0
opacity = 0.20
[theme.material]
style = "glass" # glass, translucent, or solid
style = "solid" # solid or translucent
blur_radius = 12.0 # logical pixels; translucent only, 0 disables blur
```
Glass roles use live backdrop sampling, a dark tint, diagonal lighting, inset
edges, static overlay grain, and role-specific shadows. Configure them with:
Material roles use the configured surface color and role-specific shadows.
Solid is the default. Translucent adds backdrop blur behind the same surface
color, without grain, glossy lighting, saturation boosts, or inset highlights.
The radius is clamped to 32 logical pixels. A shader/allocation failure falls
back to a plain translucent fill. Shell popovers report rounded card regions;
their outer padding and gaps stay transparent. Each surface shares one capture
across its cards, so cards do not sample already blurred neighboring cards.
Use `--preview --regions` with the effects probe to inspect two cards over a
patterned background, including the sharp gap between them.
```toml
[appearance]
corner_radius = 14.0
reduced_effects = false # true selects reduced_blur or a stricter quality tier
[appearance.glass]
tint_color = "#1C202E"
blur_scale = 1.0 # clamped to 0.5–1.5; non-finite values are rejected
grain = true
quality = "full"
```
Quality tiers are `full`, `no_grain`, `no_saturation`, `reduced_blur`
(at most 12 logical pixels with fewer samples), `translucent`, and `solid`.
Each tier retains earlier reductions without reducing tint opacity or changing
layout. Backdrop shader or texture allocation failure falls back to translucent.
The legacy `surface.panel_elevated` role uses the panel tint with popover elevation.
Shell shadows use restrained logical-pixel presets: panel `1/5/4%`, popover
and menu `2/8/7%`, HUD `1/4/4%`, and notification/modal `3/10/9%`
(vertical offset / blur / opacity). Private effects capabilities
@@ -173,8 +167,7 @@ shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under
shell surfaces also omit outlines. The dark startup fallback matches the material palette.
The shell decodes wallpaper in parallel with startup and prefers the lightweight
software renderer, with GPU rendering available as a fallback. Glass effects
still run on the compositor GPU. Set `ICED_BACKEND=wgpu` to explicitly use GPU
software renderer, with GPU rendering available as a fallback. Set `ICED_BACKEND=wgpu` to explicitly use GPU
rendering for shell content. Use release builds when evaluating startup and
animation performance; debug image processing is considerably slower.
+105 -89
View File
@@ -113,12 +113,13 @@ pub struct ThemeSettings {
pub accent_color: RgbaColor,
pub shadow_color: RgbaColor,
pub surface_base_color: RgbaColor,
pub panel_opacity: f64,
pub window_radius: f64,
pub shadow_offset_y: f64,
pub shadow_blur: f64,
pub shadow_opacity: f64,
pub material_style: MaterialStyle,
pub glass: GlassSettings,
pub backdrop_blur: f64,
pub material_radius: f64,
pub panel_radius: f64,
}
@@ -126,64 +127,20 @@ pub struct ThemeSettings {
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MaterialStyle {
#[default]
Glass,
Translucent,
Solid,
}
/// Ordered quality tiers; each step retains the preceding reductions.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
#[serde(rename_all = "snake_case")]
pub enum GlassQuality {
#[default]
Full,
NoGrain,
NoSaturation,
ReducedBlur,
Translucent,
Solid,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlassSettings {
pub tint: RgbaColor,
pub blur_scale: f32,
pub grain: bool,
pub quality: GlassQuality,
}
impl Default for GlassSettings {
fn default() -> Self {
Self {
tint: RgbaColor([28.0 / 255.0, 32.0 / 255.0, 46.0 / 255.0, 1.0]),
blur_scale: 1.0,
grain: true,
quality: GlassQuality::Full,
}
}
}
#[derive(Debug, Default, Deserialize)]
struct AppearanceConfig {
corner_radius: Option<f64>,
#[serde(default)]
reduced_effects: bool,
#[serde(default)]
glass: GlassConfig,
}
#[derive(Debug, Default, Deserialize)]
struct GlassConfig {
tint_color: Option<String>,
blur_scale: Option<f64>,
grain: Option<bool>,
#[serde(default)]
quality: GlassQuality,
}
#[derive(Debug, Default, Deserialize)]
struct ThemeConfig {
#[serde(default)]
surface: ThemeSurfaceConfig,
#[serde(default)]
colors: ThemeColorsConfig,
#[serde(default)]
@@ -195,9 +152,35 @@ struct ThemeConfig {
}
#[derive(Debug, Default, Deserialize)]
struct ThemeSurfaceConfig {
#[serde(default)]
bar: ThemeBarConfig,
}
#[derive(Debug, Default, Deserialize)]
struct ThemeBarConfig {
opacity: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct ThemeMaterialConfig {
#[serde(default)]
style: MaterialStyle,
#[serde(default = "default_backdrop_blur")]
blur_radius: f64,
}
impl Default for ThemeMaterialConfig {
fn default() -> Self {
Self {
style: MaterialStyle::Solid,
blur_radius: default_backdrop_blur(),
}
}
}
fn default_backdrop_blur() -> f64 {
12.0
}
#[derive(Debug, Deserialize)]
@@ -861,22 +844,6 @@ impl Config {
let shadow_opacity =
unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
let mut glass = GlassSettings::default();
if let Some(tint) = &self.appearance.glass.tint_color {
glass.tint = parse_color(tint, "appearance.glass.tint_color")?;
// Opacity belongs to the semantic role, never the color token.
glass.tint.0[3] = 1.0;
}
glass.blur_scale = finite_theme_value(
self.appearance.glass.blur_scale.unwrap_or(1.0),
"appearance.glass.blur_scale",
)?
.clamp(0.5, 1.5) as f32;
glass.grain = self.appearance.glass.grain.unwrap_or(true);
glass.quality = self.appearance.glass.quality;
if self.appearance.reduced_effects {
glass.quality = glass.quality.max(GlassQuality::ReducedBlur);
}
let material_radius = nonnegative_theme_value(
self.appearance.corner_radius.unwrap_or(14.0),
"appearance.corner_radius",
@@ -891,12 +858,20 @@ impl Config {
&self.theme.colors.surface_base,
"colors.surface_base",
)?,
panel_opacity: unit_theme_value(
self.theme.surface.bar.opacity.unwrap_or(0.78),
"surface.bar.opacity",
)?,
window_radius,
shadow_offset_y,
shadow_blur,
shadow_opacity,
material_style: self.theme.material.style,
glass,
backdrop_blur: nonnegative_theme_value(
self.theme.material.blur_radius,
"material.blur_radius",
)?
.min(32.0),
material_radius,
panel_radius: nonnegative_theme_value(
self.theme.geometry.top_bar_radius,
@@ -1665,30 +1640,6 @@ mod tests {
assert!(invalid.gap_config().is_err());
}
#[test]
fn glass_configuration_clamps_cost_and_rejects_nonfinite_values() {
for (value, expected) in [(0.1, 0.5), (1.2, 1.2), (100.0, 1.5)] {
let config = parse(&format!("[appearance.glass]\nblur_scale = {value}"));
assert_eq!(config.theme_settings().unwrap().glass.blur_scale, expected);
}
for value in ["nan", "inf", "-inf"] {
assert!(
parse(&format!("[appearance.glass]\nblur_scale = {value}"))
.theme_settings()
.is_err()
);
}
let settings = parse("[appearance]\nreduced_effects = true\ncorner_radius = 17.0\n[appearance.glass]\ntint_color = \"#123456\"\ngrain = false")
.theme_settings().unwrap();
assert_eq!(settings.glass.quality, GlassQuality::ReducedBlur);
assert!(!settings.glass.grain);
assert_eq!(settings.material_radius, 17.0);
assert_eq!(
settings.glass.tint,
RgbaColor([18.0 / 255.0, 52.0 / 255.0, 86.0 / 255.0, 1.0])
);
}
#[test]
fn parses_and_validates_theme_window_tokens() {
let configured = parse(
@@ -1707,12 +1658,13 @@ mod tests {
accent_color: RgbaColor([170.0 / 255.0, 187.0 / 255.0, 0.8, 1.0]),
shadow_color: RgbaColor([1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 128.0 / 255.0]),
surface_base_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]),
panel_opacity: 0.78,
window_radius: 12.0,
shadow_offset_y: -2.0,
shadow_blur: 24.0,
shadow_opacity: 0.4,
material_style: MaterialStyle::Translucent,
glass: GlassSettings::default(),
backdrop_blur: 12.0,
material_radius: 14.0,
panel_radius: 0.0,
}
@@ -1723,6 +1675,70 @@ mod tests {
assert!(toml::from_str::<Config>("[theme.material]\nstyle = \"mist\"").is_err());
}
#[test]
fn materials_default_to_solid_and_reject_removed_style() {
assert_eq!(
parse("").theme_settings().unwrap().material_style,
MaterialStyle::Solid
);
assert!(toml::from_str::<Config>("[theme.material]\nstyle = \"glass\"").is_err());
}
#[test]
fn material_color_comes_from_surface_base() {
let theme = parse("[theme.colors]\nsurface_base = \"#000000\"\n[theme.surface.bar]\nbackground = \"#FFFFFF\"")
.theme_settings()
.unwrap();
assert_eq!(theme.surface_base_color, RgbaColor([0.0, 0.0, 0.0, 1.0]));
}
#[test]
fn bar_opacity_uses_config_and_rejects_invalid_values() {
assert_eq!(parse("").theme_settings().unwrap().panel_opacity, 0.78);
for opacity in [0.0, 0.65, 1.0] {
assert_eq!(
parse(&format!("[theme.surface.bar]\nopacity = {opacity}"))
.theme_settings()
.unwrap()
.panel_opacity,
opacity
);
}
for opacity in ["-0.1", "1.1", "nan", "inf"] {
assert!(
parse(&format!("[theme.surface.bar]\nopacity = {opacity}"))
.theme_settings()
.is_err()
);
}
}
#[test]
fn backdrop_blur_is_configurable_without_glass_settings() {
assert_eq!(parse("").theme_settings().unwrap().backdrop_blur, 12.0);
assert_eq!(
parse("[theme.material]\nstyle = \"translucent\"\nblur_radius = 100")
.theme_settings()
.unwrap()
.backdrop_blur,
32.0
);
assert_eq!(
parse("[theme.material]\nblur_radius = 0")
.theme_settings()
.unwrap()
.backdrop_blur,
0.0
);
for value in ["-1", "nan", "inf"] {
assert!(
parse(&format!("[theme.material]\nblur_radius = {value}"))
.theme_settings()
.is_err()
);
}
}
#[test]
fn parses_input_and_touchpad_settings() {
let config = parse(
+107 -180
View File
@@ -16,10 +16,7 @@ use smithay::{
};
use crate::{
Ferese,
config::{GlassQuality, GlassSettings, MaterialStyle},
private_client::ClientCapabilities,
state::ClientState,
Ferese, config::MaterialStyle, private_client::ClientCapabilities, state::ClientState,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -37,75 +34,27 @@ pub(crate) enum SemanticRole {
pub(crate) struct ResolvedMaterial {
pub style: MaterialStyle,
pub opacity: f32,
pub blur: f32,
pub saturation: f32,
pub brightness: f32,
pub noise: f32,
pub shadow: [f64; 3],
}
pub(crate) fn resolve_material(
role: SemanticRole,
style: MaterialStyle,
settings: GlassSettings,
) -> ResolvedMaterial {
// Restrained shell shadows in logical pixels: the full-width bar needs
// less elevation than popovers, while dialogs retain a little more depth.
let (opacity, blur, saturation, shadow) = match role {
SemanticRole::Panel => (0.55, 24.0, 1.35, [1.0, 5.0, 0.04]),
SemanticRole::PanelElevated => (0.55, 24.0, 1.35, [2.0, 8.0, 0.07]),
SemanticRole::Popover => (0.55, 22.0, 1.35, [2.0, 8.0, 0.07]),
SemanticRole::Menu => (0.60, 20.0, 1.30, [2.0, 8.0, 0.07]),
SemanticRole::Hud => (0.65, 18.0, 1.25, [1.0, 4.0, 0.04]),
SemanticRole::Notification => (0.58, 24.0, 1.35, [3.0, 10.0, 0.09]),
SemanticRole::Modal => (0.62, 28.0, 1.40, [3.0, 10.0, 0.09]),
pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle) -> ResolvedMaterial {
let shadow = match role {
SemanticRole::Panel => [1.0, 5.0, 0.04],
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => {
[2.0, 8.0, 0.07]
}
SemanticRole::Hud => [1.0, 4.0, 0.04],
SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09],
};
// Preserve the separately defined translucent family when selected
// directly. Glass degradation keeps the glass role's contrast floor.
let opacity = if style == MaterialStyle::Translucent {
match role {
SemanticRole::Panel => 0.78,
SemanticRole::Hud | SemanticRole::Modal => 0.88,
let opacity = match (style, role) {
(MaterialStyle::Solid, _) => 1.0,
(_, SemanticRole::Panel) => 0.78,
(_, SemanticRole::Hud | SemanticRole::Modal) => 0.88,
_ => 0.84,
}
} else {
opacity
};
let style = if style == MaterialStyle::Glass {
match settings.quality {
GlassQuality::Solid => MaterialStyle::Solid,
GlassQuality::Translucent => MaterialStyle::Translucent,
_ => style,
}
} else {
style
};
let glass = style == MaterialStyle::Glass;
ResolvedMaterial {
style,
opacity: if style == MaterialStyle::Solid {
1.0
} else {
opacity
},
blur: if !glass {
0.0
} else if settings.quality >= GlassQuality::ReducedBlur {
(blur * settings.blur_scale).min(12.0)
} else {
blur * settings.blur_scale
},
saturation: if glass && settings.quality < GlassQuality::NoSaturation {
saturation
} else {
1.0
},
brightness: 1.0,
noise: if glass && settings.grain && settings.quality < GlassQuality::NoGrain {
0.05
} else {
0.0
},
opacity,
shadow,
}
}
@@ -115,6 +64,7 @@ struct SurfaceEffectsState {
attached: AtomicBool,
generation: AtomicU64,
role: Mutex<Option<SemanticRole>>,
regions: Mutex<Option<Vec<[i32; 5]>>>,
}
impl SurfaceEffectsState {
@@ -123,6 +73,7 @@ impl SurfaceEffectsState {
attached: AtomicBool::new(false),
generation: AtomicU64::new(0),
role: Mutex::new(None),
regions: Mutex::new(None),
}
}
@@ -152,7 +103,39 @@ impl SurfaceEffectsUserData {
}
pub(crate) fn init_global(display: &DisplayHandle) {
display.create_global::<Ferese, FereseEffectsManagerV1, _>(1, ());
display.create_global::<Ferese, FereseEffectsManagerV1, _>(2, ());
}
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
with_states(surface, |states| {
states
.data_map
.get::<SurfaceEffectsState>()?
.regions
.lock()
.unwrap()
.clone()
})
}
fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
if bytes.len() % 20 != 0 || bytes.len() > 32 * 20 {
return None;
}
bytes
.chunks_exact(20)
.map(|tuple| {
let values: Vec<_> = tuple
.chunks_exact(4)
.map(|v| i32::from_ne_bytes(v.try_into().unwrap()))
.collect();
(values[2] > 0
&& values[3] > 0
&& values[4] >= 0
&& values.iter().all(|v| v.abs_diff(0) <= 32768))
.then(|| [values[0], values[1], values[2], values[3], values[4]])
})
.collect()
}
// Consumed by the material renderer in the next M7 slice.
@@ -254,19 +237,36 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
WEnum::Unknown(_) | WEnum::Value(_) => return,
};
if set_surface_role(&surface, Some(role)) {
state.invalidate_material_scene();
crate::backends::direct::render_all(state);
}
}
ferese_surface_effects_v1::Request::SetRegions { regions } => {
let Some(regions) = decode_regions(&regions) else {
return;
};
let changed = with_states(&surface, |states| {
let Some(effects) = states.data_map.get::<SurfaceEffectsState>() else {
return false;
};
let mut old = effects.regions.lock().unwrap();
if old.as_ref() == Some(&regions) {
return false;
}
*old = Some(regions);
effects.generation.fetch_add(1, Ordering::Release);
true
});
if changed {
crate::backends::direct::render_all(state);
}
}
ferese_surface_effects_v1::Request::ClearRole => {
if set_surface_role(&surface, None) {
state.invalidate_material_scene();
crate::backends::direct::render_all(state);
}
}
ferese_surface_effects_v1::Request::Destroy => {
if detach(&surface) {
state.invalidate_material_scene();
crate::backends::direct::render_all(state);
}
}
@@ -310,6 +310,31 @@ fn detach(surface: &WlSurface) -> bool {
mod tests {
use super::*;
#[test]
fn rounded_regions_are_bounded_and_validated() {
let region = [12_i32, 8, 120, 48, 11];
let bytes: Vec<_> = region.into_iter().flat_map(i32::to_ne_bytes).collect();
assert_eq!(decode_regions(&bytes), Some(vec![region]));
assert_eq!(decode_regions(&[]), Some(vec![]));
assert!(decode_regions(&bytes[..19]).is_none());
assert!(decode_regions(&bytes.repeat(33)).is_none());
for invalid in [
[0_i32, 0, 0, 48, 11],
[0, 0, 120, 48, -1],
[i32::MIN, 0, 120, 48, 11],
] {
assert!(
decode_regions(
&invalid
.into_iter()
.flat_map(i32::to_ne_bytes)
.collect::<Vec<_>>()
)
.is_none()
);
}
}
#[test]
fn role_changes_advance_generation_only_when_material_changes() {
let effects = SurfaceEffectsState::new();
@@ -324,118 +349,20 @@ mod tests {
}
#[test]
fn styles_preserve_semantics_but_disable_unsupported_effects() {
let glass = resolve_material(
SemanticRole::Popover,
MaterialStyle::Glass,
GlassSettings::default(),
);
let translucent = resolve_material(
SemanticRole::Popover,
MaterialStyle::Translucent,
GlassSettings::default(),
);
let solid = resolve_material(
SemanticRole::Popover,
MaterialStyle::Solid,
GlassSettings::default(),
);
assert!(glass.blur > 0.0);
assert!(glass.noise > 0.0);
assert!(glass.opacity <= translucent.opacity);
assert_eq!(translucent.blur, 0.0);
assert_eq!(translucent.noise, 0.0);
assert_eq!(solid.opacity, 1.0);
assert_eq!(solid.blur, 0.0);
}
#[test]
fn glass_presets_use_role_specific_tints_and_restrained_shadows() {
for (role, blur, saturation, opacity, shadow) in [
(SemanticRole::Panel, 24.0, 1.35, 0.55, [1.0, 5.0, 0.04]),
(SemanticRole::Popover, 22.0, 1.35, 0.55, [2.0, 8.0, 0.07]),
(SemanticRole::Menu, 20.0, 1.30, 0.60, [2.0, 8.0, 0.07]),
(SemanticRole::Hud, 18.0, 1.25, 0.65, [1.0, 4.0, 0.04]),
(
SemanticRole::Notification,
24.0,
1.35,
0.58,
[3.0, 10.0, 0.09],
),
(SemanticRole::Modal, 28.0, 1.40, 0.62, [3.0, 10.0, 0.09]),
] {
let material = resolve_material(role, MaterialStyle::Glass, GlassSettings::default());
assert_eq!(
(
material.blur,
material.saturation,
material.opacity,
material.shadow
),
(blur, saturation, opacity, shadow)
);
}
}
#[test]
fn degradation_is_ordered_and_preserves_contrast_and_elevation() {
let reference = resolve_material(
SemanticRole::Modal,
MaterialStyle::Glass,
GlassSettings::default(),
);
for quality in [
GlassQuality::Full,
GlassQuality::NoGrain,
GlassQuality::NoSaturation,
GlassQuality::ReducedBlur,
GlassQuality::Translucent,
GlassQuality::Solid,
] {
let material = resolve_material(
SemanticRole::Modal,
MaterialStyle::Glass,
GlassSettings {
quality,
..GlassSettings::default()
},
);
assert!(material.opacity >= reference.opacity);
assert_eq!(material.shadow, reference.shadow);
if quality >= GlassQuality::NoGrain {
assert_eq!(material.noise, 0.0);
}
if quality >= GlassQuality::NoSaturation {
assert_eq!(material.saturation, 1.0);
}
if quality >= GlassQuality::ReducedBlur {
assert!(material.blur <= 12.0);
}
if quality >= GlassQuality::Translucent {
assert_eq!(material.blur, 0.0);
}
if quality == GlassQuality::Solid {
assert_eq!(material.opacity, 1.0);
}
}
}
#[test]
fn popover_uses_the_same_tint_with_a_smaller_blur_than_panel() {
let panel = resolve_material(
fn solid_materials_are_opaque_and_keep_role_specific_elevation() {
for role in [
SemanticRole::Panel,
MaterialStyle::Glass,
GlassSettings::default(),
);
let popover = resolve_material(
SemanticRole::Popover,
MaterialStyle::Glass,
GlassSettings::default(),
);
assert_eq!(popover.opacity, panel.opacity);
assert!(popover.blur < panel.blur);
SemanticRole::Menu,
SemanticRole::Hud,
SemanticRole::Notification,
SemanticRole::Modal,
] {
let solid = resolve_material(role, MaterialStyle::Solid);
let translucent = resolve_material(role, MaterialStyle::Translucent);
assert_eq!(solid.opacity, 1.0);
assert!(translucent.opacity < 1.0);
assert_eq!(solid.shadow, translucent.shadow);
}
}
}
+2 -4
View File
@@ -31,8 +31,8 @@ impl CompositorHandler for Ferese {
}
fn commit(&mut self, surface: &WlSurface) {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
on_commit_buffer_handler::<Self>(surface);
self.invalidate_material_scene();
if !is_sync_subsurface(surface) {
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
@@ -63,9 +63,7 @@ impl CompositorHandler for Ferese {
}
fn destroyed(&mut self, surface: &WlSurface) {
// A removed popup changes the backdrop even without a final buffer
// commit. Invalidate expanded sampling regions as well as its bounds.
self.invalidate_material_scene();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
crate::backends::direct::render_all(self);
if self.idle_inhibitors.remove(surface).is_some() {
self.idle_notifier_state
+3 -27
View File
@@ -5,10 +5,7 @@ use smithay::utils::{Physical, Rectangle};
const REPORT_INTERVAL: Duration = Duration::from_secs(5);
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct FrameEffectMetrics {
pub(crate) blur_regions: u64,
pub(crate) blur_sample_pixels: u64,
}
pub(crate) struct FrameEffectMetrics;
#[derive(Debug)]
pub(crate) struct RenderMetrics {
@@ -19,9 +16,6 @@ pub(crate) struct RenderMetrics {
damaged_pixels: u64,
total_render_time: Duration,
longest_render_time: Duration,
blur_frames: u64,
blur_regions: u64,
blur_sample_pixels: u64,
}
impl RenderMetrics {
@@ -42,9 +36,6 @@ impl RenderMetrics {
damaged_pixels: 0,
total_render_time: Duration::ZERO,
longest_render_time: Duration::ZERO,
blur_frames: 0,
blur_regions: 0,
blur_sample_pixels: 0,
}
}
@@ -53,7 +44,7 @@ impl RenderMetrics {
render_time: Duration,
damage: &[Rectangle<i32, Physical>],
missed_deadlines: u64,
effects: FrameEffectMetrics,
_effects: FrameEffectMetrics,
) {
if !self.enabled {
return;
@@ -63,9 +54,6 @@ impl RenderMetrics {
self.damaged_pixels += damage.iter().map(rectangle_area).sum::<u64>();
self.total_render_time += render_time;
self.longest_render_time = self.longest_render_time.max(render_time);
self.blur_frames += u64::from(effects.blur_regions > 0);
self.blur_regions += effects.blur_regions;
self.blur_sample_pixels += effects.blur_sample_pixels;
let now = Instant::now();
let elapsed = now.saturating_duration_since(self.interval_started);
@@ -87,9 +75,6 @@ impl RenderMetrics {
average_render_us,
longest_render_us = self.longest_render_time.as_micros(),
missed_deadlines,
blur_frames = self.blur_frames,
blur_regions = self.blur_regions,
blur_sample_pixels = self.blur_sample_pixels,
"render performance"
);
@@ -98,9 +83,6 @@ impl RenderMetrics {
self.damaged_pixels = 0;
self.total_render_time = Duration::ZERO;
self.longest_render_time = Duration::ZERO;
self.blur_frames = 0;
self.blur_regions = 0;
self.blur_sample_pixels = 0;
}
}
@@ -139,18 +121,12 @@ mod tests {
Rectangle::new((10, 10).into(), (20, 10).into()),
],
0,
FrameEffectMetrics {
blur_regions: 2,
blur_sample_pixels: 4_096,
},
FrameEffectMetrics,
);
assert_eq!(metrics.rendered_frames, 1);
assert_eq!(metrics.damaged_pixels, 5_200);
assert_eq!(metrics.total_render_time, Duration::from_millis(2));
assert_eq!(metrics.longest_render_time, Duration::from_millis(2));
assert_eq!(metrics.blur_frames, 1);
assert_eq!(metrics.blur_regions, 2);
assert_eq!(metrics.blur_sample_pixels, 4_096);
}
}
+6 -14
View File
@@ -125,9 +125,9 @@ pub struct Ferese {
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
pub(crate) overview_scrims: HashMap<OutputId, crate::winit::OverviewScrim>,
pub(crate) material_programs: HashMap<ErasedContextId, crate::winit::MaterialProgram>,
pub(crate) backdrop_programs: HashMap<ErasedContextId, crate::winit::BackdropProgram>,
pub(crate) material_buffers: HashMap<WlSurface, crate::winit::MaterialBuffers>,
material_scene_generation: u64,
pub(crate) blur_programs: HashMap<ErasedContextId, crate::winit::BlurProgram>,
pub(crate) backdrop_generation: u64,
closing_windows: HashMap<WindowId, ClosingAnimation>,
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
@@ -457,9 +457,9 @@ impl Ferese {
rounded_clip_programs: HashMap::new(),
overview_scrims: HashMap::new(),
material_programs: HashMap::new(),
backdrop_programs: HashMap::new(),
material_buffers: HashMap::new(),
material_scene_generation: 0,
blur_programs: HashMap::new(),
backdrop_generation: 0,
closing_windows: HashMap::new(),
viewport_animations: HashMap::new(),
scrolling_world_x: HashMap::new(),
@@ -930,6 +930,7 @@ impl Ferese {
}
pub fn relayout(&mut self) {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.arrange_layers();
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
@@ -1346,20 +1347,11 @@ impl Ferese {
self.sync_window_stacking();
if active_animation {
self.invalidate_material_scene();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
}
active_animation
}
pub(crate) fn invalidate_material_scene(&mut self) {
self.material_scene_generation = self.material_scene_generation.wrapping_add(1);
}
pub(crate) fn material_scene_generation(&self) -> u64 {
self.material_scene_generation
}
pub(crate) fn animations_enabled(&self) -> bool {
self.animations_enabled
}
+308 -573
View File
File diff suppressed because it is too large. Load diff
+12 -2
View File
@@ -22,7 +22,7 @@
DEALINGS IN THE SOFTWARE.
</copyright>
<interface name="ferese_effects_manager_v1" version="1">
<interface name="ferese_effects_manager_v1" version="2">
<description summary="assign semantic material roles to surfaces">
This private global is visible only to clients granted the Ferese effects
capability. Roles describe compositor-owned appearance policy and do not
@@ -48,7 +48,7 @@
</enum>
</interface>
<interface name="ferese_surface_effects_v1" version="1">
<interface name="ferese_surface_effects_v1" version="2">
<description summary="semantic material role for one surface">
The role affects only compositor rendering. It does not change surface
geometry, input, focus, stacking, or the surface's Wayland role.
@@ -78,5 +78,15 @@
<entry name="hud" value="5" summary="heads-up display surface"/>
<entry name="modal" value="6" summary="modal surface"/>
</enum>
<request name="set_regions" since="2">
<description summary="restrict material to rounded surface-local rectangles">
An array of native-endian signed 32-bit tuples (x, y, width, height,
radius), measured in logical pixels. At most 32 rectangles are allowed.
An empty array disables material painting without clearing the role.
Surfaces which never send this request retain full-surface materials.
</description>
<arg name="regions" type="array"/>
</request>
</interface>
</protocol>
+1 -1
View File
@@ -86,7 +86,7 @@ impl Default for ShellTheme {
}
impl ShellTheme {
// The bar has its own foreground tokens, matched to its dark glass surface.
// The bar has its own foreground tokens, matched to its surface.
pub(crate) fn for_bar(mut self) -> Self {
self.text_primary = self.bar_text_primary;
self.text_muted = self.bar_text_muted;
+80 -15
View File
@@ -22,8 +22,7 @@ use cosmic::iced::platform_specific::{
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
};
use cosmic::iced::{
Background, Border, Color, ContentFit, Event, Length, Limits, Shadow, Subscription, Vector,
window,
Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, window,
};
use cosmic::theme;
use cosmic::widget::{button, container, icon, image, row};
@@ -218,6 +217,9 @@ impl cosmic::Application for FereseShell {
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
output: IcedOutput::Active,
namespace: "ferese-shell-wallpaper".to_owned(),
// Wallpaper covers the full output, including beneath the bar
// and its floating margins. Zero would use the remaining workspace.
exclusive_zone: -1,
size: Some((None, None)),
size_limits: Limits::NONE,
..Default::default()
@@ -294,6 +296,14 @@ impl cosmic::Application for FereseShell {
}
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
if let Some(menu) = &self.menu
&& let Some(effects) = &menu.effects
{
let regions = menu.regions.lock().unwrap().clone();
if let Err(error) = effects.set_regions(&regions) {
eprintln!("ferese-shell: could not update card materials: {error}");
}
}
match message {
Message::WallpaperLoaded(result) => {
match result {
@@ -312,10 +322,7 @@ impl cosmic::Application for FereseShell {
&& menu.id == id
&& menu.effects.is_none()
{
match EffectsBinding::attach_role(
&surface,
Some(ferese_surface_effects_v1::Role::Popover),
) {
match EffectsBinding::attach_role(&surface, menu.kind.material_role()) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => {
eprintln!("ferese-shell: popover material unavailable: {error}")
@@ -467,7 +474,7 @@ impl FereseShell {
if menu.effects.is_none() {
match EffectsBinding::attach_role(
&surface,
Some(ferese_surface_effects_v1::Role::Popover),
menu.kind.material_role(),
) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => eprintln!(
@@ -481,7 +488,7 @@ impl FereseShell {
if menu.effects.is_none() {
match EffectsBinding::attach_role(
&surface,
Some(ferese_surface_effects_v1::Role::Popover),
menu.kind.material_role(),
) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => eprintln!(
@@ -513,10 +520,7 @@ impl FereseShell {
))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => {
if let Some(menu) = &mut self.menu {
if menu.effects.is_none() {
match EffectsBinding::attach_role(
&surface,
Some(ferese_surface_effects_v1::Role::Popover),
) {
match EffectsBinding::attach_role(&surface, menu.kind.material_role()) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => {
eprintln!("ferese-shell: popover material unavailable: {error}")
@@ -634,11 +638,14 @@ impl FereseShell {
});
windows = windows.push(
button::custom(
container(
text(label)
.size(bar.text_size)
.wrapping(cosmic::iced::core::text::Wrapping::None)
.class(theme::Text::Color(foreground)),
)
.center_y(bar.control_height),
)
.padding([0, 8])
.height(bar.control_height)
.class(status_ui::button_style(foreground, window.focused, 1.0))
@@ -664,7 +671,7 @@ impl FereseShell {
)
.padding([0, 6]);
let right = row![self.view_status_bar(), clock]
.spacing(10)
.spacing(6)
.align_y(cosmic::iced::Alignment::Center);
let content = row![
container(left).width(Length::Fill),
@@ -860,7 +867,23 @@ fn display_app_name(window: &control::WindowSnapshot) -> String {
}
fn current_time() -> String {
Zoned::now().strftime("%a · %H:%M").to_string()
format_bar_time(&Zoned::now())
}
fn format_bar_time(now: &Zoned) -> String {
let months = [
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sept", "oct", "nov", "dec",
];
let hour = now.hour();
let hour12 = if hour % 12 == 0 { 12 } else { hour % 12 };
format!(
"{} {}, {}:{:02} {}",
now.day(),
months[(now.month() - 1) as usize],
hour12,
now.minute(),
if hour < 12 { "am" } else { "pm" }
)
}
fn compact_window_label(window: &crate::control::WindowSnapshot) -> String {
@@ -963,6 +986,7 @@ struct EffectsBinding {
_manager: FereseEffectsManagerV1,
surface: FereseSurfaceEffectsV1,
_queue: EventQueue<EffectsState>,
regions: std::cell::RefCell<Option<Vec<[i32; 5]>>>,
}
impl EffectsBinding {
@@ -987,7 +1011,7 @@ impl EffectsBinding {
let connection = Connection::from_backend(backend);
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
let qh = queue.handle();
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=1, ())?;
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=2, ())?;
let effects = manager.get_surface_effects(surface, &qh, ());
if let Some(role) = role {
@@ -1002,9 +1026,31 @@ impl EffectsBinding {
_manager: manager,
surface: effects,
_queue: queue,
regions: Default::default(),
})
}
fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) {
return Ok(());
}
self.surface.set_regions(
regions
.iter()
.flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes()))
.collect(),
);
if regions.is_empty() {
self.surface.clear_role();
} else {
self.surface
.set_role(ferese_surface_effects_v1::Role::Popover);
}
self.connection.flush()?;
*self.regions.borrow_mut() = Some(regions.to_vec());
Ok(())
}
fn set_visible(&self, visible: bool) -> Result<(), Box<dyn std::error::Error>> {
if visible {
self.surface
@@ -1037,6 +1083,25 @@ delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
#[cfg(test)]
mod tests {
#[test]
fn clock_uses_lowercase_date_and_twelve_hour_time() {
for (stamp, expected) in [
(
"2026-09-28T21:32:00+01:00[Africa/Lagos]",
"28 sept, 9:32 pm",
),
(
"2026-09-28T00:05:00+01:00[Africa/Lagos]",
"28 sept, 12:05 am",
),
(
"2026-09-28T12:00:00+01:00[Africa/Lagos]",
"28 sept, 12:00 pm",
),
] {
assert_eq!(super::format_bar_time(&stamp.parse().unwrap()), expected);
}
}
use super::*;
use crate::control::{OutputSnapshot, WindowSnapshot};
+91 -2
View File
@@ -4,12 +4,23 @@ use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, re
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
use cosmic::{Element, Theme};
pub fn animated<'a, M: 'a>(content: Element<'a, M>, progress: f32) -> Element<'a, M> {
Element::new(Motion { content, progress })
pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[i32; 5]>>>;
pub fn animated<'a, M: 'a>(
content: Element<'a, M>,
progress: f32,
regions: Regions,
) -> Element<'a, M> {
Element::new(Motion {
content,
progress,
regions,
})
}
struct Motion<'a, M> {
content: Element<'a, M>,
progress: f32,
regions: Regions,
}
impl<M> Motion<'_, M> {
fn translation(&self) -> Vector {
@@ -56,6 +67,35 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
shell: &mut Shell<'_, M>,
viewport: &Rectangle,
) {
let mut collector = CollectRegions(Vec::new());
self.content
.as_widget_mut()
.operate(tree, layout, renderer, &mut collector);
if collector.0.is_empty() {
// A single-content popover (e.g. battery) has one inner card, not
// a full-surface rectangle including the outer transparent padding.
if let Some(content) = layout.children().next() {
collector.0.push(content.bounds());
}
}
let origin = layout.bounds().position();
let translation = self.translation();
let regions = collector
.0
.into_iter()
.take(32)
.filter(|r| r.width > 0.0 && r.height > 0.0)
.map(|r| {
[
(r.x - origin.x + translation.x).round() as i32,
(r.y - origin.y + translation.y).round() as i32,
r.width.round() as i32,
r.height.round() as i32,
11,
]
})
.collect();
*self.regions.lock().unwrap() = regions;
let cursor = self.cursor(cursor);
self.content.as_widget_mut().update(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
@@ -113,3 +153,52 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
.operate(tree, layout, renderer, operation);
}
}
struct CollectRegions(Vec<Rectangle>);
impl widget::Operation for CollectRegions {
fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) {
children(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("ferese-blur-card")) {
let contains = |outer: &Rectangle, inner: &Rectangle| {
outer.x <= inner.x
&& outer.y <= inner.y
&& outer.x + outer.width >= inner.x + inner.width
&& outer.y + outer.height >= inner.y + inner.height
};
// Nested controls belong to their enclosing card's single blur pass.
if self.0.iter().any(|outer| contains(outer, &bounds)) {
return;
}
self.0.retain(|inner| !contains(&bounds, inner));
self.0.push(bounds);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn material_regions_include_cards_but_not_the_parent_or_gaps() {
let mut collector = CollectRegions(Vec::new());
let id = widget::Id::new("ferese-blur-card");
widget::Operation::container(
&mut collector,
None,
Rectangle::new((0.0, 0.0).into(), (320.0, 200.0).into()),
);
let first = Rectangle::new((12.0, 12.0).into(), (140.0, 80.0).into());
let second = Rectangle::new((160.0, 12.0).into(), (140.0, 80.0).into());
widget::Operation::container(&mut collector, Some(&id), first);
widget::Operation::container(&mut collector, Some(&id), second);
assert_eq!(collector.0, vec![first, second]);
widget::Operation::container(
&mut collector,
Some(&id),
Rectangle::new((20.0, 20.0).into(), (40.0, 40.0).into()),
);
assert_eq!(collector.0, vec![first, second]);
}
}
+23 -51
View File
@@ -17,15 +17,9 @@ pub enum Menu {
System,
}
impl Menu {
fn title(self) -> &'static str {
match self {
Self::Network => "Wi-Fi",
Self::Bluetooth => "Bluetooth",
Self::Audio => "Volume",
Self::Battery => "Battery",
Self::Notifications => "Notifications",
Self::System => "Control Center",
}
pub(super) fn material_role(self) -> Option<ferese_surface_effects_v1::Role> {
// Popovers contain independent cards, with no shared material backing.
None
}
fn available(self, status: &Snapshot) -> bool {
match self {
@@ -45,6 +39,7 @@ pub struct OpenMenu {
opened: Instant,
pub confirm: Option<Action>,
pub effects: Option<EffectsBinding>,
pub regions: super::motion::Regions,
}
impl OpenMenu {
pub fn progress(&self) -> f32 {
@@ -77,6 +72,7 @@ impl FereseShell {
opened: Instant::now(),
confirm: None,
effects: None,
regions: Default::default(),
});
super::EFFECT_FRAME_PENDING.store(true, std::sync::atomic::Ordering::Relaxed);
self.status_error = None;
@@ -150,7 +146,7 @@ impl FereseShell {
pub fn view_status_bar(&self) -> Element<'_, cosmic::Action<Message>> {
let theme = self.config.theme.for_bar();
let metrics = BarMetrics::from(theme);
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
let mut controls = row::with_capacity(6).spacing(1).align_y(Alignment::Center);
for kind in [
Menu::System,
Menu::Network,
@@ -210,7 +206,7 @@ impl FereseShell {
.name(status_label(kind, &self.status))
.padding([
0.0,
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(7.0),
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(4.0),
])
.height(metrics.height)
.class(button_style(foreground, selected, 1.0))
@@ -238,15 +234,7 @@ impl FereseShell {
let p = menu.progress();
let primary = color_with_opacity(theme.text_primary, p);
let muted = color_with_opacity(theme.text_muted, p);
// The bar has already established whether this connection supports
// Ferese materials. Keep a glass popup's client buffer transparent
// from its FIRST frame, not only after its own focus/effects event.
// Otherwise the initial opaque fallback can be captured beneath glass
// while popup registration and client repaint happen asynchronously.
let compositor_material = self.effects.is_some();
let mut rows = column::with_capacity(12).spacing(8).width(Length::Fill);
let mut heading =
row![text(menu.kind.title()).size(16).width(Length::Fill)].align_y(Alignment::Center);
let radio = match menu.kind {
Menu::Network => self
.status
@@ -261,14 +249,13 @@ impl FereseShell {
_ => None,
};
if let Some((enabled, action)) = radio {
heading = heading.push(menu_button(
rows = rows.push(menu_button(
if enabled { "Turn off" } else { "Turn on" },
Message::Control(action),
primary,
p,
));
}
rows = rows.push(heading);
if let Some(action) = &menu.confirm {
let title = match action {
Action::Poweroff => "Power off this computer?",
@@ -471,10 +458,8 @@ impl FereseShell {
}
if combined || menu.kind == Menu::Notifications {
if let Some(n) = &self.status.notifications {
rows = rows.push(toggle_row(
"Do Not Disturb",
n.dnd,
Action::Dnd(!n.dnd),
rows = rows.push(control_card(
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p),
primary,
p,
));
@@ -581,7 +566,7 @@ impl FereseShell {
p,
));
}
rows = rows.push(actions.wrap());
rows = rows.push(control_card(actions.wrap().into(), primary, p));
}
if !menu.kind.available(&self.status) {
rows = rows.push(
@@ -601,39 +586,18 @@ impl FereseShell {
.width(WIDTH)
.padding(12)
.class(theme::Container::custom(move |_| container::Style {
background: if compositor_material {
None
} else {
Some(Background::Color(color_with_opacity(
theme.surface_popover,
p,
)))
},
background: None,
text_color: Some(primary),
icon_color: Some(primary),
border: Border {
radius: theme.material_radius.into(),
..Default::default()
},
// Iced's shadow fills the silhouette behind the quad. With
// transparent client content that fill remains visible over
// the compositor's glass, creating a second dark rectangle.
shadow: if compositor_material {
Shadow::default()
} else {
Shadow {
color: color_with_opacity(theme.shadow, p * theme.shadow_opacity.min(0.18)),
offset: Vector::new(0.0, 2.0),
blur_radius: 6.0,
}
},
..Default::default()
}));
// Keep the client panel and compositor material on the same surface
// bounds. A second outer padding box exposes a glass ring around the
// fallback fill and makes the first committed frame visibly jump.
// Keep the outer surface transparent; only inner cards paint a background.
cosmic::widget::autosize::autosize(
super::motion::animated(panel.into(), p),
super::motion::animated(panel.into(), p, menu.regions.clone()),
cosmic::iced::advanced::widget::Id::new("ferese-status-menu"),
)
.limits(
@@ -684,6 +648,7 @@ fn status_summary<'a>(
.spacing(12)
.align_y(Alignment::Center),
)
.id("ferese-blur-card")
.padding([12, 0])
.into()
}
@@ -730,6 +695,7 @@ fn control_card<'a>(
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
container(content)
.id("ferese-blur-card")
.padding(10)
.width(Length::Fill)
.class(theme::Container::custom(move |_| container::Style {
@@ -798,15 +764,20 @@ fn connection_control<'a>(
// giving it a misleading clickable/disabled-button appearance.
let Some(action) = action else {
return container(content)
.id("ferese-blur-card")
.width(Length::FillPortion(1))
.padding(4)
.into();
};
container(
button::custom(content)
.width(Length::FillPortion(1))
.padding(4)
.class(button_style(foreground, false, opacity))
.on_press(cosmic::Action::App(Message::Control(action)))
.on_press(cosmic::Action::App(Message::Control(action))),
)
.id("ferese-blur-card")
.width(Length::FillPortion(1))
.into()
}
@@ -1056,6 +1027,7 @@ mod tests {
opened: past,
confirm: None,
effects: None,
regions: Default::default(),
};
assert_eq!(menu.progress(), 1.0);
assert!(!menu.animating());
+40 -26
View File
@@ -8,7 +8,7 @@ use ferese_protocols::effects::v1::client::{
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
};
use wayland_client::{
Connection, Dispatch, QueueHandle, delegate_noop,
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
protocol::{wl_buffer, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface},
};
use wayland_protocols_wlr::layer_shell::v1::client::{
@@ -65,7 +65,7 @@ fn main() -> Result<(), Box<dyn Error>> {
effects.set_role(role);
queue.roundtrip(&mut state)?;
// Exercise each role in an actual presented frame, including changes
// to the cached backdrop allocation and shadow bounds.
// to the material geometry and shadow bounds.
std::thread::sleep(Duration::from_millis(50));
if state.configure_count != initial_configures {
return Err(format!("semantic role {role:?} changed layer geometry").into());
@@ -73,9 +73,23 @@ fn main() -> Result<(), Box<dyn Error>> {
}
if preview {
if std::env::args().any(|arg| arg == "--regions") {
if effects.version() < 2 {
return Err("rounded material regions require effects v2".into());
}
let regions = [[20_i32, 20, 200, 180, 14], [240, 20, 200, 180, 14]];
effects.set_regions(
regions
.iter()
.flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes()))
.collect(),
);
effects.set_role(ferese_surface_effects_v1::Role::Popover);
} else {
effects.set_role(ferese_surface_effects_v1::Role::Panel);
}
queue.roundtrip(&mut state)?;
println!("PREVIEW glass panel is mapped over the patterned card; press Ctrl+C to stop");
println!("PREVIEW material is mapped over the patterned card; press Ctrl+C to stop");
while !state.closed {
queue.blocking_dispatch(&mut state)?;
@@ -84,7 +98,7 @@ fn main() -> Result<(), Box<dyn Error>> {
return Ok(());
}
// Keep the final glass role mapped long enough for the compositor to run
// Keep the final solid role mapped long enough for the compositor to run
// at least one presentation. This turns shader and capture failures into
// probe failures instead of disconnecting before the first rendered frame.
std::thread::sleep(Duration::from_millis(100));
@@ -112,9 +126,9 @@ struct ProbeState {
layer_surface: Option<ZwlrLayerSurfaceV1>,
effects: Option<FereseSurfaceEffectsV1>,
buffer: Option<wl_buffer::WlBuffer>,
preview_backdrop_surface: Option<wl_surface::WlSurface>,
preview_backdrop_layer: Option<ZwlrLayerSurfaceV1>,
preview_backdrop_buffer: Option<wl_buffer::WlBuffer>,
preview_background_surface: Option<wl_surface::WlSurface>,
preview_background_layer: Option<ZwlrLayerSurfaceV1>,
preview_background_buffer: Option<wl_buffer::WlBuffer>,
preview: bool,
configured: bool,
closed: bool,
@@ -174,25 +188,25 @@ impl ProbeState {
surface.commit();
if self.preview {
let backdrop_surface = compositor.create_surface(qh, ());
let backdrop_layer = layer_shell.get_layer_surface(
&backdrop_surface,
let background_surface = compositor.create_surface(qh, ());
let background_layer = layer_shell.get_layer_surface(
&background_surface,
None,
zwlr_layer_shell_v1::Layer::Top,
"ferese-effects-preview-backdrop".to_owned(),
"ferese-effects-preview-background".to_owned(),
qh,
(),
);
backdrop_layer.set_anchor(
background_layer.set_anchor(
zwlr_layer_surface_v1::Anchor::Top | zwlr_layer_surface_v1::Anchor::Right,
);
backdrop_layer.set_margin(12, 12, 0, 0);
backdrop_layer.set_size(520, 260);
backdrop_layer.set_exclusive_zone(0);
backdrop_surface.commit();
background_layer.set_margin(12, 12, 0, 0);
background_layer.set_size(520, 260);
background_layer.set_exclusive_zone(0);
background_surface.commit();
self.preview_backdrop_surface = Some(backdrop_surface);
self.preview_backdrop_layer = Some(backdrop_layer);
self.preview_background_surface = Some(background_surface);
self.preview_background_layer = Some(background_layer);
}
self.surface = Some(surface);
@@ -237,7 +251,7 @@ fn create_buffer(
Ok(buffer)
}
fn create_preview_backdrop_buffer(
fn create_preview_background_buffer(
shm: &wl_shm::WlShm,
qh: &QueueHandle<ProbeState>,
width: u32,
@@ -304,7 +318,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for ProbeState {
state.layer_shell = Some(registry.bind(name, version.min(4), qh, ()))
}
"ferese_effects_manager_v1" => {
state.effects_manager = Some(registry.bind(name, 1, qh, ()))
state.effects_manager = Some(registry.bind(name, version.min(2), qh, ()))
}
_ => {}
}
@@ -328,20 +342,20 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for ProbeState {
} => {
layer_surface.ack_configure(serial);
if state.preview_backdrop_layer.as_ref() == Some(layer_surface) {
if state.preview_background_layer.as_ref() == Some(layer_surface) {
let width = if width == 0 { 520 } else { width };
let height = if height == 0 { 260 } else { height };
let surface = state
.preview_backdrop_surface
.preview_background_surface
.clone()
.expect("preview backdrop surface initialized");
.expect("preview background surface initialized");
let shm = state.shm.as_ref().expect("shm initialized");
let buffer = create_preview_backdrop_buffer(shm, qh, width, height)
.expect("create preview backdrop buffer");
let buffer = create_preview_background_buffer(shm, qh, width, height)
.expect("create preview background buffer");
surface.attach(Some(&buffer), 0, 0);
surface.damage_buffer(0, 0, width as i32, height as i32);
surface.commit();
state.preview_backdrop_buffer = Some(buffer);
state.preview_background_buffer = Some(buffer);
return;
}