Split shell, settings, and compositor into focused modules
This commit is contained in:
1 parent
2b9f30fa5d
commit
cbf048566a
38 files changed
+9997
-9816
No files matched your search
@@ -35,7 +35,7 @@ use smithay::wayland::presentation::Refresh;
|
|||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||||
use crate::metrics::RenderMetrics;
|
use crate::metrics::RenderMetrics;
|
||||||
use crate::winit::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
use crate::render::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
||||||
|
|
||||||
pub struct DirectBackendState {
|
pub struct DirectBackendState {
|
||||||
pub session: LibSeatSession,
|
pub session: LibSeatSession,
|
||||||
@@ -57,7 +57,7 @@ impl DirectBackendState {
|
|||||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
remaining: usize,
|
remaining: usize,
|
||||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
) -> Result<Option<crate::render::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
||||||
let Some(device) = self
|
let Some(device) = self
|
||||||
.devices
|
.devices
|
||||||
.values_mut()
|
.values_mut()
|
||||||
@@ -65,7 +65,7 @@ impl DirectBackendState {
|
|||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
crate::winit::capture_resize_snapshot(
|
crate::render::capture_resize_snapshot(
|
||||||
&mut device.renderer,
|
&mut device.renderer,
|
||||||
window,
|
window,
|
||||||
geometry,
|
geometry,
|
||||||
@@ -85,7 +85,7 @@ impl DirectBackendState {
|
|||||||
.values_mut()
|
.values_mut()
|
||||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||||
.ok_or("Window output is unavailable")?;
|
.ok_or("Window output is unavailable")?;
|
||||||
crate::winit::capture_window_buffer(
|
crate::render::capture_window_buffer(
|
||||||
&mut device.renderer,
|
&mut device.renderer,
|
||||||
window,
|
window,
|
||||||
geometry,
|
geometry,
|
||||||
@@ -104,7 +104,7 @@ impl DirectBackendState {
|
|||||||
.values_mut()
|
.values_mut()
|
||||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||||
.ok_or("Window output is unavailable")?;
|
.ok_or("Window output is unavailable")?;
|
||||||
crate::winit::capture_window_frame(
|
crate::render::capture_window_frame(
|
||||||
&mut device.renderer,
|
&mut device.renderer,
|
||||||
window,
|
window,
|
||||||
window.geometry(),
|
window.geometry(),
|
||||||
|
|||||||
+15
-1522
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,414 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
|
||||||
|
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||||
|
let focus_ring_width =
|
||||||
|
nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?;
|
||||||
|
let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||||
|
let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||||
|
let shadow_blur = nonnegative_theme_value(self.theme.shadow.soft.blur, "shadow.soft.blur")?;
|
||||||
|
let shadow_opacity = unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||||
|
|
||||||
|
let material_radius = nonnegative_theme_value(
|
||||||
|
self.theme
|
||||||
|
.geometry
|
||||||
|
.shell_radius
|
||||||
|
.or(self.appearance.corner_radius)
|
||||||
|
.or(self.theme.geometry.top_bar_radius)
|
||||||
|
.unwrap_or(14.0),
|
||||||
|
"geometry.shell_radius",
|
||||||
|
)?;
|
||||||
|
Ok(ThemeSettings {
|
||||||
|
border_width,
|
||||||
|
focus_ring_width,
|
||||||
|
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||||
|
text_primary_color: parse_color(&self.theme.colors.text_primary, "colors.text_primary")?,
|
||||||
|
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||||
|
border_gradient: self.theme.border.settings("border")?,
|
||||||
|
focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?,
|
||||||
|
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
||||||
|
surface_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?,
|
||||||
|
bar_background_color: parse_color(
|
||||||
|
self.theme
|
||||||
|
.surface
|
||||||
|
.bar
|
||||||
|
.background
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(&self.theme.colors.surface_base),
|
||||||
|
"surface.bar.background",
|
||||||
|
)?,
|
||||||
|
shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?,
|
||||||
|
inactive_dim: InactiveDimSettings {
|
||||||
|
enabled: self.appearance.inactive_dim.enabled,
|
||||||
|
amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?,
|
||||||
|
duration_ms: nonnegative_theme_value(
|
||||||
|
self.appearance.inactive_dim.duration_ms,
|
||||||
|
"appearance.inactive_dim.duration_ms",
|
||||||
|
)?,
|
||||||
|
},
|
||||||
|
window_radius,
|
||||||
|
shadow_offset_y,
|
||||||
|
shadow_blur,
|
||||||
|
shadow_opacity,
|
||||||
|
material_style: self.theme.material.style,
|
||||||
|
backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0),
|
||||||
|
material_tint_strength: unit_theme_value(self.theme.material.tint_strength, "material.tint_strength")?,
|
||||||
|
material_radius,
|
||||||
|
panel_radius: material_radius,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn nonnegative_theme_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && value >= 0.0 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn finite_theme_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn unit_theme_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && (0.0..=1.0).contains(&value) {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_color(value: &str, field: &'static str) -> Result<RgbaColor, ConfigError> {
|
||||||
|
let digits = value.strip_prefix('#').unwrap_or(value);
|
||||||
|
if !digits.is_ascii() || !matches!(digits.len(), 6 | 8) {
|
||||||
|
return Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_channel = |offset| u8::from_str_radix(&digits[offset..offset + 2], 16).ok();
|
||||||
|
let Some((red, green, blue)) = parse_channel(0)
|
||||||
|
.zip(parse_channel(2))
|
||||||
|
.zip(parse_channel(4))
|
||||||
|
.map(|((red, green), blue)| (red, green, blue))
|
||||||
|
else {
|
||||||
|
return Err(ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let alpha = if digits.len() == 8 {
|
||||||
|
parse_channel(6).ok_or_else(|| ConfigError::InvalidThemeValue {
|
||||||
|
field,
|
||||||
|
value: value.to_owned(),
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
u8::MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RgbaColor([
|
||||||
|
f32::from(red) / 255.0,
|
||||||
|
f32::from(green) / 255.0,
|
||||||
|
f32::from(blue) / 255.0,
|
||||||
|
f32::from(alpha) / 255.0,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_border_color() -> String {
|
||||||
|
"#FFFFFF18".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_text_primary_color() -> String {
|
||||||
|
"#F4F7FB".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_surface_base_color() -> String {
|
||||||
|
"#111821".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_accent_color() -> String {
|
||||||
|
"#3D7BE6".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_shadow_color() -> String {
|
||||||
|
"#00000055".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RgbaColor(pub [f32; 4]);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct BorderGradient {
|
||||||
|
pub from: RgbaColor,
|
||||||
|
pub to: RgbaColor,
|
||||||
|
pub angle: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct BorderPaintConfig {
|
||||||
|
pub(super) gradient: Option<BorderGradientConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct BorderGradientConfig {
|
||||||
|
pub(super) from: String,
|
||||||
|
pub(super) to: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) angle: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderPaintConfig {
|
||||||
|
fn settings(&self, name: &str) -> Result<Option<BorderGradient>, ConfigError> {
|
||||||
|
let (from_name, to_name, angle_name) = if name == "focus_ring" {
|
||||||
|
(
|
||||||
|
"focus_ring.gradient.from",
|
||||||
|
"focus_ring.gradient.to",
|
||||||
|
"focus_ring.gradient.angle",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("border.gradient.from", "border.gradient.to", "border.gradient.angle")
|
||||||
|
};
|
||||||
|
self.gradient
|
||||||
|
.as_ref()
|
||||||
|
.map(|gradient| {
|
||||||
|
Ok(BorderGradient {
|
||||||
|
from: parse_color(&gradient.from, from_name)?,
|
||||||
|
to: parse_color(&gradient.to, to_name)?,
|
||||||
|
angle: finite_theme_value(gradient.angle, angle_name)?.rem_euclid(360.0),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ThemeSettings {
|
||||||
|
pub border_width: f64,
|
||||||
|
pub focus_ring_width: f64,
|
||||||
|
pub border_color: RgbaColor,
|
||||||
|
pub accent_color: RgbaColor,
|
||||||
|
pub border_gradient: Option<BorderGradient>,
|
||||||
|
pub focus_ring_gradient: Option<BorderGradient>,
|
||||||
|
pub shadow_color: RgbaColor,
|
||||||
|
pub surface_base_color: RgbaColor,
|
||||||
|
pub bar_background_color: RgbaColor,
|
||||||
|
pub text_primary_color: RgbaColor,
|
||||||
|
pub shell_opacity: f64,
|
||||||
|
pub inactive_dim: InactiveDimSettings,
|
||||||
|
pub window_radius: f64,
|
||||||
|
pub shadow_offset_y: f64,
|
||||||
|
pub shadow_blur: f64,
|
||||||
|
pub shadow_opacity: f64,
|
||||||
|
pub material_style: MaterialStyle,
|
||||||
|
pub backdrop_blur: f64,
|
||||||
|
pub material_tint_strength: f64,
|
||||||
|
pub material_radius: f64,
|
||||||
|
pub panel_radius: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum MaterialStyle {
|
||||||
|
Translucent,
|
||||||
|
#[default]
|
||||||
|
Solid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct AppearanceConfig {
|
||||||
|
pub(super) corner_radius: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) inactive_dim: InactiveDimConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct InactiveDimSettings {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub amount: f64,
|
||||||
|
pub duration_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) struct InactiveDimConfig {
|
||||||
|
pub(super) enabled: bool,
|
||||||
|
pub(super) amount: f64,
|
||||||
|
pub(super) duration_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InactiveDimConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
amount: 0.15,
|
||||||
|
duration_ms: 150.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) border: BorderPaintConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) focus_ring: BorderPaintConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) typography: OverviewTypographyConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) background: crate::wallpaper::WallpaperConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) colors: ThemeColorsConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) geometry: ThemeGeometryConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) shadow: ThemeShadowConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) material: ThemeMaterialConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) surface: ThemeSurfaceConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct ThemeSurfaceConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) bar: ThemeBarConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct ThemeBarConfig {
|
||||||
|
pub(super) background: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct OverviewTypographyConfig {
|
||||||
|
pub(super) font_family: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct ThemeMaterialConfig {
|
||||||
|
#[serde(default = "default_shell_opacity")]
|
||||||
|
pub(super) opacity: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) style: MaterialStyle,
|
||||||
|
#[serde(default = "default_backdrop_blur")]
|
||||||
|
pub(super) blur_radius: f64,
|
||||||
|
#[serde(default = "default_material_tint_strength")]
|
||||||
|
pub(super) tint_strength: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeMaterialConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
style: MaterialStyle::Solid,
|
||||||
|
opacity: default_shell_opacity(),
|
||||||
|
blur_radius: default_backdrop_blur(),
|
||||||
|
tint_strength: default_material_tint_strength(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_shell_opacity() -> f64 {
|
||||||
|
ferese_config::DEFAULT_MATERIAL_OPACITY
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_material_tint_strength() -> f64 {
|
||||||
|
0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_backdrop_blur() -> f64 {
|
||||||
|
12.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct ThemeColorsConfig {
|
||||||
|
#[serde(default = "default_text_primary_color")]
|
||||||
|
pub(super) text_primary: String,
|
||||||
|
#[serde(default = "default_surface_base_color")]
|
||||||
|
pub(super) surface_base: String,
|
||||||
|
#[serde(default = "default_border_color")]
|
||||||
|
pub(super) border: String,
|
||||||
|
#[serde(default = "default_accent_color")]
|
||||||
|
pub(super) accent: String,
|
||||||
|
#[serde(default = "default_shadow_color")]
|
||||||
|
pub(super) shadow: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeColorsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
surface_base: default_surface_base_color(),
|
||||||
|
text_primary: default_text_primary_color(),
|
||||||
|
border: default_border_color(),
|
||||||
|
accent: default_accent_color(),
|
||||||
|
shadow: default_shadow_color(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
pub(super) struct ThemeShadowConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) soft: SoftShadowConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct SoftShadowConfig {
|
||||||
|
#[serde(default = "default_shadow_offset_y")]
|
||||||
|
pub(super) offset_y: f64,
|
||||||
|
#[serde(default = "default_shadow_blur")]
|
||||||
|
pub(super) blur: f64,
|
||||||
|
#[serde(default = "default_shadow_opacity")]
|
||||||
|
pub(super) opacity: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SoftShadowConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
offset_y: default_shadow_offset_y(),
|
||||||
|
blur: default_shadow_blur(),
|
||||||
|
opacity: default_shadow_opacity(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct ThemeGeometryConfig {
|
||||||
|
#[serde(default = "default_border_width")]
|
||||||
|
pub(super) border_width: f64,
|
||||||
|
#[serde(default = "default_focus_ring_width")]
|
||||||
|
pub(super) focus_ring_width: f64,
|
||||||
|
#[serde(default = "default_window_radius")]
|
||||||
|
pub(super) window_radius: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) top_bar_radius: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) shell_radius: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ThemeGeometryConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
border_width: default_border_width(),
|
||||||
|
focus_ring_width: default_focus_ring_width(),
|
||||||
|
window_radius: default_window_radius(),
|
||||||
|
top_bar_radius: None,
|
||||||
|
shell_radius: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,621 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
|
||||||
|
let mut commands = HashMap::from([
|
||||||
|
("terminal".to_owned(), vec!["foot".to_owned()]),
|
||||||
|
("screenshot".to_owned(), vec!["ferese-screenshot".to_owned()]),
|
||||||
|
(
|
||||||
|
"screenshot-full".to_owned(),
|
||||||
|
vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
commands.extend(self.commands.clone());
|
||||||
|
validate_commands(&commands)?;
|
||||||
|
|
||||||
|
let keymap = physical_keymap(input)?;
|
||||||
|
let mut bindings = default_bindings()
|
||||||
|
.into_iter()
|
||||||
|
.map(|binding| parse_binding(&binding, &commands, &keymap))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let mut supplied = HashSet::new();
|
||||||
|
|
||||||
|
for configured in &self.bindings {
|
||||||
|
let identity = binding_identity(configured, &keymap)?;
|
||||||
|
if !supplied.insert(identity.clone()) {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"duplicate binding {:?}",
|
||||||
|
configured.keys
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings.retain(|binding| binding_identity_for_runtime(binding) != identity);
|
||||||
|
if configured.disabled {
|
||||||
|
if configured.action.is_some() || configured.argument.is_some() {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"disabled binding {:?} cannot have an action or argument",
|
||||||
|
configured.keys
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bindings.push(parse_binding(configured, &commands, &keymap)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bindings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> {
|
||||||
|
for (name, argv) in commands {
|
||||||
|
if name.trim().is_empty() {
|
||||||
|
return Err(ConfigError::InvalidBinding("command names cannot be empty".to_owned()));
|
||||||
|
}
|
||||||
|
if argv.is_empty() || argv[0].is_empty() {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"command {name:?} must contain a program"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn physical_keymap(input: &InputSettings) -> Result<xkb::Keymap, ConfigError> {
|
||||||
|
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||||
|
let options = (!input.xkb_options.is_empty()).then(|| input.xkb_options.join(","));
|
||||||
|
|
||||||
|
xkb::Keymap::new_from_names(
|
||||||
|
&context,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
&input.xkb_layout,
|
||||||
|
&input.xkb_variant,
|
||||||
|
options,
|
||||||
|
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| ConfigError::InvalidBinding("failed to compile the XKB keymap".to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_binding(
|
||||||
|
configured: &BindingConfig,
|
||||||
|
commands: &HashMap<String, Vec<String>>,
|
||||||
|
keymap: &xkb::Keymap,
|
||||||
|
) -> Result<Binding, ConfigError> {
|
||||||
|
if configured.disabled {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"disabled binding {:?} cannot be executed",
|
||||||
|
configured.keys
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity = binding_identity(configured, keymap)?;
|
||||||
|
let action = configured
|
||||||
|
.action
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)))?;
|
||||||
|
let action = parse_action(action, configured.argument.as_deref(), commands)?;
|
||||||
|
let trigger = match identity.match_mode {
|
||||||
|
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||||
|
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
||||||
|
BindingMatch::Swipe => BindingTrigger::Swipe {
|
||||||
|
fingers: identity.key / 4,
|
||||||
|
direction: [
|
||||||
|
crate::gestures::SwipeDirection::Up,
|
||||||
|
crate::gestures::SwipeDirection::Down,
|
||||||
|
crate::gestures::SwipeDirection::Left,
|
||||||
|
crate::gestures::SwipeDirection::Right,
|
||||||
|
][(identity.key % 4) as usize],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Binding {
|
||||||
|
modifiers: identity.modifiers,
|
||||||
|
trigger,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn binding_identity(
|
||||||
|
configured: &BindingConfig,
|
||||||
|
keymap: &xkb::Keymap,
|
||||||
|
) -> Result<BindingIdentity, ConfigError> {
|
||||||
|
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||||
|
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||||
|
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
||||||
|
return Err(ConfigError::InvalidBinding(
|
||||||
|
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(BindingIdentity {
|
||||||
|
modifiers,
|
||||||
|
match_mode: BindingMatch::Swipe,
|
||||||
|
key: fingers * 4 + direction as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let key = match configured.match_mode {
|
||||||
|
BindingMatch::Keysym => parse_keysym(&key)?,
|
||||||
|
BindingMatch::Physical => keymap
|
||||||
|
.key_by_name(&key.to_ascii_uppercase())
|
||||||
|
.map(Keycode::raw)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ConfigError::InvalidBinding(format!(
|
||||||
|
"unknown XKB physical key name {key:?} in {:?}",
|
||||||
|
configured.keys
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
BindingMatch::Swipe => {
|
||||||
|
return Err(ConfigError::InvalidBinding(
|
||||||
|
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(BindingIdentity {
|
||||||
|
modifiers,
|
||||||
|
match_mode: configured.match_mode,
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity {
|
||||||
|
let (match_mode, key) = match binding.trigger {
|
||||||
|
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||||
|
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||||
|
BindingTrigger::Swipe { fingers, direction } => (BindingMatch::Swipe, fingers * 4 + direction as u32),
|
||||||
|
};
|
||||||
|
|
||||||
|
BindingIdentity {
|
||||||
|
modifiers: binding.modifiers,
|
||||||
|
match_mode,
|
||||||
|
key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||||
|
let mut modifiers = BindingModifiers::default();
|
||||||
|
let mut key = None;
|
||||||
|
|
||||||
|
for component in chord.split('+').map(str::trim) {
|
||||||
|
if component.is_empty() {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!("invalid key chord {chord:?}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = match component.to_ascii_lowercase().as_str() {
|
||||||
|
"super" | "logo" | "mod4" => Some(&mut modifiers.logo),
|
||||||
|
"ctrl" | "control" => Some(&mut modifiers.ctrl),
|
||||||
|
"alt" => Some(&mut modifiers.alt),
|
||||||
|
"shift" => Some(&mut modifiers.shift),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(slot) = slot {
|
||||||
|
if *slot {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}")));
|
||||||
|
}
|
||||||
|
*slot = true;
|
||||||
|
} else if key.replace(component.to_owned()).is_some() {
|
||||||
|
return Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"key chord {chord:?} contains more than one key"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = key.ok_or_else(|| ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")))?;
|
||||||
|
Ok((modifiers, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_swipe(key: &str) -> Option<(u32, crate::gestures::SwipeDirection)> {
|
||||||
|
use crate::gestures::SwipeDirection;
|
||||||
|
let lower = key.to_ascii_lowercase();
|
||||||
|
let rest = lower.strip_prefix("swipe")?;
|
||||||
|
let fingers = rest.get(..1)?.parse::<u32>().ok()?;
|
||||||
|
if !(3..=5).contains(&fingers) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let direction = match rest.get(1..)? {
|
||||||
|
"up" => SwipeDirection::Up,
|
||||||
|
"down" => SwipeDirection::Down,
|
||||||
|
"left" => SwipeDirection::Left,
|
||||||
|
"right" => SwipeDirection::Right,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((fingers, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
||||||
|
let normalized = match name {
|
||||||
|
"Enter" | "enter" => "Return".to_owned(),
|
||||||
|
"Space" | "space" => "space".to_owned(),
|
||||||
|
"[" => "bracketleft".to_owned(),
|
||||||
|
"]" => "bracketright".to_owned(),
|
||||||
|
name if name.len() == 1 => name.to_ascii_lowercase(),
|
||||||
|
name => name.to_owned(),
|
||||||
|
};
|
||||||
|
let mut symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_NO_FLAGS);
|
||||||
|
if symbol.raw() == keysyms::KEY_NoSymbol {
|
||||||
|
symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE);
|
||||||
|
}
|
||||||
|
if symbol.raw() == keysyms::KEY_NoSymbol {
|
||||||
|
Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}")))
|
||||||
|
} else {
|
||||||
|
Ok(symbol.raw())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_action(
|
||||||
|
action: &str,
|
||||||
|
argument: Option<&str>,
|
||||||
|
commands: &HashMap<String, Vec<String>>,
|
||||||
|
) -> Result<BindingAction, ConfigError> {
|
||||||
|
let no_argument = || {
|
||||||
|
if argument.is_some() {
|
||||||
|
Err(ConfigError::InvalidBinding(format!(
|
||||||
|
"action {action:?} does not accept an argument"
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let required_argument =
|
||||||
|
|| argument.ok_or_else(|| ConfigError::InvalidBinding(format!("action {action:?} requires an argument")));
|
||||||
|
|
||||||
|
match action {
|
||||||
|
"none" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::None)
|
||||||
|
}
|
||||||
|
"workspace-next" | "workspace-previous" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next"))
|
||||||
|
}
|
||||||
|
"workspace-back-and-forth" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::WorkspaceBackAndForth)
|
||||||
|
}
|
||||||
|
"spawn" => {
|
||||||
|
let command = required_argument()?;
|
||||||
|
let argv = commands
|
||||||
|
.get(command)
|
||||||
|
.ok_or_else(|| ConfigError::InvalidBinding(format!("unknown command {command:?}")))?;
|
||||||
|
Ok(BindingAction::Spawn(argv.clone()))
|
||||||
|
}
|
||||||
|
"close" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::Close)
|
||||||
|
}
|
||||||
|
"exit" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::Exit)
|
||||||
|
}
|
||||||
|
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||||
|
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||||
|
"resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)),
|
||||||
|
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)),
|
||||||
|
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)),
|
||||||
|
"toggle-maximized" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleMaximized)
|
||||||
|
}
|
||||||
|
"toggle-fullscreen" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleFullscreen)
|
||||||
|
}
|
||||||
|
"toggle-layout" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleLayout)
|
||||||
|
}
|
||||||
|
"cycle-column-width" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::CycleColumnWidth)
|
||||||
|
}
|
||||||
|
"center-column" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::CenterColumn)
|
||||||
|
}
|
||||||
|
"consume" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::Consume)
|
||||||
|
}
|
||||||
|
"expel" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::Expel)
|
||||||
|
}
|
||||||
|
"toggle-floating" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleFloating)
|
||||||
|
}
|
||||||
|
"toggle-overview" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleOverview)
|
||||||
|
}
|
||||||
|
"toggle-keybinding-guide" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::ToggleKeybindingGuide)
|
||||||
|
}
|
||||||
|
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_direction(argument: &str) -> Result<Direction, ConfigError> {
|
||||||
|
match argument {
|
||||||
|
"left" => Ok(Direction::Left),
|
||||||
|
"right" => Ok(Direction::Right),
|
||||||
|
"up" => Ok(Direction::Up),
|
||||||
|
"down" => Ok(Direction::Down),
|
||||||
|
_ => Err(ConfigError::InvalidBinding(format!("invalid direction {argument:?}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||||
|
match argument.parse() {
|
||||||
|
Ok(workspace) if workspace > 0 => Ok(workspace),
|
||||||
|
_ => Err(ConfigError::InvalidBinding(format!("invalid workspace {argument:?}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||||
|
let mut bindings = vec![
|
||||||
|
binding("Super+Enter", "spawn", Some("terminal")),
|
||||||
|
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
||||||
|
binding("Print", "spawn", Some("screenshot-full")),
|
||||||
|
binding("Swipe3Up", "workspace-next", None),
|
||||||
|
binding("Swipe3Down", "workspace-previous", None),
|
||||||
|
binding("Swipe3Left", "focus", Some("right")),
|
||||||
|
binding("Swipe3Right", "focus", Some("left")),
|
||||||
|
binding("Super+Q", "close", None),
|
||||||
|
binding("Super+F", "toggle-maximized", None),
|
||||||
|
binding("Super+Shift+F", "toggle-fullscreen", None),
|
||||||
|
binding("Super+M", "toggle-layout", None),
|
||||||
|
binding("Super+R", "cycle-column-width", None),
|
||||||
|
binding("Super+C", "center-column", None),
|
||||||
|
binding("Super+[", "consume", None),
|
||||||
|
binding("Super+]", "expel", None),
|
||||||
|
binding("Super+Shift+Space", "toggle-floating", None),
|
||||||
|
binding("Super+Tab", "toggle-overview", None),
|
||||||
|
binding("Super+F1", "toggle-keybinding-guide", None),
|
||||||
|
binding("Super+Escape", "workspace-back-and-forth", None),
|
||||||
|
binding("Super+Shift+E", "exit", None),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
|
||||||
|
bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction)));
|
||||||
|
bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction)));
|
||||||
|
bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for workspace in 1..=9 {
|
||||||
|
let workspace = workspace.to_string();
|
||||||
|
bindings.push(binding(format!("Super+{workspace}"), "workspace", Some(&workspace)));
|
||||||
|
bindings.push(binding(
|
||||||
|
format!("Super+Shift+{workspace}"),
|
||||||
|
"move-to-workspace",
|
||||||
|
Some(&workspace),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn binding(keys: impl Into<String>, action: impl Into<String>, argument: Option<&str>) -> BindingConfig {
|
||||||
|
BindingConfig {
|
||||||
|
keys: keys.into(),
|
||||||
|
match_mode: BindingMatch::Keysym,
|
||||||
|
action: Some(action.into()),
|
||||||
|
argument: argument.map(str::to_owned),
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Binding {
|
||||||
|
pub(super) modifiers: BindingModifiers,
|
||||||
|
pub(super) trigger: BindingTrigger,
|
||||||
|
pub action: BindingAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(super) enum BindingTrigger {
|
||||||
|
Keysym(u32),
|
||||||
|
Physical(Keycode),
|
||||||
|
Swipe {
|
||||||
|
fingers: u32,
|
||||||
|
direction: crate::gestures::SwipeDirection,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||||
|
pub(super) struct BindingModifiers {
|
||||||
|
pub(super) logo: bool,
|
||||||
|
pub(super) ctrl: bool,
|
||||||
|
pub(super) alt: bool,
|
||||||
|
pub(super) shift: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum BindingAction {
|
||||||
|
None,
|
||||||
|
Spawn(Vec<String>),
|
||||||
|
Close,
|
||||||
|
Exit,
|
||||||
|
Focus(ferese_layout::Direction),
|
||||||
|
Move(ferese_layout::Direction),
|
||||||
|
Resize(ferese_layout::Direction),
|
||||||
|
SwitchWorkspace(u8),
|
||||||
|
SwitchRelativeWorkspace(bool),
|
||||||
|
WorkspaceBackAndForth,
|
||||||
|
MoveToWorkspace(u8),
|
||||||
|
ToggleFullscreen,
|
||||||
|
ToggleMaximized,
|
||||||
|
ToggleLayout,
|
||||||
|
CycleColumnWidth,
|
||||||
|
CenterColumn,
|
||||||
|
Consume,
|
||||||
|
Expel,
|
||||||
|
ToggleFloating,
|
||||||
|
ToggleOverview,
|
||||||
|
ToggleKeybindingGuide,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Binding {
|
||||||
|
pub(crate) fn portal_trigger(chord: &str) -> Result<Self, ConfigError> {
|
||||||
|
if chord.len() > 256 {
|
||||||
|
return Err(ConfigError::InvalidBinding("Shortcut is too long".into()));
|
||||||
|
}
|
||||||
|
let parts = chord.split('+').collect::<Vec<_>>();
|
||||||
|
if parts
|
||||||
|
.iter()
|
||||||
|
.filter(|part| part.trim().eq_ignore_ascii_case("num"))
|
||||||
|
.count()
|
||||||
|
> 1
|
||||||
|
{
|
||||||
|
return Err(ConfigError::InvalidBinding("Duplicate NUM modifier".into()));
|
||||||
|
}
|
||||||
|
let chord = parts
|
||||||
|
.into_iter()
|
||||||
|
.filter(|part| !part.trim().eq_ignore_ascii_case("num"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("+");
|
||||||
|
let (modifiers, key) = parse_chord(&chord)?;
|
||||||
|
let symbol = parse_keysym(&key)?;
|
||||||
|
if (modifiers.ctrl
|
||||||
|
&& modifiers.alt
|
||||||
|
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
||||||
|
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||||
|
{
|
||||||
|
return Err(ConfigError::InvalidBinding("Reserved system shortcut".into()));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
modifiers,
|
||||||
|
trigger: BindingTrigger::Keysym(symbol),
|
||||||
|
action: BindingAction::None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn conflicts(&self, other: &Self, map: &xkb::Keymap) -> bool {
|
||||||
|
if self.modifiers != other.modifiers {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match (&self.trigger, &other.trigger) {
|
||||||
|
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
|
||||||
|
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
|
||||||
|
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code))
|
||||||
|
.any(|layout| {
|
||||||
|
map.key_get_syms_by_level(*code, layout, 0)
|
||||||
|
.iter()
|
||||||
|
.any(|key| key.raw() == *symbol)
|
||||||
|
}),
|
||||||
|
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool {
|
||||||
|
self.modifiers == BindingModifiers { logo, ctrl, alt, shift }
|
||||||
|
&& match self.trigger {
|
||||||
|
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||||
|
BindingTrigger::Physical(expected) => expected == keycode,
|
||||||
|
BindingTrigger::Swipe { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn guide_entry(&self, keymap: Option<&xkb::Keymap>) -> Option<serde_json::Value> {
|
||||||
|
let description = match &self.action {
|
||||||
|
BindingAction::None => return None,
|
||||||
|
BindingAction::Spawn(argv) if argv.first().is_some_and(|program| program == "ferese-screenshot") => {
|
||||||
|
if argv.iter().any(|arg| arg == "--full") {
|
||||||
|
"Capture whole screen"
|
||||||
|
} else {
|
||||||
|
"Capture selected area"
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
BindingAction::Spawn(argv) => format!(
|
||||||
|
"Launch {}",
|
||||||
|
argv.first()
|
||||||
|
.and_then(|program| std::path::Path::new(program).file_name())
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or("application")
|
||||||
|
),
|
||||||
|
BindingAction::Close => "Close window".into(),
|
||||||
|
BindingAction::Exit => "Log out (with confirmation)".into(),
|
||||||
|
BindingAction::Focus(direction) => format!("Focus window {direction:?}"),
|
||||||
|
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
||||||
|
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||||
|
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||||
|
BindingAction::WorkspaceBackAndForth => "Return to last workspace on this monitor".into(),
|
||||||
|
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
|
||||||
|
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||||
|
if *next { "Next workspace" } else { "Previous workspace" }.into()
|
||||||
|
}
|
||||||
|
BindingAction::ToggleFullscreen => "Toggle fullscreen".into(),
|
||||||
|
BindingAction::ToggleMaximized => "Toggle full-width window".into(),
|
||||||
|
BindingAction::ToggleLayout => "Switch workspace layout".into(),
|
||||||
|
BindingAction::CycleColumnWidth => "Cycle column width".into(),
|
||||||
|
BindingAction::CenterColumn => "Center focused column".into(),
|
||||||
|
BindingAction::Consume => "Join window into column".into(),
|
||||||
|
BindingAction::Expel => "Move window out of column".into(),
|
||||||
|
BindingAction::ToggleFloating => "Toggle floating window".into(),
|
||||||
|
BindingAction::ToggleOverview => "Open or close overview".into(),
|
||||||
|
BindingAction::ToggleKeybindingGuide => "Open or close shortcut hint".into(),
|
||||||
|
};
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for (enabled, label) in [
|
||||||
|
(self.modifiers.logo, "Super"),
|
||||||
|
(self.modifiers.ctrl, "Ctrl"),
|
||||||
|
(self.modifiers.alt, "Alt"),
|
||||||
|
(self.modifiers.shift, "Shift"),
|
||||||
|
] {
|
||||||
|
if enabled {
|
||||||
|
parts.push(label.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push(match self.trigger {
|
||||||
|
BindingTrigger::Keysym(symbol) => {
|
||||||
|
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
|
||||||
|
match name.as_str() {
|
||||||
|
"Return" => "Enter".into(),
|
||||||
|
"Print" => "Print Screen".into(),
|
||||||
|
"space" => "Space".into(),
|
||||||
|
_ if name.len() == 1 => name.to_uppercase(),
|
||||||
|
_ => name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BindingTrigger::Physical(code) => keymap
|
||||||
|
.and_then(|map| map.key_get_name(code))
|
||||||
|
.map(|name| format!("Physical {name}"))
|
||||||
|
.unwrap_or_else(|| format!("Keycode {}", u32::from(code))),
|
||||||
|
BindingTrigger::Swipe { fingers, direction } => {
|
||||||
|
format!("{fingers}-finger swipe {direction:?}")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(serde_json::json!({"keys": parts.join(" + "), "description": description}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn swipe_fingers(&self, count: u32) -> bool {
|
||||||
|
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn matches_swipe(&self, count: u32, target: crate::gestures::SwipeDirection) -> bool {
|
||||||
|
matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub(super) enum BindingMatch {
|
||||||
|
#[default]
|
||||||
|
Keysym,
|
||||||
|
Physical,
|
||||||
|
Swipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(super) struct BindingConfig {
|
||||||
|
pub(super) keys: String,
|
||||||
|
#[serde(default, rename = "match")]
|
||||||
|
pub(super) match_mode: BindingMatch,
|
||||||
|
pub(super) action: Option<String>,
|
||||||
|
pub(super) argument: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) disabled: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn input_settings(&self) -> Result<InputSettings, ConfigError> {
|
||||||
|
if self.input.xkb_layout.trim().is_empty() {
|
||||||
|
return Err(ConfigError::InvalidInputValue {
|
||||||
|
field: "xkb_layout",
|
||||||
|
value: self.input.xkb_layout.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.input.repeat_rate <= 0 {
|
||||||
|
return Err(ConfigError::InvalidInputValue {
|
||||||
|
field: "repeat_rate",
|
||||||
|
value: self.input.repeat_rate.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.input.repeat_delay_ms < 0 {
|
||||||
|
return Err(ConfigError::InvalidInputValue {
|
||||||
|
field: "repeat_delay_ms",
|
||||||
|
value: self.input.repeat_delay_ms.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(16..=1000).contains(&self.input.touchpad.swipe_threshold) {
|
||||||
|
return Err(ConfigError::InvalidInputValue {
|
||||||
|
field: "touchpad.swipe_threshold",
|
||||||
|
value: self.input.touchpad.swipe_threshold.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(InputSettings {
|
||||||
|
focus_follows_mouse: self.input.focus_follows_mouse,
|
||||||
|
xkb_layout: self.input.xkb_layout.clone(),
|
||||||
|
xkb_variant: self.input.xkb_variant.clone(),
|
||||||
|
xkb_options: self.input.xkb_options.clone(),
|
||||||
|
repeat_rate: self.input.repeat_rate,
|
||||||
|
repeat_delay_ms: self.input.repeat_delay_ms,
|
||||||
|
touchpad: TouchpadSettings {
|
||||||
|
tap: self.input.touchpad.tap,
|
||||||
|
natural_scroll: self.input.touchpad.natural_scroll,
|
||||||
|
disable_while_typing: self.input.touchpad.disable_while_typing,
|
||||||
|
swipe_threshold: self.input.touchpad.swipe_threshold,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animations_enabled(&self) -> bool {
|
||||||
|
self.animations.enabled && !self.animations.reduced_motion
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn animation_speed(&self) -> Result<f64, ConfigError> {
|
||||||
|
positive_animation_value(self.animations.speed, "speed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||||
|
let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?;
|
||||||
|
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||||
|
let damping = self.animations.spring.damping;
|
||||||
|
if !damping.is_finite() || damping < 0.0 {
|
||||||
|
return Err(ConfigError::InvalidAnimationValue {
|
||||||
|
field: "spring.damping",
|
||||||
|
value: damping,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SpringConfig {
|
||||||
|
mass,
|
||||||
|
stiffness,
|
||||||
|
damping,
|
||||||
|
..SpringConfig::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||||
|
let mass = positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||||
|
let stiffness =
|
||||||
|
positive_animation_value(self.animations.viewport_spring.stiffness, "viewport_spring.stiffness")?;
|
||||||
|
let damping_ratio = positive_animation_value(
|
||||||
|
self.animations.viewport_spring.damping_ratio,
|
||||||
|
"viewport_spring.damping_ratio",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(SpringConfig {
|
||||||
|
mass,
|
||||||
|
stiffness,
|
||||||
|
damping: 2.0 * damping_ratio * (stiffness * mass).sqrt(),
|
||||||
|
..SpringConfig::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn positive_animation_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && value > 0.0 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidAnimationValue { field, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn default_xkb_layout() -> String {
|
||||||
|
"us".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct InputSettings {
|
||||||
|
pub focus_follows_mouse: bool,
|
||||||
|
pub xkb_layout: String,
|
||||||
|
pub xkb_variant: String,
|
||||||
|
pub xkb_options: Vec<String>,
|
||||||
|
pub repeat_rate: i32,
|
||||||
|
pub repeat_delay_ms: i32,
|
||||||
|
pub touchpad: TouchpadSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct TouchpadSettings {
|
||||||
|
pub tap: bool,
|
||||||
|
pub natural_scroll: bool,
|
||||||
|
pub disable_while_typing: bool,
|
||||||
|
pub swipe_threshold: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_swipe_threshold() -> u16 {
|
||||||
|
80
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct InputConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) focus_follows_mouse: bool,
|
||||||
|
#[serde(default = "default_xkb_layout")]
|
||||||
|
pub(super) xkb_layout: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) xkb_variant: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) xkb_options: Vec<String>,
|
||||||
|
#[serde(default = "default_repeat_rate")]
|
||||||
|
pub(super) repeat_rate: i32,
|
||||||
|
#[serde(default = "default_repeat_delay")]
|
||||||
|
pub(super) repeat_delay_ms: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) touchpad: TouchpadConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InputConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
focus_follows_mouse: false,
|
||||||
|
xkb_layout: default_xkb_layout(),
|
||||||
|
xkb_variant: String::new(),
|
||||||
|
xkb_options: Vec::new(),
|
||||||
|
repeat_rate: default_repeat_rate(),
|
||||||
|
repeat_delay_ms: default_repeat_delay(),
|
||||||
|
touchpad: TouchpadConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct TouchpadConfig {
|
||||||
|
#[serde(default = "enabled_by_default")]
|
||||||
|
pub(super) tap: bool,
|
||||||
|
#[serde(default = "enabled_by_default")]
|
||||||
|
pub(super) natural_scroll: bool,
|
||||||
|
#[serde(default = "enabled_by_default")]
|
||||||
|
pub(super) disable_while_typing: bool,
|
||||||
|
#[serde(default = "default_swipe_threshold")]
|
||||||
|
pub(super) swipe_threshold: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TouchpadConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tap: true,
|
||||||
|
natural_scroll: true,
|
||||||
|
disable_while_typing: true,
|
||||||
|
swipe_threshold: default_swipe_threshold(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct AnimationsConfig {
|
||||||
|
#[serde(default = "enabled_by_default")]
|
||||||
|
pub(super) enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) reduced_motion: bool,
|
||||||
|
#[serde(default = "default_animation_speed")]
|
||||||
|
pub(super) speed: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) spring: SpringSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) viewport_spring: ViewportSpringSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
reduced_motion: false,
|
||||||
|
speed: 1.0,
|
||||||
|
spring: SpringSettings::default(),
|
||||||
|
viewport_spring: ViewportSpringSettings::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct ViewportSpringSettings {
|
||||||
|
#[serde(default = "default_viewport_mass")]
|
||||||
|
pub(super) mass: f64,
|
||||||
|
#[serde(default = "default_viewport_stiffness")]
|
||||||
|
pub(super) stiffness: f64,
|
||||||
|
#[serde(default = "default_viewport_damping_ratio")]
|
||||||
|
pub(super) damping_ratio: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ViewportSpringSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mass: default_viewport_mass(),
|
||||||
|
stiffness: default_viewport_stiffness(),
|
||||||
|
damping_ratio: default_viewport_damping_ratio(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct SpringSettings {
|
||||||
|
#[serde(default = "default_spring_mass")]
|
||||||
|
pub(super) mass: f64,
|
||||||
|
#[serde(default = "default_spring_stiffness")]
|
||||||
|
pub(super) stiffness: f64,
|
||||||
|
#[serde(default = "default_spring_damping")]
|
||||||
|
pub(super) damping: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SpringSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mass: default_spring_mass(),
|
||||||
|
stiffness: default_spring_stiffness(),
|
||||||
|
damping: default_spring_damping(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn layout_mode(&self) -> LayoutMode {
|
||||||
|
match self.layout.mode.unwrap_or(LayoutModeValue::Scrolling) {
|
||||||
|
LayoutModeValue::Scrolling => LayoutMode::Scrolling,
|
||||||
|
LayoutModeValue::Tree => LayoutMode::Tree,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gap_config(&self) -> Result<GapConfig, ConfigError> {
|
||||||
|
let inner = nonnegative_layout_value(self.layout.inner_gap, "inner_gap")?;
|
||||||
|
let outer = nonnegative_layout_value(self.layout.outer_gap, "outer_gap")?;
|
||||||
|
|
||||||
|
Ok(GapConfig {
|
||||||
|
inner,
|
||||||
|
outer,
|
||||||
|
smart: self.layout.smart_gaps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_column_width(&self) -> Result<ColumnWidth, ConfigError> {
|
||||||
|
parse_column_width(
|
||||||
|
self.scrolling
|
||||||
|
.default_column_width
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&ColumnWidthValue::Proportion(0.5)),
|
||||||
|
"default_column_width",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width_presets(&self) -> Result<Vec<ColumnWidth>, ConfigError> {
|
||||||
|
if self.scrolling.width_presets.is_empty() {
|
||||||
|
return Ok(vec![
|
||||||
|
ColumnWidth::Proportion(1.0 / 3.0),
|
||||||
|
ColumnWidth::Proportion(0.5),
|
||||||
|
ColumnWidth::Proportion(2.0 / 3.0),
|
||||||
|
ColumnWidth::Full,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.scrolling
|
||||||
|
.width_presets
|
||||||
|
.iter()
|
||||||
|
.map(|value| parse_column_width(value, "width_presets"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy {
|
||||||
|
match self.scrolling.focus_strategy.unwrap_or(FocusStrategyValue::Minimal) {
|
||||||
|
FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal,
|
||||||
|
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
|
||||||
|
FocusStrategyValue::Paged => ViewportFocusStrategy::Paged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||||
|
if value.is_finite() && value >= 0.0 {
|
||||||
|
Ok(value)
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::InvalidLayoutValue { field, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_column_width(value: &ColumnWidthValue, field: &'static str) -> Result<ColumnWidth, ConfigError> {
|
||||||
|
match value {
|
||||||
|
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
||||||
|
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
||||||
|
value => Err(ConfigError::InvalidColumnWidth {
|
||||||
|
field,
|
||||||
|
value: match value {
|
||||||
|
ColumnWidthValue::Proportion(value) => value.to_string(),
|
||||||
|
ColumnWidthValue::Named(value) => format!("{value:?}"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn output_profiles(&self) -> Result<Vec<OutputProfile>, ConfigError> {
|
||||||
|
let mut names = HashSet::new();
|
||||||
|
|
||||||
|
self.output_profiles
|
||||||
|
.iter()
|
||||||
|
.map(|profile| {
|
||||||
|
let name = profile.name.trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(
|
||||||
|
"profile names cannot be empty".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !names.insert(name.to_owned()) {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(format!(
|
||||||
|
"duplicate profile name {name:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if profile.outputs.is_empty() {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(format!(
|
||||||
|
"profile {name:?} has no outputs"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut matchers = HashSet::new();
|
||||||
|
let outputs = profile
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.map(|output| {
|
||||||
|
let matcher = output.matcher.trim();
|
||||||
|
if matcher.is_empty() {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(format!(
|
||||||
|
"profile {name:?} contains an empty output matcher"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !matchers.insert(matcher.to_owned()) {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(format!(
|
||||||
|
"profile {name:?} repeats output matcher {matcher:?}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !output.scale.is_finite() || output.scale <= 0.0 {
|
||||||
|
return Err(ConfigError::InvalidOutputProfile(format!(
|
||||||
|
"profile {name:?} output {matcher:?} has invalid scale {}",
|
||||||
|
output.scale
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OutputSettings {
|
||||||
|
auto_refresh: output.auto_refresh,
|
||||||
|
matcher: matcher.to_owned(),
|
||||||
|
enabled: output.enabled,
|
||||||
|
mode: output
|
||||||
|
.mode
|
||||||
|
.as_deref()
|
||||||
|
.map(parse_output_mode)
|
||||||
|
.transpose()
|
||||||
|
.map_err(ConfigError::InvalidOutputProfile)?,
|
||||||
|
scale: output.scale,
|
||||||
|
transform: output.transform,
|
||||||
|
position: output.position,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, ConfigError>>()?;
|
||||||
|
|
||||||
|
Ok(OutputProfile {
|
||||||
|
name: name.to_owned(),
|
||||||
|
outputs,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String> {
|
||||||
|
let (size, refresh) = value
|
||||||
|
.trim()
|
||||||
|
.split_once('@')
|
||||||
|
.map_or((value.trim(), None), |(size, refresh)| (size, Some(refresh)));
|
||||||
|
let (width, height) = size
|
||||||
|
.split_once('x')
|
||||||
|
.ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?;
|
||||||
|
let width = width
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| format!("mode {value:?} has an invalid width"))?;
|
||||||
|
let height = height
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| format!("mode {value:?} has an invalid height"))?;
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err(format!("mode {value:?} dimensions must be positive"));
|
||||||
|
}
|
||||||
|
let refresh_millihertz = refresh
|
||||||
|
.map(|refresh| {
|
||||||
|
let hertz = refresh
|
||||||
|
.parse::<f64>()
|
||||||
|
.map_err(|_| format!("mode {value:?} has an invalid refresh rate"))?;
|
||||||
|
if !hertz.is_finite() || hertz <= 0.0 || hertz > u32::MAX as f64 / 1_000.0 {
|
||||||
|
return Err(format!("mode {value:?} has an invalid refresh rate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((hertz * 1_000.0).round() as u32)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
Ok(OutputModeRequest {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_millihertz,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) const fn default_output_scale() -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct OutputProfile {
|
||||||
|
pub name: String,
|
||||||
|
pub outputs: Vec<OutputSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct OutputSettings {
|
||||||
|
pub auto_refresh: bool,
|
||||||
|
pub matcher: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub mode: Option<OutputModeRequest>,
|
||||||
|
pub scale: f64,
|
||||||
|
pub transform: OutputTransform,
|
||||||
|
pub position: Option<[i32; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct OutputModeRequest {
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub refresh_millihertz: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum OutputTransform {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
#[serde(rename = "rotate_90")]
|
||||||
|
Rotate90,
|
||||||
|
#[serde(rename = "rotate_180")]
|
||||||
|
Rotate180,
|
||||||
|
#[serde(rename = "rotate_270")]
|
||||||
|
Rotate270,
|
||||||
|
Flipped,
|
||||||
|
#[serde(rename = "flipped_90")]
|
||||||
|
Flipped90,
|
||||||
|
#[serde(rename = "flipped_180")]
|
||||||
|
Flipped180,
|
||||||
|
#[serde(rename = "flipped_270")]
|
||||||
|
Flipped270,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(super) struct OutputProfileConfig {
|
||||||
|
pub(super) name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) outputs: Vec<OutputConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(super) struct OutputConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) auto_refresh: bool,
|
||||||
|
#[serde(rename = "match")]
|
||||||
|
pub(super) matcher: String,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub(super) enabled: bool,
|
||||||
|
pub(super) mode: Option<String>,
|
||||||
|
#[serde(default = "default_output_scale")]
|
||||||
|
pub(super) scale: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(super) transform: OutputTransform,
|
||||||
|
pub(super) position: Option<[i32; 2]>,
|
||||||
|
}
|
||||||
@@ -235,7 +235,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||||
let mut backend = backend.try_borrow_mut().ok()?;
|
let mut backend = backend.try_borrow_mut().ok()?;
|
||||||
crate::winit::capture_window_frame(
|
crate::render::capture_window_frame(
|
||||||
backend.renderer(),
|
backend.renderer(),
|
||||||
&window,
|
&window,
|
||||||
geometry,
|
geometry,
|
||||||
|
|||||||
@@ -864,7 +864,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let result = if let Some(backend) = &self.nested_backend {
|
let result = if let Some(backend) = &self.nested_backend {
|
||||||
match backend.try_borrow_mut() {
|
match backend.try_borrow_mut() {
|
||||||
Ok(mut backend) => crate::winit::capture_window_buffer(backend.renderer(), &window, geometry, scale),
|
Ok(mut backend) => crate::render::capture_window_buffer(backend.renderer(), &window, geometry, scale),
|
||||||
Err(_) => Err("Window renderer is busy".into()),
|
Err(_) => Err("Window renderer is busy".into()),
|
||||||
}
|
}
|
||||||
} else if let Some(backend) = &mut self.direct_backend {
|
} else if let Some(backend) = &mut self.direct_backend {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ mod portal_shortcuts;
|
|||||||
mod presentation;
|
mod presentation;
|
||||||
mod private_client;
|
mod private_client;
|
||||||
mod reload;
|
mod reload;
|
||||||
|
mod render;
|
||||||
mod resize_transaction;
|
mod resize_transaction;
|
||||||
mod session_lock;
|
mod session_lock;
|
||||||
mod shell_control;
|
mod shell_control;
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) fn capture_resize_snapshot(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
window: &smithay::desktop::Window,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
remaining: usize,
|
||||||
|
) -> Result<Option<ResizeSnapshot>, GlesError> {
|
||||||
|
let Some(toplevel) = window.toplevel() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let size: Size<i32, Physical> = geometry.size.to_physical_precise_round(scale);
|
||||||
|
let bytes = (size.w.max(0) as usize)
|
||||||
|
.saturating_mul(size.h.max(0) as usize)
|
||||||
|
.saturating_mul(4);
|
||||||
|
if bytes == 0 || bytes > remaining {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
// Called before on_commit_buffer_handler: renderer surface state still
|
||||||
|
// owns the previous buffer. Copy only during an actual resize handoff.
|
||||||
|
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
toplevel.wl_surface(),
|
||||||
|
Point::<i32, Logical>::from((-geometry.loc.x, -geometry.loc.y)).to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
1.0,
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
);
|
||||||
|
if content.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, (size.w, size.h).into())?;
|
||||||
|
{
|
||||||
|
let mut target = renderer.bind(&mut texture)?;
|
||||||
|
let mut frame = renderer.render(&mut target, size, Transform::Normal)?;
|
||||||
|
let damage = Rectangle::from_size(size);
|
||||||
|
frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?;
|
||||||
|
draw_render_elements(&mut frame, scale, &content, &[damage])?;
|
||||||
|
let _ = frame.finish()?;
|
||||||
|
}
|
||||||
|
Ok(Some(ResizeSnapshot {
|
||||||
|
texture,
|
||||||
|
context: renderer.context_id().erased(),
|
||||||
|
id: Id::new(),
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
elapsed: Duration::ZERO,
|
||||||
|
last_tick: None,
|
||||||
|
scale,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capture_window_buffer(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
window: &smithay::desktop::Window,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||||
|
capture_window_frame(renderer, window, geometry, scale, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capture_window_frame(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
window: &smithay::desktop::Window,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
cursor: Option<(&Ferese, Rectangle<i32, Logical>)>,
|
||||||
|
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||||
|
let mut snapshot = capture_resize_snapshot(renderer, window, geometry, scale, 128 * 1024 * 1024)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or("Window has no capturable content")?;
|
||||||
|
let size = snapshot.texture.size();
|
||||||
|
if let Some((state, cursor_geometry)) = cursor {
|
||||||
|
let elements = cursor_elements(state, renderer, cursor_geometry, scale);
|
||||||
|
let mut target = renderer.bind(&mut snapshot.texture).map_err(|e| e.to_string())?;
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(&mut target, (size.w, size.h).into(), Transform::Normal)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
draw_render_elements(
|
||||||
|
&mut frame,
|
||||||
|
scale,
|
||||||
|
&elements,
|
||||||
|
&[Rectangle::from_size((size.w, size.h).into())],
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let _ = frame.finish().map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_texture(&snapshot.texture, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
if mapping.format() != Some(Fourcc::Abgr8888) {
|
||||||
|
return Err("Unsupported window readback format".into());
|
||||||
|
}
|
||||||
|
let bytes = (size.w as usize)
|
||||||
|
.checked_mul(size.h as usize)
|
||||||
|
.and_then(|pixels| pixels.checked_mul(4))
|
||||||
|
.ok_or("Window is too large")?;
|
||||||
|
let source = renderer.map_texture(&mapping).map_err(|error| error.to_string())?;
|
||||||
|
if source.len() < bytes {
|
||||||
|
return Err("Incomplete window readback".into());
|
||||||
|
}
|
||||||
|
let mut pixels = source[..bytes].to_vec();
|
||||||
|
for pixel in pixels.as_chunks_mut::<4>().0 {
|
||||||
|
pixel.swap(0, 2);
|
||||||
|
let alpha = pixel[3] as u16;
|
||||||
|
for channel in &mut pixel[..3] {
|
||||||
|
*channel = if alpha == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((*channel as u16 * 255 + alpha / 2) / alpha).min(255) as u8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(crate::handlers::screenshot::CaptureBuffer {
|
||||||
|
width: size.w,
|
||||||
|
height: size.h,
|
||||||
|
stride: size.w as usize * 4,
|
||||||
|
pixels,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<RoundedClipPrograms> {
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
if let Some(program) = state.rounded_clip_programs.get(&context) {
|
||||||
|
return Some(program.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let texture_uniforms = [
|
||||||
|
UniformName::new("clip_rect", UniformType::_4f),
|
||||||
|
UniformName::new("radius", UniformType::_1f),
|
||||||
|
];
|
||||||
|
let border_uniforms = [
|
||||||
|
UniformName::new("clip_rect", UniformType::_4f),
|
||||||
|
UniformName::new("radius", UniformType::_1f),
|
||||||
|
UniformName::new("border_width", UniformType::_1f),
|
||||||
|
UniformName::new("border_color", UniformType::_4f),
|
||||||
|
UniformName::new("border_color_to", UniformType::_4f),
|
||||||
|
UniformName::new("gradient_line", UniformType::_4f),
|
||||||
|
UniformName::new("focus_color", UniformType::_4f),
|
||||||
|
UniformName::new("focus_color_to", UniformType::_4f),
|
||||||
|
UniformName::new("focus_gradient_line", UniformType::_4f),
|
||||||
|
UniformName::new("focus_mix", UniformType::_1f),
|
||||||
|
];
|
||||||
|
let shadow_uniforms = [
|
||||||
|
UniformName::new("shadow_rect", UniformType::_4f),
|
||||||
|
UniformName::new("radius", UniformType::_1f),
|
||||||
|
UniformName::new("blur", UniformType::_1f),
|
||||||
|
UniformName::new("opacity", UniformType::_1f),
|
||||||
|
UniformName::new("shadow_color", UniformType::_4f),
|
||||||
|
];
|
||||||
|
let texture = renderer.compile_custom_texture_shader(ROUNDED_TEXTURE_SHADER, &texture_uniforms);
|
||||||
|
let border = renderer.compile_custom_pixel_shader(ROUNDED_BORDER_SHADER, &border_uniforms);
|
||||||
|
let shadow = renderer.compile_custom_pixel_shader(WINDOW_SHADOW_SHADER, &shadow_uniforms);
|
||||||
|
let compiled = texture.and_then(|texture| border.and_then(|border| shadow.map(|shadow| (texture, border, shadow))));
|
||||||
|
match compiled {
|
||||||
|
Ok((texture, border, shadow)) => {
|
||||||
|
let programs = RoundedClipPrograms {
|
||||||
|
texture,
|
||||||
|
border,
|
||||||
|
shadow,
|
||||||
|
};
|
||||||
|
state.rounded_clip_programs.insert(context, programs.clone());
|
||||||
|
Some(programs)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, "failed to compile rounded-window shader");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn window_border_element(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &GlesRenderer,
|
||||||
|
id: ferese_layout::WindowId,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
|
scale: f64,
|
||||||
|
requested_radius: f64,
|
||||||
|
requested_width: f64,
|
||||||
|
color: [f32; 4],
|
||||||
|
gradient: Option<crate::config::BorderGradient>,
|
||||||
|
focus_mix: f32,
|
||||||
|
opacity: f32,
|
||||||
|
output: &Output,
|
||||||
|
programs: &RoundedClipPrograms,
|
||||||
|
) -> Option<PhysicalShaderElement> {
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
let width = scaled_effect_value(requested_width, geometry, scale);
|
||||||
|
if width == 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (from, to, gradient_line) = match gradient {
|
||||||
|
Some(gradient) => (
|
||||||
|
gradient.from.0,
|
||||||
|
gradient.to.0,
|
||||||
|
border_gradient_line(physical, mode.size, output.current_transform().invert(), gradient.angle),
|
||||||
|
),
|
||||||
|
None => (color, color, [0.0; 4]),
|
||||||
|
};
|
||||||
|
let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient {
|
||||||
|
Some(g) => (
|
||||||
|
g.from.0,
|
||||||
|
g.to.0,
|
||||||
|
border_gradient_line(physical, mode.size, output.current_transform().invert(), g.angle),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
state.theme_settings.accent_color.0,
|
||||||
|
state.theme_settings.accent_color.0,
|
||||||
|
[0.0; 4],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let parameters = BorderParameters {
|
||||||
|
geometry,
|
||||||
|
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||||
|
radius: scaled_effect_value(requested_radius, geometry, scale),
|
||||||
|
width,
|
||||||
|
color: color_with_alpha(from, opacity),
|
||||||
|
color_to: color_with_alpha(to, opacity),
|
||||||
|
gradient_line,
|
||||||
|
focus_color: color_with_alpha(focus_from, opacity),
|
||||||
|
focus_color_to: color_with_alpha(focus_to, opacity),
|
||||||
|
focus_gradient_line,
|
||||||
|
focus_mix,
|
||||||
|
};
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let buffers = state.window_borders.entry(id).or_default();
|
||||||
|
if !buffers.contexts.contains_key(&context) {
|
||||||
|
let element = PixelShaderElement::new(
|
||||||
|
programs.border.clone(),
|
||||||
|
geometry,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
border_uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
);
|
||||||
|
buffers.contexts.insert(
|
||||||
|
context.clone(),
|
||||||
|
CachedBorder {
|
||||||
|
element,
|
||||||
|
parameters: parameters.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached = buffers.contexts.get_mut(&context)?;
|
||||||
|
if cached.parameters != parameters {
|
||||||
|
if cached.parameters.geometry != parameters.geometry {
|
||||||
|
cached.element.resize(parameters.geometry, None);
|
||||||
|
}
|
||||||
|
cached.element.update_uniforms(border_uniforms(¶meters));
|
||||||
|
cached.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: physical,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
|
||||||
|
vec![
|
||||||
|
Uniform::new("clip_rect", parameters.clip_rect).into_owned(),
|
||||||
|
Uniform::new("radius", parameters.radius).into_owned(),
|
||||||
|
Uniform::new("border_width", parameters.width).into_owned(),
|
||||||
|
Uniform::new("border_color", parameters.color).into_owned(),
|
||||||
|
Uniform::new("border_color_to", parameters.color_to).into_owned(),
|
||||||
|
Uniform::new("gradient_line", parameters.gradient_line).into_owned(),
|
||||||
|
Uniform::new("focus_color", parameters.focus_color).into_owned(),
|
||||||
|
Uniform::new("focus_color_to", parameters.focus_color_to).into_owned(),
|
||||||
|
Uniform::new("focus_gradient_line", parameters.focus_gradient_line).into_owned(),
|
||||||
|
Uniform::new("focus_mix", parameters.focus_mix).into_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn window_tint_element(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
id: ferese_layout::WindowId,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
|
scale: f64,
|
||||||
|
radius: f64,
|
||||||
|
color: [f32; 4],
|
||||||
|
resize_fill: bool,
|
||||||
|
output: &Output,
|
||||||
|
) -> Option<PhysicalShaderElement> {
|
||||||
|
if color[3] <= 0.0 {
|
||||||
|
if resize_fill {
|
||||||
|
state.window_resize_fills.remove(&id);
|
||||||
|
} else {
|
||||||
|
state.window_dims.remove(&id);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
let program = material_program(state, renderer)?;
|
||||||
|
let parameters = BorderParameters {
|
||||||
|
geometry,
|
||||||
|
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||||
|
radius: scaled_effect_value(radius, geometry, scale),
|
||||||
|
width: 0.0,
|
||||||
|
color,
|
||||||
|
color_to: color,
|
||||||
|
gradient_line: [0.0; 4],
|
||||||
|
focus_color: color,
|
||||||
|
focus_color_to: color,
|
||||||
|
focus_gradient_line: [0.0; 4],
|
||||||
|
focus_mix: 0.0,
|
||||||
|
};
|
||||||
|
let uniforms = |p: &BorderParameters| {
|
||||||
|
vec![
|
||||||
|
Uniform::new("visible_rect", p.clip_rect).into_owned(),
|
||||||
|
Uniform::new("material_radius", p.radius).into_owned(),
|
||||||
|
Uniform::new("tint", p.color).into_owned(),
|
||||||
|
Uniform::new("paint_mode", 0.0_f32).into_owned(),
|
||||||
|
Uniform::new("shadow_rect", p.clip_rect).into_owned(),
|
||||||
|
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let buffers = if resize_fill {
|
||||||
|
state.window_resize_fills.entry(id).or_default()
|
||||||
|
} else {
|
||||||
|
state.window_dims.entry(id).or_default()
|
||||||
|
};
|
||||||
|
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
||||||
|
element: PixelShaderElement::new(
|
||||||
|
program.0,
|
||||||
|
geometry,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
),
|
||||||
|
parameters: parameters.clone(),
|
||||||
|
});
|
||||||
|
if cached.parameters != parameters {
|
||||||
|
if cached.parameters.geometry != geometry {
|
||||||
|
cached.element.resize(geometry, None);
|
||||||
|
}
|
||||||
|
// Updating uniforms advances the element's commit so unchanged client
|
||||||
|
// buffers still repaint while focus dimming animates.
|
||||||
|
cached.element.update_uniforms(uniforms(¶meters));
|
||||||
|
cached.parameters = parameters;
|
||||||
|
}
|
||||||
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: physical,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn window_shadow_element(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &GlesRenderer,
|
||||||
|
id: ferese_layout::WindowId,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
|
scale: f64,
|
||||||
|
requested_radius: f64,
|
||||||
|
offset_y: f64,
|
||||||
|
blur: f64,
|
||||||
|
opacity: f64,
|
||||||
|
color: [f32; 4],
|
||||||
|
output: &Output,
|
||||||
|
programs: &RoundedClipPrograms,
|
||||||
|
) -> Option<PhysicalShaderElement> {
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
if opacity == 0.0 || color[3] == 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shadow_geometry = Rectangle::new(
|
||||||
|
(physical.loc.x, physical.loc.y + (offset_y * scale).round() as i32).into(),
|
||||||
|
physical.size,
|
||||||
|
);
|
||||||
|
let bounds = shadow_bounds(geometry, offset_y, blur);
|
||||||
|
let parameters = ShadowParameters {
|
||||||
|
blur: (blur * scale) as f32,
|
||||||
|
bounds,
|
||||||
|
shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, output.current_transform().invert()),
|
||||||
|
radius: scaled_effect_value(requested_radius, geometry, scale),
|
||||||
|
opacity: opacity as f32,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let buffers = state.window_shadows.entry(id).or_default();
|
||||||
|
if !buffers.contexts.contains_key(&context) {
|
||||||
|
let element = PixelShaderElement::new(
|
||||||
|
programs.shadow.clone(),
|
||||||
|
bounds,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
shadow_uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
);
|
||||||
|
buffers.contexts.insert(
|
||||||
|
context.clone(),
|
||||||
|
CachedShadow {
|
||||||
|
element,
|
||||||
|
parameters: parameters.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached = buffers.contexts.get_mut(&context)?;
|
||||||
|
if cached.parameters != parameters {
|
||||||
|
if cached.parameters.bounds != parameters.bounds {
|
||||||
|
cached.element.resize(parameters.bounds, None);
|
||||||
|
}
|
||||||
|
cached.element.update_uniforms(shadow_uniforms(¶meters));
|
||||||
|
cached.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extent = (blur * scale * 2.0).ceil() as i32;
|
||||||
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: Rectangle::new(
|
||||||
|
(shadow_geometry.loc.x - extent, shadow_geometry.loc.y - extent).into(),
|
||||||
|
(shadow_geometry.size.w + 2 * extent, shadow_geometry.size.h + 2 * extent).into(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
|
||||||
|
vec![
|
||||||
|
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(),
|
||||||
|
Uniform::new("radius", parameters.radius).into_owned(),
|
||||||
|
Uniform::new("blur", parameters.blur).into_owned(),
|
||||||
|
Uniform::new("opacity", parameters.opacity).into_owned(),
|
||||||
|
Uniform::new("shadow_color", parameters.color).into_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn shadow_bounds(geometry: Rectangle<i32, Logical>, offset_y: f64, blur: f64) -> Rectangle<i32, Logical> {
|
||||||
|
let extent = (blur * 2.0).ceil() as i32;
|
||||||
|
let offset_y = offset_y.round() as i32;
|
||||||
|
|
||||||
|
Rectangle::new(
|
||||||
|
(geometry.loc.x - extent, geometry.loc.y + offset_y - extent).into(),
|
||||||
|
(geometry.size.w + extent * 2, geometry.size.h + extent * 2).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn scaled_effect_value(requested: f64, geometry: Rectangle<i32, Logical>, scale: f64) -> f32 {
|
||||||
|
requested
|
||||||
|
.min(f64::from(geometry.size.w.min(geometry.size.h)) / 2.0)
|
||||||
|
.max(0.0) as f32
|
||||||
|
* scale as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn rounded_window_elements(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
window: &smithay::desktop::Window,
|
||||||
|
constrain: Rectangle<i32, Logical>,
|
||||||
|
physical_constrain: Rectangle<i32, Physical>,
|
||||||
|
scale: f64,
|
||||||
|
requested_radius: f64,
|
||||||
|
alpha: f32,
|
||||||
|
clip_changed: bool,
|
||||||
|
output: &Output,
|
||||||
|
program: GlesTexProgram,
|
||||||
|
behavior: ConstrainScaleBehavior,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let Some(toplevel) = window.toplevel() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(mode) = output.current_mode() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let geometry = window.geometry();
|
||||||
|
let reference = geometry.to_physical_precise_round(scale);
|
||||||
|
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
||||||
|
let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert());
|
||||||
|
let radius = scaled_effect_value(requested_radius, constrain, scale);
|
||||||
|
let surface = toplevel.wl_surface();
|
||||||
|
|
||||||
|
let mut content = PopupManager::popups_for_surface(surface)
|
||||||
|
.flat_map(|(popup, popup_offset)| {
|
||||||
|
let offset = (geometry.loc + popup_offset - popup.geometry().loc).to_physical_precise_round(scale);
|
||||||
|
|
||||||
|
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
popup.wl_surface(),
|
||||||
|
location + offset,
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(WindowContentRenderElement::from)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
content.extend(
|
||||||
|
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
surface,
|
||||||
|
location,
|
||||||
|
scale,
|
||||||
|
alpha,
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|inner| {
|
||||||
|
RoundedSurfaceRenderElement {
|
||||||
|
inner,
|
||||||
|
program: program.clone(),
|
||||||
|
clip_rect: clip,
|
||||||
|
radius,
|
||||||
|
clip_changed,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
constrain_render_elements(
|
||||||
|
content,
|
||||||
|
location,
|
||||||
|
physical_constrain,
|
||||||
|
reference,
|
||||||
|
behavior,
|
||||||
|
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
|
scale,
|
||||||
|
)
|
||||||
|
.map(Into::into)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn resize_content_behavior(intentional_scale: bool) -> ConstrainScaleBehavior {
|
||||||
|
if intentional_scale {
|
||||||
|
ConstrainScaleBehavior::Stretch
|
||||||
|
} else {
|
||||||
|
ConstrainScaleBehavior::CutOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||||
|
let scale = scale.clamp(0.0, 1.0);
|
||||||
|
let width = rect.width * scale;
|
||||||
|
let height = rect.height * scale;
|
||||||
|
|
||||||
|
ferese_layout::Rect::new(
|
||||||
|
rect.x + (rect.width - width) / 2.0,
|
||||||
|
rect.y + (rect.height - height) / 2.0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn color_with_alpha(mut color: [f32; 4], alpha: f32) -> [f32; 4] {
|
||||||
|
color[3] *= alpha;
|
||||||
|
color
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rounded_visual_rect(
|
||||||
|
rect: ferese_layout::Rect,
|
||||||
|
output_location: Point<i32, Logical>,
|
||||||
|
) -> Rectangle<i32, Logical> {
|
||||||
|
let left = (rect.x - f64::from(output_location.x)).round() as i32;
|
||||||
|
let top = (rect.y - f64::from(output_location.y)).round() as i32;
|
||||||
|
let right = (rect.x + rect.width - f64::from(output_location.x)).round() as i32;
|
||||||
|
let bottom = (rect.y + rect.height - f64::from(output_location.y)).round() as i32;
|
||||||
|
|
||||||
|
Rectangle::new(
|
||||||
|
(left, top).into(),
|
||||||
|
((right - left).max(1), (bottom - top).max(1)).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn framebuffer_clip_rect(
|
||||||
|
geometry: Rectangle<i32, Physical>,
|
||||||
|
output_size: smithay::utils::Size<i32, Physical>,
|
||||||
|
transform: Transform,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
// Match GlesFrame's projection: Smithay already accounts for GL's Y axis.
|
||||||
|
// An extra bottom-left conversion here mirrors shader clips independently
|
||||||
|
// of the surface geometry (notably with the nested Flipped180 output).
|
||||||
|
let element_size = transform.transform_size(output_size);
|
||||||
|
let transformed = transform.transform_rect_in(geometry, &element_size);
|
||||||
|
|
||||||
|
[
|
||||||
|
transformed.loc.x as f32,
|
||||||
|
transformed.loc.y as f32,
|
||||||
|
transformed.size.w as f32,
|
||||||
|
transformed.size.h as f32,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn border_gradient_line(
|
||||||
|
geometry: Rectangle<i32, Physical>,
|
||||||
|
output_size: Size<i32, Physical>,
|
||||||
|
transform: Transform,
|
||||||
|
angle: f64,
|
||||||
|
) -> [f32; 4] {
|
||||||
|
let angle = angle.to_radians();
|
||||||
|
let direction = (angle.cos(), angle.sin());
|
||||||
|
let width = f64::from(geometry.size.w);
|
||||||
|
let height = f64::from(geometry.size.h);
|
||||||
|
let extent = (width * direction.0.abs() + height * direction.1.abs()) * 0.5;
|
||||||
|
let center = geometry.loc.to_f64() + Point::from((width * 0.5, height * 0.5));
|
||||||
|
let offset: Point<f64, Physical> = (direction.0 * extent, direction.1 * extent).into();
|
||||||
|
// Apply the same output-to-framebuffer transform as the rounded clip.
|
||||||
|
// This keeps angles in window coordinates on rotated/flipped monitors.
|
||||||
|
let area = transform.transform_size(output_size).to_f64();
|
||||||
|
let start = transform.transform_point_in(center - offset, &area);
|
||||||
|
let end = transform.transform_point_in(center + offset, &area);
|
||||||
|
let delta = end - start;
|
||||||
|
let length_squared = (delta.x * delta.x + delta.y * delta.y).max(0.000001);
|
||||||
|
[
|
||||||
|
start.x as f32,
|
||||||
|
start.y as f32,
|
||||||
|
(delta.x / length_squared) as f32,
|
||||||
|
(delta.y / length_squared) as f32,
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn append_material_surface(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
content_origin: Point<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
output_crop: Rectangle<i32, Physical>,
|
||||||
|
elements: &mut Vec<AnimatedWindowRenderElement>,
|
||||||
|
) {
|
||||||
|
let radius =
|
||||||
|
if crate::effects::surface_role(surface).is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel) {
|
||||||
|
state.theme_settings.panel_radius
|
||||||
|
} else {
|
||||||
|
state.theme_settings.material_radius
|
||||||
|
};
|
||||||
|
let regions = crate::effects::surface_regions(surface);
|
||||||
|
let targets: Vec<_> = match ®ions {
|
||||||
|
None => vec![(geometry, radius as f32)],
|
||||||
|
Some(regions) => regions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| {
|
||||||
|
Rectangle::new(
|
||||||
|
(content_origin.x + r[0], content_origin.y + r[1]).into(),
|
||||||
|
(r[2], r[3]).into(),
|
||||||
|
)
|
||||||
|
.intersection(geometry)
|
||||||
|
.map(|rect| (rect, r[4] as f32))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let materials: Vec<_> = targets
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, (rect, radius))| {
|
||||||
|
material_element(state, renderer, output, surface, *rect, *radius, index, geometry, 1.0)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Some(buffers) = state.material_buffers.get_mut(surface) {
|
||||||
|
buffers.contexts.retain(|(_, index), _| *index < targets.len());
|
||||||
|
}
|
||||||
|
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
||||||
|
let clip = material.as_ref().and_then(|_| {
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
let program = rounded_clip_program(state, renderer)?;
|
||||||
|
Some((
|
||||||
|
program.texture,
|
||||||
|
framebuffer_clip_rect(
|
||||||
|
geometry.to_physical_precise_round(scale),
|
||||||
|
mode.size,
|
||||||
|
output.current_transform().invert(),
|
||||||
|
),
|
||||||
|
scaled_effect_value(radius, geometry, scale),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
// Smithay element lists are front to back: content, material, shadow.
|
||||||
|
let content = render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
surface,
|
||||||
|
content_origin.to_physical_precise_round(scale),
|
||||||
|
scale,
|
||||||
|
crate::effects::surface_opacity(surface),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
);
|
||||||
|
elements.extend(content.into_iter().filter_map(|element| {
|
||||||
|
let origin = Point::<i32, Physical>::default();
|
||||||
|
let element = if let Some((program, clip_rect, radius)) = &clip {
|
||||||
|
WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement {
|
||||||
|
inner: element,
|
||||||
|
program: program.clone(),
|
||||||
|
clip_rect: *clip_rect,
|
||||||
|
radius: *radius,
|
||||||
|
clip_changed: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
WindowContentRenderElement::Popup(element)
|
||||||
|
};
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||||
|
}));
|
||||||
|
for (background, shadow) in materials {
|
||||||
|
elements.push(background);
|
||||||
|
elements.push(shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn material_element(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
radius: f32,
|
||||||
|
index: usize,
|
||||||
|
capture_geometry: Rectangle<i32, Logical>,
|
||||||
|
alpha: f32,
|
||||||
|
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
||||||
|
let (role, generation) = crate::effects::surface_role(surface)?;
|
||||||
|
let material = crate::effects::resolve_material(
|
||||||
|
role,
|
||||||
|
state.theme_settings.material_style,
|
||||||
|
state.theme_settings.shell_opacity as f32,
|
||||||
|
);
|
||||||
|
let presentation_alpha = crate::effects::surface_opacity(surface) * alpha;
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
let transform = output.current_transform().invert();
|
||||||
|
let background = if role == crate::effects::SemanticRole::Panel {
|
||||||
|
state.theme_settings.bar_background_color
|
||||||
|
} else {
|
||||||
|
state.theme_settings.surface_base_color
|
||||||
|
};
|
||||||
|
let [red, green, blue, _] = background.0;
|
||||||
|
let radius = radius.min(geometry.size.w.min(geometry.size.h) as f32 * 0.5);
|
||||||
|
let [offset_y, shadow_blur, shadow_opacity] = material.shadow;
|
||||||
|
let edge_bar = role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0;
|
||||||
|
let shadow_opacity = if edge_bar {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
shadow_opacity * (state.theme_settings.shadow_opacity / 0.2) * f64::from(presentation_alpha)
|
||||||
|
};
|
||||||
|
let shadow_geometry = Rectangle::new(
|
||||||
|
(geometry.loc.x, geometry.loc.y + offset_y.round() as i32).into(),
|
||||||
|
geometry.size,
|
||||||
|
);
|
||||||
|
let background_opacity = material.opacity;
|
||||||
|
let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur);
|
||||||
|
let output_size = output
|
||||||
|
.current_transform()
|
||||||
|
.transform_size(mode.size)
|
||||||
|
.to_f64()
|
||||||
|
.to_logical(scale)
|
||||||
|
.to_i32_ceil();
|
||||||
|
let sample_geometry = expanded_blur_region(capture_geometry, blur.ceil() as i32, output_size);
|
||||||
|
let sample_physical = sample_geometry.to_physical_precise_round(scale);
|
||||||
|
let mut parameters = MaterialParameters {
|
||||||
|
presentation_alpha,
|
||||||
|
background_opacity,
|
||||||
|
blur: (blur * scale) as f32,
|
||||||
|
scene_generation: if blur > 0.0 { state.backdrop_generation } else { 0 },
|
||||||
|
sample_geometry,
|
||||||
|
sample_physical,
|
||||||
|
sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform),
|
||||||
|
radius: radius * scale as f32,
|
||||||
|
shadow_rect: framebuffer_clip_rect(shadow_geometry.to_physical_precise_round(scale), mode.size, transform),
|
||||||
|
shadow_values: [(shadow_blur * scale) as f32, shadow_opacity as f32],
|
||||||
|
shadow_bounds: shadow_bounds(geometry, offset_y, shadow_blur),
|
||||||
|
geometry,
|
||||||
|
tint: [red, green, blue, material.opacity * presentation_alpha],
|
||||||
|
generation,
|
||||||
|
opaque: material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0,
|
||||||
|
visible_framebuffer: framebuffer_clip_rect(geometry.to_physical_precise_round(scale), mode.size, transform),
|
||||||
|
};
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let program = material_program(state, renderer)?;
|
||||||
|
let blur_program = (blur > 0.0).then(|| blur_program(state, renderer)).flatten();
|
||||||
|
let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
||||||
|
let capture_size = Size::from((capture_rect[2], capture_rect[3]));
|
||||||
|
let buffers = state.material_buffers.entry(surface.clone()).or_default();
|
||||||
|
let capture = if let Some(blur_program) = blur_program {
|
||||||
|
if buffers
|
||||||
|
.captures
|
||||||
|
.get(&context)
|
||||||
|
.is_none_or(|c| c.geometry != sample_geometry || c.texture.size() != capture_size)
|
||||||
|
{
|
||||||
|
let texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, capture_size)
|
||||||
|
.map_err(|error| {
|
||||||
|
tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed");
|
||||||
|
error
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
if let Some(texture) = texture {
|
||||||
|
buffers.captures.insert(
|
||||||
|
context.clone(),
|
||||||
|
BlurCapture {
|
||||||
|
texture,
|
||||||
|
dirty: Arc::new(AtomicBool::new(true)),
|
||||||
|
geometry: sample_geometry,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buffers.captures.remove(&context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffers
|
||||||
|
.captures
|
||||||
|
.get(&context)
|
||||||
|
.cloned()
|
||||||
|
.map(|capture| (capture, blur_program))
|
||||||
|
} else {
|
||||||
|
buffers.captures.remove(&context);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if capture.is_some() {
|
||||||
|
parameters.tint[3] = state.theme_settings.material_tint_strength as f32;
|
||||||
|
}
|
||||||
|
let make_element = || {
|
||||||
|
if let Some((capture, program)) = &capture {
|
||||||
|
MaterialElement::Blur(BlurRenderElement::new(
|
||||||
|
capture.texture.clone(),
|
||||||
|
program.0.clone(),
|
||||||
|
¶meters,
|
||||||
|
capture.dirty.clone(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
MaterialElement::Fill(PixelShaderElement::new(
|
||||||
|
program.0.clone(),
|
||||||
|
geometry,
|
||||||
|
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||||
|
1.0,
|
||||||
|
material_uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cached = buffers
|
||||||
|
.contexts
|
||||||
|
.entry((context, index))
|
||||||
|
.or_insert_with(|| CachedMaterial {
|
||||||
|
element: make_element(),
|
||||||
|
parameters: parameters.clone(),
|
||||||
|
shadow: PixelShaderElement::new(
|
||||||
|
program.0.clone(),
|
||||||
|
parameters.shadow_bounds,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
decoration_uniforms(¶meters, 2.0),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
let replace = match &cached.element {
|
||||||
|
MaterialElement::Fill(_) => capture.is_some(),
|
||||||
|
MaterialElement::Blur(element) => capture
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|(capture, _)| capture.texture.tex_id() != element.texture.tex_id()),
|
||||||
|
};
|
||||||
|
if replace {
|
||||||
|
cached.element = make_element();
|
||||||
|
}
|
||||||
|
if cached.parameters != parameters {
|
||||||
|
cached.shadow.resize(parameters.shadow_bounds, None);
|
||||||
|
cached.shadow.update_uniforms(decoration_uniforms(¶meters, 2.0));
|
||||||
|
match &mut cached.element {
|
||||||
|
MaterialElement::Fill(element) => {
|
||||||
|
element.resize(
|
||||||
|
geometry,
|
||||||
|
parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]),
|
||||||
|
);
|
||||||
|
element.update_uniforms(material_uniforms(¶meters));
|
||||||
|
}
|
||||||
|
MaterialElement::Blur(element) => element.update(¶meters),
|
||||||
|
}
|
||||||
|
cached.parameters = parameters;
|
||||||
|
}
|
||||||
|
let background = match &cached.element {
|
||||||
|
MaterialElement::Fill(element) => element.clone().into(),
|
||||||
|
MaterialElement::Blur(element) => element.clone().into(),
|
||||||
|
};
|
||||||
|
Some((background, cached.shadow.clone().into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity: f32, radius: f64) -> f64 {
|
||||||
|
if style == crate::config::MaterialStyle::Translucent && opacity > 0.0 {
|
||||||
|
radius
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn blur_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<BlurProgram> {
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
if let Some(program) = state.blur_programs.get(&context) {
|
||||||
|
return Some(program.clone());
|
||||||
|
}
|
||||||
|
match renderer.compile_custom_texture_shader(BLUR_SHADER, &blur_uniform_names()) {
|
||||||
|
Ok(program) => {
|
||||||
|
let program = BlurProgram(program);
|
||||||
|
state.blur_programs.insert(context, program.clone());
|
||||||
|
Some(program)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(%error,"backdrop blur unavailable; using plain translucent fill");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn blur_uniform_names() -> [UniformName<'static>; 8] {
|
||||||
|
[
|
||||||
|
UniformName::new("visible_rect", UniformType::_4f),
|
||||||
|
UniformName::new("material_radius", UniformType::_1f),
|
||||||
|
UniformName::new("texture_size", UniformType::_2f),
|
||||||
|
UniformName::new("capture_origin", UniformType::_2f),
|
||||||
|
UniformName::new("blur_radius", UniformType::_1f),
|
||||||
|
UniformName::new("presentation_alpha", UniformType::_1f),
|
||||||
|
UniformName::new("background_opacity", UniformType::_1f),
|
||||||
|
UniformName::new("tint", UniformType::_4f),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec<Uniform<'static>> {
|
||||||
|
vec![
|
||||||
|
Uniform::new("visible_rect", p.visible_framebuffer).into_owned(),
|
||||||
|
Uniform::new("material_radius", p.radius).into_owned(),
|
||||||
|
Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]).into_owned(),
|
||||||
|
Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]).into_owned(),
|
||||||
|
Uniform::new("blur_radius", p.blur).into_owned(),
|
||||||
|
Uniform::new("presentation_alpha", p.presentation_alpha).into_owned(),
|
||||||
|
Uniform::new("background_opacity", p.background_opacity).into_owned(),
|
||||||
|
Uniform::new("tint", p.tint).into_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn framebuffer_capture_rect(rect: [f32; 4]) -> [i32; 4] {
|
||||||
|
rect.map(|v| v.round() as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn expanded_blur_region(
|
||||||
|
visible: Rectangle<i32, Logical>,
|
||||||
|
radius: i32,
|
||||||
|
output_size: Size<i32, Logical>,
|
||||||
|
) -> Rectangle<i32, Logical> {
|
||||||
|
let left = (visible.loc.x - radius).max(0);
|
||||||
|
let top = (visible.loc.y - radius).max(0);
|
||||||
|
let right = (visible.loc.x + visible.size.w + radius).min(output_size.w);
|
||||||
|
let bottom = (visible.loc.y + visible.size.h + radius).min(output_size.h);
|
||||||
|
Rectangle::new(
|
||||||
|
(left, top).into(),
|
||||||
|
((right - left).max(1), (bottom - top).max(1)).into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<MaterialProgram> {
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
if let Some(program) = state.material_programs.get(&context) {
|
||||||
|
return Some(program.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let uniforms = [
|
||||||
|
UniformName::new("visible_rect", UniformType::_4f),
|
||||||
|
UniformName::new("material_radius", UniformType::_1f),
|
||||||
|
UniformName::new("tint", UniformType::_4f),
|
||||||
|
UniformName::new("paint_mode", UniformType::_1f),
|
||||||
|
UniformName::new("shadow_rect", UniformType::_4f),
|
||||||
|
UniformName::new("shadow_values", UniformType::_2f),
|
||||||
|
];
|
||||||
|
match renderer.compile_custom_pixel_shader(MATERIAL_SHADER, &uniforms) {
|
||||||
|
Ok(program) => {
|
||||||
|
let program = MaterialProgram(program);
|
||||||
|
state.material_programs.insert(context, program.clone());
|
||||||
|
Some(program)
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, "failed to compile semantic material shader");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn material_uniforms(parameters: &MaterialParameters) -> Vec<Uniform<'static>> {
|
||||||
|
decoration_uniforms(parameters, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn decoration_uniforms(parameters: &MaterialParameters, paint_mode: f32) -> Vec<Uniform<'static>> {
|
||||||
|
vec![
|
||||||
|
Uniform::new("paint_mode", paint_mode).into_owned(),
|
||||||
|
Uniform::new("shadow_rect", parameters.shadow_rect).into_owned(),
|
||||||
|
Uniform::new("shadow_values", parameters.shadow_values).into_owned(),
|
||||||
|
Uniform::new("visible_rect", parameters.visible_framebuffer).into_owned(),
|
||||||
|
Uniform::new("material_radius", parameters.radius).into_owned(),
|
||||||
|
Uniform::new("tint", parameters.tint).into_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn blur_damage(
|
||||||
|
size: Size<i32, Physical>,
|
||||||
|
current: CommitCounter,
|
||||||
|
previous: Option<CommitCounter>,
|
||||||
|
) -> DamageSet<i32, Physical> {
|
||||||
|
if previous == Some(current) {
|
||||||
|
DamageSet::default()
|
||||||
|
} else {
|
||||||
|
// A new scene needs its entire sampling halo repainted before capture.
|
||||||
|
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,890 @@
|
|||||||
|
mod capture;
|
||||||
|
mod decorations;
|
||||||
|
mod materials;
|
||||||
|
mod overview;
|
||||||
|
mod scene;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture_window_frame};
|
||||||
|
use decorations::*;
|
||||||
|
use materials::*;
|
||||||
|
use overview::*;
|
||||||
|
use scene::cursor_elements;
|
||||||
|
pub(crate) use scene::{
|
||||||
|
animated_window_elements, cursorless_window_elements, frame_effect_metrics, layer_surfaces, output_elements,
|
||||||
|
};
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||||
|
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||||
|
use smithay::backend::renderer::element::surface::{
|
||||||
|
WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::utils::{
|
||||||
|
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||||
|
constrain_render_elements,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::element::{
|
||||||
|
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||||
|
use smithay::backend::renderer::gles::{
|
||||||
|
GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName,
|
||||||
|
UniformType,
|
||||||
|
};
|
||||||
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions, draw_render_elements};
|
||||||
|
use smithay::backend::renderer::{Bind, Color32F, ErasedContextId, ExportMem, Frame, Offscreen, Renderer, Texture};
|
||||||
|
use smithay::desktop::space::{ConstrainBehavior, ConstrainReference, constrain_space_element};
|
||||||
|
use smithay::desktop::{LayerSurface, PopupManager, layer_map_for_output};
|
||||||
|
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData};
|
||||||
|
use smithay::output::Output;
|
||||||
|
use smithay::reexports::wayland_server::Resource;
|
||||||
|
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform};
|
||||||
|
use smithay::wayland::compositor::with_states;
|
||||||
|
use smithay::wayland::shell::wlr_layer::Layer;
|
||||||
|
|
||||||
|
use crate::Ferese;
|
||||||
|
use crate::metrics::FrameEffectMetrics;
|
||||||
|
use crate::presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect};
|
||||||
|
|
||||||
|
type SurfaceRenderElement =
|
||||||
|
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
||||||
|
|
||||||
|
type WindowRenderElement = CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowContentRenderElement>>>;
|
||||||
|
|
||||||
|
type MemoryRenderElement =
|
||||||
|
CropRenderElement<RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>>;
|
||||||
|
|
||||||
|
render_elements! {
|
||||||
|
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
|
||||||
|
Window=WindowRenderElement,
|
||||||
|
Surface=SurfaceRenderElement,
|
||||||
|
Memory=MemoryRenderElement,
|
||||||
|
Solid=SolidColorRenderElement,
|
||||||
|
LockSurface=WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
|
Border=PixelShaderElement,
|
||||||
|
Blur=BlurRenderElement,
|
||||||
|
Effect=PhysicalShaderElement,
|
||||||
|
Native=NativeTextureElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct ResizeSnapshot {
|
||||||
|
pub texture: GlesTexture,
|
||||||
|
pub context: ErasedContextId,
|
||||||
|
pub id: Id,
|
||||||
|
pub commit: CommitCounter,
|
||||||
|
pub elapsed: Duration,
|
||||||
|
pub last_tick: Option<Duration>,
|
||||||
|
pub scale: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResizeSnapshot {
|
||||||
|
pub(crate) fn bytes(&self) -> usize {
|
||||||
|
self.texture.size().w as usize * self.texture.size().h as usize * 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_elements! {
|
||||||
|
WindowContentRenderElement<=GlesRenderer>;
|
||||||
|
Rounded=RoundedSurfaceRenderElement,
|
||||||
|
Popup=WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag");
|
||||||
|
|
||||||
|
const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag");
|
||||||
|
|
||||||
|
const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag");
|
||||||
|
|
||||||
|
// The material program paints either the fallback tint, the edges above client
|
||||||
|
// content, or the shadow outside the surface. All coordinates are physical px.
|
||||||
|
const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag");
|
||||||
|
|
||||||
|
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct BlurProgram(GlesTexProgram);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct BlurCapture {
|
||||||
|
texture: GlesTexture,
|
||||||
|
dirty: Arc<AtomicBool>,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct BlurRenderElement {
|
||||||
|
capture_dirty: Arc<AtomicBool>,
|
||||||
|
texture: GlesTexture,
|
||||||
|
program: GlesTexProgram,
|
||||||
|
id: Id,
|
||||||
|
commit: CommitCounter,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
capture_rect: [i32; 4],
|
||||||
|
uniforms: Vec<Uniform<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlurRenderElement {
|
||||||
|
fn new(
|
||||||
|
texture: GlesTexture,
|
||||||
|
program: GlesTexProgram,
|
||||||
|
parameters: &MaterialParameters,
|
||||||
|
capture_dirty: Arc<AtomicBool>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
capture_dirty,
|
||||||
|
texture,
|
||||||
|
program,
|
||||||
|
id: Id::new(),
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
geometry: parameters.sample_geometry,
|
||||||
|
capture_rect: framebuffer_capture_rect(parameters.sample_framebuffer),
|
||||||
|
uniforms: blur_uniforms(parameters),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, parameters: &MaterialParameters) {
|
||||||
|
self.capture_dirty.store(true, Ordering::Relaxed);
|
||||||
|
self.geometry = parameters.sample_geometry;
|
||||||
|
self.capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
||||||
|
self.uniforms = blur_uniforms(parameters);
|
||||||
|
self.commit.increment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for BlurRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.commit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
Rectangle::from_size(self.texture.size().to_f64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: RenderScale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.geometry.to_physical_precise_round(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||||
|
blur_damage(self.geometry(scale).size, self.commit, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, _scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||||
|
// Backdrop dependencies must remain visible to damage tracking even
|
||||||
|
// though the shader produces opaque pixels inside the visible region.
|
||||||
|
OpaqueRegions::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> RenderElementKind {
|
||||||
|
RenderElementKind::Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for BlurRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
opaque_regions: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
let texture = self.texture.tex_id();
|
||||||
|
let [x, y, width, height] = self.capture_rect;
|
||||||
|
// Buffer-age restoration and unrelated partial damage must reuse the
|
||||||
|
// clean captured scene. Recapturing a partly repainted framebuffer
|
||||||
|
// would feed previous blurred pixels back into the blur.
|
||||||
|
if self.capture_dirty.load(Ordering::Relaxed) {
|
||||||
|
let error = frame.with_context(|gl| unsafe {
|
||||||
|
gl.GetError();
|
||||||
|
let mut framebuffer = 0;
|
||||||
|
let mut previous_read_buffer = 0;
|
||||||
|
gl.GetIntegerv(
|
||||||
|
smithay::backend::renderer::gles::ffi::READ_FRAMEBUFFER_BINDING,
|
||||||
|
&mut framebuffer,
|
||||||
|
);
|
||||||
|
gl.GetIntegerv(
|
||||||
|
smithay::backend::renderer::gles::ffi::READ_BUFFER,
|
||||||
|
&mut previous_read_buffer,
|
||||||
|
);
|
||||||
|
gl.ReadBuffer(if framebuffer == 0 {
|
||||||
|
smithay::backend::renderer::gles::ffi::BACK
|
||||||
|
} else {
|
||||||
|
smithay::backend::renderer::gles::ffi::COLOR_ATTACHMENT0
|
||||||
|
});
|
||||||
|
gl.BindTexture(smithay::backend::renderer::gles::ffi::TEXTURE_2D, texture);
|
||||||
|
gl.TexParameteri(
|
||||||
|
smithay::backend::renderer::gles::ffi::TEXTURE_2D,
|
||||||
|
smithay::backend::renderer::gles::ffi::TEXTURE_WRAP_S,
|
||||||
|
smithay::backend::renderer::gles::ffi::CLAMP_TO_EDGE as i32,
|
||||||
|
);
|
||||||
|
gl.TexParameteri(
|
||||||
|
smithay::backend::renderer::gles::ffi::TEXTURE_2D,
|
||||||
|
smithay::backend::renderer::gles::ffi::TEXTURE_WRAP_T,
|
||||||
|
smithay::backend::renderer::gles::ffi::CLAMP_TO_EDGE as i32,
|
||||||
|
);
|
||||||
|
gl.CopyTexSubImage2D(
|
||||||
|
smithay::backend::renderer::gles::ffi::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
gl.BindTexture(smithay::backend::renderer::gles::ffi::TEXTURE_2D, 0);
|
||||||
|
let error = gl.GetError();
|
||||||
|
gl.ReadBuffer(previous_read_buffer as u32);
|
||||||
|
error
|
||||||
|
})?;
|
||||||
|
if error != smithay::backend::renderer::gles::ffi::NO_ERROR {
|
||||||
|
tracing::warn!(error, "backdrop framebuffer capture failed");
|
||||||
|
}
|
||||||
|
self.capture_dirty.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.render_texture_from_to(
|
||||||
|
&self.texture,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
damage,
|
||||||
|
opaque_regions,
|
||||||
|
Transform::Normal,
|
||||||
|
1.0,
|
||||||
|
Some(&self.program),
|
||||||
|
&self.uniforms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct RoundedClipPrograms {
|
||||||
|
texture: GlesTexProgram,
|
||||||
|
border: GlesPixelProgram,
|
||||||
|
shadow: GlesPixelProgram,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub(crate) enum OverviewChromePart {
|
||||||
|
Card,
|
||||||
|
Outline,
|
||||||
|
Strip,
|
||||||
|
Caption,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct OverviewScrim {
|
||||||
|
chrome: HashMap<(u64, OverviewChromePart), HashMap<ErasedContextId, CachedBorder>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct MaterialProgram(GlesPixelProgram);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct MaterialParameters {
|
||||||
|
presentation_alpha: f32,
|
||||||
|
background_opacity: f32,
|
||||||
|
blur: f32,
|
||||||
|
scene_generation: u64,
|
||||||
|
sample_geometry: Rectangle<i32, Logical>,
|
||||||
|
sample_physical: Rectangle<i32, Physical>,
|
||||||
|
sample_framebuffer: [f32; 4],
|
||||||
|
radius: f32,
|
||||||
|
shadow_rect: [f32; 4],
|
||||||
|
shadow_values: [f32; 2],
|
||||||
|
shadow_bounds: Rectangle<i32, Logical>,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
tint: [f32; 4],
|
||||||
|
generation: u64,
|
||||||
|
opaque: bool,
|
||||||
|
visible_framebuffer: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum MaterialElement {
|
||||||
|
Fill(PixelShaderElement),
|
||||||
|
Blur(BlurRenderElement),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CachedMaterial {
|
||||||
|
element: MaterialElement,
|
||||||
|
parameters: MaterialParameters,
|
||||||
|
shadow: PixelShaderElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct MaterialBuffers {
|
||||||
|
contexts: HashMap<(ErasedContextId, usize), CachedMaterial>,
|
||||||
|
captures: HashMap<ErasedContextId, BlurCapture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct BorderParameters {
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
clip_rect: [f32; 4],
|
||||||
|
radius: f32,
|
||||||
|
width: f32,
|
||||||
|
color: [f32; 4],
|
||||||
|
color_to: [f32; 4],
|
||||||
|
gradient_line: [f32; 4],
|
||||||
|
focus_color: [f32; 4],
|
||||||
|
focus_color_to: [f32; 4],
|
||||||
|
focus_gradient_line: [f32; 4],
|
||||||
|
focus_mix: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CachedBorder {
|
||||||
|
element: PixelShaderElement,
|
||||||
|
parameters: BorderParameters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct WindowBorderBuffers {
|
||||||
|
contexts: HashMap<ErasedContextId, CachedBorder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct ShadowParameters {
|
||||||
|
blur: f32,
|
||||||
|
bounds: Rectangle<i32, Logical>,
|
||||||
|
shadow_rect: [f32; 4],
|
||||||
|
radius: f32,
|
||||||
|
opacity: f32,
|
||||||
|
color: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CachedShadow {
|
||||||
|
element: PixelShaderElement,
|
||||||
|
parameters: ShadowParameters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct WindowShadowBuffers {
|
||||||
|
contexts: HashMap<ErasedContextId, CachedShadow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RoundedSurfaceRenderElement {
|
||||||
|
inner: WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
|
program: GlesTexProgram,
|
||||||
|
clip_rect: [f32; 4],
|
||||||
|
radius: f32,
|
||||||
|
clip_changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for RoundedSurfaceRenderElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
self.inner.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.inner.current_commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: RenderScale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.inner.geometry(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self) -> Transform {
|
||||||
|
self.inner.transform()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
self.inner.src()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(&self, scale: RenderScale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||||
|
if self.clip_changed {
|
||||||
|
return DamageSet::from_slice(&[Rectangle::from_size(self.inner.geometry(scale).size)]);
|
||||||
|
}
|
||||||
|
self.inner.damage_since(scale, commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||||
|
if self.radius == 0.0 {
|
||||||
|
self.inner.opaque_regions(scale)
|
||||||
|
} else {
|
||||||
|
OpaqueRegions::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.inner.alpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> RenderElementKind {
|
||||||
|
self.inner.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderElement<GlesRenderer> for RoundedSurfaceRenderElement {
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
frame: &mut GlesFrame<'_, '_>,
|
||||||
|
src: Rectangle<f64, Buffer>,
|
||||||
|
dst: Rectangle<i32, Physical>,
|
||||||
|
damage: &[Rectangle<i32, Physical>],
|
||||||
|
opaque_regions: &[Rectangle<i32, Physical>],
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
match self.inner.texture() {
|
||||||
|
WaylandSurfaceTexture::Texture(texture) => frame.render_texture_from_to(
|
||||||
|
texture,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
damage,
|
||||||
|
opaque_regions,
|
||||||
|
self.transform(),
|
||||||
|
self.alpha(),
|
||||||
|
Some(&self.program),
|
||||||
|
&[
|
||||||
|
Uniform::new("clip_rect", self.clip_rect),
|
||||||
|
Uniform::new("radius", self.radius),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
WaylandSurfaceTexture::SolidColor(_) => self.inner.draw(frame, src, dst, damage, opaque_regions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||||
|
self.inner.underlying_storage(renderer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn redraw_output<R, E>(
|
||||||
|
renderer: &mut R,
|
||||||
|
framebuffer: &mut R::Framebuffer<'_>,
|
||||||
|
output: &Output,
|
||||||
|
elements: &[E],
|
||||||
|
) -> Result<(), R::Error>
|
||||||
|
where
|
||||||
|
R: Renderer,
|
||||||
|
R::TextureId: 'static,
|
||||||
|
E: RenderElement<R>,
|
||||||
|
{
|
||||||
|
let mode = output.current_mode().expect("output has a mode");
|
||||||
|
let transform = output.current_transform().invert();
|
||||||
|
let damage = Rectangle::from_size(transform.transform_size(mode.size));
|
||||||
|
let mut frame = renderer.render(framebuffer, mode.size, transform)?;
|
||||||
|
|
||||||
|
frame.clear(Color32F::new(0.035, 0.04, 0.055, 1.0), &[damage])?;
|
||||||
|
draw_render_elements(
|
||||||
|
&mut frame,
|
||||||
|
output.current_scale().fractional_scale(),
|
||||||
|
elements,
|
||||||
|
&[damage],
|
||||||
|
)?;
|
||||||
|
let _ = frame.finish()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||||
|
use smithay::backend::renderer::element::{Element, Id};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior, rounded_visual_rect,
|
||||||
|
scaled_visual_rect, shadow_bounds,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn translucent_blur_does_not_leak_the_sharp_backdrop() {
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform};
|
||||||
|
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer};
|
||||||
|
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let context = EGLContext::new(&display).unwrap();
|
||||||
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
|
let program = renderer
|
||||||
|
.compile_custom_texture_shader(super::BLUR_SHADER, &super::blur_uniform_names())
|
||||||
|
.unwrap();
|
||||||
|
let size = (32, 32).into();
|
||||||
|
let mut pixels = vec![128u8; 32 * 32 * 4];
|
||||||
|
for pixel in pixels.chunks_exact_mut(4) {
|
||||||
|
pixel[3] = 255;
|
||||||
|
}
|
||||||
|
let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap();
|
||||||
|
let mut target_texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||||
|
let damage = Rectangle::<i32, Physical>::from_size((32, 32).into());
|
||||||
|
|
||||||
|
for (opacity, strength, tint) in [
|
||||||
|
(0.2f32, 0.5f32, 0.1f32),
|
||||||
|
(0.71, 0.5, 0.1),
|
||||||
|
(1.0, 0.5, 0.1),
|
||||||
|
(0.94, 1.0, 245.0 / 255.0),
|
||||||
|
] {
|
||||||
|
let uniforms = vec![
|
||||||
|
Uniform::new("visible_rect", [0.0f32, 0.0, 32.0, 32.0]),
|
||||||
|
Uniform::new("material_radius", 0.0f32),
|
||||||
|
Uniform::new("texture_size", [32.0f32, 32.0]),
|
||||||
|
Uniform::new("capture_origin", [0.0f32, 0.0]),
|
||||||
|
Uniform::new("blur_radius", 12.0f32),
|
||||||
|
Uniform::new("presentation_alpha", 1.0f32),
|
||||||
|
Uniform::new("background_opacity", opacity),
|
||||||
|
Uniform::new("tint", [tint, tint, tint, strength]),
|
||||||
|
];
|
||||||
|
let mut target = renderer.bind(&mut target_texture).unwrap();
|
||||||
|
{
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(&mut target, (32, 32).into(), Transform::Normal)
|
||||||
|
.unwrap();
|
||||||
|
frame.clear(Color32F::new(0.0, 0.0, 0.0, 1.0), &[damage]).unwrap();
|
||||||
|
frame
|
||||||
|
.draw_solid(
|
||||||
|
Rectangle::new((0, 0).into(), (16, 32).into()),
|
||||||
|
&[damage],
|
||||||
|
Color32F::new(1.0, 1.0, 1.0, 1.0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
frame
|
||||||
|
.render_texture_from_to(
|
||||||
|
&blurred,
|
||||||
|
Rectangle::from_size(size.to_f64()),
|
||||||
|
damage,
|
||||||
|
&[damage],
|
||||||
|
&[],
|
||||||
|
Transform::Normal,
|
||||||
|
1.0,
|
||||||
|
Some(&program),
|
||||||
|
&uniforms,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let _ = frame.finish().unwrap();
|
||||||
|
}
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
let rendered = renderer.map_texture(&mapping).unwrap();
|
||||||
|
let left = rendered[(16 * 32 + 8) * 4];
|
||||||
|
let right = rendered[(16 * 32 + 24) * 4];
|
||||||
|
assert!(
|
||||||
|
left.abs_diff(right) <= 2,
|
||||||
|
"opacity {opacity} leaked the original backdrop: {left} versus {right}"
|
||||||
|
);
|
||||||
|
let expected = (128.0 * (1.0 - strength * opacity) + tint * 255.0 * strength * opacity).round() as u8;
|
||||||
|
assert!(
|
||||||
|
left.abs_diff(expected) <= 2,
|
||||||
|
"opacity {opacity} failed to control tint: {left} versus {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_direction_survives_every_output_transform() {
|
||||||
|
let mode = (2560, 1600).into();
|
||||||
|
for transform in [
|
||||||
|
Transform::Normal,
|
||||||
|
Transform::_90,
|
||||||
|
Transform::_180,
|
||||||
|
Transform::_270,
|
||||||
|
Transform::Flipped,
|
||||||
|
Transform::Flipped90,
|
||||||
|
Transform::Flipped180,
|
||||||
|
Transform::Flipped270,
|
||||||
|
] {
|
||||||
|
let area = transform.transform_size(mode).to_f64();
|
||||||
|
let rect: Rectangle<i32, Physical> = Rectangle::new((110, 60).into(), (800, 600).into());
|
||||||
|
for (angle, first, last) in [
|
||||||
|
(0.0, (110.0, 360.0), (910.0, 360.0)),
|
||||||
|
(90.0, (510.0, 60.0), (510.0, 660.0)),
|
||||||
|
(45.0, (110.0, 60.0), (910.0, 660.0)),
|
||||||
|
(135.0, (910.0, 60.0), (110.0, 660.0)),
|
||||||
|
] {
|
||||||
|
let line = border_gradient_line(rect, mode, transform, angle);
|
||||||
|
let progress = |point: smithay::utils::Point<f64, Physical>| {
|
||||||
|
let point = transform.transform_point_in(point, &area);
|
||||||
|
(point.x - f64::from(line[0])) * f64::from(line[2])
|
||||||
|
+ (point.y - f64::from(line[1])) * f64::from(line[3])
|
||||||
|
};
|
||||||
|
assert!(progress(first.into()).abs() < 0.00001, "{transform:?}, {angle}");
|
||||||
|
assert!((progress(last.into()) - 1.0).abs() < 0.00001, "{transform:?}, {angle}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gradient_coordinates_follow_fractional_resize_and_translation() {
|
||||||
|
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||||
|
for width in [500.0, 733.25, 1000.0] {
|
||||||
|
let rect = crate::presentation::physical_rect(
|
||||||
|
ferese_layout::Rect::new(40.25, 60.75, width, 600.5),
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
let line = border_gradient_line(rect, (3840, 2160).into(), Transform::Normal, 0.0);
|
||||||
|
assert!((f64::from(line[0]) - f64::from(rect.loc.x)).abs() < 0.001);
|
||||||
|
assert!((f64::from(line[2]) * f64::from(rect.size.w) - 1.0).abs() < 0.00001);
|
||||||
|
assert!(line.iter().all(|component| component.is_finite()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DamageElement {
|
||||||
|
id: Id,
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
commit: CommitCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DamageElement {
|
||||||
|
fn new(geometry: Rectangle<i32, Logical>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Id::new(),
|
||||||
|
geometry,
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for DamageElement {
|
||||||
|
fn id(&self) -> &Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_commit(&self) -> CommitCounter {
|
||||||
|
self.commit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||||
|
Rectangle::from_size(self.geometry.size.to_f64().to_buffer(1.0, Transform::Normal))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||||
|
self.geometry.to_physical_precise_round(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn damage_since(
|
||||||
|
&self,
|
||||||
|
scale: Scale<f64>,
|
||||||
|
commit: Option<CommitCounter>,
|
||||||
|
) -> smithay::backend::renderer::utils::DamageSet<i32, Physical> {
|
||||||
|
if commit == Some(self.current_commit()) {
|
||||||
|
smithay::backend::renderer::utils::DamageSet::default()
|
||||||
|
} else {
|
||||||
|
smithay::backend::renderer::utils::DamageSet::from_slice(&[Rectangle::from_size(
|
||||||
|
self.geometry(scale).size,
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
|
||||||
|
use smithay::backend::renderer::element::utils::{ConstrainAlign, constrain_render_elements};
|
||||||
|
for scale in [1.0, 1.5, 1.8, 2.0] {
|
||||||
|
for width in [300, 600, 1200] {
|
||||||
|
let source = Rectangle::<i32, Logical>::from_size((600, 800).into());
|
||||||
|
let destination = Rectangle::<i32, Logical>::from_size((width, 800).into());
|
||||||
|
let element = constrain_render_elements(
|
||||||
|
[DamageElement::new(source)],
|
||||||
|
(0, 0),
|
||||||
|
destination.to_physical_precise_round(scale),
|
||||||
|
source.to_physical_precise_round(scale),
|
||||||
|
resize_content_behavior(false),
|
||||||
|
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
|
scale,
|
||||||
|
)
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
// Comparing destination pixels to sampled source pixels catches
|
||||||
|
// stretching, unlike tests of configure timing alone.
|
||||||
|
let physical = element.geometry(scale.into());
|
||||||
|
let sampled = element.src();
|
||||||
|
assert!((f64::from(physical.size.w) / sampled.size.w - scale).abs() < 0.01);
|
||||||
|
assert!((f64::from(physical.size.h) / sampled.size.h - scale).abs() < 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overview_retains_intentional_content_scaling() {
|
||||||
|
assert!(matches!(
|
||||||
|
resize_content_behavior(true),
|
||||||
|
smithay::backend::renderer::element::utils::ConstrainScaleBehavior::Stretch
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clip_rect_matches_smithay_gles_projection() {
|
||||||
|
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
framebuffer_clip_rect(geometry, (100, 80).into(), Transform::Normal),
|
||||||
|
[10.0, 5.0, 30.0, 40.0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clip_rect_follows_output_transform() {
|
||||||
|
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
framebuffer_clip_rect(geometry, (100, 80).into(), Transform::Flipped180),
|
||||||
|
[10.0, 35.0, 30.0, 40.0]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
framebuffer_clip_rect(geometry, (100, 80).into(), Transform::_180),
|
||||||
|
[60.0, 35.0, 30.0, 40.0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_clearance_clip_matches_gles_projection_at_fractional_scales() {
|
||||||
|
for scale in [1.0, 1.25, 1.75, 2.0] {
|
||||||
|
for transform in [
|
||||||
|
Transform::Normal,
|
||||||
|
Transform::_90,
|
||||||
|
Transform::_180,
|
||||||
|
Transform::_270,
|
||||||
|
Transform::Flipped,
|
||||||
|
Transform::Flipped90,
|
||||||
|
Transform::Flipped180,
|
||||||
|
Transform::Flipped270,
|
||||||
|
] {
|
||||||
|
let output_size =
|
||||||
|
smithay::utils::Size::<i32, Physical>::from(((1280.0 * scale) as i32, (800.0 * scale) as i32));
|
||||||
|
let element_size = transform.transform_size(output_size);
|
||||||
|
let logical_height = f64::from(element_size.h) / scale;
|
||||||
|
// Bar and exclusion end at 40; the 10px outer gap ends at 50.
|
||||||
|
let geometry =
|
||||||
|
Rectangle::<f64, Logical>::new((10.0, 50.0).into(), (400.0, logical_height - 60.0).into())
|
||||||
|
.to_physical_precise_round(scale);
|
||||||
|
let clip = framebuffer_clip_rect(geometry, output_size, transform);
|
||||||
|
// Independently reproduce GlesFrame's orthographic projection,
|
||||||
|
// transform matrix, GL flip, and viewport mapping for each corner.
|
||||||
|
let matrix = transform.matrix();
|
||||||
|
let mut xs = Vec::new();
|
||||||
|
let mut ys = Vec::new();
|
||||||
|
for x in [geometry.loc.x, geometry.loc.x + geometry.size.w] {
|
||||||
|
for y in [geometry.loc.y, geometry.loc.y + geometry.size.h] {
|
||||||
|
let nx = 2.0 * x as f32 / element_size.w as f32 - 1.0;
|
||||||
|
let ny = 1.0 - 2.0 * y as f32 / element_size.h as f32;
|
||||||
|
let tx = matrix[0][0] * nx + matrix[1][0] * ny;
|
||||||
|
let ty = matrix[0][1] * nx + matrix[1][1] * ny;
|
||||||
|
xs.push((tx + 1.0) * output_size.w as f32 / 2.0);
|
||||||
|
ys.push((1.0 - ty) * output_size.h as f32 / 2.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let min_x = xs.iter().copied().fold(f32::INFINITY, f32::min);
|
||||||
|
let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min);
|
||||||
|
let max_y = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max);
|
||||||
|
for (actual, expected) in clip.into_iter().zip([min_x, min_y, max_x - min_x, max_y - min_y]) {
|
||||||
|
assert!(
|
||||||
|
(actual - expected).abs() < 0.001,
|
||||||
|
"{transform:?}, scale={scale}: {actual} != {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadow_bounds_include_blur_and_offset() {
|
||||||
|
let geometry = Rectangle::<i32, Logical>::new((100, 80).into(), (400, 300).into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
shadow_bounds(geometry, 4.0, 18.0),
|
||||||
|
Rectangle::new((64, 48).into(), (472, 372).into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shadow_bounds(geometry, -6.0, 10.0),
|
||||||
|
Rectangle::new((80, 54).into(), (440, 340).into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn damage_tracks_old_and_new_expanded_shadow_bounds() {
|
||||||
|
let old_window = Rectangle::<i32, Logical>::new((100, 80).into(), (400, 300).into());
|
||||||
|
let new_window = Rectangle::<i32, Logical>::new((300, 180).into(), (400, 300).into());
|
||||||
|
let mut shadow = DamageElement::new(shadow_bounds(old_window, 4.0, 18.0));
|
||||||
|
let mut tracker = OutputDamageTracker::new((1_000, 800), Scale::from(1.0), Transform::Normal);
|
||||||
|
|
||||||
|
tracker.damage_output(0, &[&shadow]).unwrap();
|
||||||
|
shadow.geometry = shadow_bounds(new_window, 4.0, 18.0);
|
||||||
|
let damage = tracker.damage_output(1, &[&shadow]).unwrap().0.cloned().unwrap();
|
||||||
|
|
||||||
|
assert!(damage.iter().any(|rect| rect.contains((64, 48))));
|
||||||
|
assert!(damage.iter().any(|rect| rect.contains((735, 519))));
|
||||||
|
|
||||||
|
let removal_damage = tracker
|
||||||
|
.damage_output::<&DamageElement>(1, &[])
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.cloned()
|
||||||
|
.unwrap();
|
||||||
|
assert!(removal_damage.iter().any(|rect| rect.contains((300, 180))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translucent_materials_blur_at_full_opacity() {
|
||||||
|
use crate::config::MaterialStyle;
|
||||||
|
assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 0.0, 18.0), 0.0);
|
||||||
|
assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 1.0, 18.0), 18.0);
|
||||||
|
assert_eq!(
|
||||||
|
super::material_blur_radius(MaterialStyle::Translucent, 0.79, 18.0),
|
||||||
|
18.0
|
||||||
|
);
|
||||||
|
assert_eq!(super::material_blur_radius(MaterialStyle::Solid, 0.79, 18.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_transform_scales_about_the_window_center() {
|
||||||
|
let rect = ferese_layout::Rect::new(100.0, 50.0, 400.0, 300.0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scaled_visual_rect(rect, 0.98),
|
||||||
|
ferese_layout::Rect::new(104.0, 53.0, 392.0, 294.0)
|
||||||
|
);
|
||||||
|
assert_eq!(scaled_visual_rect(rect, 1.0), rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_opacity_only_changes_the_alpha_channel() {
|
||||||
|
assert_eq!(color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5), [0.2, 0.4, 0.6, 0.4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visual_rect_rounds_shared_edges_instead_of_size() {
|
||||||
|
let rect = ferese_layout::Rect::new(100.5, 50.25, 399.5, 299.75);
|
||||||
|
let rounded = rounded_visual_rect(rect, (0, 0).into());
|
||||||
|
|
||||||
|
assert_eq!(rounded, Rectangle::new((101, 50).into(), (399, 300).into()));
|
||||||
|
assert_eq!(rounded.loc.x + rounded.size.w, 500);
|
||||||
|
assert_eq!(rounded.loc.y + rounded.size.h, 350);
|
||||||
|
|
||||||
|
for tenth in 0..=10 {
|
||||||
|
let left = 100.0 + f64::from(tenth) / 10.0;
|
||||||
|
let frame = rounded_visual_rect(ferese_layout::Rect::new(left, 50.0, 500.0 - left, 300.0), (0, 0).into());
|
||||||
|
|
||||||
|
assert_eq!(frame.loc.x + frame.size.w, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(super) fn overview_chrome_element(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
key: (u64, OverviewChromePart),
|
||||||
|
rect: ferese_layout::Rect,
|
||||||
|
color: [f32; 4],
|
||||||
|
width: f64,
|
||||||
|
radius: f64,
|
||||||
|
) -> Option<PhysicalShaderElement> {
|
||||||
|
let output_id = state.output_id(output)?;
|
||||||
|
let geometry = rounded_visual_rect(rect, output_geometry.loc);
|
||||||
|
let physical = physical_rect(rect, output_geometry.loc, scale);
|
||||||
|
let mode = output.current_mode()?;
|
||||||
|
let parameters = BorderParameters {
|
||||||
|
geometry,
|
||||||
|
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||||
|
radius: (radius * scale) as f32,
|
||||||
|
width: (width * scale) as f32,
|
||||||
|
color,
|
||||||
|
color_to: color,
|
||||||
|
gradient_line: [0.0; 4],
|
||||||
|
focus_color: color,
|
||||||
|
focus_color_to: color,
|
||||||
|
focus_gradient_line: [0.0; 4],
|
||||||
|
focus_mix: 0.0,
|
||||||
|
};
|
||||||
|
let program = if key.1 == OverviewChromePart::Outline {
|
||||||
|
rounded_clip_program(state, renderer)?.border
|
||||||
|
} else {
|
||||||
|
material_program(state, renderer)?.0
|
||||||
|
};
|
||||||
|
let uniforms = |p: &BorderParameters| {
|
||||||
|
if key.1 == OverviewChromePart::Outline {
|
||||||
|
border_uniforms(p)
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
Uniform::new("visible_rect", p.clip_rect).into_owned(),
|
||||||
|
Uniform::new("material_radius", p.radius).into_owned(),
|
||||||
|
Uniform::new("tint", p.color).into_owned(),
|
||||||
|
Uniform::new("paint_mode", 0.0_f32).into_owned(),
|
||||||
|
Uniform::new("shadow_rect", p.clip_rect).into_owned(),
|
||||||
|
Uniform::new("shadow_values", [0.0_f32; 2]).into_owned(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let context = renderer.context_id().erased();
|
||||||
|
let chrome = &mut state
|
||||||
|
.overview_scrims
|
||||||
|
.entry(output_id)
|
||||||
|
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
||||||
|
.chrome;
|
||||||
|
let cached = chrome
|
||||||
|
.entry(key)
|
||||||
|
.or_default()
|
||||||
|
.entry(context)
|
||||||
|
.or_insert_with(|| CachedBorder {
|
||||||
|
element: PixelShaderElement::new(
|
||||||
|
program,
|
||||||
|
geometry,
|
||||||
|
None,
|
||||||
|
1.0,
|
||||||
|
uniforms(¶meters),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
),
|
||||||
|
parameters: parameters.clone(),
|
||||||
|
});
|
||||||
|
if cached.parameters != parameters {
|
||||||
|
if cached.parameters.geometry != geometry {
|
||||||
|
cached.element.resize(geometry, None);
|
||||||
|
}
|
||||||
|
cached.element.update_uniforms(uniforms(¶meters));
|
||||||
|
cached.parameters = parameters;
|
||||||
|
}
|
||||||
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: physical,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn overview_strip_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let alpha = state.overview.opacity();
|
||||||
|
if alpha <= 0.001 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let Some(bounds) = state.output_bounds_for(output) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let cards = state.overview_workspace_cards(output);
|
||||||
|
let strip = crate::overview::workspace_strip(bounds);
|
||||||
|
let panel = match (cards.first(), cards.last()) {
|
||||||
|
(Some(first), Some(last)) => ferese_layout::Rect::new(
|
||||||
|
first.rect.x - 12.0,
|
||||||
|
strip.y,
|
||||||
|
last.rect.x + last.rect.width - first.rect.x + 24.0,
|
||||||
|
strip.height,
|
||||||
|
),
|
||||||
|
_ => strip,
|
||||||
|
};
|
||||||
|
let radius = state.theme_settings.material_radius;
|
||||||
|
let material_opacity = crate::effects::resolve_material(
|
||||||
|
crate::effects::SemanticRole::Panel,
|
||||||
|
state.theme_settings.material_style,
|
||||||
|
state.theme_settings.shell_opacity as f32,
|
||||||
|
)
|
||||||
|
.opacity;
|
||||||
|
let mut elements = Vec::new();
|
||||||
|
let programs = rounded_clip_program(state, renderer);
|
||||||
|
// Resolve IDs once per strip, rather than scanning all managed windows
|
||||||
|
// separately for every miniature. Own the handles so rendering can mutate state.
|
||||||
|
let windows_by_id: std::collections::HashMap<_, _> = state
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.map(|(window, id)| (*id, window.clone()))
|
||||||
|
.collect();
|
||||||
|
if let Some(output_id) = state.output_id(output)
|
||||||
|
&& let Some(cache) = state.overview_scrims.get_mut(&output_id)
|
||||||
|
{
|
||||||
|
cache.chrome.retain(|(id, part), _| match part {
|
||||||
|
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
|
||||||
|
OverviewChromePart::Card | OverviewChromePart::Outline => cards.iter().any(|card| card.workspace.0 == *id),
|
||||||
|
OverviewChromePart::Strip => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Cached title textures are independent of window content and stay readable
|
||||||
|
// when thumbnails are small. Their pills use the shell's material and radius.
|
||||||
|
for (id, window) in &windows_by_id {
|
||||||
|
if !state.window_belongs_to_output(*id, output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(rect) = state.presented_window_rect(*id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let width = (f64::from(size.w) / scale + 16.0).min(rect.width);
|
||||||
|
let caption = ferese_layout::Rect::new(
|
||||||
|
rect.x + (rect.width - width) * 0.5,
|
||||||
|
rect.y + rect.height + 8.0,
|
||||||
|
width,
|
||||||
|
26.0,
|
||||||
|
);
|
||||||
|
let location = Point::<i32, Physical>::from((
|
||||||
|
((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round() as i32 - size.w / 2,
|
||||||
|
((caption.y - f64::from(output_geometry.loc.y) + 4.0) * scale).round() as i32,
|
||||||
|
));
|
||||||
|
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
location.to_f64(),
|
||||||
|
&buffer,
|
||||||
|
Some(alpha),
|
||||||
|
Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())),
|
||||||
|
Some(
|
||||||
|
(
|
||||||
|
(f64::from(size.w) / scale).round() as i32,
|
||||||
|
(f64::from(size.h) / scale).round() as i32,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
) {
|
||||||
|
let element = RescaleRenderElement::from_element(element, location, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
|
||||||
|
if let Some(element) =
|
||||||
|
CropRenderElement::from_element(element, scale, physical_rect(caption, output_geometry.loc, scale))
|
||||||
|
{
|
||||||
|
elements.push(element.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut fill = state.theme_settings.surface_base_color.0;
|
||||||
|
fill[3] = alpha * material_opacity;
|
||||||
|
if let Some(background) = overview_chrome_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
(id.0, OverviewChromePart::Caption),
|
||||||
|
caption,
|
||||||
|
fill,
|
||||||
|
0.0,
|
||||||
|
radius.min(13.0),
|
||||||
|
) {
|
||||||
|
elements.push(background.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for card in cards {
|
||||||
|
// Front-to-back: outline and live miniatures above each card, all above
|
||||||
|
// the strip. Nothing is painted behind the main overview window grid.
|
||||||
|
if card.selected {
|
||||||
|
let mut color = state.theme_settings.accent_color.0;
|
||||||
|
color[3] *= alpha;
|
||||||
|
if let Some(border) = overview_chrome_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
(card.workspace.0, OverviewChromePart::Outline),
|
||||||
|
card.rect,
|
||||||
|
color,
|
||||||
|
1.0,
|
||||||
|
radius,
|
||||||
|
) {
|
||||||
|
elements.push(border.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(programs) = &programs {
|
||||||
|
for (id, rect) in &card.windows {
|
||||||
|
if let Some(window) = windows_by_id.get(id) {
|
||||||
|
elements.extend(rounded_window_elements(
|
||||||
|
renderer,
|
||||||
|
window,
|
||||||
|
rounded_visual_rect(*rect, output_geometry.loc),
|
||||||
|
physical_rect(*rect, output_geometry.loc, scale),
|
||||||
|
scale,
|
||||||
|
4.0,
|
||||||
|
alpha,
|
||||||
|
true,
|
||||||
|
output,
|
||||||
|
programs.texture.clone(),
|
||||||
|
ConstrainScaleBehavior::Stretch,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((buffer, size)) = state.overview_workspace_label(card.workspace, scale) {
|
||||||
|
let location = Point::<i32, Physical>::from((
|
||||||
|
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32
|
||||||
|
- size.w / 2,
|
||||||
|
((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale).round() as i32,
|
||||||
|
));
|
||||||
|
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
location.to_f64(),
|
||||||
|
&buffer,
|
||||||
|
Some(alpha),
|
||||||
|
Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())),
|
||||||
|
Some(
|
||||||
|
(
|
||||||
|
(f64::from(size.w) / scale).round() as i32,
|
||||||
|
(f64::from(size.h) / scale).round() as i32,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
) {
|
||||||
|
let element = RescaleRenderElement::from_element(element, location, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
|
||||||
|
if let Some(element) = CropRenderElement::from_element(
|
||||||
|
element,
|
||||||
|
scale,
|
||||||
|
physical_rect(card.rect, output_geometry.loc, scale),
|
||||||
|
) {
|
||||||
|
elements.push(element.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut card_color = state.theme_settings.surface_base_color.0;
|
||||||
|
for component in &mut card_color[..3] {
|
||||||
|
*component = (*component + 0.035).min(1.0);
|
||||||
|
}
|
||||||
|
card_color[3] = alpha * material_opacity;
|
||||||
|
if let Some(card_fill) = overview_chrome_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
(card.workspace.0, OverviewChromePart::Card),
|
||||||
|
card.rect,
|
||||||
|
card_color,
|
||||||
|
0.0,
|
||||||
|
radius,
|
||||||
|
) {
|
||||||
|
elements.push(card_fill.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut color = state.theme_settings.surface_base_color.0;
|
||||||
|
color[3] = alpha
|
||||||
|
* crate::effects::resolve_material(
|
||||||
|
crate::effects::SemanticRole::Panel,
|
||||||
|
state.theme_settings.material_style,
|
||||||
|
state.theme_settings.shell_opacity as f32,
|
||||||
|
)
|
||||||
|
.opacity;
|
||||||
|
if let Some(strip) = overview_chrome_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
(0, OverviewChromePart::Strip),
|
||||||
|
panel,
|
||||||
|
color,
|
||||||
|
0.0,
|
||||||
|
radius,
|
||||||
|
) {
|
||||||
|
elements.push(strip.into());
|
||||||
|
}
|
||||||
|
elements
|
||||||
|
}
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(crate) fn animated_window_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
output_elements(state, renderer, output, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cursorless_window_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
output_elements(state, renderer, output, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics {
|
||||||
|
FrameEffectMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
include_cursor: bool,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
if state.session_lock.active {
|
||||||
|
state.configure_lock_surfaces();
|
||||||
|
let Some(geometry) = state.space.output_geometry(output) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
let background = state.session_lock.backgrounds.entry(output.clone()).or_insert_with(|| {
|
||||||
|
smithay::backend::renderer::element::solid::SolidColorBuffer::new(geometry.size, [0.0, 0.0, 0.0, 1.0])
|
||||||
|
});
|
||||||
|
background.resize(geometry.size);
|
||||||
|
let mut elements = Vec::new();
|
||||||
|
let opacity = state.session_lock.idle_opacity;
|
||||||
|
if opacity > 0.0 {
|
||||||
|
let overlay = state
|
||||||
|
.session_lock
|
||||||
|
.idle_overlays
|
||||||
|
.entry(output.clone())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
smithay::backend::renderer::element::solid::SolidColorBuffer::new(
|
||||||
|
geometry.size,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
overlay.resize(geometry.size);
|
||||||
|
elements.push(
|
||||||
|
SolidColorRenderElement::from_buffer(overlay, (0, 0), scale, opacity, RenderElementKind::Unspecified)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(surface) = state
|
||||||
|
.session_lock
|
||||||
|
.surfaces
|
||||||
|
.get(output)
|
||||||
|
.filter(|surface| surface.alive())
|
||||||
|
{
|
||||||
|
elements.extend(
|
||||||
|
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
surface.wl_surface(),
|
||||||
|
(0, 0),
|
||||||
|
scale,
|
||||||
|
1.0,
|
||||||
|
RenderElementKind::Unspecified,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(AnimatedWindowRenderElement::from),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
elements.push(
|
||||||
|
SolidColorRenderElement::from_buffer(background, (0, 0), scale, 1.0, RenderElementKind::Unspecified).into(),
|
||||||
|
);
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
let rounded_clip_program = rounded_clip_program(state, renderer);
|
||||||
|
|
||||||
|
let mut elements = if include_cursor {
|
||||||
|
cursor_elements(state, renderer, output_geometry, scale)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
|
||||||
|
&[Layer::Overlay]
|
||||||
|
} else {
|
||||||
|
&[Layer::Overlay, Layer::Top]
|
||||||
|
};
|
||||||
|
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||||
|
if state.overview.is_presenting() {
|
||||||
|
elements.extend(overview_strip_elements(state, renderer, output, output_geometry, scale));
|
||||||
|
}
|
||||||
|
let mut candidates = if state.overview.is_active() {
|
||||||
|
state.window_ids.keys().cloned().collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
if state.overview.is_active() {
|
||||||
|
candidates.sort_by_key(|window| std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0)));
|
||||||
|
}
|
||||||
|
let windows = candidates
|
||||||
|
.iter()
|
||||||
|
.filter_map(|window| {
|
||||||
|
// Configure immediately, but present the frame/shadow only after
|
||||||
|
// the first buffer commit has settled the actual client geometry.
|
||||||
|
if !state.window_content_ready(window) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let id = *state.window_ids.get(window)?;
|
||||||
|
// Scrolling columns may sit outside their monitor's rectangle.
|
||||||
|
// They must not reappear on a neighboring output just because
|
||||||
|
// their global animated coordinates overlap it.
|
||||||
|
if !state.window_belongs_to_output(id, output) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let visual = state.presented_window_rect(id)?;
|
||||||
|
let (close_scale, close_alpha) = state.closing_visual(id);
|
||||||
|
let visual = scaled_visual_rect(visual, close_scale);
|
||||||
|
let decoration_progress = state
|
||||||
|
.window_geometry
|
||||||
|
.get(&id)
|
||||||
|
.map_or(1.0, |geometry| geometry.decorations.clamp(0.0, 1.0));
|
||||||
|
|
||||||
|
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (window, id, visual, decoration_progress, close_alpha) in windows {
|
||||||
|
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||||
|
let pixels = physical_rect(visual, output_geometry.loc, scale);
|
||||||
|
let material_surface = window
|
||||||
|
.toplevel()
|
||||||
|
.map(|toplevel| toplevel.wl_surface())
|
||||||
|
.filter(|surface| {
|
||||||
|
crate::effects::surface_role(surface)
|
||||||
|
.is_some_and(|(role, _)| role == crate::effects::SemanticRole::Modal)
|
||||||
|
});
|
||||||
|
let window_radius = if material_surface.is_some() {
|
||||||
|
state.theme_settings.material_radius
|
||||||
|
} else {
|
||||||
|
state.theme_settings.window_radius
|
||||||
|
} * decoration_progress;
|
||||||
|
// Only overview/close intentionally scale the complete application.
|
||||||
|
let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0;
|
||||||
|
let behavior = resize_content_behavior(scale_content);
|
||||||
|
|
||||||
|
let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current);
|
||||||
|
if let Some(overlay) = window_tint_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
id,
|
||||||
|
constrain,
|
||||||
|
pixels,
|
||||||
|
scale,
|
||||||
|
window_radius,
|
||||||
|
[0.0, 0.0, 0.0, dim as f32 * close_alpha],
|
||||||
|
false,
|
||||||
|
output,
|
||||||
|
) {
|
||||||
|
// Front-to-back: dim the application and its border, not its shadow
|
||||||
|
// or other windows/layer surfaces. The overlay is input-transparent.
|
||||||
|
elements.push(overlay.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(programs) = rounded_clip_program.clone() {
|
||||||
|
let shadow_offset_y = state.theme_settings.shadow_offset_y;
|
||||||
|
let shadow_blur = state.theme_settings.shadow_blur;
|
||||||
|
let shadow_opacity = state.theme_settings.shadow_opacity;
|
||||||
|
let shadow_color = state.theme_settings.shadow_color.0;
|
||||||
|
let focused = if state.overview.is_active() {
|
||||||
|
state.overview_selected(id)
|
||||||
|
} else {
|
||||||
|
state.focused_window == Some(id)
|
||||||
|
};
|
||||||
|
let focus = state
|
||||||
|
.window_focus
|
||||||
|
.get(&id)
|
||||||
|
.map_or(if focused { 1.0 } else { 0.0 }, |v| v.current);
|
||||||
|
let border_width = state.theme_settings.border_width
|
||||||
|
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
|
||||||
|
let border_color = state.theme_settings.border_color.0;
|
||||||
|
let gradient = state.theme_settings.border_gradient;
|
||||||
|
|
||||||
|
if let Some(border) = window_border_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
id,
|
||||||
|
constrain,
|
||||||
|
pixels,
|
||||||
|
scale,
|
||||||
|
window_radius,
|
||||||
|
border_width,
|
||||||
|
border_color,
|
||||||
|
gradient,
|
||||||
|
focus as f32,
|
||||||
|
close_alpha * decoration_progress as f32,
|
||||||
|
output,
|
||||||
|
&programs,
|
||||||
|
) {
|
||||||
|
elements.push(border.into());
|
||||||
|
}
|
||||||
|
let shadow = window_shadow_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
id,
|
||||||
|
constrain,
|
||||||
|
pixels,
|
||||||
|
scale,
|
||||||
|
window_radius,
|
||||||
|
shadow_offset_y,
|
||||||
|
shadow_blur,
|
||||||
|
shadow_opacity * f64::from(close_alpha) * decoration_progress,
|
||||||
|
shadow_color,
|
||||||
|
output,
|
||||||
|
&programs,
|
||||||
|
);
|
||||||
|
if !scale_content
|
||||||
|
&& let Some(snapshot) = state.resize_snapshots.get(&id)
|
||||||
|
&& snapshot.context == renderer.context_id().erased()
|
||||||
|
&& (snapshot.scale - scale).abs() < 0.001
|
||||||
|
{
|
||||||
|
let size = snapshot.texture.size();
|
||||||
|
let visible = Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels);
|
||||||
|
if let Some(visible) = visible {
|
||||||
|
let clip = framebuffer_clip_rect(
|
||||||
|
pixels,
|
||||||
|
output.current_mode().unwrap().size,
|
||||||
|
output.current_transform().invert(),
|
||||||
|
);
|
||||||
|
elements.push(
|
||||||
|
NativeTextureElement {
|
||||||
|
id: snapshot.id.clone(),
|
||||||
|
commit: snapshot.commit,
|
||||||
|
texture: snapshot.texture.clone(),
|
||||||
|
geometry: visible,
|
||||||
|
source: Rectangle::from_size(Size::from((
|
||||||
|
f64::from(visible.size.w),
|
||||||
|
f64::from(visible.size.h),
|
||||||
|
))),
|
||||||
|
alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha,
|
||||||
|
program: Some(programs.texture.clone()),
|
||||||
|
uniforms: vec![
|
||||||
|
Uniform::new("clip_rect", clip).into_owned(),
|
||||||
|
Uniform::new("radius", scaled_effect_value(window_radius, constrain, scale))
|
||||||
|
.into_owned(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elements.extend(rounded_window_elements(
|
||||||
|
renderer,
|
||||||
|
&window,
|
||||||
|
constrain,
|
||||||
|
pixels,
|
||||||
|
scale,
|
||||||
|
window_radius,
|
||||||
|
close_alpha,
|
||||||
|
state
|
||||||
|
.window_geometry
|
||||||
|
.get(&id)
|
||||||
|
.is_some_and(|geometry| geometry.presentation_changed),
|
||||||
|
output,
|
||||||
|
programs.texture.clone(),
|
||||||
|
behavior,
|
||||||
|
));
|
||||||
|
let source = window.geometry().size;
|
||||||
|
if material_surface.is_none()
|
||||||
|
&& !scale_content
|
||||||
|
&& (source.w < constrain.size.w || source.h < constrain.size.h)
|
||||||
|
{
|
||||||
|
let mut color = state.theme_settings.surface_base_color.0;
|
||||||
|
color[3] = close_alpha;
|
||||||
|
if let Some(fill) = window_tint_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
id,
|
||||||
|
constrain,
|
||||||
|
pixels,
|
||||||
|
scale,
|
||||||
|
window_radius,
|
||||||
|
color,
|
||||||
|
true,
|
||||||
|
output,
|
||||||
|
) {
|
||||||
|
// Front-to-back: fill uncovered strips behind the native
|
||||||
|
// content instead of stretching it or exposing wallpaper.
|
||||||
|
elements.push(fill.into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.window_resize_fills.remove(&id);
|
||||||
|
}
|
||||||
|
if let Some(surface) = material_surface
|
||||||
|
&& let Some((background, _)) = material_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
surface,
|
||||||
|
constrain,
|
||||||
|
window_radius as f32,
|
||||||
|
0,
|
||||||
|
constrain,
|
||||||
|
close_alpha,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
elements.push(background);
|
||||||
|
}
|
||||||
|
if let Some(shadow) = shadow {
|
||||||
|
elements.push(shadow.into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements.extend(constrain_space_element::<GlesRenderer, _, AnimatedWindowRenderElement>(
|
||||||
|
renderer,
|
||||||
|
&window,
|
||||||
|
constrain.loc,
|
||||||
|
close_alpha,
|
||||||
|
scale,
|
||||||
|
constrain,
|
||||||
|
ConstrainBehavior {
|
||||||
|
reference: ConstrainReference::Geometry,
|
||||||
|
behavior,
|
||||||
|
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elements.extend(layer_elements(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
&[Layer::Bottom, Layer::Background],
|
||||||
|
));
|
||||||
|
state.prepare_theme_wallpaper(renderer, output);
|
||||||
|
let progress = state.theme_progress();
|
||||||
|
let wallpaper = state.wallpaper.element(renderer, output);
|
||||||
|
let has_wallpaper = wallpaper.is_some();
|
||||||
|
if let Some(mut wallpaper) = wallpaper {
|
||||||
|
wallpaper.alpha = progress;
|
||||||
|
elements.push(wallpaper.into());
|
||||||
|
}
|
||||||
|
if let Some(mut previous) = state.previous_theme_wallpaper(renderer, output) {
|
||||||
|
if !has_wallpaper {
|
||||||
|
previous.alpha = 1. - progress;
|
||||||
|
}
|
||||||
|
elements.push(previous.into());
|
||||||
|
}
|
||||||
|
elements
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn layer_surfaces(output: &Output) -> Vec<LayerSurface> {
|
||||||
|
layer_map_for_output(output).layers().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn layer_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
requested_layers: &[Layer],
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
let Some(mode) = output.current_mode() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let output_crop = Rectangle::<i32, Physical>::from_size(mode.size);
|
||||||
|
let layers = {
|
||||||
|
let map = layer_map_for_output(output);
|
||||||
|
|
||||||
|
requested_layers
|
||||||
|
.iter()
|
||||||
|
.flat_map(|requested| {
|
||||||
|
map.layers_on(*requested)
|
||||||
|
.rev()
|
||||||
|
.filter_map(|layer| map.layer_geometry(layer).map(|geometry| (geometry, layer.clone())))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
state.material_buffers.retain(|surface, _| surface.is_alive());
|
||||||
|
|
||||||
|
let mut elements = Vec::new();
|
||||||
|
for (geometry, layer) in layers {
|
||||||
|
for (popup, offset) in PopupManager::popups_for_surface(layer.wl_surface()) {
|
||||||
|
let mut bounds = popup.geometry();
|
||||||
|
let content_origin = geometry.loc + offset - bounds.loc;
|
||||||
|
bounds.loc = geometry.loc + offset;
|
||||||
|
append_material_surface(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
popup.wl_surface(),
|
||||||
|
bounds,
|
||||||
|
content_origin,
|
||||||
|
scale,
|
||||||
|
output_crop,
|
||||||
|
&mut elements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
append_material_surface(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
layer.wl_surface(),
|
||||||
|
geometry,
|
||||||
|
geometry.loc,
|
||||||
|
scale,
|
||||||
|
output_crop,
|
||||||
|
&mut elements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
elements
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cursor_elements(
|
||||||
|
state: &Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
if state.input_capture.active() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let Some(pointer) = state.seat.get_pointer() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let output_crop = Rectangle::<i32, Logical>::from_size(output_geometry.size).to_physical_precise_round(scale);
|
||||||
|
let origin = Point::<i32, Physical>::default();
|
||||||
|
let pointer_location = pointer.current_location() - output_geometry.loc.to_f64();
|
||||||
|
match &state.cursor_status {
|
||||||
|
CursorImageStatus::Surface(surface) => {
|
||||||
|
let hotspot = with_states(surface, |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.get::<CursorImageSurfaceData>()
|
||||||
|
.map(|attributes| attributes.lock().unwrap().hotspot)
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let physical_location = (pointer_location - hotspot.to_f64()).to_physical_precise_round(scale);
|
||||||
|
|
||||||
|
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
||||||
|
renderer,
|
||||||
|
surface,
|
||||||
|
physical_location,
|
||||||
|
scale,
|
||||||
|
1.0,
|
||||||
|
RenderElementKind::Cursor,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|element| {
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
|
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
CursorImageStatus::Named(icon) => {
|
||||||
|
let Some(cursor) = state.named_cursors.get(icon) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let hotspot = Point::<i32, Physical>::from((cursor.hotspot.x, cursor.hotspot.y));
|
||||||
|
let physical_location = pointer_location.to_physical_precise_round(scale) - hotspot;
|
||||||
|
let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
physical_location.to_f64(),
|
||||||
|
&cursor.buffer,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
RenderElementKind::Cursor,
|
||||||
|
) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
|
CropRenderElement::from_element(element, scale, output_crop)
|
||||||
|
.map(|element| vec![element.into()])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
CursorImageStatus::Hidden => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#version 100
|
||||||
|
//_DEFINES_
|
||||||
|
#if defined(EXTERNAL)
|
||||||
|
#extension GL_OES_EGL_image_external : require
|
||||||
|
#endif
|
||||||
|
precision highp float;
|
||||||
|
#if defined(EXTERNAL)
|
||||||
|
uniform samplerExternalOES tex;
|
||||||
|
#else
|
||||||
|
uniform sampler2D tex;
|
||||||
|
#endif
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 visible_rect;
|
||||||
|
uniform float material_radius;
|
||||||
|
uniform vec2 texture_size;
|
||||||
|
uniform vec2 capture_origin;
|
||||||
|
uniform float blur_radius;
|
||||||
|
uniform float presentation_alpha;
|
||||||
|
uniform float background_opacity;
|
||||||
|
uniform vec4 tint;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
void main() {
|
||||||
|
vec2 half_size = visible_rect.zw * 0.5;
|
||||||
|
vec2 d = abs(gl_FragCoord.xy - visible_rect.xy - half_size) - (half_size - vec2(material_radius));
|
||||||
|
float sdf = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - material_radius;
|
||||||
|
float coverage = alpha * presentation_alpha * (1.0 - smoothstep(-0.5, 0.5, sdf));
|
||||||
|
if (coverage <= 0.0) { gl_FragColor = vec4(0.0); return; }
|
||||||
|
vec2 coords = (gl_FragCoord.xy - capture_origin) / texture_size;
|
||||||
|
vec4 color = vec4(0.0);
|
||||||
|
float weights = 0.0;
|
||||||
|
// A spiral avoids aligning the sampling lattice with wallpaper patterns.
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
float radius = sqrt((float(i) + 0.5) / 256.0);
|
||||||
|
float angle = float(i) * 2.39996323;
|
||||||
|
vec2 offset = vec2(cos(angle), sin(angle)) * radius * blur_radius / texture_size;
|
||||||
|
float weight = exp(-4.5 * radius * radius);
|
||||||
|
color += texture2D(tex, coords + offset) * weight;
|
||||||
|
weights += weight;
|
||||||
|
}
|
||||||
|
vec3 background = color.rgb / weights;
|
||||||
|
gl_FragColor = vec4(mix(background, tint.rgb, tint.a * background_opacity) * coverage, coverage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
precision highp float;
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 tint;
|
||||||
|
uniform vec4 visible_rect;
|
||||||
|
uniform float material_radius;
|
||||||
|
uniform float paint_mode;
|
||||||
|
uniform vec4 shadow_rect;
|
||||||
|
uniform vec2 shadow_values;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
float rounded_distance(vec2 point, vec4 rect) {
|
||||||
|
vec2 half_size = rect.zw * 0.5;
|
||||||
|
float radius = min(material_radius, min(half_size.x, half_size.y));
|
||||||
|
vec2 d = abs(point - rect.xy - half_size) - (half_size - vec2(radius));
|
||||||
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
void main() {
|
||||||
|
float sdf = rounded_distance(gl_FragCoord.xy, visible_rect);
|
||||||
|
float coverage = 1.0 - smoothstep(-0.5, 0.5, sdf);
|
||||||
|
if (paint_mode > 0.5) {
|
||||||
|
float distance = max(rounded_distance(gl_FragCoord.xy, shadow_rect), 0.0);
|
||||||
|
float sigma = max(shadow_values.x * 0.5, 0.5);
|
||||||
|
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
||||||
|
* shadow_values.y * (1.0 - coverage) * alpha;
|
||||||
|
gl_FragColor = vec4(0.0, 0.0, 0.0, opacity);
|
||||||
|
} else {
|
||||||
|
float opacity = tint.a * coverage * alpha;
|
||||||
|
gl_FragColor = vec4(tint.rgb * opacity, opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 clip_rect;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float border_width;
|
||||||
|
uniform vec4 border_color;
|
||||||
|
uniform vec4 border_color_to;
|
||||||
|
uniform vec4 gradient_line;
|
||||||
|
uniform vec4 focus_color;
|
||||||
|
uniform vec4 focus_color_to;
|
||||||
|
uniform vec4 focus_gradient_line;
|
||||||
|
uniform float focus_mix;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
uniform float tint;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 point = gl_FragCoord.xy - clip_rect.xy;
|
||||||
|
vec2 half_size = clip_rect.zw * 0.5;
|
||||||
|
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
||||||
|
float signed_distance = length(max(distance, 0.0))
|
||||||
|
+ min(max(distance.x, distance.y), 0.0)
|
||||||
|
- radius;
|
||||||
|
float outer_coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance);
|
||||||
|
|
||||||
|
vec2 inner_half_size = max(half_size - vec2(border_width), vec2(0.0));
|
||||||
|
float inner_radius = max(radius - border_width, 0.0);
|
||||||
|
vec2 inner_distance = abs(point - half_size)
|
||||||
|
- (inner_half_size - vec2(inner_radius));
|
||||||
|
float inner_signed_distance = length(max(inner_distance, 0.0))
|
||||||
|
+ min(max(inner_distance.x, inner_distance.y), 0.0)
|
||||||
|
- inner_radius;
|
||||||
|
float inner_coverage = 1.0 - smoothstep(-0.5, 0.5, inner_signed_distance);
|
||||||
|
float coverage = max(outer_coverage - inner_coverage, 0.0);
|
||||||
|
float progress = clamp(dot(gl_FragCoord.xy - gradient_line.xy, gradient_line.zw), 0.0, 1.0);
|
||||||
|
// Interpolate premultiplied endpoints: a transparent endpoint must not
|
||||||
|
// leak its RGB into the visible border or create a dark halo.
|
||||||
|
vec4 from = vec4(border_color.rgb * border_color.a, border_color.a);
|
||||||
|
vec4 to = vec4(border_color_to.rgb * border_color_to.a, border_color_to.a);
|
||||||
|
float focus_progress = clamp(dot(gl_FragCoord.xy - focus_gradient_line.xy, focus_gradient_line.zw), 0.0, 1.0);
|
||||||
|
vec4 focus_from = vec4(focus_color.rgb * focus_color.a, focus_color.a);
|
||||||
|
vec4 focus_to = vec4(focus_color_to.rgb * focus_color_to.a, focus_color_to.a);
|
||||||
|
vec4 color = mix(mix(from, to, progress), mix(focus_from, focus_to, focus_progress), focus_mix) * coverage * alpha;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
if (tint == 1.0)
|
||||||
|
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#version 100
|
||||||
|
|
||||||
|
//_DEFINES_
|
||||||
|
|
||||||
|
#if defined(EXTERNAL)
|
||||||
|
#extension GL_OES_EGL_image_external : require
|
||||||
|
#endif
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
#if defined(EXTERNAL)
|
||||||
|
uniform samplerExternalOES tex;
|
||||||
|
#else
|
||||||
|
uniform sampler2D tex;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 clip_rect;
|
||||||
|
uniform float radius;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
uniform float tint;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture2D(tex, v_coords);
|
||||||
|
|
||||||
|
#if defined(NO_ALPHA)
|
||||||
|
color = vec4(color.rgb, 1.0) * alpha;
|
||||||
|
#else
|
||||||
|
color = color * alpha;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
vec2 point = gl_FragCoord.xy - clip_rect.xy;
|
||||||
|
vec2 half_size = clip_rect.zw * 0.5;
|
||||||
|
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
||||||
|
float signed_distance = length(max(distance, 0.0))
|
||||||
|
+ min(max(distance.x, distance.y), 0.0)
|
||||||
|
- radius;
|
||||||
|
float coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance);
|
||||||
|
color *= coverage;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
if (tint == 1.0)
|
||||||
|
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 shadow_rect;
|
||||||
|
uniform float radius;
|
||||||
|
uniform float blur;
|
||||||
|
uniform float opacity;
|
||||||
|
uniform vec4 shadow_color;
|
||||||
|
varying vec2 v_coords;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
uniform float tint;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 point = gl_FragCoord.xy - shadow_rect.xy;
|
||||||
|
vec2 half_size = shadow_rect.zw * 0.5;
|
||||||
|
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
||||||
|
float signed_distance = length(max(distance, 0.0))
|
||||||
|
+ min(max(distance.x, distance.y), 0.0)
|
||||||
|
- radius;
|
||||||
|
float sigma = max(blur * 0.5, 0.5);
|
||||||
|
float normalized_distance = max(signed_distance, 0.0) / sigma;
|
||||||
|
float coverage = exp(-0.5 * normalized_distance * normalized_distance);
|
||||||
|
vec4 color = vec4(shadow_color.rgb * shadow_color.a, shadow_color.a)
|
||||||
|
* coverage * opacity * alpha;
|
||||||
|
|
||||||
|
#if defined(DEBUG_FLAGS)
|
||||||
|
if (tint == 1.0)
|
||||||
|
color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
+13
-2806
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,352 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub fn advance_animations(&mut self, now: Instant) -> bool {
|
||||||
|
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
|
||||||
|
self.advance_animations_by(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_drm_presentation(&mut self, node: DrmNode, crtc: crtc::Handle, time: DrmEventTime, sequence: u32) {
|
||||||
|
if let Some(backend) = self.direct_backend.as_mut() {
|
||||||
|
backend.record_presentation(node, crtc, time, sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn animation_fallback_deadline(&self, interval: std::time::Duration) -> Instant {
|
||||||
|
self.last_animation_tick + interval.saturating_mul(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_animation_clock(&mut self) {
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
|
||||||
|
let delta = delta.mul_f64(self.animation_speed);
|
||||||
|
if self
|
||||||
|
.focus_swipe
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
|
||||||
|
{
|
||||||
|
self.focus_swipe = None;
|
||||||
|
}
|
||||||
|
let visible_workspaces = if self.viewport_animations.is_empty() {
|
||||||
|
HashSet::new()
|
||||||
|
} else {
|
||||||
|
self.space
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|output| self.output_ids.get(output))
|
||||||
|
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
};
|
||||||
|
let windows = self
|
||||||
|
.space
|
||||||
|
.elements()
|
||||||
|
.filter_map(|window| self.window_ids.get(window).copied().map(|id| (window.clone(), id)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut active_animation = false;
|
||||||
|
let mut completed_slides = Vec::new();
|
||||||
|
for (output, slide) in &mut self.workspace_slides {
|
||||||
|
if slide.held_progress.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if slide.advance(delta) {
|
||||||
|
active_animation = true;
|
||||||
|
} else {
|
||||||
|
completed_slides.push(*output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
|
||||||
|
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
|
||||||
|
let active = motion.advance_visual(0.0, delta, duration);
|
||||||
|
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
|
||||||
|
active_animation |= active;
|
||||||
|
if !active {
|
||||||
|
let _ = PopupManager::dismiss_popup(root, popup);
|
||||||
|
}
|
||||||
|
active
|
||||||
|
});
|
||||||
|
|
||||||
|
let dim_settings = self.theme_settings.inactive_dim;
|
||||||
|
let duration = if self.animations_enabled {
|
||||||
|
dim_settings.duration_ms
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let mut dim_changed = false;
|
||||||
|
// Include hidden workspace windows: overview can present them too.
|
||||||
|
// Read selection once so these immutable fields can be borrowed alongside
|
||||||
|
// window_focus without allocating a temporary window-ID vector.
|
||||||
|
let selected_window = if self.overview.is_active() {
|
||||||
|
self.overview.selected()
|
||||||
|
} else {
|
||||||
|
self.focused_window
|
||||||
|
};
|
||||||
|
for id in self.window_ids.values().copied() {
|
||||||
|
let selected = selected_window == Some(id);
|
||||||
|
let target = if selected { 1.0 } else { 0.0 };
|
||||||
|
let focus = self
|
||||||
|
.window_focus
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
|
let previous = focus.current;
|
||||||
|
active_animation |= focus.advance_visual(target, delta, duration);
|
||||||
|
dim_changed |= previous != focus.current;
|
||||||
|
}
|
||||||
|
for (_, id) in &windows {
|
||||||
|
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
|
||||||
|
let dim = self
|
||||||
|
.window_dimming
|
||||||
|
.entry(*id)
|
||||||
|
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
|
let previous = dim.current;
|
||||||
|
active_animation |= dim.advance_visual(target, delta, duration);
|
||||||
|
dim_changed |= previous != dim.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ready_to_close = Vec::new();
|
||||||
|
for (id, animation) in &mut self.closing_windows {
|
||||||
|
if animation.advance(delta, self.animations_enabled) {
|
||||||
|
ready_to_close.push(*id);
|
||||||
|
} else if !animation.close_sent {
|
||||||
|
active_animation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in ready_to_close {
|
||||||
|
self.send_window_close(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = self.start_time.elapsed();
|
||||||
|
self.resize_transactions.retain(|id, transaction| {
|
||||||
|
if transaction.expired(now) {
|
||||||
|
tracing::warn!(?id, "resize presentation deadline reached");
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let blocked_workspaces = self
|
||||||
|
.resize_transactions
|
||||||
|
.keys()
|
||||||
|
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let animations_enabled = self.animations_enabled;
|
||||||
|
let animation_speed = self.animation_speed;
|
||||||
|
self.resize_snapshots.retain(|id, snapshot| {
|
||||||
|
if !animations_enabled {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let waiting_for_client = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(*id)
|
||||||
|
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
|
||||||
|
// A shrinking client's destination buffer arrives before the
|
||||||
|
// animated bounds reach it. Keep the old native pixels covering
|
||||||
|
// that strip rather than fading them into the neutral resize fill.
|
||||||
|
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
|
||||||
|
geometry.client.committed_size.is_some_and(|size| {
|
||||||
|
crate::presentation::resize_needs_old_frame(
|
||||||
|
geometry.visual.current,
|
||||||
|
geometry.logical,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let blocked = waiting_for_client || uncovered;
|
||||||
|
let active = crate::presentation::advance_handoff(
|
||||||
|
&mut snapshot.elapsed,
|
||||||
|
&mut snapshot.last_tick,
|
||||||
|
now,
|
||||||
|
blocked,
|
||||||
|
animation_speed,
|
||||||
|
);
|
||||||
|
if !blocked {
|
||||||
|
snapshot.commit.increment();
|
||||||
|
}
|
||||||
|
if !active {
|
||||||
|
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
|
||||||
|
}
|
||||||
|
active
|
||||||
|
});
|
||||||
|
active_animation |= !self.resize_snapshots.is_empty();
|
||||||
|
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
||||||
|
active_animation |= !blocked_workspaces.is_empty();
|
||||||
|
for (workspace, viewport) in &mut self.viewport_animations {
|
||||||
|
if !visible_workspaces.contains(workspace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if blocked_workspaces.contains(workspace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
||||||
|
viewport.current = swipe.position();
|
||||||
|
viewport.velocity = 0.0;
|
||||||
|
} else if self.animations_enabled {
|
||||||
|
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||||
|
} else {
|
||||||
|
viewport.snap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut settled_coupled_widths = Vec::new();
|
||||||
|
for (window, id) in windows {
|
||||||
|
if self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.is_some_and(|workspace| blocked_workspaces.contains(&workspace))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let zooming = geometry.is_zooming();
|
||||||
|
|
||||||
|
let coupled_target = self.viewport_coupled_widths.get(&id).map(|(_, width)| width.target);
|
||||||
|
if coupled_target.is_some() {
|
||||||
|
geometry.visual.target.width = geometry.visual.current.width;
|
||||||
|
geometry.visual.velocity.width = 0.0;
|
||||||
|
}
|
||||||
|
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
|
||||||
|
if let Some(target) = coupled_target {
|
||||||
|
geometry.visual.target.width = target;
|
||||||
|
}
|
||||||
|
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
||||||
|
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
||||||
|
{
|
||||||
|
if zooming {
|
||||||
|
world_x.current = geometry.visual.current.x + viewport.current;
|
||||||
|
world_x.velocity = geometry.visual.velocity.x + viewport.velocity;
|
||||||
|
} else if self.animations_enabled {
|
||||||
|
active_animation |= world_x.advance(delta, self.spring_config);
|
||||||
|
} else {
|
||||||
|
world_x.snap();
|
||||||
|
}
|
||||||
|
if !zooming {
|
||||||
|
geometry.visual.current.x = world_x.current - viewport.current;
|
||||||
|
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
|
||||||
|
let width_active = if self.animations_enabled {
|
||||||
|
width.advance(delta, self.viewport_spring_config)
|
||||||
|
} else {
|
||||||
|
width.snap();
|
||||||
|
false
|
||||||
|
};
|
||||||
|
active_animation |= width_active;
|
||||||
|
geometry.visual.current.width = width.current;
|
||||||
|
geometry.visual.velocity.width = width.velocity;
|
||||||
|
if !width_active {
|
||||||
|
settled_coupled_widths.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(size) = geometry.client.expire_wait(now) {
|
||||||
|
tracing::warn!(
|
||||||
|
?id,
|
||||||
|
configured_width = size.width,
|
||||||
|
configured_height = size.height,
|
||||||
|
committed = ?geometry.client.committed_size,
|
||||||
|
"client did not commit the final configured size within 500 ms"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(size) = geometry.presentation_size_request(now)
|
||||||
|
&& !self.natural_floating_pending.contains(&id)
|
||||||
|
&& let Some(toplevel) = window.toplevel()
|
||||||
|
{
|
||||||
|
toplevel.with_pending_state(|state| {
|
||||||
|
state.size = Some((size.width, size.height).into());
|
||||||
|
});
|
||||||
|
toplevel.send_pending_configure();
|
||||||
|
}
|
||||||
|
let visual = geometry.visual.current;
|
||||||
|
self.space
|
||||||
|
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
|
||||||
|
}
|
||||||
|
for id in settled_coupled_widths {
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
}
|
||||||
|
if !completed_slides.is_empty() {
|
||||||
|
for output in completed_slides {
|
||||||
|
self.workspace_slides.remove(&output);
|
||||||
|
}
|
||||||
|
let visible = self.visible_workspace_ids();
|
||||||
|
let visible_windows = self
|
||||||
|
.window_ids
|
||||||
|
.values()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| {
|
||||||
|
self.workspaces
|
||||||
|
.workspace_for_window(*id)
|
||||||
|
.is_none_or(|workspace| visible.contains(&workspace))
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
self.unmap_invisible_windows(&visible_windows);
|
||||||
|
self.prune_empty_workspaces(&visible);
|
||||||
|
self.send_shell_snapshots();
|
||||||
|
active_animation = true;
|
||||||
|
}
|
||||||
|
active_animation |= self
|
||||||
|
.overview
|
||||||
|
.advance(delta, self.spring_config, self.animations_enabled);
|
||||||
|
self.sync_window_stacking();
|
||||||
|
|
||||||
|
if active_animation || dim_changed {
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
active_animation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn animations_enabled(&self) -> bool {
|
||||||
|
self.animations_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn animation_duration(&self, duration: Duration) -> Duration {
|
||||||
|
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
||||||
|
let Some(animation) = self.closing_windows.get(&id) else {
|
||||||
|
return (1.0, 1.0);
|
||||||
|
};
|
||||||
|
let eased = smoothstep(animation.progress);
|
||||||
|
|
||||||
|
(1.0 - eased * 0.02, (1.0 - eased) as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
|
||||||
|
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
};
|
||||||
|
let Some(output_id) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.contains(workspace).then_some(*output))
|
||||||
|
.or_else(|| self.output_workspaces.output_for_workspace(workspace))
|
||||||
|
else {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
};
|
||||||
|
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
};
|
||||||
|
let Some(size) = self
|
||||||
|
.output_ids
|
||||||
|
.iter()
|
||||||
|
.find(|(_, id)| **id == output_id)
|
||||||
|
.and_then(|(output, _)| self.space.output_geometry(output))
|
||||||
|
.map(|geometry| geometry.size)
|
||||||
|
else {
|
||||||
|
return (0.0, 0.0);
|
||||||
|
};
|
||||||
|
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_workspace_slides(&mut self) -> bool {
|
||||||
|
let active = !self.workspace_slides.is_empty();
|
||||||
|
self.workspace_slides.clear();
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub fn toggle_focused_floating(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut floating_rect = match self.workspaces.placement(window) {
|
||||||
|
Some(WindowPlacement::Tiled) => self
|
||||||
|
.tiled_layout(bounds)
|
||||||
|
.ok()
|
||||||
|
.and_then(|layout| layout.geometry.get(&window).copied())
|
||||||
|
.unwrap_or_else(|| centered_floating_rect(bounds)),
|
||||||
|
Some(WindowPlacement::Floating { rect }) => rect,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
if let Some(handle) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
|
||||||
|
{
|
||||||
|
if matches!(
|
||||||
|
self.workspaces.placement(window),
|
||||||
|
Some(WindowPlacement::Floating { .. })
|
||||||
|
) {
|
||||||
|
self.remember_floating(&handle);
|
||||||
|
} else {
|
||||||
|
floating_rect = self.place_floating(
|
||||||
|
&handle,
|
||||||
|
Some(window),
|
||||||
|
bounds,
|
||||||
|
(floating_rect.width, floating_rect.height),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let axis = self
|
||||||
|
.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.automatic_axis(Some(window), bounds)
|
||||||
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.toggle_floating(window, floating_rect, axis, 0.5) {
|
||||||
|
tracing::error!(%error, ?window, "failed to toggle floating window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_focused_fullscreen(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.toggle_fullscreen(window) {
|
||||||
|
tracing::error!(%error, ?window, "failed to toggle fullscreen window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_focused_maximized(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Super+F from true fullscreen enters decorated maximization.
|
||||||
|
let fullscreen = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(window)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
||||||
|
let enabled = fullscreen || !self.maximized_windows.contains(&window);
|
||||||
|
self.set_window_maximized(window, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) {
|
||||||
|
if self.workspaces.workspace_for_window(window).is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if enabled {
|
||||||
|
let _ = self.workspaces.set_fullscreen(window, false);
|
||||||
|
self.maximized_windows.insert(window);
|
||||||
|
} else {
|
||||||
|
self.maximized_windows.remove(&window);
|
||||||
|
}
|
||||||
|
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
||||||
|
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
||||||
|
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
|
||||||
|
&& let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
|
||||||
|
{
|
||||||
|
if enabled {
|
||||||
|
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
|
||||||
|
self.maximized_column_widths.entry(window).or_insert(column.width);
|
||||||
|
}
|
||||||
|
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
|
||||||
|
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
|
||||||
|
let _ = layout.set_column_width(window, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
||||||
|
!self.overview.is_presenting()
|
||||||
|
&& self
|
||||||
|
.output_id(output)
|
||||||
|
.and_then(|id| self.output_workspaces.active_workspace(id))
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) {
|
||||||
|
match self.workspaces.set_fullscreen(window, enabled) {
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, ?window, enabled, "failed to set fullscreen window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_floating_window(&self, window: &Window) -> bool {
|
||||||
|
self.window_ids
|
||||||
|
.get(window)
|
||||||
|
.and_then(|id| self.workspaces.placement(*id))
|
||||||
|
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_floating_window_geometry(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
location: Point<i32, Logical>,
|
||||||
|
size: Size<i32, Logical>,
|
||||||
|
) {
|
||||||
|
let Some(id) = self.window_ids.get(window).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.floating_resize_anchors.remove(&id);
|
||||||
|
let rect = Rect::new(
|
||||||
|
location.x as f64,
|
||||||
|
location.y as f64,
|
||||||
|
size.w.max(1) as f64,
|
||||||
|
size.h.max(1) as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.set_floating_rect(id, rect) {
|
||||||
|
tracing::error!(%error, ?id, "failed to update floating window geometry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct manipulation follows the pointer, including while the client is
|
||||||
|
// still drawing its next buffer. Rendering and hit testing share this rect.
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
self.resize_snapshots.remove(&id);
|
||||||
|
let geometry = self
|
||||||
|
.window_geometry
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
||||||
|
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
||||||
|
&& let Some(toplevel) = window.toplevel()
|
||||||
|
{
|
||||||
|
toplevel.with_pending_state(|state| {
|
||||||
|
state.size = Some((size.width, size.height).into());
|
||||||
|
});
|
||||||
|
toplevel.send_pending_configure();
|
||||||
|
}
|
||||||
|
self.scrolling_world_x.remove(&id);
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
self.space.map_element(window.clone(), location, false);
|
||||||
|
self.sync_window_stacking();
|
||||||
|
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
||||||
|
// Cursor rendering reads current_location(), which would lock it again.
|
||||||
|
// The event-loop epilogue coalesces this redraw after the grab returns.
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn output_bounds(&self) -> Option<Rect> {
|
||||||
|
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
|
||||||
|
self.output_bounds_for(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn floating_outputs(&self) -> Vec<(String, Rect)> {
|
||||||
|
self.space
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|output| self.output_bounds_for(output).map(|work| (output.name(), work)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn floating_metadata(window: &Window) -> (Option<String>, Option<WlSurface>) {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.map(|top| {
|
||||||
|
let app = with_states(top.wl_surface(), |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.and_then(|data| data.lock().ok().and_then(|data| data.app_id.clone()))
|
||||||
|
});
|
||||||
|
(app, top.parent())
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn floating_obstacles(&self, exclude: Option<WindowId>, work: Rect) -> Vec<(u64, Rect)> {
|
||||||
|
self.space
|
||||||
|
.elements()
|
||||||
|
.filter_map(|window| {
|
||||||
|
let id = *self.window_ids.get(window)?;
|
||||||
|
if Some(id) == exclude {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let workspace = self.workspaces.workspace_for_window(id)?;
|
||||||
|
let owner = self.output_workspaces.output_for_workspace(workspace)?;
|
||||||
|
if self.output_workspaces.active_workspace(owner) != Some(workspace) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.workspaces
|
||||||
|
.workspace(workspace)?
|
||||||
|
.fullscreen
|
||||||
|
.is_some_and(|full| full != id)
|
||||||
|
&& !matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rect = self.presented_window_rect(id)?;
|
||||||
|
crate::floating::intersection(rect, work).map(|visible| (id.0, visible))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn place_floating(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
id: Option<WindowId>,
|
||||||
|
work: Rect,
|
||||||
|
size: (f64, f64),
|
||||||
|
overrides: Option<(Option<f64>, Option<f64>)>,
|
||||||
|
) -> Rect {
|
||||||
|
use crate::floating::{centered, clamp_to_work, intersection, min_overlap};
|
||||||
|
let outputs = self.floating_outputs();
|
||||||
|
let output = outputs
|
||||||
|
.iter()
|
||||||
|
.find(|(_, area)| *area == work)
|
||||||
|
.map(|(name, _)| name.as_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let (app, parent) = Self::floating_metadata(window);
|
||||||
|
let parent_rect = parent
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|surface| {
|
||||||
|
self.window_ids.iter().find_map(|(window, id)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|top| top.wl_surface() == surface)
|
||||||
|
.then_some(*id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|parent| self.presented_window_rect(parent))
|
||||||
|
.and_then(|rect| intersection(rect, work));
|
||||||
|
let remembered = id
|
||||||
|
.and_then(|id| self.floating_window_memory.get(&id))
|
||||||
|
.or_else(|| {
|
||||||
|
if parent.is_none() {
|
||||||
|
app.as_deref().and_then(|app| self.floating_memory.get(app))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|entry| entry.output == output)
|
||||||
|
.and_then(|entry| entry.restore(&outputs));
|
||||||
|
let (width, height) = overrides.unwrap_or_default();
|
||||||
|
let constraints = window
|
||||||
|
.toplevel()
|
||||||
|
.map(|top| {
|
||||||
|
with_states(top.wl_surface(), |states| {
|
||||||
|
let mut cache = states.cached_state.get::<SurfaceCachedState>();
|
||||||
|
let state = cache.current();
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: state.min_size.w.max(1) as f64,
|
||||||
|
min_height: state.min_size.h.max(1) as f64,
|
||||||
|
max_width: (state.max_size.w > 0).then_some(state.max_size.w as f64),
|
||||||
|
max_height: (state.max_size.h > 0).then_some(state.max_size.h as f64),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let stored_size = if parent_rect.is_none() {
|
||||||
|
remembered.map(|r| (r.width, r.height)).unwrap_or(size)
|
||||||
|
} else {
|
||||||
|
size
|
||||||
|
};
|
||||||
|
let sized = constrained_floating_rect(
|
||||||
|
Rect::new(0., 0., width.unwrap_or(stored_size.0), height.unwrap_or(stored_size.1)),
|
||||||
|
constraints,
|
||||||
|
);
|
||||||
|
let dimensions = (sized.width, sized.height);
|
||||||
|
let rect = if let Some(parent) = parent_rect {
|
||||||
|
centered(dimensions, parent)
|
||||||
|
} else if let Some(mut rect) = remembered {
|
||||||
|
rect.width = dimensions.0;
|
||||||
|
rect.height = dimensions.1;
|
||||||
|
rect
|
||||||
|
} else {
|
||||||
|
let others = self
|
||||||
|
.floating_obstacles(id, work)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, r)| r)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let anchor = id
|
||||||
|
.and_then(|id| self.floating_placement_anchors.get(&id).copied())
|
||||||
|
.or_else(|| {
|
||||||
|
self.focused_window
|
||||||
|
.filter(|focused| Some(*focused) != id)
|
||||||
|
.and_then(|focused| self.presented_window_rect(focused))
|
||||||
|
})
|
||||||
|
.and_then(|rect| intersection(rect, work))
|
||||||
|
.unwrap_or(work);
|
||||||
|
min_overlap(
|
||||||
|
dimensions,
|
||||||
|
work,
|
||||||
|
&others,
|
||||||
|
(anchor.x + anchor.width / 2., anchor.y + anchor.height / 2.),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|| self.floating_cascade.place(output, work, dimensions))
|
||||||
|
};
|
||||||
|
clamp_to_work(rect, work)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remember_floating(&mut self, window: &Window) {
|
||||||
|
let Some(id) = self.window_ids.get(window).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (app, parent) = Self::floating_metadata(window);
|
||||||
|
if parent.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A drag can finish on another output without changing workspace ownership.
|
||||||
|
let Some((output, work)) = self
|
||||||
|
.floating_outputs()
|
||||||
|
.into_iter()
|
||||||
|
.max_by(|(_, a), (_, b)| {
|
||||||
|
let area = |work| crate::floating::intersection(rect, work).map_or(0., |r| r.width * r.height);
|
||||||
|
area(*a).total_cmp(&area(*b))
|
||||||
|
})
|
||||||
|
.filter(|(_, work)| crate::floating::intersection(rect, *work).is_some())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let entry = crate::floating::Remembered::new(output, rect, work);
|
||||||
|
self.floating_window_memory.insert(id, entry.clone());
|
||||||
|
if let Some(app) = app {
|
||||||
|
self.floating_memory.save(app, entry);
|
||||||
|
}
|
||||||
|
if self.nested_backend.is_none()
|
||||||
|
&& let Some(path) = crate::floating::Memory::path()
|
||||||
|
&& let Err(error) = self.floating_save_worker.queue(path, &self.floating_memory)
|
||||||
|
{
|
||||||
|
tracing::warn!(%error,"could not queue floating window placement save");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn floating_snap_lines(
|
||||||
|
&self,
|
||||||
|
window: &Window,
|
||||||
|
raw: Rect,
|
||||||
|
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
|
||||||
|
let id = self.window_ids.get(window).copied();
|
||||||
|
let work = self
|
||||||
|
.floating_outputs()
|
||||||
|
.into_iter()
|
||||||
|
.max_by(|(_, a), (_, b)| {
|
||||||
|
let area = |work| crate::floating::intersection(raw, work).map_or(0., |r| r.width * r.height);
|
||||||
|
area(*a).total_cmp(&area(*b))
|
||||||
|
})
|
||||||
|
.filter(|(_, work)| crate::floating::intersection(raw, *work).is_some())
|
||||||
|
.map(|(_, work)| work)
|
||||||
|
.or_else(|| id.and_then(|id| self.floating_bounds_for_window(id)));
|
||||||
|
let Some(work) = work else {
|
||||||
|
return Default::default();
|
||||||
|
};
|
||||||
|
crate::floating::snap_lines(raw, work, &self.floating_obstacles(id, work))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn floating_snap_bypassed(&self) -> bool {
|
||||||
|
self.seat
|
||||||
|
.get_keyboard()
|
||||||
|
.is_some_and(|keyboard| keyboard.modifier_state().shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn floating_bounds_for_window(&self, window: WindowId) -> Option<Rect> {
|
||||||
|
let output_id = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(window)
|
||||||
|
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace));
|
||||||
|
self.output_ids
|
||||||
|
.iter()
|
||||||
|
.find(|(_, id)| Some(**id) == output_id)
|
||||||
|
.and_then(|(output, _)| self.output_bounds_for(output))
|
||||||
|
.or_else(|| self.output_bounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_bounds_for(&self, output: &Output) -> Option<Rect> {
|
||||||
|
let geometry = self.space.output_geometry(output)?;
|
||||||
|
let zone = layer_map_for_output(output).non_exclusive_zone();
|
||||||
|
|
||||||
|
Some(Rect::new(
|
||||||
|
(geometry.loc.x + zone.loc.x) as f64,
|
||||||
|
(geometry.loc.y + zone.loc.y) as f64,
|
||||||
|
zone.size.w.max(0) as f64,
|
||||||
|
zone.size.h.max(0) as f64,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn full_output_bounds_for(&self, output: &Output) -> Option<Rect> {
|
||||||
|
let geometry = self.space.output_geometry(output)?;
|
||||||
|
|
||||||
|
Some(Rect::new(
|
||||||
|
geometry.loc.x as f64,
|
||||||
|
geometry.loc.y as f64,
|
||||||
|
geometry.size.w as f64,
|
||||||
|
geometry.size.h as f64,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub fn surface_under(&self, position: Point<f64, Logical>) -> Option<(WlSurface, Point<f64, Logical>)> {
|
||||||
|
if self.session_lock.active {
|
||||||
|
return self.lock_surface_under(position);
|
||||||
|
}
|
||||||
|
if self.input_capture.active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.layer_surface_under(position, &[Layer::Overlay, Layer::Top])
|
||||||
|
.map(|(_, surface, origin)| (surface, origin))
|
||||||
|
.or_else(|| self.window_surface_under(position))
|
||||||
|
.or_else(|| {
|
||||||
|
self.layer_surface_under(position, &[Layer::Bottom, Layer::Background])
|
||||||
|
.map(|(_, surface, origin)| (surface, origin))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layer_under(&self, position: Point<f64, Logical>) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
|
||||||
|
if self.session_lock.active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Some(layer) = self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) {
|
||||||
|
return Some(layer);
|
||||||
|
}
|
||||||
|
if self.window_surface_under(position).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layer_surface_under(position, &[Layer::Bottom, Layer::Background])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn window_surface_under(
|
||||||
|
&self,
|
||||||
|
position: Point<f64, Logical>,
|
||||||
|
) -> Option<(WlSurface, Point<f64, Logical>)> {
|
||||||
|
if self.overview.is_active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
|
self.space.elements().rev().find_map(|window| {
|
||||||
|
let id = self.window_ids.get(window)?;
|
||||||
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let visual = self.presented_window_rect(*id)?;
|
||||||
|
let inside_visual = position.x >= visual.x
|
||||||
|
&& position.y >= visual.y
|
||||||
|
&& position.x < visual.x + visual.width
|
||||||
|
&& position.y < visual.y + visual.height;
|
||||||
|
if !inside_visual {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (source_x, source_y) = self.inverse_presented_window_point(*id, position.x, position.y)?;
|
||||||
|
let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64();
|
||||||
|
|
||||||
|
let surface = window.surface_under(source_point, WindowSurfaceType::ALL);
|
||||||
|
let (surface, surface_location) = match surface {
|
||||||
|
Some(hit) => hit,
|
||||||
|
None if self.window_fills_visual_bounds(*id, workspace) => {
|
||||||
|
(window.toplevel()?.wl_surface().clone(), Point::from((0, 0)))
|
||||||
|
}
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let surface_point = source_point - surface_location.to_f64();
|
||||||
|
Some((surface, position - surface_point))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
|
||||||
|
self.maximized_windows.contains(&id)
|
||||||
|
|| self
|
||||||
|
.workspaces
|
||||||
|
.workspace(workspace)
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn layer_surface_under(
|
||||||
|
&self,
|
||||||
|
position: Point<f64, Logical>,
|
||||||
|
layers: &[Layer],
|
||||||
|
) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
|
||||||
|
let output = self.space.output_under(position).next()?;
|
||||||
|
let output_geometry = self.space.output_geometry(output)?;
|
||||||
|
let output_position = position - output_geometry.loc.to_f64();
|
||||||
|
let map = layer_map_for_output(output);
|
||||||
|
|
||||||
|
for requested in layers {
|
||||||
|
if *requested == Layer::Top && self.output_has_fullscreen(output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for layer in map.layers_on(*requested).rev() {
|
||||||
|
let geometry = map.layer_geometry(layer)?;
|
||||||
|
let layer_position = output_position - geometry.loc.to_f64();
|
||||||
|
let Some((surface, surface_location)) = layer.surface_under(layer_position, WindowSurfaceType::ALL)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let origin = output_geometry.loc + geometry.loc + surface_location;
|
||||||
|
|
||||||
|
return Some((layer.clone(), surface, origin.to_f64()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_cursor_frame(&self, output: &Output) {
|
||||||
|
let CursorImageStatus::Surface(surface) = &self.cursor_status else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(pointer) = self.seat.get_pointer() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.space.output_under(pointer.current_location()).next() != Some(output) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
send_frames_surface_tree(
|
||||||
|
surface,
|
||||||
|
output,
|
||||||
|
self.start_time.elapsed(),
|
||||||
|
Some(Duration::ZERO),
|
||||||
|
|_, _| Some(output.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
|
||||||
|
let overview_active = self.overview.is_active();
|
||||||
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
|
|
||||||
|
let hit = |window: &Window| -> Option<Window> {
|
||||||
|
if !self.window_content_ready(window) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = self.window_ids.get(window)?;
|
||||||
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let visual = self.presented_window_rect(*id)?;
|
||||||
|
let caption_height = if overview_active { 34.0 } else { 0.0 };
|
||||||
|
let inside_visual = position.x >= visual.x
|
||||||
|
&& position.y >= visual.y
|
||||||
|
&& position.x < visual.x + visual.width
|
||||||
|
&& position.y < visual.y + visual.height + caption_height;
|
||||||
|
if !inside_visual {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !overview_active {
|
||||||
|
let (source_x, source_y) = self.inverse_presented_window_point(*id, position.x, position.y)?;
|
||||||
|
let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64();
|
||||||
|
if window.surface_under(source_point, WindowSurfaceType::ALL).is_none()
|
||||||
|
&& !self.window_fills_visual_bounds(*id, workspace)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(window.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
if overview_active {
|
||||||
|
let mut candidates = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
||||||
|
// Match Overview's front-to-back render order during overlapping motion.
|
||||||
|
candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0)));
|
||||||
|
candidates.iter().find_map(hit)
|
||||||
|
} else {
|
||||||
|
self.space.elements().rev().find_map(hit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> {
|
||||||
|
let output = self.space.output_under(position).next()?;
|
||||||
|
self.output_workspaces.active_workspace(self.output_id(output)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool {
|
||||||
|
let Some(output_id) = self.output_id(output) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let workspace = self.workspaces.workspace_for_window(window);
|
||||||
|
workspace == self.output_workspaces.active_workspace(output_id)
|
||||||
|
|| self
|
||||||
|
.workspace_slides
|
||||||
|
.get(&output_id)
|
||||||
|
.is_some_and(|slide| workspace.is_some_and(|workspace| slide.contains(workspace)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
|
||||||
|
if !self.overview.is_presenting() {
|
||||||
|
return Some((1.0, 1.0));
|
||||||
|
}
|
||||||
|
let id = self.window_ids.get(window)?;
|
||||||
|
let geometry = self.window_geometry.get(id)?;
|
||||||
|
let source = geometry.client.committed_size?;
|
||||||
|
let presented = self.presented_window_rect(*id)?;
|
||||||
|
|
||||||
|
if source.width <= 0 || source.height <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((
|
||||||
|
presented.width / f64::from(source.width),
|
||||||
|
presented.height / f64::from(source.height),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
|
||||||
|
let id = self.window_ids.get(window)?;
|
||||||
|
let rect = self.presented_window_rect(*id)?;
|
||||||
|
let size = ClientSize::from_rect(rect);
|
||||||
|
Some(Rectangle::new(
|
||||||
|
(rect.x.round() as i32, rect.y.round() as i32).into(),
|
||||||
|
(size.width, size.height).into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
|
||||||
|
window_has_buffer(window)
|
||||||
|
&& self
|
||||||
|
.window_ids
|
||||||
|
.get(window)
|
||||||
|
.and_then(|id| self.window_geometry.get(id))
|
||||||
|
.is_some_and(|geometry| geometry.client.committed_size.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub(super) fn visible_workspace_ids(&self) -> HashSet<WorkspaceId> {
|
||||||
|
let mut visible = self
|
||||||
|
.output_workspaces
|
||||||
|
.connected_outputs()
|
||||||
|
.filter_map(|output| self.output_workspaces.active_workspace(output))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
visible.extend(
|
||||||
|
self.workspace_slides
|
||||||
|
.values()
|
||||||
|
.flat_map(|slide| slide.items.iter().map(|item| item.workspace)),
|
||||||
|
);
|
||||||
|
visible
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn prune_empty_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
|
||||||
|
let mut protected = visible.clone();
|
||||||
|
protected.extend(self.output_workspaces.history_workspaces());
|
||||||
|
for workspace in self.workspaces.prune_empty(&protected) {
|
||||||
|
self.output_workspaces.forget_workspace(workspace);
|
||||||
|
self.viewport_animations.remove(&workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
for (window, id) in &self.window_ids {
|
||||||
|
if visible.contains(id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let was_mapped = self.space.element_location(window).is_some();
|
||||||
|
changed |= was_mapped;
|
||||||
|
if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) {
|
||||||
|
geometry.settle_presentation();
|
||||||
|
}
|
||||||
|
self.space.unmap_elem(window);
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn relayout(&mut self) {
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
|
let visible_workspaces = self.visible_workspace_ids();
|
||||||
|
self.prune_empty_workspaces(&visible_workspaces);
|
||||||
|
if self.session_lock.active {
|
||||||
|
self.configure_lock_surfaces();
|
||||||
|
}
|
||||||
|
// Consume idle time before setting new targets; it must not become a
|
||||||
|
// large first animation step after a keypress on an idle desktop.
|
||||||
|
self.advance_animations(Instant::now());
|
||||||
|
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);
|
||||||
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
|
let constraints = self.window_constraints();
|
||||||
|
let mut visible = HashSet::new();
|
||||||
|
let mut placements = Vec::new();
|
||||||
|
let mut visible_workspace_pairs = Vec::new();
|
||||||
|
|
||||||
|
for output in outputs {
|
||||||
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
visible_workspace_pairs.push((output.clone(), active_workspace));
|
||||||
|
if let Some(slide) = self.workspace_slides.get(&output_id) {
|
||||||
|
for item in &slide.items {
|
||||||
|
if item.workspace != active_workspace {
|
||||||
|
visible_workspace_pairs.push((output.clone(), item.workspace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (output, workspace_id) in visible_workspace_pairs {
|
||||||
|
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
for layer in layer_map_for_output(&output).layers() {
|
||||||
|
crate::handlers::set_surface_tree_scale(layer.wl_surface(), scale);
|
||||||
|
}
|
||||||
|
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let workspace_fullscreen = workspace.fullscreen;
|
||||||
|
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
|
||||||
|
let workspace_focus = workspace.last_focused;
|
||||||
|
let focused = self
|
||||||
|
.focused_window
|
||||||
|
.filter(|window| self.workspaces.workspace_for_window(*window) == Some(workspace_id))
|
||||||
|
.or(workspace_focus);
|
||||||
|
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let layout =
|
||||||
|
match workspace
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||||
|
{
|
||||||
|
Ok(layout) => layout,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let viewport_target = workspace.layout.viewport_x();
|
||||||
|
let viewport_motion = viewport_target.map(|target| {
|
||||||
|
let viewport = self
|
||||||
|
.viewport_animations
|
||||||
|
.entry(workspace_id)
|
||||||
|
.or_insert_with(|| AnimatedValue::new(target));
|
||||||
|
let target_changed = (viewport.target - target).abs() > 0.001;
|
||||||
|
viewport.retarget_preserving_motion(target);
|
||||||
|
if !self.animations_enabled {
|
||||||
|
viewport.snap();
|
||||||
|
}
|
||||||
|
(viewport.current, target_changed)
|
||||||
|
});
|
||||||
|
let viewport_current = viewport_motion.map(|(current, _)| current);
|
||||||
|
let viewport_target_changed = viewport_motion.is_some_and(|(_, changed)| changed);
|
||||||
|
|
||||||
|
for warning in layout.warnings {
|
||||||
|
tracing::warn!(
|
||||||
|
window = ?warning.window,
|
||||||
|
kind = ?warning.kind,
|
||||||
|
requested = warning.requested,
|
||||||
|
assigned = warning.assigned,
|
||||||
|
"window size constraint could not be satisfied exactly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (window, id) in &self.window_ids {
|
||||||
|
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(toplevel) = window.toplevel() {
|
||||||
|
crate::handlers::set_surface_tree_scale(toplevel.wl_surface(), scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
|
||||||
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
|
fullscreen_bounds
|
||||||
|
} else if is_maximized && !is_scrolling_layout {
|
||||||
|
maximized_rect(bounds, self.gap_config.outer)
|
||||||
|
} else {
|
||||||
|
match self.workspaces.placement(*id) {
|
||||||
|
Some(WindowPlacement::Tiled) => {
|
||||||
|
let Some(rect) = layout.geometry.get(id).copied() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
rect
|
||||||
|
}
|
||||||
|
Some(WindowPlacement::Floating { rect }) => {
|
||||||
|
let constrained =
|
||||||
|
constrained_floating_rect(rect, constraints.get(id).copied().unwrap_or_default());
|
||||||
|
if constrained != rect {
|
||||||
|
let _ = self.workspaces.set_floating_rect(*id, constrained);
|
||||||
|
}
|
||||||
|
constrained
|
||||||
|
}
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let is_fullscreen = workspace_fullscreen == Some(*id);
|
||||||
|
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
|
||||||
|
|
||||||
|
visible.insert(*id);
|
||||||
|
let scrolling = if !is_fullscreen && !is_floating && (!is_maximized || is_scrolling_layout) {
|
||||||
|
viewport_target
|
||||||
|
.zip(viewport_current)
|
||||||
|
.map(|(target, current)| (workspace_id, rect.x + target, current))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
placements.push((
|
||||||
|
window.clone(),
|
||||||
|
*id,
|
||||||
|
rect,
|
||||||
|
is_fullscreen,
|
||||||
|
is_maximized,
|
||||||
|
is_floating,
|
||||||
|
scrolling,
|
||||||
|
pending_column_width_cycles.contains(id) && viewport_target_changed,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut layout_changed = self.unmap_invisible_windows(&visible);
|
||||||
|
|
||||||
|
let now = self.start_time.elapsed();
|
||||||
|
|
||||||
|
let mut scrolling_world_x = HashMap::new();
|
||||||
|
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
||||||
|
|
||||||
|
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements {
|
||||||
|
let was_mapped = self.space.element_location(&window).is_some();
|
||||||
|
// A viewport-coupled width must never override fullscreen/floating geometry.
|
||||||
|
if scrolling.is_none() {
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
}
|
||||||
|
let (geometry, had_geometry) = match self.window_geometry.entry(id) {
|
||||||
|
Entry::Occupied(entry) => (entry.into_mut(), true),
|
||||||
|
Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false),
|
||||||
|
};
|
||||||
|
let mode = if is_fullscreen {
|
||||||
|
PresentationMode::Fullscreen
|
||||||
|
} else if is_maximized {
|
||||||
|
PresentationMode::Maximized
|
||||||
|
} else {
|
||||||
|
PresentationMode::Normal
|
||||||
|
};
|
||||||
|
layout_changed |= !had_geometry || geometry.logical != rect;
|
||||||
|
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
|
||||||
|
if !was_mapped && (had_geometry || is_fullscreen) {
|
||||||
|
geometry.settle_presentation();
|
||||||
|
}
|
||||||
|
if !self.animations_enabled {
|
||||||
|
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||||
|
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
||||||
|
}
|
||||||
|
if geometry.is_zooming() {
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
}
|
||||||
|
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
||||||
|
let restored_world_x =
|
||||||
|
restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x);
|
||||||
|
let mut animated_world_x = previous_scrolling_world_x
|
||||||
|
.remove(&id)
|
||||||
|
.filter(|(previous_workspace, _)| *previous_workspace == workspace)
|
||||||
|
.map(|(_, world_x)| world_x)
|
||||||
|
.unwrap_or_else(|| AnimatedValue::new(restored_world_x));
|
||||||
|
animated_world_x.set_target(world_x);
|
||||||
|
if !self.animations_enabled {
|
||||||
|
animated_world_x.snap();
|
||||||
|
}
|
||||||
|
if !geometry.is_zooming() {
|
||||||
|
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
||||||
|
geometry.visual.velocity.x = animated_world_x.velocity;
|
||||||
|
}
|
||||||
|
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
||||||
|
|
||||||
|
let coupled = !geometry.is_zooming()
|
||||||
|
&& (couple_width
|
||||||
|
|| self
|
||||||
|
.viewport_coupled_widths
|
||||||
|
.get(&id)
|
||||||
|
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace));
|
||||||
|
if coupled {
|
||||||
|
let width = self
|
||||||
|
.viewport_coupled_widths
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width)));
|
||||||
|
if width.0 != workspace {
|
||||||
|
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
|
||||||
|
}
|
||||||
|
width.1.retarget_preserving_motion(rect.width);
|
||||||
|
if !self.animations_enabled {
|
||||||
|
width.1.snap();
|
||||||
|
}
|
||||||
|
geometry.visual.current.width = width.1.current;
|
||||||
|
geometry.visual.velocity.width = width.1.velocity;
|
||||||
|
} else if pending_column_width_cycles.contains(&id) {
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let visual = geometry.visual.current;
|
||||||
|
let natural_pending =
|
||||||
|
self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized;
|
||||||
|
if natural_pending {
|
||||||
|
requested_size = None;
|
||||||
|
}
|
||||||
|
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
||||||
|
|
||||||
|
self.space.map_element(window.clone(), location, false);
|
||||||
|
if let Some(toplevel) = window.toplevel() {
|
||||||
|
let state_changed = toplevel.with_pending_state(|state| {
|
||||||
|
if natural_pending {
|
||||||
|
state.size = None;
|
||||||
|
} else if let Some(size) = requested_size {
|
||||||
|
state.size = Some((size.width, size.height).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullscreen_changed = if is_fullscreen {
|
||||||
|
state.states.set(xdg_toplevel::State::Fullscreen)
|
||||||
|
} else {
|
||||||
|
state.states.unset(xdg_toplevel::State::Fullscreen)
|
||||||
|
};
|
||||||
|
|
||||||
|
let maximized_changed = if is_maximized {
|
||||||
|
state.states.set(xdg_toplevel::State::Maximized)
|
||||||
|
} else {
|
||||||
|
state.states.unset(xdg_toplevel::State::Maximized)
|
||||||
|
};
|
||||||
|
let tiled = !is_floating && !is_fullscreen;
|
||||||
|
let tiled_changed = if tiled {
|
||||||
|
state.states.set(xdg_toplevel::State::TiledLeft)
|
||||||
|
| state.states.set(xdg_toplevel::State::TiledRight)
|
||||||
|
| state.states.set(xdg_toplevel::State::TiledTop)
|
||||||
|
| state.states.set(xdg_toplevel::State::TiledBottom)
|
||||||
|
} else {
|
||||||
|
state.states.unset(xdg_toplevel::State::TiledLeft)
|
||||||
|
| state.states.unset(xdg_toplevel::State::TiledRight)
|
||||||
|
| state.states.unset(xdg_toplevel::State::TiledTop)
|
||||||
|
| state.states.unset(xdg_toplevel::State::TiledBottom)
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoration_mode = if is_floating && !is_fullscreen && !is_maximized {
|
||||||
|
DecorationMode::ClientSide
|
||||||
|
} else {
|
||||||
|
DecorationMode::ServerSide
|
||||||
|
};
|
||||||
|
let decoration_changed = state.decoration_mode != Some(decoration_mode);
|
||||||
|
state.decoration_mode = Some(decoration_mode);
|
||||||
|
|
||||||
|
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
|
||||||
|
});
|
||||||
|
|
||||||
|
if (natural_pending || requested_size.is_some() || state_changed)
|
||||||
|
&& let Some(serial) = toplevel.send_pending_configure()
|
||||||
|
&& requested_size.is_some()
|
||||||
|
&& had_geometry
|
||||||
|
&& self.animations_enabled
|
||||||
|
{
|
||||||
|
self.resize_transactions.insert(
|
||||||
|
id,
|
||||||
|
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
||||||
|
.with_source_geometry(window.geometry()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.scrolling_world_x = scrolling_world_x;
|
||||||
|
if layout_changed {
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
self.sync_window_stacking();
|
||||||
|
self.retarget_overview();
|
||||||
|
self.send_shell_snapshots();
|
||||||
|
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||||
|
if let Some(id) = self.window_ids.get(window) {
|
||||||
|
self.window_stack.raise(*id);
|
||||||
|
}
|
||||||
|
self.space.raise_element(window, activate);
|
||||||
|
self.sync_window_stacking();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync_window_stacking(&mut self) {
|
||||||
|
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
||||||
|
windows.sort_by_key(|window| {
|
||||||
|
let id = self.window_ids.get(window).copied();
|
||||||
|
let geometry = id.and_then(|id| self.window_geometry.get(&id));
|
||||||
|
let floating =
|
||||||
|
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
|
||||||
|
let priority = crate::stacking::layer_priority(
|
||||||
|
floating,
|
||||||
|
geometry.is_some_and(|geometry| geometry.is_zooming()),
|
||||||
|
geometry.is_some_and(|geometry| geometry.is_fullscreen()),
|
||||||
|
floating
|
||||||
|
&& id.is_some_and(|id| {
|
||||||
|
self.floating_above_fullscreen.get(&id).is_some_and(|parent| {
|
||||||
|
self.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(*parent))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
(priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id)))
|
||||||
|
});
|
||||||
|
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for window in windows {
|
||||||
|
self.space.raise_element(&window, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
||||||
|
let constraints = self.window_constraints();
|
||||||
|
let focused = self.focused_window;
|
||||||
|
self.workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
|
self.window_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(window, id)| {
|
||||||
|
let toplevel = window.toplevel()?;
|
||||||
|
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||||
|
let state = cached.current();
|
||||||
|
(state.min_size, state.max_size)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some((
|
||||||
|
*id,
|
||||||
|
SizeConstraints {
|
||||||
|
min_width: minimum.w.max(1) as f64,
|
||||||
|
min_height: minimum.h.max(1) as f64,
|
||||||
|
max_width: (maximum.w > 0).then_some(maximum.w as f64),
|
||||||
|
max_height: (maximum.h > 0).then_some(maximum.h as f64),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn arrange_layers(&self) {
|
||||||
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for output in outputs {
|
||||||
|
layer_map_for_output(&output).arrange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub fn focus_direction(&mut self, direction: Direction) {
|
||||||
|
self.focus_direction_with_slide(direction, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn focus_swipe_is_current(&self, swipe: &FocusSwipe) -> bool {
|
||||||
|
self.animations_enabled
|
||||||
|
&& !self.session_lock.active
|
||||||
|
&& !self.overview.is_presenting()
|
||||||
|
&& self.workspaces.active_id() == swipe.workspace
|
||||||
|
&& self.focused_window == Some(swipe.from)
|
||||||
|
&& self.workspaces.workspace(swipe.workspace).is_some_and(|workspace| {
|
||||||
|
workspace.fullscreen.is_none()
|
||||||
|
&& workspace.layout.contains(swipe.from)
|
||||||
|
&& workspace.layout.contains(swipe.to)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preview_focus_swipe(
|
||||||
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
|
gesture_direction: SwipeDirection,
|
||||||
|
progress: f64,
|
||||||
|
) {
|
||||||
|
if self.focus_swipe.is_none() {
|
||||||
|
if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(from) = self.focused_window else { return };
|
||||||
|
let Some(bounds) = self.output_bounds() else { return };
|
||||||
|
let workspace = self.workspaces.active();
|
||||||
|
if workspace.fullscreen.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(Some(to)) = workspace.layout.directional_neighbor(from, direction, bounds) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let workspace_id = workspace.id;
|
||||||
|
let start = self
|
||||||
|
.viewport_animations
|
||||||
|
.get(&workspace_id)
|
||||||
|
.map(|viewport| viewport.current)
|
||||||
|
.or_else(|| workspace.layout.viewport_x())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let mut candidate = workspace.layout.clone();
|
||||||
|
if let WorkspaceLayout::Scrolling(layout) = &mut candidate
|
||||||
|
&& layout.slide_focus_from(from, to).is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if candidate
|
||||||
|
.geometry_with_constraints(bounds, self.gap_config, &self.window_constraints(), Some(to))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.focus_swipe = Some(FocusSwipe {
|
||||||
|
workspace: workspace_id,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
direction,
|
||||||
|
gesture_direction,
|
||||||
|
start,
|
||||||
|
destination: candidate.viewport_x().unwrap_or(start),
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
self.swipe.mark_preview_started();
|
||||||
|
}
|
||||||
|
if let Some(swipe) = &mut self.focus_swipe {
|
||||||
|
swipe.progress = progress;
|
||||||
|
}
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
|
||||||
|
let Some(swipe) = self.focus_swipe.take() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let current = self.focus_swipe_is_current(&swipe);
|
||||||
|
// Release from the last input position even if no frame rendered that update.
|
||||||
|
if current && let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) {
|
||||||
|
viewport.current = swipe.position();
|
||||||
|
viewport.velocity = 0.0;
|
||||||
|
}
|
||||||
|
let neighbor = self.output_bounds().and_then(|bounds| {
|
||||||
|
self.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.directional_neighbor(swipe.from, swipe.direction, bounds)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
});
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
if current && direction == Some(swipe.gesture_direction) && neighbor == Some(swipe.to) {
|
||||||
|
self.focus_direction_from_swipe(swipe.direction);
|
||||||
|
} else {
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_direction_from_swipe(&mut self, direction: Direction) {
|
||||||
|
self.focus_direction_with_slide(direction, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn focus_direction_with_slide(&mut self, direction: Direction, slide: bool) {
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.focus_overview_direction(direction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(current) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(Some(next)) = self
|
||||||
|
.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.directional_neighbor(current, direction, bounds)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.focus_window(next) {
|
||||||
|
tracing::error!(%error, ?next, "failed to update workspace focus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if slide
|
||||||
|
&& let WorkspaceLayout::Scrolling(layout) = &mut self.workspaces.active_mut().layout
|
||||||
|
&& let Err(error) = layout.slide_focus_from(current, next)
|
||||||
|
{
|
||||||
|
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
|
||||||
|
}
|
||||||
|
let Some(window) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(surface) = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.focused_window = Some(next);
|
||||||
|
self.raise_window(&window, true);
|
||||||
|
self.seat.get_keyboard().expect("seat has a keyboard").set_focus(
|
||||||
|
self,
|
||||||
|
Some(surface),
|
||||||
|
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for window in self.space.elements() {
|
||||||
|
if let Some(toplevel) = window.toplevel() {
|
||||||
|
toplevel.send_pending_configure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_direction(&mut self, direction: Direction) {
|
||||||
|
let Some(current) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self
|
||||||
|
.workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.move_window(current, direction, bounds)
|
||||||
|
{
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?current, "failed to move tiled window"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize_direction(&mut self, direction: Direction) {
|
||||||
|
const RESIZE_STEP: f64 = 0.05;
|
||||||
|
|
||||||
|
let Some(current) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self
|
||||||
|
.workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.resize_window(current, direction, RESIZE_STEP)
|
||||||
|
{
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?current, "failed to resize tiled window"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_layout_mode(&mut self) {
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mode = match self.workspaces.active().layout.mode() {
|
||||||
|
LayoutMode::Scrolling => LayoutMode::Tree,
|
||||||
|
LayoutMode::Tree => LayoutMode::Scrolling,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.workspaces.set_active_layout_mode(mode, bounds) {
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?mode, "failed to change layout mode"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_focused_window(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.workspaces.active().layout.mode() != LayoutMode::Scrolling {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = self
|
||||||
|
.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.directional_neighbor(window, Direction::Left, bounds)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.or_else(|| {
|
||||||
|
self.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.directional_neighbor(window, Direction::Right, bounds)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
});
|
||||||
|
let Some(target) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.stack_window(window, target) {
|
||||||
|
tracing::error!(%error, ?window, ?target, "failed to consume window into column");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expel_focused_window(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.workspaces.extract_window(window) {
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?window, "failed to expel window from column"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cycle_focused_column_width(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
|
||||||
|
Ok(true) => {
|
||||||
|
self.pending_column_width_cycles.insert(window);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?window, "failed to cycle column width"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_focused_column(&mut self) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let constraints = self.window_constraints();
|
||||||
|
|
||||||
|
match self
|
||||||
|
.workspaces
|
||||||
|
.center_window(window, bounds, self.gap_config, &constraints)
|
||||||
|
{
|
||||||
|
Ok(true) => self.relayout(),
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(error) => tracing::error!(%error, ?window, "failed to center column"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
|
let Some(window) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let workspace = self.workspaces.active_id();
|
||||||
|
|
||||||
|
if let Some(output) = self.output_workspaces.output_for_workspace(workspace) {
|
||||||
|
self.workspace_slides.remove(&output);
|
||||||
|
}
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
self.raise_window(&window, true);
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_managed_workspace(&mut self, workspace: WorkspaceId) -> bool {
|
||||||
|
self.activate_managed_workspace_internal(workspace, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_managed_workspace_from_swipe(
|
||||||
|
&mut self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
direction: SwipeDirection,
|
||||||
|
) -> bool {
|
||||||
|
self.activate_managed_workspace_internal(workspace, Some(direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn activate_managed_workspace_internal(
|
||||||
|
&mut self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
slide_direction: Option<SwipeDirection>,
|
||||||
|
) -> bool {
|
||||||
|
if self.workspaces.workspace(workspace).is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = self
|
||||||
|
.output_workspaces
|
||||||
|
.focused_output()
|
||||||
|
.or_else(|| self.output_workspaces.output_for_workspace(workspace));
|
||||||
|
let Some(output) = output else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let previous = self.output_workspaces.active_workspace(output);
|
||||||
|
let owner = match self.output_workspaces.switch_workspace(output, workspace) {
|
||||||
|
Ok(ferese_core::WorkspaceSwitch::Activated(output))
|
||||||
|
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_workspace_slide(output, owner, previous, workspace, slide_direction);
|
||||||
|
self.activate_output_workspace(owner, workspace);
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_workspace(&mut self, index: u32) {
|
||||||
|
self.switch_workspace_internal(index, None, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn switch_workspace_from_binding(&mut self, index: u32) {
|
||||||
|
self.switch_workspace_internal(index, None, self.workspace_auto_back_and_forth);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn workspace_back_and_forth(&mut self) {
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(workspace) = self.output_workspaces.previous_workspace(output) {
|
||||||
|
self.activate_managed_workspace(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn switch_workspace_internal(
|
||||||
|
&mut self,
|
||||||
|
index: u32,
|
||||||
|
slide_direction: Option<SwipeDirection>,
|
||||||
|
auto_back_and_forth: bool,
|
||||||
|
) {
|
||||||
|
let workspace = match self.workspaces.ensure_numeric(index) {
|
||||||
|
Ok(workspace) => workspace,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, index, "failed to switch workspace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let previous = self.output_workspaces.active_workspace(output);
|
||||||
|
let owner = match self
|
||||||
|
.output_workspaces
|
||||||
|
.select_workspace(output, workspace, auto_back_and_forth)
|
||||||
|
{
|
||||||
|
Ok(ferese_core::WorkspaceSwitch::Activated(output))
|
||||||
|
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, index, "failed to assign workspace to output");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let workspace = self
|
||||||
|
.output_workspaces
|
||||||
|
.active_workspace(owner)
|
||||||
|
.expect("workspace owner is connected");
|
||||||
|
self.update_workspace_slide(output, owner, previous, workspace, slide_direction);
|
||||||
|
self.activate_output_workspace(owner, workspace);
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preview_workspace_swipe(&mut self, next: bool, direction: SwipeDirection, progress: f64) {
|
||||||
|
if !self.animations_enabled || self.overview.is_presenting() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some((output, (from, to, _))) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
|
||||||
|
{
|
||||||
|
if !workspace_swipe_is_current(&self.output_workspaces, output, from, to) {
|
||||||
|
self.finish_workspace_swipe(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.workspace_slides
|
||||||
|
.get_mut(&output)
|
||||||
|
.expect("gesture output exists")
|
||||||
|
.held_progress = Some(progress);
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.swipe.preview_started() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(from) = self.output_workspaces.active_workspace(output) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(to) = self.relative_workspace_target(next) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let previous = self.workspace_slides.remove(&output);
|
||||||
|
let mut slide = WorkspaceSlide::new(previous, from, to, direction);
|
||||||
|
slide.held_progress = Some(progress);
|
||||||
|
slide.gesture = Some((from, to, direction));
|
||||||
|
self.workspace_slides.insert(output, slide);
|
||||||
|
self.swipe.mark_preview_started();
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_workspace_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
|
||||||
|
let Some((output, (from, to, expected))) = self
|
||||||
|
.workspace_slides
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let committed = direction == Some(expected)
|
||||||
|
&& workspace_swipe_is_current(&self.output_workspaces, output, from, to)
|
||||||
|
&& self.activate_managed_workspace_from_swipe(to, expected);
|
||||||
|
if !committed {
|
||||||
|
if let Some(slide) = self.workspace_slides.get_mut(&output) {
|
||||||
|
slide.release(false);
|
||||||
|
}
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn update_workspace_slide(
|
||||||
|
&mut self,
|
||||||
|
requested_output: OutputId,
|
||||||
|
owner: OutputId,
|
||||||
|
previous: Option<WorkspaceId>,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
slide_direction: Option<SwipeDirection>,
|
||||||
|
) {
|
||||||
|
let mut previous_slide = self.workspace_slides.remove(&owner);
|
||||||
|
if owner == requested_output
|
||||||
|
&& let Some(slide) = previous_slide.as_mut()
|
||||||
|
&& slide
|
||||||
|
.gesture
|
||||||
|
.is_some_and(|(_, to, direction)| to == workspace && Some(direction) == slide_direction)
|
||||||
|
{
|
||||||
|
slide.release(true);
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
self.workspace_slides.insert(owner, previous_slide.unwrap());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if owner == requested_output
|
||||||
|
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
||||||
|
&& from != workspace
|
||||||
|
&& self.animations_enabled
|
||||||
|
{
|
||||||
|
self.last_animation_tick = Instant::now();
|
||||||
|
self.workspace_slides
|
||||||
|
.insert(owner, WorkspaceSlide::new(previous_slide, from, workspace, direction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_focused_to_workspace(&mut self, index: u32) {
|
||||||
|
let Some(window) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let axis = self
|
||||||
|
.output_bounds()
|
||||||
|
.and_then(|bounds| {
|
||||||
|
let workspace = self.workspaces.ensure_numeric(index).ok()?;
|
||||||
|
let target = self.workspaces.workspace(workspace)?;
|
||||||
|
target.layout.automatic_axis(target.last_focused, bounds).ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
|
let destination = match self.workspaces.move_window_to_numeric(window, index, axis, 0.5) {
|
||||||
|
Ok(destination) => destination,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, ?window, index, "failed to move window to workspace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(output) = self.output_workspaces.focused_output()
|
||||||
|
&& let Err(error) = self.output_workspaces.assign_workspace(output, destination)
|
||||||
|
{
|
||||||
|
tracing::error!(%error, ?destination, "failed to assign destination workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_keyboard_focus(&mut self) {
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.session_lock.active {
|
||||||
|
self.focus_lock_surface();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let surface = self.focused_window.and_then(|focused| {
|
||||||
|
self.window_ids.iter().find_map(|(window, id)| {
|
||||||
|
(*id == focused)
|
||||||
|
.then(|| window.toplevel())
|
||||||
|
.flatten()
|
||||||
|
.map(|toplevel| toplevel.wl_surface().clone())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.seat.get_keyboard().expect("seat has a keyboard").set_focus(
|
||||||
|
self,
|
||||||
|
surface,
|
||||||
|
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub fn register_output(&mut self, output: &Output, identity: String) {
|
||||||
|
if self.session_lock.active {
|
||||||
|
self.session_lock.output_added(output);
|
||||||
|
self.lock_input_activity();
|
||||||
|
}
|
||||||
|
let Some(geometry) = self.space.output_geometry(output) else {
|
||||||
|
tracing::error!(output = %output.name(), "cannot register an unmapped output");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let output_id = *self.output_identity_ids.entry(identity).or_insert_with(|| {
|
||||||
|
let id = OutputId(self.next_output_id);
|
||||||
|
self.next_output_id = self.next_output_id.saturating_add(1);
|
||||||
|
id
|
||||||
|
});
|
||||||
|
let fallback_workspace = (1..)
|
||||||
|
.find_map(|index| {
|
||||||
|
let workspace = self.workspaces.ensure_numeric(index).ok()?;
|
||||||
|
self.output_workspaces
|
||||||
|
.output_for_workspace(workspace)
|
||||||
|
.is_none()
|
||||||
|
.then_some(workspace)
|
||||||
|
})
|
||||||
|
.expect("numeric workspaces are inexhaustible");
|
||||||
|
let geometry = OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h);
|
||||||
|
|
||||||
|
let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace);
|
||||||
|
|
||||||
|
match registration {
|
||||||
|
Ok(workspace) => {
|
||||||
|
self.output_ids.insert(output.clone(), output_id);
|
||||||
|
if self.output_workspaces.focused_output() == Some(output_id) {
|
||||||
|
self.activate_output_workspace(output_id, workspace);
|
||||||
|
}
|
||||||
|
self.send_shell_snapshots();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, output = %output.name(), "failed to register output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_output(&mut self, output: &Output) {
|
||||||
|
let Some(output_id) = self.output_ids.remove(output) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.workspace_slides.remove(&output_id);
|
||||||
|
self.overview_scrims.remove(&output_id);
|
||||||
|
self.pending_screencopies.retain(|capture| {
|
||||||
|
if capture.output == *output {
|
||||||
|
capture.fail();
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.space.unmap_output(output);
|
||||||
|
self.session_lock.surfaces.remove(output);
|
||||||
|
self.session_lock.backgrounds.remove(output);
|
||||||
|
self.session_lock.output_removed(output);
|
||||||
|
|
||||||
|
match self.output_workspaces.disconnect(output_id) {
|
||||||
|
Ok(Some(target)) => {
|
||||||
|
if let Some(workspace) = self.output_workspaces.active_workspace(target) {
|
||||||
|
self.activate_output_workspace(target, workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
self.focused_window = None;
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(%error, output = %output.name(), "failed to unregister output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
|
||||||
|
let floats = self
|
||||||
|
.window_ids
|
||||||
|
.values()
|
||||||
|
.filter_map(|id| {
|
||||||
|
let workspace = self.workspaces.workspace_for_window(*id)?;
|
||||||
|
if self.output_workspaces.output_for_workspace(workspace) != Some(output) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match self.workspaces.placement(*id)? {
|
||||||
|
WindowPlacement::Floating { rect } => Some((*id, moved_floating_rect(rect, old, new))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (id, rect) in floats {
|
||||||
|
let _ = self.workspaces.set_floating_rect(id, rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) {
|
||||||
|
if let Err(error) = self.output_workspaces.focus_output(output) {
|
||||||
|
tracing::error!(%error, "failed to focus output");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.workspaces.activate(workspace) {
|
||||||
|
Ok(focus) => self.focused_window = focus,
|
||||||
|
Err(error) => tracing::error!(%error, "failed to activate output workspace"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focused_output(&self) -> Option<&Output> {
|
||||||
|
let focused = self.output_workspaces.focused_output()?;
|
||||||
|
self.output_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, id)| (*id == focused).then_some(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_output_focus(&mut self) {
|
||||||
|
if let Some(output) = self.output_workspaces.focused_output()
|
||||||
|
&& let Some(workspace) = self.output_workspaces.active_workspace(output)
|
||||||
|
{
|
||||||
|
self.activate_output_workspace(output, workspace);
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_id(&self, output: &Output) -> Option<OutputId> {
|
||||||
|
self.output_ids.get(output).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_output_at(&mut self, position: Point<f64, Logical>) {
|
||||||
|
let output = self.space.outputs().find_map(|output| {
|
||||||
|
let geometry = self.space.output_geometry(output)?;
|
||||||
|
(position.x >= f64::from(geometry.loc.x)
|
||||||
|
&& position.y >= f64::from(geometry.loc.y)
|
||||||
|
&& position.x < f64::from(geometry.loc.x + geometry.size.w)
|
||||||
|
&& position.y < f64::from(geometry.loc.y + geometry.size.h))
|
||||||
|
.then(|| output.clone())
|
||||||
|
});
|
||||||
|
let Some(output) = output else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.output_workspaces.focused_output() == Some(output_id) && self.workspaces.active_id() == workspace {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.activate_output_workspace(output_id, workspace);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub(crate) fn restore_initial_floating_size(&mut self, window: &Window) {
|
||||||
|
let Some(toplevel) = window.toplevel() else { return };
|
||||||
|
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
||||||
|
(
|
||||||
|
attributes.app_id.clone(),
|
||||||
|
attributes.title.clone(),
|
||||||
|
attributes.parent.is_some(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let special_mode = toplevel.with_pending_state(|pending| {
|
||||||
|
pending.states.contains(xdg_toplevel::State::Fullscreen)
|
||||||
|
|| pending.states.contains(xdg_toplevel::State::Maximized)
|
||||||
|
});
|
||||||
|
let rule = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient);
|
||||||
|
if transient
|
||||||
|
|| special_mode
|
||||||
|
|| rule.fullscreen == Some(true)
|
||||||
|
|| !rule.floating.unwrap_or(rule.width.is_some() || rule.height.is_some())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let outputs = self.floating_outputs();
|
||||||
|
let Some((remembered, work)) = app_id
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
|
.and_then(|entry| {
|
||||||
|
let rect = entry.restore(&outputs)?;
|
||||||
|
let work = outputs.iter().find(|(name, _)| *name == entry.output)?.1;
|
||||||
|
Some((rect, work))
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Metadata is available on the initial bufferless commit. Restore the
|
||||||
|
// size before the client creates its first buffer, without mapping or
|
||||||
|
// focusing the window. Use the same rule/constraint precedence as map.
|
||||||
|
let rect = self.place_floating(
|
||||||
|
window,
|
||||||
|
None,
|
||||||
|
work,
|
||||||
|
(remembered.width, remembered.height),
|
||||||
|
Some((rule.width, rule.height)),
|
||||||
|
);
|
||||||
|
let size = ClientSize::from_rect(rect);
|
||||||
|
toplevel.with_pending_state(|pending| pending.size = Some((size.width, size.height).into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_rule_placed_window(
|
||||||
|
&mut self,
|
||||||
|
window: Window,
|
||||||
|
app_id: Option<&str>,
|
||||||
|
title: Option<&str>,
|
||||||
|
parent: Option<WindowId>,
|
||||||
|
) {
|
||||||
|
let rule = resolve_window_rules(&self.window_rules, app_id, title, parent.is_some());
|
||||||
|
let floating = rule
|
||||||
|
.floating
|
||||||
|
.unwrap_or(parent.is_some() || rule.width.is_some() || rule.height.is_some());
|
||||||
|
if !floating {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
self.add_transient_window(window, parent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
self.focus_output_at(pointer.current_location());
|
||||||
|
}
|
||||||
|
let remembered_work = app_id.and_then(|app| self.floating_memory.get(app)).and_then(|entry| {
|
||||||
|
let outputs = self.floating_outputs();
|
||||||
|
entry.restore(&outputs)?;
|
||||||
|
outputs
|
||||||
|
.into_iter()
|
||||||
|
.find(|(name, _)| *name == entry.output)
|
||||||
|
.map(|(_, work)| work)
|
||||||
|
});
|
||||||
|
if let Some(work) = remembered_work {
|
||||||
|
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
|
||||||
|
}
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let id = WindowId(self.next_window_id);
|
||||||
|
self.next_window_id += 1;
|
||||||
|
let size_rect = client_size(&window)
|
||||||
|
.map(|size| natural_floating_rect(bounds, size))
|
||||||
|
.unwrap_or_else(|| centered_floating_rect(bounds));
|
||||||
|
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
||||||
|
let anchor = self
|
||||||
|
.focused_window
|
||||||
|
.and_then(|focused| self.presented_window_rect(focused))
|
||||||
|
.unwrap_or(bounds);
|
||||||
|
self.floating_placement_anchors.insert(id, anchor);
|
||||||
|
let fullscreen = self.workspaces.active().fullscreen;
|
||||||
|
let focus = true;
|
||||||
|
if let Err(error) = self
|
||||||
|
.workspaces
|
||||||
|
.insert_floating_window(id, self.workspaces.active_id(), rect, focus)
|
||||||
|
{
|
||||||
|
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
|
||||||
|
{
|
||||||
|
self.natural_floating_pending.insert(id);
|
||||||
|
}
|
||||||
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.window_stack.insert(id);
|
||||||
|
if let Some(fullscreen) = fullscreen {
|
||||||
|
self.floating_above_fullscreen.insert(id, fullscreen);
|
||||||
|
}
|
||||||
|
if focus {
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
}
|
||||||
|
self.space.map_element(window, (0, 0), focus);
|
||||||
|
// apply_initial_window_rules performs the first relayout/configure.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_tiled_window(&mut self, window: Window) {
|
||||||
|
let id = WindowId(self.next_window_id);
|
||||||
|
self.next_window_id += 1;
|
||||||
|
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
||||||
|
|
||||||
|
let axis = self
|
||||||
|
.output_bounds()
|
||||||
|
.and_then(|bounds| {
|
||||||
|
self.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.automatic_axis(self.focused_window, bounds)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
|
if let Err(error) = self.workspaces.insert_window(id, axis, 0.5) {
|
||||||
|
tracing::error!(%error, ?id, "failed to insert window into layout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.window_stack.insert(id);
|
||||||
|
if focus_new_window {
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
}
|
||||||
|
self.space.map_element(window, (0, 0), focus_new_window);
|
||||||
|
self.relayout();
|
||||||
|
|
||||||
|
if focus_new_window {
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_transient_window(&mut self, window: Window, parent: WindowId) {
|
||||||
|
let Some(workspace) = self.workspaces.workspace_for_window(parent) else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
|
let size_rect = client_size(&window)
|
||||||
|
.map(|s| natural_floating_rect(bounds, s))
|
||||||
|
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
||||||
|
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
||||||
|
let id = WindowId(self.next_window_id);
|
||||||
|
self.next_window_id += 1;
|
||||||
|
|
||||||
|
let focus = workspace == self.workspaces.active_id()
|
||||||
|
&& (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent));
|
||||||
|
if let Err(error) = self.workspaces.insert_floating_window(id, workspace, rect, focus) {
|
||||||
|
tracing::error!(%error, ?id, ?parent, "failed to insert transient window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.window_stack.insert(id);
|
||||||
|
if client_size(&window).is_none() {
|
||||||
|
self.natural_floating_pending.insert(id);
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.workspaces
|
||||||
|
.workspace(workspace)
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(parent))
|
||||||
|
{
|
||||||
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
|
}
|
||||||
|
if focus {
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
}
|
||||||
|
self.space.map_element(window, (0, 0), focus);
|
||||||
|
self.relayout();
|
||||||
|
if focus {
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_initial_window_rules(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
app_id: Option<&str>,
|
||||||
|
title: Option<&str>,
|
||||||
|
transient: bool,
|
||||||
|
) {
|
||||||
|
let Some(id) = self.window_ids.get(window).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !self.window_rules_applied.insert(id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rule = resolve_window_rules(&self.window_rules, app_id, title, transient);
|
||||||
|
self.apply_window_rule_result(window, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
|
||||||
|
let windows = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
||||||
|
for window in windows {
|
||||||
|
let Some(toplevel) = window.toplevel() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
||||||
|
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
||||||
|
(
|
||||||
|
attributes.app_id.clone(),
|
||||||
|
attributes.title.clone(),
|
||||||
|
attributes.parent.is_some(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let old = resolve_window_rules(old_rules, app_id.as_deref(), title.as_deref(), transient);
|
||||||
|
let new = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient);
|
||||||
|
if let Some(new) = crate::window_rules::live_result(old, new, transient) {
|
||||||
|
self.apply_window_rule_result(&window, new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) {
|
||||||
|
let Some(id) = self.window_ids.get(window).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.floating_bounds_for_window(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if rule == Default::default() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let axis = self
|
||||||
|
.workspaces
|
||||||
|
.active()
|
||||||
|
.layout
|
||||||
|
.automatic_axis(self.focused_window, bounds)
|
||||||
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
|
if let Some(workspace) = rule.workspace
|
||||||
|
&& let Err(error) = self.workspaces.move_window_to_numeric(id, workspace, axis, 0.5)
|
||||||
|
{
|
||||||
|
tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule");
|
||||||
|
}
|
||||||
|
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
|
||||||
|
|
||||||
|
let should_float = rule
|
||||||
|
.floating
|
||||||
|
.or((rule.width.is_some() || rule.height.is_some()).then_some(true));
|
||||||
|
if let Some(should_float) = should_float {
|
||||||
|
let is_floating = matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }));
|
||||||
|
let mut rect = centered_floating_rect(bounds);
|
||||||
|
if should_float && rule.width.is_none() && rule.height.is_none() {
|
||||||
|
if let Some(size) = client_size(window) {
|
||||||
|
rect = natural_floating_rect(bounds, size);
|
||||||
|
} else if Self::floating_metadata(window)
|
||||||
|
.0
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
|
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
self.natural_floating_pending.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
|
||||||
|
rect.height = rule.height.unwrap_or(rect.height).min(bounds.height);
|
||||||
|
if should_float {
|
||||||
|
rect = self.place_floating(
|
||||||
|
window,
|
||||||
|
Some(id),
|
||||||
|
bounds,
|
||||||
|
(rect.width, rect.height),
|
||||||
|
Some((rule.width, rule.height)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = if should_float != is_floating {
|
||||||
|
self.workspaces.toggle_floating(id, rect, axis, 0.5).map(|_| ())
|
||||||
|
} else if should_float && (rule.width.is_some() || rule.height.is_some()) {
|
||||||
|
self.workspaces.set_floating_rect(id, rect)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
if let Err(error) = result {
|
||||||
|
tracing::warn!(%error, ?id, "failed to apply floating window rule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fullscreen) = rule.fullscreen
|
||||||
|
&& let Err(error) = self.workspaces.set_fullscreen(id, fullscreen)
|
||||||
|
{
|
||||||
|
tracing::warn!(%error, ?id, fullscreen, "failed to apply fullscreen window rule");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.workspaces.workspace_for_window(id) != Some(self.workspaces.active_id())
|
||||||
|
&& self.focused_window == Some(id)
|
||||||
|
{
|
||||||
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) {
|
||||||
|
let Some(id) = self.window_ids.get(window).copied() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) {
|
||||||
|
tracing::warn!(?id, ?parent, "ignored transient parent on another workspace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
|
let size_rect = client_size(window)
|
||||||
|
.map(|s| natural_floating_rect(bounds, s))
|
||||||
|
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
||||||
|
let rect = self.place_floating(window, Some(id), bounds, (size_rect.width, size_rect.height), None);
|
||||||
|
let result = match self.workspaces.placement(id) {
|
||||||
|
Some(WindowPlacement::Tiled) => self
|
||||||
|
.workspaces
|
||||||
|
.toggle_floating(id, rect, Axis::Horizontal, 0.5)
|
||||||
|
.map(|_| ()),
|
||||||
|
Some(WindowPlacement::Floating { .. }) => self.workspaces.set_floating_rect(id, rect),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = result {
|
||||||
|
tracing::warn!(%error, ?id, ?parent, "failed to apply transient placement");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(parent)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(parent))
|
||||||
|
{
|
||||||
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
|
}
|
||||||
|
if self.focused_window == Some(parent) {
|
||||||
|
if let Err(error) = self.workspaces.focus_window(id) {
|
||||||
|
tracing::warn!(%error, ?id, "failed to focus transient window");
|
||||||
|
} else {
|
||||||
|
self.focused_window = Some(id);
|
||||||
|
self.window_stack.raise(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_tiled_window(&mut self, window: &Window) {
|
||||||
|
// Uncommitted toplevels have not entered a workspace yet.
|
||||||
|
self.space.unmap_elem(window);
|
||||||
|
let Some(id) = self.window_ids.remove(window) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.window_geometry.remove(&id);
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
self.resize_snapshots.remove(&id);
|
||||||
|
self.window_resize_fills.remove(&id);
|
||||||
|
self.maximized_windows.remove(&id);
|
||||||
|
self.maximized_column_widths.remove(&id);
|
||||||
|
self.window_stack.remove(id);
|
||||||
|
self.floating_above_fullscreen.remove(&id);
|
||||||
|
self.natural_floating_pending.remove(&id);
|
||||||
|
self.floating_window_memory.remove(&id);
|
||||||
|
self.floating_placement_anchors.remove(&id);
|
||||||
|
self.floating_resize_anchors.remove(&id);
|
||||||
|
self.window_borders.remove(&id);
|
||||||
|
self.window_dims.remove(&id);
|
||||||
|
self.window_dimming.remove(&id);
|
||||||
|
self.window_focus.remove(&id);
|
||||||
|
self.window_shadows.remove(&id);
|
||||||
|
self.closing_windows.remove(&id);
|
||||||
|
self.window_rules_applied.remove(&id);
|
||||||
|
self.scrolling_world_x.remove(&id);
|
||||||
|
self.viewport_coupled_widths.remove(&id);
|
||||||
|
self.pending_column_width_cycles.remove(&id);
|
||||||
|
if let Err(error) = self.workspaces.remove_window(id) {
|
||||||
|
tracing::error!(%error, ?id, "failed to remove window from layout");
|
||||||
|
}
|
||||||
|
if self.focused_window == Some(id) {
|
||||||
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
||||||
|
let Some((window, id)) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find(|(window, _)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
||||||
|
})
|
||||||
|
.map(|(window, id)| (window.clone(), *id))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(transaction) = self.resize_transactions.get(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let serial = with_states(surface, |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
|
||||||
|
});
|
||||||
|
if !transaction.accepts(serial) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(output) = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|output| self.window_belongs_to_output(id, output))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry());
|
||||||
|
let scale = output.current_scale().fractional_scale();
|
||||||
|
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
||||||
|
if self.resize_snapshots.get(&id).is_some_and(|snapshot| {
|
||||||
|
crate::presentation::snapshot_covers_source(
|
||||||
|
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
||||||
|
source_size,
|
||||||
|
snapshot.scale,
|
||||||
|
scale,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let used: usize = self.resize_snapshots.values().map(|snapshot| snapshot.bytes()).sum();
|
||||||
|
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||||
|
let result = if let Some(backend) = &self.nested_backend {
|
||||||
|
// Commit dispatch does not run inside the Winit event callback;
|
||||||
|
// still never risk reentrant renderer access or a compositor panic.
|
||||||
|
match backend.try_borrow_mut() {
|
||||||
|
Ok(mut backend) => crate::render::capture_resize_snapshot(
|
||||||
|
backend.renderer(),
|
||||||
|
&window,
|
||||||
|
source_geometry,
|
||||||
|
output.current_scale().fractional_scale(),
|
||||||
|
remaining,
|
||||||
|
),
|
||||||
|
Err(_) => return,
|
||||||
|
}
|
||||||
|
} else if let Some(backend) = &mut self.direct_backend {
|
||||||
|
backend.capture_resize_snapshot(&window, source_geometry, &output, remaining)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match result {
|
||||||
|
Ok(Some(snapshot)) => {
|
||||||
|
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
|
||||||
|
self.resize_snapshots.insert(id, snapshot);
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::debug!(%error, ?id, "resize snapshot unavailable; using native crop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_client_commit(&mut self, window: &Window) {
|
||||||
|
let Some(&id) = self.window_ids.get(window) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// current_serial is promoted by Smithay on commit, unlike
|
||||||
|
// configure_serial, which only records an acknowledgement.
|
||||||
|
let committed_serial = window.toplevel().and_then(|toplevel| {
|
||||||
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.get::<XdgToplevelSurfaceData>()
|
||||||
|
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if self
|
||||||
|
.resize_transactions
|
||||||
|
.get(&id)
|
||||||
|
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
||||||
|
{
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
}
|
||||||
|
let Some(size) = client_size(window) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let fullscreen = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
|
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||||
|
let normal_geometry = !fullscreen
|
||||||
|
&& !self.maximized_windows.contains(&id)
|
||||||
|
&& self
|
||||||
|
.window_geometry
|
||||||
|
.get(&id)
|
||||||
|
.is_none_or(|geometry| !geometry.is_zooming());
|
||||||
|
let initial_floating =
|
||||||
|
self.natural_floating_pending.remove(&id) || self.floating_placement_anchors.contains_key(&id);
|
||||||
|
if initial_floating
|
||||||
|
&& normal_geometry
|
||||||
|
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
||||||
|
&& let Some(bounds) = self.floating_bounds_for_window(id)
|
||||||
|
{
|
||||||
|
let rect = self.place_floating(
|
||||||
|
window,
|
||||||
|
Some(id),
|
||||||
|
bounds,
|
||||||
|
(f64::from(size.width), f64::from(size.height)),
|
||||||
|
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
|
||||||
|
);
|
||||||
|
self.floating_placement_anchors.remove(&id);
|
||||||
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
|
// This is the first mapped client buffer, not a user resize.
|
||||||
|
// Do not animate from the temporary placement box.
|
||||||
|
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A normal floating client may choose a different size (minimum sizes,
|
||||||
|
// terminal cell grids, or a dialog changing its contents). Its committed
|
||||||
|
// window geometry is authoritative once the latest configure is committed.
|
||||||
|
// Never let an old buffer undo a newer resize or a fullscreen transition.
|
||||||
|
self.floating_placement_anchors.remove(&id);
|
||||||
|
let settled_configure = window.toplevel().is_some_and(|toplevel| {
|
||||||
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
|
states.data_map.get::<XdgToplevelSurfaceData>().is_some_and(|data| {
|
||||||
|
let Ok(data) = data.lock() else { return false };
|
||||||
|
floating_commit_is_current(
|
||||||
|
data.pending_configures().is_empty(),
|
||||||
|
data.current_serial,
|
||||||
|
data.configure_serial,
|
||||||
|
data.current.states.contains(xdg_toplevel::State::Resizing),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if settled_configure
|
||||||
|
&& normal_geometry
|
||||||
|
&& let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id)
|
||||||
|
&& ClientSize::from_rect(rect) != size
|
||||||
|
{
|
||||||
|
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
|
||||||
|
if let Some((left, top, anchor)) = self.floating_resize_anchors.get(&id) {
|
||||||
|
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
|
||||||
|
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
|
||||||
|
rect = crate::floating::clamp_to_work(rect, work);
|
||||||
|
}
|
||||||
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
|
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
self.resize_snapshots.remove(&id);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settled_configure && self.floating_resize_anchors.remove(&id).is_some() {
|
||||||
|
self.remember_floating(window);
|
||||||
|
}
|
||||||
|
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let matches_target = geometry.client.commit(size);
|
||||||
|
tracing::debug!(?id, ?size, matches_target, "recorded client geometry commit");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_focused_window(&mut self) {
|
||||||
|
let Some(focused) = self.focused_window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.animations_enabled {
|
||||||
|
self.send_window_close(focused);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.closing_windows.entry(focused).or_default();
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
|
if !self.window_ids.values().any(|window_id| *window_id == id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.animations_enabled {
|
||||||
|
self.closing_windows.entry(id).or_default();
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
} else {
|
||||||
|
self.send_window_close(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn send_window_close(&self, id: WindowId) {
|
||||||
|
let Some(toplevel) = self
|
||||||
|
.window_ids
|
||||||
|
.iter()
|
||||||
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
toplevel.send_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2900
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,247 @@
|
|||||||
|
use cosmic::widget::column;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
Alignment, App, Element, Field, Kind, Length, Message, button, container, displays, fonts, row, set, slider,
|
||||||
|
text_input, visuals, widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(super) fn label<'a>(
|
||||||
|
&self,
|
||||||
|
text: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
||||||
|
size: f32,
|
||||||
|
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||||
|
ferese_theme::text(text, self.font).size(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn note(&self, text: &str) -> Element<'static, Message> {
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
self.label(text.to_owned(), 12.)
|
||||||
|
.class(cosmic::theme::Text::Color(palette.muted))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn field(&self, mut field: Field) -> Element<'static, Message> {
|
||||||
|
if field.path == "theme.material.opacity" {
|
||||||
|
let appearance = self.resolved.theme.appearance;
|
||||||
|
let name = match appearance {
|
||||||
|
ferese_config::theme::Appearance::Light => "light",
|
||||||
|
ferese_config::theme::Appearance::Dark => "dark",
|
||||||
|
};
|
||||||
|
field.path = format!("theme.{name}.material.opacity");
|
||||||
|
field.description = format!("Opacity for the current {name} appearance. Text and icons stay opaque.");
|
||||||
|
if let Kind::Range { default, .. } = &mut field.kind {
|
||||||
|
*default = self.resolved.theme.tokens.material.opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
let mut labels = column([]).spacing(3).push(self.label(field.label.clone(), 13.));
|
||||||
|
if !field.description.is_empty() {
|
||||||
|
labels = labels.push(
|
||||||
|
self.label(field.description.clone(), 11.)
|
||||||
|
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let labels = labels.width(Length::Fill);
|
||||||
|
let path = field.path.clone();
|
||||||
|
let control: Element<'static, Message> = match field.kind.clone() {
|
||||||
|
Kind::Font => {
|
||||||
|
let value = self
|
||||||
|
.inputs
|
||||||
|
.get(&path)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| self.draft.string(&path, ""));
|
||||||
|
let families = fonts::families();
|
||||||
|
let selected = if value.is_empty() {
|
||||||
|
Some(0)
|
||||||
|
} else {
|
||||||
|
families.iter().position(|family| family == &value)
|
||||||
|
};
|
||||||
|
let selection_path = path.clone();
|
||||||
|
let commit = field.clone();
|
||||||
|
column([])
|
||||||
|
.spacing(6)
|
||||||
|
.push(
|
||||||
|
cosmic::iced::widget::pick_list(
|
||||||
|
families,
|
||||||
|
selected.map(|index| families[index].clone()),
|
||||||
|
move |family: String| {
|
||||||
|
let value = if family == "Default font" {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
family
|
||||||
|
};
|
||||||
|
Message::SelectFont(selection_path.clone(), value)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.font(self.font)
|
||||||
|
.text_size(12)
|
||||||
|
.width(225),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
text_input("Default font", value)
|
||||||
|
.font(self.font)
|
||||||
|
.style(visuals::input_style(palette))
|
||||||
|
.on_input(move |value| Message::Draft(path.clone(), value))
|
||||||
|
.on_submit(move |_| Message::Commit(field.clone()))
|
||||||
|
.on_unfocus(Message::Commit(commit))
|
||||||
|
.width(225)
|
||||||
|
.size(12),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Kind::Toggle(default) => {
|
||||||
|
let enabled = self.draft.boolean(&path, default);
|
||||||
|
ferese_theme::controls::switch(enabled, palette)
|
||||||
|
.name(format!("{}: {}", field.label, if enabled { "on" } else { "off" }))
|
||||||
|
.on_press(Message::Change(set(&path, !enabled)))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Kind::Range {
|
||||||
|
default,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
suffix,
|
||||||
|
integer,
|
||||||
|
} => {
|
||||||
|
let value = self
|
||||||
|
.ranges
|
||||||
|
.get(&path)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| self.draft.number(&path, default));
|
||||||
|
let display = if integer || step >= 1. {
|
||||||
|
format!("{value:.0}{suffix}")
|
||||||
|
} else {
|
||||||
|
format!("{value:.2}{suffix}")
|
||||||
|
};
|
||||||
|
let release = field.clone();
|
||||||
|
row([])
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.spacing(14)
|
||||||
|
.push(
|
||||||
|
slider(min..=max, value, move |value| Message::Range(field.clone(), value))
|
||||||
|
.step(step)
|
||||||
|
.class(ferese_theme::menus::slider(palette.accent, 1.))
|
||||||
|
.on_release(Message::Release(release))
|
||||||
|
.width(145),
|
||||||
|
)
|
||||||
|
.push(self.label(display, 12.).width(65))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Kind::Choice { default, choices } => {
|
||||||
|
let value = self.draft.string(&path, default);
|
||||||
|
let mut options = row([]).spacing(2);
|
||||||
|
for (key, label) in choices {
|
||||||
|
options = options.push(
|
||||||
|
button::custom(self.label(*label, 12.))
|
||||||
|
.padding([6, 9])
|
||||||
|
.class(visuals::button_style(palette, value == *key))
|
||||||
|
.on_press(Message::Change(set(&path, *key))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
container(options)
|
||||||
|
.padding(3)
|
||||||
|
.class(visuals::surface(palette.sidebar, 10.))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Kind::Text { default, argv } => {
|
||||||
|
let value = self.inputs.get(&path).cloned().unwrap_or_else(|| {
|
||||||
|
if argv {
|
||||||
|
self.draft.argv(&path)
|
||||||
|
} else {
|
||||||
|
self.draft.string(&path, default)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let commit = field.clone();
|
||||||
|
let is_color = path.starts_with("theme.colors.");
|
||||||
|
let swatch = visuals::color(&value, palette.accent);
|
||||||
|
let input = text_input(default, value)
|
||||||
|
.font(self.font)
|
||||||
|
.style(visuals::input_style(palette))
|
||||||
|
.on_input(move |value| Message::Draft(path.clone(), value))
|
||||||
|
.on_submit(move |_| Message::Commit(field.clone()))
|
||||||
|
.on_unfocus(Message::Commit(commit))
|
||||||
|
.width(if is_color { 155 } else { 225 })
|
||||||
|
.size(12);
|
||||||
|
if is_color {
|
||||||
|
row([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(container(widget::Space::new().width(24).height(24)).class(visuals::surface(swatch, 6.)))
|
||||||
|
.push(input)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
input.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
container(
|
||||||
|
row([])
|
||||||
|
.spacing(16)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(labels)
|
||||||
|
.push(control),
|
||||||
|
)
|
||||||
|
.padding([12, 12])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.class(visuals::surface(palette.card, 0.))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn refresh_controls(&self, prefix: &str) -> Element<'_, Message> {
|
||||||
|
let matcher = self.draft.string(&format!("{prefix}.match"), "");
|
||||||
|
let configured = self.draft.string(&format!("{prefix}.mode"), "");
|
||||||
|
let automatic = self.draft.boolean(&format!("{prefix}.auto_refresh"), false);
|
||||||
|
let Some(display) = self
|
||||||
|
.displays
|
||||||
|
.iter()
|
||||||
|
.find(|display| display.connector == matcher || display.identity == matcher)
|
||||||
|
else {
|
||||||
|
return self.note("Connect this display to choose a supported refresh rate.");
|
||||||
|
};
|
||||||
|
let modes = displays::choices(display, &configured);
|
||||||
|
let mut controls = row([]).spacing(8).align_y(Alignment::Center);
|
||||||
|
for mode in &modes {
|
||||||
|
let selected = !automatic
|
||||||
|
&& configured
|
||||||
|
.split('@')
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|rate| rate.parse::<f64>().ok())
|
||||||
|
.is_some_and(|rate| (rate * 1000. - f64::from(mode.refresh)).abs() < 1.);
|
||||||
|
controls = controls.push(
|
||||||
|
ferese_theme::controls::text_button(
|
||||||
|
mode.label(),
|
||||||
|
self.font,
|
||||||
|
visuals::Palette::from_resolved(&self.resolved.presented),
|
||||||
|
selected,
|
||||||
|
)
|
||||||
|
.on_press(Message::RefreshRate(prefix.to_owned(), *mode, false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let supports_auto = modes.iter().any(|mode| (59_000..=61_000).contains(&mode.refresh));
|
||||||
|
if supports_auto && let Some(mode) = modes.last() {
|
||||||
|
controls = controls.push(
|
||||||
|
ferese_theme::controls::text_button(
|
||||||
|
"Auto",
|
||||||
|
self.font,
|
||||||
|
visuals::Palette::from_resolved(&self.resolved.presented),
|
||||||
|
automatic,
|
||||||
|
)
|
||||||
|
.on_press(Message::RefreshRate(prefix.to_owned(), *mode, true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let current = display.current.map(|mode| mode.label()).unwrap_or_else(|| "Off".into());
|
||||||
|
let profile = display.profile.as_deref().unwrap_or("automatic configuration");
|
||||||
|
column![
|
||||||
|
self.label("Refresh rate", 14.),
|
||||||
|
self.note(&format!("Current: {current} · active profile: {profile}")),
|
||||||
|
controls,
|
||||||
|
self.note(if supports_auto {
|
||||||
|
"Auto uses 60 Hz below 30% on battery. Normal refresh returns on AC or at 35%. Switching may briefly blank the display. Choosing a rate turns Auto off."
|
||||||
|
} else {
|
||||||
|
"This resolution has no supported 60 Hz mode for automatic switching."
|
||||||
|
}),
|
||||||
|
].spacing(8).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,531 @@
|
|||||||
|
use super::{
|
||||||
|
Alignment, App, Element, Field, Kind, Length, Message, Page, button, column, container, row, schema, scrollable,
|
||||||
|
text_input, visuals, widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(super) fn page_view(&self) -> Element<'_, Message> {
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
let mut sidebar = column([])
|
||||||
|
.spacing(3)
|
||||||
|
.push(
|
||||||
|
widget::mouse_area(
|
||||||
|
row([])
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(visuals::brand_icon())
|
||||||
|
.push(self.label("Ferese", 16.))
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.on_press(Message::DragWindow)
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||||||
|
)
|
||||||
|
.push(widget::Space::new().height(14))
|
||||||
|
.push(
|
||||||
|
text_input("Search settings", self.search.clone())
|
||||||
|
.on_input(Message::Search)
|
||||||
|
.font(self.font)
|
||||||
|
.style(visuals::input_style(palette))
|
||||||
|
.size(12),
|
||||||
|
)
|
||||||
|
.push(widget::Space::new().height(8));
|
||||||
|
for page in Page::ALL {
|
||||||
|
sidebar = sidebar.push(
|
||||||
|
button::custom(
|
||||||
|
row([])
|
||||||
|
.spacing(10)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(visuals::icon(
|
||||||
|
page,
|
||||||
|
if self.page == page {
|
||||||
|
palette.accent
|
||||||
|
} else {
|
||||||
|
palette.muted
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.push(self.label(page.title(), 13.)),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([8, 8])
|
||||||
|
.class(visuals::navigation_style(palette, self.page == page))
|
||||||
|
.on_press(Message::Page(page)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let sidebar = container(sidebar.push(widget::Space::new().height(Length::Fill)))
|
||||||
|
.width(204)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding([20, 10])
|
||||||
|
.class(visuals::surface(palette.sidebar, 0.));
|
||||||
|
let mut heading = row([]).align_y(Alignment::Center).spacing(20).push(
|
||||||
|
column([])
|
||||||
|
.spacing(5)
|
||||||
|
.push(self.label(
|
||||||
|
if self.search.is_empty() {
|
||||||
|
self.page.title()
|
||||||
|
} else {
|
||||||
|
"Search"
|
||||||
|
},
|
||||||
|
24.,
|
||||||
|
))
|
||||||
|
.push(
|
||||||
|
self.label(
|
||||||
|
if self.search.is_empty() {
|
||||||
|
self.page.subtitle()
|
||||||
|
} else {
|
||||||
|
"Find a setting for your desktop."
|
||||||
|
},
|
||||||
|
12.,
|
||||||
|
)
|
||||||
|
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||||
|
)
|
||||||
|
.width(Length::Fill),
|
||||||
|
);
|
||||||
|
if self.page == Page::Appearance && self.search.is_empty() {
|
||||||
|
let automatic = self.draft.string("theme.mode", "dark") == "auto";
|
||||||
|
heading = heading.push(
|
||||||
|
row([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(self.label("Auto", 12.))
|
||||||
|
.push(
|
||||||
|
ferese_theme::controls::switch(automatic, palette)
|
||||||
|
.name(if automatic {
|
||||||
|
"Auto appearance: on"
|
||||||
|
} else {
|
||||||
|
"Auto appearance: off"
|
||||||
|
})
|
||||||
|
.on_press(Message::AutoAppearance(!automatic)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let scheduled = self.draft.string("theme.schedule.source", "schedule") == "schedule";
|
||||||
|
let source = if scheduled { "Schedule" } else { "System" };
|
||||||
|
let label = if automatic {
|
||||||
|
format!(
|
||||||
|
"{source} · {}",
|
||||||
|
if self.resolved.theme.appearance == ferese_config::theme::Appearance::Light {
|
||||||
|
"Light"
|
||||||
|
} else {
|
||||||
|
"Dark"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
source.to_owned()
|
||||||
|
};
|
||||||
|
let explanation = if scheduled {
|
||||||
|
format!(
|
||||||
|
"Auto uses Light from {} and Dark from {} ({})",
|
||||||
|
self.draft.string("theme.schedule.light_at", "07:00"),
|
||||||
|
self.draft.string("theme.schedule.dark_at", "19:00"),
|
||||||
|
self.draft.string("theme.schedule.timezone", "system"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"Auto follows the GTK/GNOME appearance preference".to_owned()
|
||||||
|
};
|
||||||
|
heading = heading.push(widget::tooltip(
|
||||||
|
ferese_theme::controls::text_button(label, self.font, palette, self.auto_details)
|
||||||
|
.name("Automatic appearance settings")
|
||||||
|
.on_press(Message::AutoDetails(!self.auto_details)),
|
||||||
|
self.label(explanation, 12.),
|
||||||
|
widget::tooltip::Position::Bottom,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = column([]).spacing(12);
|
||||||
|
|
||||||
|
if !self.search.is_empty() {
|
||||||
|
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
||||||
|
body = body.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||||
|
}
|
||||||
|
for page in Page::ALL.into_iter().filter(|p| p.matches(&self.search)) {
|
||||||
|
body = body.push(
|
||||||
|
button::custom(
|
||||||
|
column([])
|
||||||
|
.spacing(5)
|
||||||
|
.push(self.label(page.title(), 16.))
|
||||||
|
.push(self.label(page.subtitle(), 12.)),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(20)
|
||||||
|
.class(visuals::button_style(palette, false))
|
||||||
|
.on_press(Message::Page(page)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if matches!(self.page, Page::Bar | Page::Windows) {
|
||||||
|
body = body.push(visuals::preview_resolved(&self.draft, &self.resolved.presented));
|
||||||
|
}
|
||||||
|
if self.page == Page::Appearance {
|
||||||
|
let split = visuals::split(&self.draft);
|
||||||
|
if split {
|
||||||
|
body = body.push(self.theme_gallery(Some(ferese_config::theme::Appearance::Light)));
|
||||||
|
body = body.push(self.theme_gallery(Some(ferese_config::theme::Appearance::Dark)));
|
||||||
|
} else {
|
||||||
|
body = body.push(self.theme_gallery(None));
|
||||||
|
}
|
||||||
|
body = body.push(
|
||||||
|
widget::checkbox(split)
|
||||||
|
.label("Use different themes for light and dark")
|
||||||
|
.text_size(12)
|
||||||
|
.font(self.font)
|
||||||
|
.on_toggle(Message::SplitThemes),
|
||||||
|
);
|
||||||
|
if let Some(note) = &self.resolved.fallback_note {
|
||||||
|
body = body.push(self.note(note));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.page == Page::Wallpaper {
|
||||||
|
if let Some(handle) = &self.thumbnail {
|
||||||
|
body = body.push(
|
||||||
|
widget::image(handle.clone())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(190)
|
||||||
|
.content_fit(cosmic::iced::ContentFit::Contain),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = body.push(self.note(if self.thumbnail_loading {
|
||||||
|
"Loading wallpaper preview…"
|
||||||
|
} else {
|
||||||
|
self.thumbnail_error
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("Choose an image to preview your wallpaper.")
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
body = body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||||
|
}
|
||||||
|
let fields = schema::fields(self.page);
|
||||||
|
|
||||||
|
if !fields.is_empty() {
|
||||||
|
let mut group = column([]).spacing(1);
|
||||||
|
if self.page == Page::Appearance && self.auto_details {
|
||||||
|
group = group.push(self.auto_controls());
|
||||||
|
}
|
||||||
|
for field in fields {
|
||||||
|
if self.page == Page::Appearance
|
||||||
|
&& (field.path == "theme.mode"
|
||||||
|
|| field.path.starts_with("theme.schedule.")
|
||||||
|
|| field.path.ends_with(".file"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
group = group.push(self.field(field));
|
||||||
|
}
|
||||||
|
if self.page == Page::Appearance {
|
||||||
|
group = group.push(
|
||||||
|
button::custom(
|
||||||
|
row([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(self.label("Advanced customization", 13.))
|
||||||
|
.push(visuals::action_icon(
|
||||||
|
if self.advanced_theme {
|
||||||
|
"M6 15l6-6 6 6"
|
||||||
|
} else {
|
||||||
|
"M6 9l6 6 6-6"
|
||||||
|
},
|
||||||
|
palette.muted,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.name(if self.advanced_theme {
|
||||||
|
"Collapse advanced customization"
|
||||||
|
} else {
|
||||||
|
"Expand advanced customization"
|
||||||
|
})
|
||||||
|
.padding(12)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.class(visuals::button_style(palette, false))
|
||||||
|
.on_press(Message::AdvancedTheme(!self.advanced_theme)),
|
||||||
|
);
|
||||||
|
if self.advanced_theme {
|
||||||
|
group = group.push(self.theme_file_picker());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.page {
|
||||||
|
Page::LockScreen => {
|
||||||
|
body = body.push(self.note("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens."));
|
||||||
|
body = body.push(button::standard("Preview lock screen").on_press(Message::PreviewLock));
|
||||||
|
body = body.push(self.note("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items."));
|
||||||
|
}
|
||||||
|
Page::Desktop => {
|
||||||
|
let can_change_list = !self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||||||
|
body = body.push(self.label("Sticky notes", 16.));
|
||||||
|
body = body.push(self.note("Edit here; notes save after you pause typing. Desktop cards stay behind windows and are click-through."));
|
||||||
|
for index in 0..self.draft.records("desktop_widgets.notes") {
|
||||||
|
let id = self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||||
|
let mut group = column([]).spacing(8);
|
||||||
|
|
||||||
|
for field in schema::note_fields(index) {
|
||||||
|
group = group.push(self.field(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(editor) = self.note_editors.get(&id) {
|
||||||
|
let id = id.clone();
|
||||||
|
group = group.push(
|
||||||
|
widget::TextEditor::new(&editor.content)
|
||||||
|
.height(160)
|
||||||
|
.font(self.font)
|
||||||
|
.size(14.)
|
||||||
|
.on_action(move |action| Message::NoteAction(id.clone(), action)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
group = group.push(button::destructive("Remove note").on_press_maybe(
|
||||||
|
can_change_list.then_some(Message::Remove("desktop_widgets.notes".into(), index)),
|
||||||
|
));
|
||||||
|
body = body.push(container(group).padding(12).class(visuals::surface(palette.card, 14.)));
|
||||||
|
}
|
||||||
|
body = body.push(
|
||||||
|
button::standard("Add note").on_press_maybe(
|
||||||
|
(can_change_list && self.draft.records("desktop_widgets.notes") < 32)
|
||||||
|
.then_some(Message::AddNote),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Page::Startup => {
|
||||||
|
body = body.push(self.note("Login items update live. Disabling or removing an item stops the session-owned process; enabling one starts it."));
|
||||||
|
|
||||||
|
for index in 0..self.draft.records("autostart") {
|
||||||
|
let prefix = format!("autostart.{index}");
|
||||||
|
let group = column([])
|
||||||
|
.spacing(1)
|
||||||
|
.push(self.field(Field::new(
|
||||||
|
format!("{prefix}.command"),
|
||||||
|
"Program",
|
||||||
|
"A program and its arguments, not a shell script.",
|
||||||
|
Kind::Text {
|
||||||
|
default: "",
|
||||||
|
argv: true,
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
.push(self.field(Field::new(
|
||||||
|
format!("{prefix}.enabled"),
|
||||||
|
"Open at login",
|
||||||
|
"",
|
||||||
|
Kind::Toggle(true),
|
||||||
|
)))
|
||||||
|
.push(self.field(Field::new(
|
||||||
|
format!("{prefix}.restart"),
|
||||||
|
"Restart if it exits",
|
||||||
|
"Managed by the Ferese session.",
|
||||||
|
Kind::Toggle(true),
|
||||||
|
)))
|
||||||
|
.push(
|
||||||
|
container(button::destructive("Remove item").on_press_maybe(
|
||||||
|
(!self.saving).then_some(Message::Remove("autostart".into(), index)),
|
||||||
|
))
|
||||||
|
.padding(12),
|
||||||
|
);
|
||||||
|
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||||
|
}
|
||||||
|
body = body.push(
|
||||||
|
row([])
|
||||||
|
.spacing(10)
|
||||||
|
.push(
|
||||||
|
text_input("Program and arguments", self.new_command.clone())
|
||||||
|
.on_input(Message::NewCommand),
|
||||||
|
)
|
||||||
|
.push(button::standard("Add login item").on_press_maybe(
|
||||||
|
(!self.saving && !self.new_command.trim().is_empty()).then_some(Message::AddCommand),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Page::Shortcuts => {
|
||||||
|
body = body.push(self.note("Keyboard and swipe bindings share the same actions. Use Swipe3Up, Swipe3Down, Swipe3Left, or Swipe3Right as a shortcut (also supports 4 or 5 fingers). Actions include toggle-overview, spawn, focus, move, workspace-next, workspace-previous, workspace-back-and-forth, and none. Leave Argument empty for actions such as toggle-overview or none."));
|
||||||
|
let mut gestures = row([]).spacing(6);
|
||||||
|
for (keys, label) in [
|
||||||
|
("Swipe3Up", "Customize swipe up"),
|
||||||
|
("Swipe3Down", "Customize swipe down"),
|
||||||
|
("Swipe3Left", "Customize swipe left"),
|
||||||
|
("Swipe3Right", "Customize swipe right"),
|
||||||
|
] {
|
||||||
|
let exists = (0..self.draft.records("bindings")).any(|index| {
|
||||||
|
self.draft
|
||||||
|
.string(&format!("bindings.{index}.keys"), "")
|
||||||
|
.eq_ignore_ascii_case(keys)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
gestures = gestures.push(
|
||||||
|
button::standard(label)
|
||||||
|
.on_press_maybe((!self.saving).then_some(Message::AddSwipe(keys))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body = body.push(gestures);
|
||||||
|
|
||||||
|
for index in 0..self.draft.records("bindings") {
|
||||||
|
let prefix = format!("bindings.{index}");
|
||||||
|
let group = column([])
|
||||||
|
.spacing(1)
|
||||||
|
.push(self.field(schema::text(
|
||||||
|
format!("{prefix}.keys"),
|
||||||
|
"Shortcut",
|
||||||
|
"For example: Super+Return or Swipe3Up",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
.push(self.field(schema::text(
|
||||||
|
format!("{prefix}.action"),
|
||||||
|
"Action",
|
||||||
|
"Action such as toggle-overview, spawn, focus, move, or none.",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
.push(self.field(schema::text(
|
||||||
|
format!("{prefix}.argument"),
|
||||||
|
"Argument",
|
||||||
|
"Command name, direction, or action argument.",
|
||||||
|
"",
|
||||||
|
)));
|
||||||
|
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||||
|
}
|
||||||
|
if self.draft.records("bindings") == 0 {
|
||||||
|
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Page::Displays => {
|
||||||
|
body = body.push(self.note("Display profiles update connected outputs live. Keep your config open for adding profiles or changing display positions."));
|
||||||
|
for profile in 0..self.draft.records("output_profiles") {
|
||||||
|
let prefix = format!("output_profiles.{profile}");
|
||||||
|
body =
|
||||||
|
body.push(self.label(self.draft.string(&format!("{prefix}.name"), "Display profile"), 17.));
|
||||||
|
|
||||||
|
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
||||||
|
let prefix = format!("{prefix}.outputs.{output}");
|
||||||
|
let group = column([])
|
||||||
|
.spacing(1)
|
||||||
|
.push(self.refresh_controls(&prefix))
|
||||||
|
.push(self.field(schema::text(
|
||||||
|
format!("{prefix}.match"),
|
||||||
|
"Display",
|
||||||
|
"Output name or matching pattern.",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
.push(self.field(schema::text(
|
||||||
|
format!("{prefix}.mode"),
|
||||||
|
"Resolution",
|
||||||
|
"For example: 2560x1440@60. Leave unchanged to retain automatic selection.",
|
||||||
|
"",
|
||||||
|
)))
|
||||||
|
.push(self.field(schema::range(
|
||||||
|
format!("{prefix}.scale"),
|
||||||
|
"Scale",
|
||||||
|
"Logical size of text and controls.",
|
||||||
|
1.,
|
||||||
|
0.75,
|
||||||
|
3.,
|
||||||
|
0.25,
|
||||||
|
"×",
|
||||||
|
false,
|
||||||
|
)));
|
||||||
|
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.draft.records("output_profiles") == 0 {
|
||||||
|
body =
|
||||||
|
body.push(self.note(
|
||||||
|
"No saved profiles. Ferese currently configures connected displays automatically.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut content = column([]).spacing(16).push(
|
||||||
|
widget::mouse_area(heading.width(Length::Fill))
|
||||||
|
.on_press(Message::DragWindow)
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(error) = &self.error {
|
||||||
|
content = content.push(
|
||||||
|
container(self.label(error.clone(), 12.))
|
||||||
|
.padding(12)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.class(visuals::surface(palette.error, 10.)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.push(
|
||||||
|
scrollable(
|
||||||
|
container(body)
|
||||||
|
.padding(cosmic::iced::Padding {
|
||||||
|
top: 0.,
|
||||||
|
right: 12.,
|
||||||
|
bottom: 0.,
|
||||||
|
left: 2.,
|
||||||
|
})
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.direction(cosmic::iced::widget::scrollable::Direction::Vertical(
|
||||||
|
cosmic::iced::widget::scrollable::Scrollbar::new()
|
||||||
|
.width(10)
|
||||||
|
.scroller_width(3),
|
||||||
|
))
|
||||||
|
.height(Length::Fill),
|
||||||
|
);
|
||||||
|
let footer = row([])
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(widget::tooltip(
|
||||||
|
visuals::action_icon(
|
||||||
|
if self.saving {
|
||||||
|
"M12 3v9l5 3 M21 12a9 9 0 1 1-9-9"
|
||||||
|
} else {
|
||||||
|
"M9 12l2 2 4-4 M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"
|
||||||
|
},
|
||||||
|
palette.muted,
|
||||||
|
),
|
||||||
|
self.label(if self.saving { "Saving…" } else { &self.status }, 11.),
|
||||||
|
widget::tooltip::Position::Top,
|
||||||
|
))
|
||||||
|
.push(widget::Space::new().width(Length::Fill))
|
||||||
|
.push(widget::tooltip(
|
||||||
|
button::custom(visuals::action_icon(
|
||||||
|
"M3 10h8 M3 10V3 M3 10c3-7 17-6 17 3a7 7 0 0 1-7 7",
|
||||||
|
if self.undo.is_some() && !self.saving {
|
||||||
|
palette.text
|
||||||
|
} else {
|
||||||
|
palette.muted
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.padding(4)
|
||||||
|
.on_press_maybe((self.undo.is_some() && !self.saving).then_some(Message::Undo)),
|
||||||
|
self.label("Undo last change", 11.),
|
||||||
|
widget::tooltip::Position::Top,
|
||||||
|
))
|
||||||
|
.push(widget::tooltip(
|
||||||
|
button::custom(visuals::action_icon(
|
||||||
|
"M21 3v6h-6 M3 21v-6h6 M3 9a9 9 0 0 1 15-6l3 6 M21 15a9 9 0 0 1-15 6l-3-6",
|
||||||
|
palette.text,
|
||||||
|
))
|
||||||
|
.padding(4)
|
||||||
|
.on_press_maybe((!self.saving).then_some(Message::Reload)),
|
||||||
|
self.label("Reload configuration", 11.),
|
||||||
|
widget::tooltip::Position::Top,
|
||||||
|
));
|
||||||
|
row([])
|
||||||
|
.push(sidebar)
|
||||||
|
.push(
|
||||||
|
container(
|
||||||
|
container(column([]).push(content.height(Length::Fill)).push(footer).spacing(6))
|
||||||
|
.padding(cosmic::iced::Padding {
|
||||||
|
top: 20.,
|
||||||
|
right: 20.,
|
||||||
|
bottom: 8.,
|
||||||
|
left: 20.,
|
||||||
|
})
|
||||||
|
.max_width(840)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.class(visuals::surface(palette.background, 0.)),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
use super::{
|
||||||
|
Alignment, App, Edit, Element, Kind, Length, Message, Page, PathBuf, button, column, container, gallery, row,
|
||||||
|
schema, set, text_input, visuals, widget,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(super) fn theme_file_picker(&self) -> Element<'static, Message> {
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
let path = ["theme.file", "theme.light.file", "theme.dark.file"][self.theme_file_target];
|
||||||
|
let file = self.draft.string(path, "");
|
||||||
|
let filename = if file.is_empty() {
|
||||||
|
"No override file".to_owned()
|
||||||
|
} else {
|
||||||
|
PathBuf::from(&file)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
let labels = column([])
|
||||||
|
.spacing(3)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.push(self.label("Theme overrides", 13.))
|
||||||
|
.push(widget::tooltip(
|
||||||
|
self.label(filename, 11.)
|
||||||
|
.class(cosmic::theme::Text::Color(palette.muted)),
|
||||||
|
self.label(
|
||||||
|
if file.is_empty() {
|
||||||
|
"Choose a partial KDL theme file".to_owned()
|
||||||
|
} else {
|
||||||
|
file.clone()
|
||||||
|
},
|
||||||
|
11.,
|
||||||
|
),
|
||||||
|
widget::tooltip::Position::Bottom,
|
||||||
|
));
|
||||||
|
let mut scopes = row([]).spacing(2);
|
||||||
|
for (index, label) in ["Shared", "Light", "Dark"].into_iter().enumerate() {
|
||||||
|
scopes = scopes.push(
|
||||||
|
button::custom(self.label(label, 12.))
|
||||||
|
.name(format!("Theme file: {label}"))
|
||||||
|
.padding([6, 8])
|
||||||
|
.class(visuals::button_style(palette, index == self.theme_file_target))
|
||||||
|
.on_press(Message::ThemeFileTarget(index)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut controls = row([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(
|
||||||
|
container(scopes)
|
||||||
|
.padding(3)
|
||||||
|
.class(visuals::surface(palette.sidebar, 10.)),
|
||||||
|
)
|
||||||
|
.push(
|
||||||
|
ferese_theme::controls::text_button(
|
||||||
|
if file.is_empty() { "Choose…" } else { "Replace…" },
|
||||||
|
self.font,
|
||||||
|
palette,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.on_press(Message::PickThemeFile(path)),
|
||||||
|
);
|
||||||
|
if !file.is_empty() {
|
||||||
|
controls = controls.push(widget::tooltip(
|
||||||
|
button::custom(visuals::action_icon("M6 6l12 12M6 18L18 6", palette.muted))
|
||||||
|
.padding(5)
|
||||||
|
.on_press(Message::Change(Edit::Unset(path.into()))),
|
||||||
|
self.label("Remove override", 11.),
|
||||||
|
widget::tooltip::Position::Bottom,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
container(
|
||||||
|
row([])
|
||||||
|
.spacing(16)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(labels)
|
||||||
|
.push(controls),
|
||||||
|
)
|
||||||
|
.padding([12, 12])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.class(visuals::surface(palette.card, 0.))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn auto_controls(&self) -> Element<'static, Message> {
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
let scheduled = self.draft.string("theme.schedule.source", "schedule") == "schedule";
|
||||||
|
let mut sources = row([]).spacing(4);
|
||||||
|
for (source, label) in [("system", "System"), ("schedule", "Schedule")] {
|
||||||
|
sources = sources.push(
|
||||||
|
ferese_theme::controls::text_button(label, self.font, palette, scheduled == (source == "schedule"))
|
||||||
|
.name(format!("Auto source: {label}"))
|
||||||
|
.on_press(Message::Change(set("theme.schedule.source", source))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let description = if scheduled {
|
||||||
|
"Switch between light and dark at set times."
|
||||||
|
} else {
|
||||||
|
"Follow the GTK/GNOME appearance preference."
|
||||||
|
};
|
||||||
|
let mut options = column([]).spacing(8).push(
|
||||||
|
row([])
|
||||||
|
.spacing(12)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(
|
||||||
|
column([])
|
||||||
|
.spacing(3)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.push(self.label("Automatic appearance", 13.))
|
||||||
|
.push(self.note(description)),
|
||||||
|
)
|
||||||
|
.push(sources),
|
||||||
|
);
|
||||||
|
if scheduled {
|
||||||
|
let mut times = row([]).spacing(16).align_y(Alignment::Center);
|
||||||
|
for field in schema::fields(Page::Appearance)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|field| field.path.starts_with("theme.schedule."))
|
||||||
|
{
|
||||||
|
let path = field.path.clone();
|
||||||
|
let Kind::Text { default, .. } = &field.kind else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = self
|
||||||
|
.inputs
|
||||||
|
.get(&path)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| self.draft.string(&path, default));
|
||||||
|
let submit = field.clone();
|
||||||
|
let input = text_input(*default, value)
|
||||||
|
.font(self.font)
|
||||||
|
.size(12)
|
||||||
|
.style(visuals::input_style(palette))
|
||||||
|
.on_input(move |value| Message::Draft(path.clone(), value))
|
||||||
|
.on_submit(move |_| Message::Commit(submit.clone()))
|
||||||
|
.on_unfocus(Message::Commit(field.clone()))
|
||||||
|
.width(if field.path.ends_with("timezone") { 150 } else { 75 });
|
||||||
|
times = times.push(
|
||||||
|
row([])
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.push(self.label(field.label, 12.))
|
||||||
|
.push(input),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options = options.push(times);
|
||||||
|
}
|
||||||
|
container(options)
|
||||||
|
.padding(12)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.class(visuals::surface(palette.card, 0.))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn theme_gallery(
|
||||||
|
&self,
|
||||||
|
appearance: Option<ferese_config::theme::Appearance>,
|
||||||
|
) -> Element<'static, Message> {
|
||||||
|
use ferese_config::theme::Appearance::{Dark, Light};
|
||||||
|
let palette = visuals::Palette::from_resolved(&self.resolved.presented);
|
||||||
|
let selected_id = visuals::family_selection(&self.draft, appearance);
|
||||||
|
let choices = self.resolved.families.clone();
|
||||||
|
let ids: Vec<_> = choices.iter().map(|family| family.id.clone()).collect();
|
||||||
|
let mut heading = row([]).align_y(Alignment::Center).spacing(12).push(
|
||||||
|
self.label(
|
||||||
|
match appearance {
|
||||||
|
Some(Light) => "Light theme",
|
||||||
|
Some(Dark) => "Dark theme",
|
||||||
|
None => "Theme",
|
||||||
|
},
|
||||||
|
15.,
|
||||||
|
)
|
||||||
|
.width(Length::Fill),
|
||||||
|
);
|
||||||
|
if appearance != Some(Dark) {
|
||||||
|
if self.undo.is_some() {
|
||||||
|
heading = heading.push(
|
||||||
|
ferese_theme::controls::text_button("Undo", self.font, palette, false)
|
||||||
|
.on_press_maybe((!self.saving).then_some(Message::Undo)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
heading = heading.push(
|
||||||
|
ferese_theme::controls::text_button("Import…", self.font, palette, false)
|
||||||
|
.on_press(Message::ImportTheme),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut gallery = column([]).spacing(10).push(heading);
|
||||||
|
for (chunk, families) in choices.chunks(3).enumerate() {
|
||||||
|
let mut tiles = row([]).spacing(12);
|
||||||
|
for (offset, family) in families.iter().enumerate() {
|
||||||
|
let selected = family.id == selected_id;
|
||||||
|
let index = chunk * 3 + offset;
|
||||||
|
let id = family.id.clone();
|
||||||
|
let active = selected
|
||||||
|
.then_some(self.resolved.theme.appearance)
|
||||||
|
.filter(|a| appearance.is_none_or(|variant| variant == *a));
|
||||||
|
let ids = ids.clone();
|
||||||
|
let tile_id = gallery_id(&id, appearance);
|
||||||
|
tiles = tiles.push(gallery::tile(
|
||||||
|
family,
|
||||||
|
appearance,
|
||||||
|
active,
|
||||||
|
selected,
|
||||||
|
palette,
|
||||||
|
self.font,
|
||||||
|
tile_id,
|
||||||
|
Message::Family(id, appearance),
|
||||||
|
move |key| {
|
||||||
|
gallery::neighbor(index, ids.len(), key)
|
||||||
|
.map(|index| Message::Family(ids[index].clone(), appearance))
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for _ in families.len()..3 {
|
||||||
|
tiles = tiles.push(widget::Space::new().width(Length::Fill));
|
||||||
|
}
|
||||||
|
gallery = gallery.push(tiles);
|
||||||
|
}
|
||||||
|
gallery::group(
|
||||||
|
container(gallery).width(Length::Fill).max_width(640),
|
||||||
|
cosmic::iced::advanced::widget::Id::new(format!("theme-gallery-{appearance:?}")),
|
||||||
|
match appearance {
|
||||||
|
Some(Light) => "Light theme",
|
||||||
|
Some(Dark) => "Dark theme",
|
||||||
|
None => "Theme",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn gallery_id(
|
||||||
|
id: &str,
|
||||||
|
appearance: Option<ferese_config::theme::Appearance>,
|
||||||
|
) -> cosmic::iced::advanced::widget::Id {
|
||||||
|
cosmic::iced::advanced::widget::Id::new(format!("theme-{appearance:?}-{id}"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
use super::{
|
||||||
|
Background, BarMetrics, Border, Color, Element, FereseShell, Length, Message, ShellSnapshot, ShellTheme, alignment,
|
||||||
|
bar_icon, button, color, color_with_opacity, container, control, motion, row, status_ui, text, theme, window,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FereseShell {
|
||||||
|
pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
|
||||||
|
let name = self.outputs.iter().find(|entry| entry.bar == id)?.name.as_deref()?;
|
||||||
|
self.snapshot.outputs.iter().find(|output| output.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bar_hidden(&self, id: window::Id) -> bool {
|
||||||
|
output_bar_hidden(&self.snapshot, self.output_for_bar(id).map(|output| output.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
if self.bar_hidden(id) {
|
||||||
|
return container(text("")).width(Length::Fill).height(Length::Fill).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let focused_output = self.output_for_bar(id);
|
||||||
|
let shell_theme = self.config.theme.for_bar();
|
||||||
|
let bar = BarMetrics::from(shell_theme);
|
||||||
|
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
|
||||||
|
workspace_row = workspace_row.push(motion::button(
|
||||||
|
button::custom(overview_control(bar, color(shell_theme.accent)))
|
||||||
|
.height(bar.control_height)
|
||||||
|
.padding([0, 7])
|
||||||
|
.on_press(cosmic::Action::App(Message::ToggleOverview)),
|
||||||
|
color(shell_theme.text_primary),
|
||||||
|
self.overview_active,
|
||||||
|
1.0,
|
||||||
|
));
|
||||||
|
let mut workspace_buttons = row::with_capacity(self.snapshot.workspaces.len())
|
||||||
|
.spacing(1)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
|
||||||
|
for workspace in &self.snapshot.workspaces {
|
||||||
|
let active = workspace_active_on_bar(workspace.id, focused_output);
|
||||||
|
let owner = workspace
|
||||||
|
.output
|
||||||
|
.and_then(|id| self.snapshot.outputs.iter().find(|output| output.id == id));
|
||||||
|
let occupied = self
|
||||||
|
.snapshot
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.workspace == workspace.id);
|
||||||
|
let indicator = workspace_indicator(
|
||||||
|
&workspace.name,
|
||||||
|
active,
|
||||||
|
workspace.active && !active,
|
||||||
|
occupied,
|
||||||
|
bar,
|
||||||
|
shell_theme,
|
||||||
|
);
|
||||||
|
let workspace_button = button::custom(indicator)
|
||||||
|
.name(format!(
|
||||||
|
"Workspace {}{}{}",
|
||||||
|
workspace.name,
|
||||||
|
if active { ", active" } else { "" },
|
||||||
|
owner.map_or(String::new(), |output| format!(", on {}", output.name))
|
||||||
|
))
|
||||||
|
.height(bar.control_height)
|
||||||
|
.padding(0)
|
||||||
|
.on_press(cosmic::Action::App(Message::ActivateWorkspace(workspace.id)));
|
||||||
|
|
||||||
|
let selector = motion::button(
|
||||||
|
workspace_button,
|
||||||
|
color(if active {
|
||||||
|
shell_theme.accent
|
||||||
|
} else {
|
||||||
|
shell_theme.text_primary
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
// Iced tooltips are overlays inside this bar-height layer surface;
|
||||||
|
// viewport clamping puts them back over the selector. Keep the
|
||||||
|
// accessible button name, without an overlay stealing its target.
|
||||||
|
workspace_buttons = workspace_buttons.push(selector);
|
||||||
|
}
|
||||||
|
workspace_row = workspace_row.push(
|
||||||
|
container(workspace_buttons)
|
||||||
|
.padding([0, 3])
|
||||||
|
.class(theme::Container::custom(move |_| bar_group_style(shell_theme))),
|
||||||
|
);
|
||||||
|
|
||||||
|
let foreground = color(shell_theme.text_primary);
|
||||||
|
let left = cosmic::iced::widget::scrollable(workspace_row)
|
||||||
|
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
||||||
|
cosmic::iced::widget::scrollable::Scrollbar::default()
|
||||||
|
.width(0)
|
||||||
|
.scroller_width(0),
|
||||||
|
))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(bar.control_height);
|
||||||
|
let (date, time) = self.clock.split_once(", ").unwrap_or(("", &self.clock));
|
||||||
|
let clock = container(motion::button(
|
||||||
|
button::custom(
|
||||||
|
row![
|
||||||
|
text(date).size(12).class(theme::Text::Color(foreground)),
|
||||||
|
text(time).size(bar.text_size).class(theme::Text::Color(foreground)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center),
|
||||||
|
)
|
||||||
|
.padding([4, 8])
|
||||||
|
.height(bar.control_height)
|
||||||
|
.name("Open calendar")
|
||||||
|
.on_press_with_rectangle(move |offset, bounds| {
|
||||||
|
cosmic::Action::App(Message::OpenMenuOn(
|
||||||
|
id,
|
||||||
|
status_ui::Menu::Calendar,
|
||||||
|
cosmic::iced::Rectangle {
|
||||||
|
x: (bounds.x - offset.x).round() as i32,
|
||||||
|
y: (bounds.y - offset.y).round() as i32,
|
||||||
|
width: bounds.width.round() as i32,
|
||||||
|
height: bounds.height.round() as i32,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
foreground,
|
||||||
|
self.menu
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|menu| menu.kind == status_ui::Menu::Calendar),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.class(theme::Container::custom(move |_| bar_group_style(shell_theme)));
|
||||||
|
let right = row![
|
||||||
|
self.view_status_bar().map(move |action| match action {
|
||||||
|
cosmic::Action::App(Message::OpenMenu(kind, anchor)) =>
|
||||||
|
cosmic::Action::App(Message::OpenMenuOn(id, kind, anchor)),
|
||||||
|
other => other,
|
||||||
|
}),
|
||||||
|
clock
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
let available = self
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|output| output.bar == id)
|
||||||
|
.and_then(|output| output.size)
|
||||||
|
.map_or(0., |(width, _)| width as f32)
|
||||||
|
- 2. * (shell_theme.panel_padding + shell_theme.bar_margin_horizontal as f32);
|
||||||
|
let title_width = (available - 920.).clamp(0., 360.);
|
||||||
|
let title = if self.config.status.window_title && title_width > 0. {
|
||||||
|
focused_bar_title(&self.snapshot, focused_output)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let center = text(title)
|
||||||
|
.size(bar.text_size)
|
||||||
|
.width(title_width)
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.wrapping(cosmic::iced::widget::text::Wrapping::None)
|
||||||
|
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
|
||||||
|
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
|
||||||
|
))
|
||||||
|
.class(theme::Text::Color(foreground));
|
||||||
|
let right = cosmic::iced::widget::scrollable(container(right).width(Length::Shrink))
|
||||||
|
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
|
||||||
|
cosmic::iced::widget::scrollable::Scrollbar::default()
|
||||||
|
.width(0)
|
||||||
|
.scroller_width(0),
|
||||||
|
))
|
||||||
|
.anchor_right()
|
||||||
|
.width(Length::Shrink)
|
||||||
|
.height(bar.control_height);
|
||||||
|
let content = row![
|
||||||
|
container(left).width(Length::Fill),
|
||||||
|
center,
|
||||||
|
container(right)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment::Horizontal::Right),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(cosmic::iced::Alignment::Center)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let compositor_material = self
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.bar == id && entry.effects.is_some());
|
||||||
|
container(content)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.padding([0, shell_theme.panel_padding.round() as u16])
|
||||||
|
.class(theme::Container::custom(move |_| {
|
||||||
|
bar_style(shell_theme, compositor_material)
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action<Message>> {
|
||||||
|
container(bar_icon(
|
||||||
|
ferese_theme::icons::FERESE,
|
||||||
|
bar.overview_icon_size,
|
||||||
|
foreground,
|
||||||
|
))
|
||||||
|
.width(bar.overview_icon_size)
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(super) fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
|
||||||
|
let output = snapshot
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|output| output.focused)
|
||||||
|
.or_else(|| snapshot.outputs.first())
|
||||||
|
.map(|output| output.id);
|
||||||
|
output_bar_hidden(snapshot, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn output_bar_hidden(snapshot: &ShellSnapshot, output: Option<u64>) -> bool {
|
||||||
|
let active_workspace = snapshot
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| Some(candidate.id) == output)
|
||||||
|
.map(|output| output.active_workspace);
|
||||||
|
|
||||||
|
active_workspace.is_some_and(|workspace| {
|
||||||
|
snapshot
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.workspace == workspace && window.fullscreen)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workspace_indicator(
|
||||||
|
name: &str,
|
||||||
|
active: bool,
|
||||||
|
active_elsewhere: bool,
|
||||||
|
occupied: bool,
|
||||||
|
bar: BarMetrics,
|
||||||
|
shell_theme: ShellTheme,
|
||||||
|
) -> Element<'static, cosmic::Action<Message>> {
|
||||||
|
let foreground = if active {
|
||||||
|
shell_theme.accent
|
||||||
|
} else if occupied {
|
||||||
|
shell_theme.text_primary
|
||||||
|
} else {
|
||||||
|
shell_theme.text_muted
|
||||||
|
};
|
||||||
|
container(
|
||||||
|
text(name.to_owned())
|
||||||
|
.size(12)
|
||||||
|
.class(theme::Text::Color(color(foreground))),
|
||||||
|
)
|
||||||
|
.width(24)
|
||||||
|
.height(bar.control_height)
|
||||||
|
.align_x(alignment::Horizontal::Center)
|
||||||
|
.align_y(alignment::Vertical::Center)
|
||||||
|
.class(theme::Container::custom(move |_| {
|
||||||
|
workspace_selector_style(active, active_elsewhere, occupied, shell_theme)
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn focused_bar_title<'a>(snapshot: &'a ShellSnapshot, output: Option<&control::OutputSnapshot>) -> &'a str {
|
||||||
|
let Some(output) = output else {
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
snapshot
|
||||||
|
.windows
|
||||||
|
.iter()
|
||||||
|
.find(|window| window.focused && window.workspace == output.active_workspace)
|
||||||
|
.map_or("", |window| {
|
||||||
|
if window.title.trim().is_empty() {
|
||||||
|
&window.app_id
|
||||||
|
} else {
|
||||||
|
&window.title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
|
||||||
|
output.is_some_and(|output| output.active_workspace == workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workspace_selector_style(
|
||||||
|
active: bool,
|
||||||
|
active_elsewhere: bool,
|
||||||
|
occupied: bool,
|
||||||
|
shell_theme: ShellTheme,
|
||||||
|
) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
background: if active {
|
||||||
|
Some(Background::Color(color_with_opacity(shell_theme.accent, 0.16)))
|
||||||
|
} else if occupied {
|
||||||
|
Some(Background::Color(color_with_opacity(shell_theme.text_primary, 0.04)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
border: Border {
|
||||||
|
color: color_with_opacity(shell_theme.accent, 0.55),
|
||||||
|
width: if active_elsewhere { 1.0 } else { 0.0 },
|
||||||
|
radius: shell_theme.material_radius.min(14.0).into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bar_style(theme: ShellTheme, compositor_material: bool) -> container::Style {
|
||||||
|
let mut style = ferese_theme::controls::surface_appearance(color(theme.bar_background), theme.bar_radius);
|
||||||
|
if compositor_material {
|
||||||
|
style.background = None;
|
||||||
|
}
|
||||||
|
style.text_color = Some(color(theme.text_primary));
|
||||||
|
style.icon_color = style.text_color;
|
||||||
|
style.snap = true;
|
||||||
|
style
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(color_with_opacity(theme.text_primary, 0.035))),
|
||||||
|
border: Border {
|
||||||
|
color: color_with_opacity(theme.text_muted, 0.12),
|
||||||
|
width: 1.0,
|
||||||
|
radius: theme.material_radius.min(16.0).into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,734 @@
|
|||||||
|
use super::{
|
||||||
|
Anchor, Background, Border, Color, Element, FereseShell, IcedMargin, IcedOutput, KeyboardInteractivity, Layer,
|
||||||
|
Length, Limits, Message, NoteDrag, SctkLayerSurfaceSettings, Task, Zoned, alignment, button, color, config,
|
||||||
|
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
|
||||||
|
set_margin, stacked_clock_digits, text, theme, window,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FereseShell {
|
||||||
|
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
|
||||||
|
if self.note_saving || self.note_pending.is_empty() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(path) = config::config_path() else {
|
||||||
|
self.note_error = Some("Configuration path unavailable.".into());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
self.note_saving = true;
|
||||||
|
self.note_inflight = std::mem::take(&mut self.note_pending);
|
||||||
|
let edits = self.note_inflight.clone();
|
||||||
|
cosmic::task::future(async move {
|
||||||
|
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = send.send(note_store::save(&path, &edits));
|
||||||
|
});
|
||||||
|
Message::NotesStored(
|
||||||
|
receive
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| Err("Note save worker stopped.".into())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_note_text(&mut self, id: &str, value: String) -> Task<Message> {
|
||||||
|
if let Some(note) = self.config.desktop_widgets.notes.iter_mut().find(|note| note.id == id)
|
||||||
|
&& (note.text != value || self.note_error.is_some())
|
||||||
|
{
|
||||||
|
note.text = value.clone();
|
||||||
|
self.note_pending.push(note_store::Edit::Text(id.to_owned(), value));
|
||||||
|
}
|
||||||
|
self.flush_note_changes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn finish_note_edit(&mut self) -> Task<Message> {
|
||||||
|
if let Some(editor) = self.note_editor.take() {
|
||||||
|
self.set_note_text(&editor.id, editor.content.text())
|
||||||
|
} else {
|
||||||
|
self.flush_note_changes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn begin_note_drag(&mut self, id: String, surface: window::Id) -> Task<Message> {
|
||||||
|
self.begin_widget_drag(Some(id), surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn begin_widget_drag(&mut self, id: Option<String>, surface: window::Id) -> Task<Message> {
|
||||||
|
if self.note_drag.is_some() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let Some(entry) = self
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.clock == Some(surface) || entry.notes.iter().any(|(_, window)| *window == surface))
|
||||||
|
else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(size) = entry.size else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let (origin, dimensions) = if let Some(id) = &id {
|
||||||
|
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| ¬e.id == id) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
(note_origin(note, size), (note.width, note.height))
|
||||||
|
} else {
|
||||||
|
let clock = &self.config.desktop_widgets.clock;
|
||||||
|
(
|
||||||
|
widget_origin(
|
||||||
|
clock.anchor,
|
||||||
|
clock.margin_x,
|
||||||
|
clock.margin_y,
|
||||||
|
(clock.width, clock.height),
|
||||||
|
size,
|
||||||
|
),
|
||||||
|
(clock.width, clock.height),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let output = entry.output.clone();
|
||||||
|
let mut obstacles = Vec::new();
|
||||||
|
let clock = &self.config.desktop_widgets.clock;
|
||||||
|
|
||||||
|
if id.is_some() && clock.on_output(entry.name.as_deref()) {
|
||||||
|
let position = widget_origin(
|
||||||
|
clock.anchor,
|
||||||
|
clock.margin_x,
|
||||||
|
clock.margin_y,
|
||||||
|
(clock.width, clock.height),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
obstacles.push(cosmic::iced::Rectangle::new(
|
||||||
|
position,
|
||||||
|
cosmic::iced::Size::new(clock.width as f32, clock.height as f32),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for note in &self.config.desktop_widgets.notes {
|
||||||
|
if Some(¬e.id) != id.as_ref() && note.on_output(entry.name.as_deref()) {
|
||||||
|
obstacles.push(cosmic::iced::Rectangle::new(
|
||||||
|
note_origin(note, size),
|
||||||
|
cosmic::iced::Size::new(note.width as f32, note.height as f32),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlay = window::Id::unique();
|
||||||
|
self.note_drag = Some(NoteDrag {
|
||||||
|
id,
|
||||||
|
source: surface,
|
||||||
|
overlay,
|
||||||
|
origin,
|
||||||
|
position: origin,
|
||||||
|
start: self.note_pointer.get(&surface).copied().unwrap_or_default(),
|
||||||
|
output_size: size,
|
||||||
|
obstacles,
|
||||||
|
});
|
||||||
|
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| SctkLayerSurfaceSettings {
|
||||||
|
id: overlay,
|
||||||
|
layer: Layer::Overlay,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity::None,
|
||||||
|
input_zone: Some(Vec::new()),
|
||||||
|
anchor: Anchor::TOP | Anchor::LEFT,
|
||||||
|
margin: IcedMargin {
|
||||||
|
top: origin.y.round() as i32,
|
||||||
|
left: origin.x.round() as i32,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
output: IcedOutput::Output(output.clone()),
|
||||||
|
namespace: "ferese-shell-note-drag".into(),
|
||||||
|
exclusive_zone: -1,
|
||||||
|
size: Some((Some(dimensions.0), Some(dimensions.1))),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
|
},
|
||||||
|
Some(Box::new(Self::view_note_drag)),
|
||||||
|
);
|
||||||
|
|
||||||
|
Task::batch([
|
||||||
|
self.finish_note_edit(),
|
||||||
|
cosmic::task::message(cosmic::Action::Surface(action)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_note_drag(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
let Some(drag) = &self.note_drag else {
|
||||||
|
return text("").into();
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(id) = &drag.id {
|
||||||
|
self.view_note_content(id, drag.source)
|
||||||
|
} else {
|
||||||
|
self.view_clock_content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn finish_note_drag(&mut self, save: bool) -> Task<Message> {
|
||||||
|
let Some(drag) = self.note_drag.take() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let mut tasks = vec![destroy_layer_surface(drag.overlay)];
|
||||||
|
|
||||||
|
if save {
|
||||||
|
if drag.id.is_none() {
|
||||||
|
let clock = &mut self.config.desktop_widgets.clock;
|
||||||
|
clock.anchor = ferese_core::desktop::Anchor::TopLeft;
|
||||||
|
clock.margin_x = drag.position.x.round() as i32;
|
||||||
|
clock.margin_y = drag.position.y.round() as i32;
|
||||||
|
for entry in &self.outputs {
|
||||||
|
if let Some(id) = entry.clock {
|
||||||
|
tasks.push(set_anchor(id, Anchor::TOP | Anchor::LEFT));
|
||||||
|
tasks.push(set_margin(id, clock.margin_y, 0, 0, clock.margin_x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.note_pending
|
||||||
|
.push(note_store::Edit::ClockPosition(clock.margin_x, clock.margin_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(note) = self
|
||||||
|
.config
|
||||||
|
.desktop_widgets
|
||||||
|
.notes
|
||||||
|
.iter_mut()
|
||||||
|
.find(|note| Some(¬e.id) == drag.id.as_ref())
|
||||||
|
{
|
||||||
|
note.anchor = ferese_core::desktop::Anchor::TopLeft;
|
||||||
|
note.margin_x = drag.position.x.round() as i32;
|
||||||
|
note.margin_y = drag.position.y.round() as i32;
|
||||||
|
|
||||||
|
for entry in &self.outputs {
|
||||||
|
if let Some((_, id)) = entry.notes.iter().find(|(id, _)| Some(id) == drag.id.as_ref()) {
|
||||||
|
tasks.push(set_anchor(*id, Anchor::TOP | Anchor::LEFT));
|
||||||
|
tasks.push(set_margin(*id, note.margin_y, 0, 0, note.margin_x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.note_pending.push(note_store::Edit::Position(
|
||||||
|
note.id.clone(),
|
||||||
|
note.margin_x,
|
||||||
|
note.margin_y,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.push(self.flush_note_changes());
|
||||||
|
}
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rebuild_notes(&mut self, reset: bool) -> Task<Message> {
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for entry in &mut self.outputs {
|
||||||
|
if reset {
|
||||||
|
for (_, id) in entry.notes.drain(..) {
|
||||||
|
self.note_pointer.remove(&id);
|
||||||
|
tasks.push(destroy_layer_surface(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for note in &self.config.desktop_widgets.notes {
|
||||||
|
if !note.on_output(entry.name.as_deref()) || entry.notes.iter().any(|(id, _)| id == ¬e.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = window::Id::unique();
|
||||||
|
entry.notes.push((note.id.clone(), id));
|
||||||
|
let note = note.clone();
|
||||||
|
let note_id = note.id.clone();
|
||||||
|
let output = entry.output.clone();
|
||||||
|
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| {
|
||||||
|
let (anchor, margin) = widget_placement(note.anchor, note.margin_x, note.margin_y);
|
||||||
|
SctkLayerSurfaceSettings {
|
||||||
|
id,
|
||||||
|
layer: Layer::Bottom,
|
||||||
|
keyboard_interactivity: if note.interactive {
|
||||||
|
KeyboardInteractivity::OnDemand
|
||||||
|
} else {
|
||||||
|
KeyboardInteractivity::None
|
||||||
|
},
|
||||||
|
input_zone: if note.interactive { None } else { Some(Vec::new()) },
|
||||||
|
anchor,
|
||||||
|
margin,
|
||||||
|
output: IcedOutput::Output(output.clone()),
|
||||||
|
namespace: "ferese-shell-sticky-note".into(),
|
||||||
|
exclusive_zone: -1,
|
||||||
|
size: Some((Some(note.width), Some(note.height))),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Box::new(move |app| app.view_note(¬e_id, id))),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_note(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
if self
|
||||||
|
.note_drag
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|drag| drag.id.as_deref() == Some(id))
|
||||||
|
{
|
||||||
|
return container(text("")).width(Length::Fill).height(Length::Fill).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.view_note_content(id, surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_note_content(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) else {
|
||||||
|
return text("").into();
|
||||||
|
};
|
||||||
|
let alignment = match note.alignment {
|
||||||
|
ferese_core::desktop::Alignment::Left => alignment::Horizontal::Left,
|
||||||
|
ferese_core::desktop::Alignment::Center => alignment::Horizontal::Center,
|
||||||
|
ferese_core::desktop::Alignment::Right => alignment::Horizontal::Right,
|
||||||
|
};
|
||||||
|
let font = configured_font(
|
||||||
|
note.font_family
|
||||||
|
.as_deref()
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.or(self.config.font_family.as_deref()),
|
||||||
|
);
|
||||||
|
let mut foreground = color(
|
||||||
|
note.color
|
||||||
|
.as_deref()
|
||||||
|
.and_then(config::parse_color)
|
||||||
|
.unwrap_or(self.config.theme.text_primary),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreground.a *= note.opacity;
|
||||||
|
|
||||||
|
let background = if note.background.as_deref() == Some("") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let mut tint = color(
|
||||||
|
note.background
|
||||||
|
.as_deref()
|
||||||
|
.and_then(config::parse_color)
|
||||||
|
.unwrap_or(self.config.theme.surface_base),
|
||||||
|
);
|
||||||
|
tint.a *= note.opacity;
|
||||||
|
Some(Background::Color(tint))
|
||||||
|
};
|
||||||
|
let mut body = cosmic::widget::column([]).spacing(note.gap).align_x(alignment);
|
||||||
|
|
||||||
|
if note.interactive {
|
||||||
|
let title = ferese_theme::text(if note.title.is_empty() { "⋮⋮" } else { ¬e.title }, font)
|
||||||
|
.size(note.title_size)
|
||||||
|
.class(theme::Text::Color(foreground));
|
||||||
|
let header = cosmic::widget::mouse_area(container(title).width(Length::Fill))
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab)
|
||||||
|
.on_press(cosmic::Action::App(Message::BeginNoteDrag(id.to_owned(), surface)));
|
||||||
|
let editing = self.note_editor.as_ref().is_some_and(|editor| editor.id == id);
|
||||||
|
|
||||||
|
body = body.push(
|
||||||
|
row([]).push(header).push(motion::button(
|
||||||
|
button::custom(text(if editing { "Done" } else { "Edit" }))
|
||||||
|
.padding([4, 8])
|
||||||
|
.on_press(cosmic::Action::App(if editing {
|
||||||
|
Message::FinishNoteEdit
|
||||||
|
} else {
|
||||||
|
Message::BeginNoteEdit(id.to_owned())
|
||||||
|
})),
|
||||||
|
foreground,
|
||||||
|
false,
|
||||||
|
1.0,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else if !note.title.is_empty() {
|
||||||
|
body = body.push(
|
||||||
|
ferese_theme::text(
|
||||||
|
note.title.clone(),
|
||||||
|
cosmic::font::Font {
|
||||||
|
weight: cosmic::iced::font::Weight::Semibold,
|
||||||
|
..font
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size(note.title_size)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment)
|
||||||
|
.class(theme::Text::Color(foreground)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(editor) = &self.note_editor
|
||||||
|
&& editor.id == id
|
||||||
|
{
|
||||||
|
let id = id.to_owned();
|
||||||
|
body = body.push(
|
||||||
|
cosmic::widget::TextEditor::new(&editor.content)
|
||||||
|
.style(|theme, status| {
|
||||||
|
use cosmic::iced::widget::text_editor::Catalog;
|
||||||
|
let mut style = theme.style(&<cosmic::Theme as Catalog>::default(), status);
|
||||||
|
style.border.radius = motion::radius(f32::MAX).into();
|
||||||
|
style
|
||||||
|
})
|
||||||
|
.height(Length::Fill)
|
||||||
|
.font(font)
|
||||||
|
.size(note.text_size)
|
||||||
|
.on_action(move |action| cosmic::Action::App(Message::NoteAction(id.clone(), action))),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = body.push(
|
||||||
|
ferese_theme::text(note.text.clone(), font)
|
||||||
|
.size(note.text_size)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment)
|
||||||
|
.class(theme::Text::Color(foreground)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = &self.note_error {
|
||||||
|
body = body.push(text(error.clone()).size(10).class(theme::Text::Color(foreground)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let radius = self.config.theme.material_radius;
|
||||||
|
|
||||||
|
container(body)
|
||||||
|
.padding(note.padding)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background,
|
||||||
|
border: Border {
|
||||||
|
radius: radius.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rebuild_clocks(&mut self, reset: bool) -> Task<Message> {
|
||||||
|
self.desktop_clock = self
|
||||||
|
.config
|
||||||
|
.desktop_widgets
|
||||||
|
.clock
|
||||||
|
.labels(&Zoned::now())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for entry in &mut self.outputs {
|
||||||
|
if !reset && entry.clock.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = entry.clock.take() {
|
||||||
|
self.note_pointer.remove(&id);
|
||||||
|
tasks.push(destroy_layer_surface(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let clock = self.config.desktop_widgets.clock.clone();
|
||||||
|
|
||||||
|
if !clock.on_output(entry.name.as_deref()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = window::Id::unique();
|
||||||
|
entry.clock = Some(id);
|
||||||
|
let output = entry.output.clone();
|
||||||
|
let action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| {
|
||||||
|
let (anchor, margin) = clock_placement(&clock);
|
||||||
|
SctkLayerSurfaceSettings {
|
||||||
|
id,
|
||||||
|
layer: Layer::Bottom,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity::OnDemand,
|
||||||
|
input_zone: None,
|
||||||
|
anchor,
|
||||||
|
margin,
|
||||||
|
output: IcedOutput::Output(output.clone()),
|
||||||
|
namespace: "ferese-shell-desktop-clock".into(),
|
||||||
|
exclusive_zone: -1,
|
||||||
|
size: Some((Some(clock.width), Some(clock.height))),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Box::new(move |app| app.view_desktop_clock(id))),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_desktop_clock(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
if self
|
||||||
|
.note_drag
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|drag| drag.id.is_none() && drag.source == id)
|
||||||
|
{
|
||||||
|
return container(text("")).width(Length::Fill).height(Length::Fill).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmic::widget::mouse_area(self.view_clock_content())
|
||||||
|
.on_press(cosmic::Action::App(Message::BeginClockDrag(id)))
|
||||||
|
.interaction(cosmic::iced::mouse::Interaction::Grab)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_clock_content(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
use ferese_core::desktop::Alignment as ClockAlignment;
|
||||||
|
let clock = &self.config.desktop_widgets.clock;
|
||||||
|
let alignment = match clock.alignment {
|
||||||
|
ClockAlignment::Left => alignment::Horizontal::Left,
|
||||||
|
ClockAlignment::Center => alignment::Horizontal::Center,
|
||||||
|
ClockAlignment::Right => alignment::Horizontal::Right,
|
||||||
|
};
|
||||||
|
let pixel = clock.style == ferese_core::desktop::ClockStyle::Pixel;
|
||||||
|
let custom_font = clock.font_family.as_deref().filter(|family| !family.trim().is_empty());
|
||||||
|
let mut font = configured_font(if pixel {
|
||||||
|
custom_font.or(Some("Cantarell"))
|
||||||
|
} else {
|
||||||
|
custom_font.or(self.config.font_family.as_deref())
|
||||||
|
});
|
||||||
|
if pixel && custom_font.is_none() {
|
||||||
|
font.weight = cosmic::iced::font::Weight::Black;
|
||||||
|
} else if clock.bold {
|
||||||
|
font.weight = cosmic::iced::font::Weight::Bold;
|
||||||
|
}
|
||||||
|
let date_font = configured_font(custom_font.or(self.config.font_family.as_deref()));
|
||||||
|
let tint = |custom: &Option<String>, fallback| {
|
||||||
|
let mut tint = color(custom.as_deref().and_then(config::parse_color).unwrap_or(fallback));
|
||||||
|
tint.a *= clock.opacity;
|
||||||
|
tint
|
||||||
|
};
|
||||||
|
let foreground = tint(&clock.color, self.config.theme.text_primary);
|
||||||
|
let accent = color(self.config.theme.accent);
|
||||||
|
let custom_color = clock.color.as_deref().and_then(config::parse_color).is_some();
|
||||||
|
let digit_colors = [0.8, 0.45].map(|text_mix| {
|
||||||
|
if custom_color {
|
||||||
|
return foreground;
|
||||||
|
}
|
||||||
|
Color {
|
||||||
|
r: foreground.r * text_mix + accent.r * (1.0 - text_mix),
|
||||||
|
g: foreground.g * text_mix + accent.g * (1.0 - text_mix),
|
||||||
|
b: foreground.b * text_mix + accent.b * (1.0 - text_mix),
|
||||||
|
a: foreground.a,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut labels = cosmic::widget::column([]).spacing(clock.gap).align_x(alignment);
|
||||||
|
|
||||||
|
if pixel && clock.show_date {
|
||||||
|
labels = labels.push(
|
||||||
|
ferese_theme::text(self.desktop_clock.1.clone(), date_font)
|
||||||
|
.size(clock.date_size)
|
||||||
|
.class(theme::Text::Color(tint(
|
||||||
|
&clock.date_color,
|
||||||
|
self.config.theme.text_primary,
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(digits) = pixel.then(|| stacked_clock_digits(&self.desktop_clock.0)).flatten() {
|
||||||
|
let available_height = (clock.height as f32
|
||||||
|
- clock.padding * 2.0
|
||||||
|
- if clock.show_date {
|
||||||
|
clock.date_size * 1.3 + clock.gap
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})
|
||||||
|
.max(16.0);
|
||||||
|
let size = clock
|
||||||
|
.time_size
|
||||||
|
.min(available_height / 1.6)
|
||||||
|
.min((clock.width as f32 - clock.padding * 2.0).max(16.0) / 1.5);
|
||||||
|
let mut stack = cosmic::widget::column([]).spacing(0).align_x(alignment);
|
||||||
|
|
||||||
|
for (line, digits) in digits.into_iter().enumerate() {
|
||||||
|
let mut digit_row = row([]).spacing(0);
|
||||||
|
|
||||||
|
for (position, digit) in digits.chars().enumerate() {
|
||||||
|
digit_row = digit_row.push(
|
||||||
|
ferese_theme::text(digit.to_string(), font)
|
||||||
|
.size(size)
|
||||||
|
.line_height(cosmic::iced::widget::text::LineHeight::Relative(0.8))
|
||||||
|
.class(theme::Text::Color(digit_colors[(line + position) % 2])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stack = stack.push(digit_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = labels.push(stack);
|
||||||
|
} else {
|
||||||
|
let characters = self.desktop_clock.0.chars().count().max(1) as f32;
|
||||||
|
let available_width = (clock.width as f32 - clock.padding * 2.0).max(1.0);
|
||||||
|
let available_height = (clock.height as f32
|
||||||
|
- clock.padding * 2.0
|
||||||
|
- if clock.show_date {
|
||||||
|
clock.date_size * 1.3 + clock.gap
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
})
|
||||||
|
.max(1.0);
|
||||||
|
let size = clock
|
||||||
|
.time_size
|
||||||
|
.min(available_width / (characters * 0.75))
|
||||||
|
.min(available_height / 1.3);
|
||||||
|
|
||||||
|
labels = labels.push(
|
||||||
|
ferese_theme::text(self.desktop_clock.0.clone(), font)
|
||||||
|
.size(size)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment)
|
||||||
|
.class(theme::Text::Color(foreground)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pixel && clock.show_date {
|
||||||
|
labels = labels.push(
|
||||||
|
ferese_theme::text(self.desktop_clock.1.clone(), font)
|
||||||
|
.size(clock.date_size)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(alignment)
|
||||||
|
.class(theme::Text::Color(tint(
|
||||||
|
&clock.date_color,
|
||||||
|
self.config.theme.text_muted,
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let background = clock.background.as_deref().and_then(config::parse_color).map(|rgba| {
|
||||||
|
let mut tint = color(rgba);
|
||||||
|
tint.a *= clock.opacity;
|
||||||
|
Background::Color(tint)
|
||||||
|
});
|
||||||
|
let radius = self.config.theme.material_radius;
|
||||||
|
|
||||||
|
container(labels)
|
||||||
|
.padding(clock.padding)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.align_x(alignment)
|
||||||
|
.center_y(Length::Fill)
|
||||||
|
.class(theme::Container::custom(move |_| container::Style {
|
||||||
|
background,
|
||||||
|
border: Border {
|
||||||
|
radius: radius.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sweep each axis, allowing edge sliding without tunnelling through a widget
|
||||||
|
/// when pointer events skip ahead. Coordinates match integer layer margins.
|
||||||
|
pub(super) fn avoid_widget_overlap(
|
||||||
|
previous: cosmic::iced::Point,
|
||||||
|
requested: cosmic::iced::Point,
|
||||||
|
size: (u32, u32),
|
||||||
|
obstacles: &[cosmic::iced::Rectangle],
|
||||||
|
) -> cosmic::iced::Point {
|
||||||
|
let mut position = previous;
|
||||||
|
let (width, height) = (size.0 as f32, size.1 as f32);
|
||||||
|
let mut x = requested.x.round();
|
||||||
|
for obstacle in obstacles {
|
||||||
|
if position.y < obstacle.y + obstacle.height && position.y + height > obstacle.y {
|
||||||
|
if x > position.x && position.x + width <= obstacle.x {
|
||||||
|
x = x.min((obstacle.x - width).floor());
|
||||||
|
} else if x < position.x && position.x >= obstacle.x + obstacle.width {
|
||||||
|
x = x.max((obstacle.x + obstacle.width).ceil());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position.x = x;
|
||||||
|
let mut y = requested.y.round();
|
||||||
|
for obstacle in obstacles {
|
||||||
|
if position.x < obstacle.x + obstacle.width && position.x + width > obstacle.x {
|
||||||
|
if y > position.y && position.y + height <= obstacle.y {
|
||||||
|
y = y.min((obstacle.y - height).floor());
|
||||||
|
} else if y < position.y && position.y >= obstacle.y + obstacle.height {
|
||||||
|
y = y.max((obstacle.y + obstacle.height).ceil());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position.y = y;
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clamp_note_position(
|
||||||
|
position: cosmic::iced::Point,
|
||||||
|
output: (i32, i32),
|
||||||
|
size: (u32, u32),
|
||||||
|
) -> cosmic::iced::Point {
|
||||||
|
cosmic::iced::Point::new(
|
||||||
|
position.x.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)),
|
||||||
|
position.y.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn note_origin(note: &ferese_core::desktop::StickyNote, output: (i32, i32)) -> cosmic::iced::Point {
|
||||||
|
widget_origin(
|
||||||
|
note.anchor,
|
||||||
|
note.margin_x,
|
||||||
|
note.margin_y,
|
||||||
|
(note.width, note.height),
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn widget_origin(
|
||||||
|
anchor: ferese_core::desktop::Anchor,
|
||||||
|
margin_x: i32,
|
||||||
|
margin_y: i32,
|
||||||
|
size: (u32, u32),
|
||||||
|
output: (i32, i32),
|
||||||
|
) -> cosmic::iced::Point {
|
||||||
|
use ferese_core::desktop::Anchor as A;
|
||||||
|
let x = match anchor {
|
||||||
|
A::TopLeft | A::CenterLeft | A::BottomLeft => margin_x as f32,
|
||||||
|
A::TopRight | A::CenterRight | A::BottomRight => output.0 as f32 - size.0 as f32 - margin_x as f32,
|
||||||
|
_ => (output.0 as f32 - size.0 as f32) / 2.,
|
||||||
|
};
|
||||||
|
let y = match anchor {
|
||||||
|
A::TopLeft | A::TopCenter | A::TopRight => margin_y as f32,
|
||||||
|
A::BottomLeft | A::BottomCenter | A::BottomRight => output.1 as f32 - size.1 as f32 - margin_y as f32,
|
||||||
|
_ => (output.1 as f32 - size.1 as f32) / 2.,
|
||||||
|
};
|
||||||
|
clamp_note_position(cosmic::iced::Point::new(x, y), output, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clock_placement(clock: &ferese_core::desktop::Clock) -> (Anchor, IcedMargin) {
|
||||||
|
widget_placement(clock.anchor, clock.margin_x, clock.margin_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn widget_placement(
|
||||||
|
position: ferese_core::desktop::Anchor,
|
||||||
|
margin_x: i32,
|
||||||
|
margin_y: i32,
|
||||||
|
) -> (Anchor, IcedMargin) {
|
||||||
|
use ferese_core::desktop::Anchor as Position;
|
||||||
|
let horizontal = match position {
|
||||||
|
Position::TopLeft | Position::CenterLeft | Position::BottomLeft => Anchor::LEFT,
|
||||||
|
Position::TopRight | Position::CenterRight | Position::BottomRight => Anchor::RIGHT,
|
||||||
|
_ => Anchor::empty(),
|
||||||
|
};
|
||||||
|
let vertical = match position {
|
||||||
|
Position::TopLeft | Position::TopCenter | Position::TopRight => Anchor::TOP,
|
||||||
|
Position::BottomLeft | Position::BottomCenter | Position::BottomRight => Anchor::BOTTOM,
|
||||||
|
_ => Anchor::empty(),
|
||||||
|
};
|
||||||
|
(
|
||||||
|
horizontal | vertical,
|
||||||
|
IcedMargin {
|
||||||
|
top: if vertical == Anchor::TOP { margin_y } else { 0 },
|
||||||
|
bottom: if vertical == Anchor::BOTTOM { margin_y } else { 0 },
|
||||||
|
left: if horizontal == Anchor::LEFT { margin_x } else { 0 },
|
||||||
|
right: if horizontal == Anchor::RIGHT { margin_x } else { 0 },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn current_time() -> String {
|
||||||
|
format_bar_time(&Zoned::now())
|
||||||
|
}
|
||||||
+8
-1595
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,556 @@
|
|||||||
|
use super::{
|
||||||
|
Anchor, Background, BarMetrics, Color, Connection, ContentFit, Dispatch, EFFECT_FRAME_PENDING, Element, Event,
|
||||||
|
EventQueue, FereseEffectsManagerV1, FereseShell, FereseSurfaceEffectsV1, GlobalListContents, IcedMargin,
|
||||||
|
IcedOutput, KeyboardInteractivity, Layer, Length, Limits, Message, Ordering, OutputSurfaces, PlatformSpecific,
|
||||||
|
Proxy, QueueHandle, SctkLayerSurfaceSettings, Task, WallpaperMode, avoid_widget_overlap, clamp_note_position,
|
||||||
|
config, container, delegate_noop, destroy_layer_surface, ferese_surface_effects_v1, image, output_bar_hidden,
|
||||||
|
registry_queue_init, set_margin, status_ui, text, theme, wayland, window, wl_output, wl_registry, wl_surface,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FereseShell {
|
||||||
|
pub(super) fn output_event(&mut self, event: wayland::OutputEvent, output: wl_output::WlOutput) -> Task<Message> {
|
||||||
|
if matches!(event, wayland::OutputEvent::Removed) {
|
||||||
|
if let Some(index) = self.outputs.iter().position(|entry| entry.output == output) {
|
||||||
|
let entry = self.outputs.remove(index);
|
||||||
|
let menu = if self.bar_surface_id == entry.bar {
|
||||||
|
self.destroy_menu()
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tasks = vec![destroy_layer_surface(entry.bar), menu, self.rebuild_system_modal()];
|
||||||
|
if self.note_drag.as_ref().is_some_and(|drag| {
|
||||||
|
entry.clock == Some(drag.source) || entry.notes.iter().any(|(_, id)| *id == drag.source)
|
||||||
|
}) {
|
||||||
|
tasks.push(self.finish_note_drag(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(wallpaper) = entry.wallpaper {
|
||||||
|
tasks.push(destroy_layer_surface(wallpaper));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(clock) = entry.clock {
|
||||||
|
tasks.push(destroy_layer_surface(clock));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, id) in entry.notes {
|
||||||
|
self.note_pointer.remove(&id);
|
||||||
|
tasks.push(destroy_layer_surface(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task::batch(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = match &event {
|
||||||
|
wayland::OutputEvent::Created(info) => info.as_ref().and_then(|info| info.logical_size),
|
||||||
|
wayland::OutputEvent::InfoUpdate(info) => info.logical_size,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let name = match event {
|
||||||
|
wayland::OutputEvent::Created(info) => info.and_then(|info| info.name),
|
||||||
|
wayland::OutputEvent::InfoUpdate(info) => info.name,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(entry) = self.outputs.iter_mut().find(|entry| entry.output == output) {
|
||||||
|
if size.is_some() {
|
||||||
|
entry.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = name.is_some() && entry.name != name;
|
||||||
|
if name.is_some() {
|
||||||
|
entry.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return if changed {
|
||||||
|
Task::batch([self.rebuild_clocks(true), self.rebuild_notes(true)])
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let bar_surface_id = window::Id::unique();
|
||||||
|
let wallpaper_surface_id = window::Id::unique();
|
||||||
|
let shell_theme = self.config.theme;
|
||||||
|
let bar = BarMetrics::from(shell_theme);
|
||||||
|
let wallpaper_output = output.clone();
|
||||||
|
let bar_output = output.clone();
|
||||||
|
let hidden = output_bar_hidden(
|
||||||
|
&self.snapshot,
|
||||||
|
self.snapshot
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.find(|output| Some(output.name.as_str()) == name.as_deref())
|
||||||
|
.map(|output| output.id),
|
||||||
|
);
|
||||||
|
self.outputs.push(OutputSurfaces {
|
||||||
|
output,
|
||||||
|
name,
|
||||||
|
bar: bar_surface_id,
|
||||||
|
wallpaper: std::env::var_os("FERESE_COMPOSITOR_WALLPAPER")
|
||||||
|
.is_none()
|
||||||
|
.then_some(wallpaper_surface_id),
|
||||||
|
effects: None,
|
||||||
|
clock: None,
|
||||||
|
notes: Vec::new(),
|
||||||
|
size,
|
||||||
|
hidden,
|
||||||
|
});
|
||||||
|
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| SctkLayerSurfaceSettings {
|
||||||
|
id: wallpaper_surface_id,
|
||||||
|
layer: Layer::Background,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity::None,
|
||||||
|
input_zone: Some(Vec::new()),
|
||||||
|
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
|
||||||
|
output: IcedOutput::Output(wallpaper_output.clone()),
|
||||||
|
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()
|
||||||
|
},
|
||||||
|
Some(Box::new(Self::view_wallpaper)),
|
||||||
|
);
|
||||||
|
let bar_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||||
|
|_| Default::default(),
|
||||||
|
move |_| SctkLayerSurfaceSettings {
|
||||||
|
id: bar_surface_id,
|
||||||
|
input_zone: hidden.then(Vec::new),
|
||||||
|
layer: Layer::Top,
|
||||||
|
keyboard_interactivity: KeyboardInteractivity::None,
|
||||||
|
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
||||||
|
output: IcedOutput::Output(bar_output.clone()),
|
||||||
|
namespace: "ferese-shell-top-bar".to_owned(),
|
||||||
|
margin: IcedMargin {
|
||||||
|
top: shell_theme.bar_margin_top,
|
||||||
|
right: shell_theme.bar_margin_horizontal,
|
||||||
|
bottom: 0,
|
||||||
|
left: shell_theme.bar_margin_horizontal,
|
||||||
|
},
|
||||||
|
size: Some((None, Some(bar.height.round() as u32))),
|
||||||
|
size_limits: Limits::NONE,
|
||||||
|
// Reserve breathing room below the visible bar; layer-shell
|
||||||
|
// accounts for the top margin separately.
|
||||||
|
exclusive_zone: (bar.height.round() as i32).saturating_add(shell_theme.bar_window_gap),
|
||||||
|
},
|
||||||
|
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submit the small interactive surface before the full-screen image.
|
||||||
|
let mut surfaces = vec![bar_action];
|
||||||
|
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_none() {
|
||||||
|
surfaces.push(wallpaper_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tasks = surfaces
|
||||||
|
.into_iter()
|
||||||
|
.map(cosmic::Action::Surface)
|
||||||
|
.map(cosmic::task::message);
|
||||||
|
|
||||||
|
Task::batch([
|
||||||
|
Task::batch(tasks),
|
||||||
|
self.rebuild_clocks(false),
|
||||||
|
self.rebuild_notes(false),
|
||||||
|
self.rebuild_system_modal(),
|
||||||
|
cosmic::task::message(cosmic::Action::App(Message::ShowGuide)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
||||||
|
if let Event::Mouse(mouse) = &event {
|
||||||
|
if !self
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.clock == Some(id) || entry.notes.iter().any(|(_, surface)| *surface == id))
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
match mouse {
|
||||||
|
cosmic::iced::mouse::Event::CursorMoved { position } => {
|
||||||
|
self.note_pointer.insert(id, *position);
|
||||||
|
|
||||||
|
if let Some(drag) = &mut self.note_drag
|
||||||
|
&& drag.source == id
|
||||||
|
{
|
||||||
|
let delta = *position - drag.start;
|
||||||
|
let dimensions = if let Some(id) = &drag.id {
|
||||||
|
self.config
|
||||||
|
.desktop_widgets
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.find(|note| ¬e.id == id)
|
||||||
|
.map(|note| (note.width, note.height))
|
||||||
|
} else {
|
||||||
|
let clock = &self.config.desktop_widgets.clock;
|
||||||
|
Some((clock.width, clock.height))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(dimensions) = dimensions {
|
||||||
|
let requested = clamp_note_position(drag.origin + delta, drag.output_size, dimensions);
|
||||||
|
drag.position = avoid_widget_overlap(drag.position, requested, dimensions, &drag.obstacles);
|
||||||
|
// Move a compact, cached buffer in the compositor instead of
|
||||||
|
// repainting an output-sized buffer for every pointer event.
|
||||||
|
return set_margin(
|
||||||
|
drag.overlay,
|
||||||
|
drag.position.y.round() as i32,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
drag.position.x.round() as i32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left)
|
||||||
|
if self.note_drag.as_ref().is_some_and(|drag| drag.source == id) =>
|
||||||
|
{
|
||||||
|
return self.finish_note_drag(true);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||||
|
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Tab),
|
||||||
|
modifiers,
|
||||||
|
..
|
||||||
|
}) = &event
|
||||||
|
&& !modifiers.control()
|
||||||
|
&& self.system_modal.as_ref().is_some_and(|modal| modal.contains(id))
|
||||||
|
{
|
||||||
|
return self.navigate_system_modal(modifiers.shift());
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
&event,
|
||||||
|
Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||||
|
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
if self.system_modal.is_some() {
|
||||||
|
return self.close_system_modal();
|
||||||
|
}
|
||||||
|
if self.note_drag.is_some() {
|
||||||
|
return self.finish_note_drag(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.note_editor.is_some() {
|
||||||
|
return self.finish_note_edit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notifications.history_open {
|
||||||
|
return self.close_notification_history();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(event, output))) => {
|
||||||
|
self.output_event(event, output)
|
||||||
|
}
|
||||||
|
Event::Window(window::Event::Opened { .. })
|
||||||
|
if self.outputs.iter().any(|entry| entry.bar == id)
|
||||||
|
|| self.menu.as_ref().is_some_and(|menu| menu.id == id)
|
||||||
|
|| self.system_modal.as_ref().is_some_and(|modal| modal.contains(id))
|
||||||
|
|| self
|
||||||
|
.notification_surface
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|surface| surface.id == id) =>
|
||||||
|
{
|
||||||
|
Task::batch([
|
||||||
|
self.focus_system_modal(id),
|
||||||
|
window::run(id, native_wayland_surface)
|
||||||
|
.map(move |surface| cosmic::Action::App(Message::NativeSurface(id, surface))),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||||
|
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
|
||||||
|
..
|
||||||
|
}) if self.menu.is_some() => self.close_menu(),
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Popup(event, surface, popup_id))) => {
|
||||||
|
if let Some(menu) = &mut self.menu
|
||||||
|
&& menu.id == popup_id
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
wayland::PopupEvent::Done => {
|
||||||
|
if menu.kind == status_ui::Menu::Notifications {
|
||||||
|
self.notifications.history_open = false;
|
||||||
|
self.notifications.hovered = None;
|
||||||
|
}
|
||||||
|
self.menu = None;
|
||||||
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
|
return self.sync_notification_surface();
|
||||||
|
}
|
||||||
|
wayland::PopupEvent::Configured { .. } => {
|
||||||
|
menu.prepare_surface(&surface);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(effects) = &menu.effects {
|
||||||
|
let _ = effects.set_opacity(menu.progress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Event::Window(window::Event::Closed) if self.menu.as_ref().is_some_and(|menu| menu.id == id) => {
|
||||||
|
self.menu = None;
|
||||||
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Event::Window(window::Event::Closed)
|
||||||
|
if self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) =>
|
||||||
|
{
|
||||||
|
self.close_system_modal()
|
||||||
|
}
|
||||||
|
Event::Window(window::Event::Closed) => Task::none(),
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id)))
|
||||||
|
if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) =>
|
||||||
|
{
|
||||||
|
if let Some(menu) = &mut self.menu
|
||||||
|
&& menu.effects.is_none()
|
||||||
|
{
|
||||||
|
menu.prepare_surface(&surface);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id)))
|
||||||
|
if self.outputs.iter().any(|entry| entry.bar == frame_id) =>
|
||||||
|
{
|
||||||
|
self.attach_effects(frame_id, &surface);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id)))
|
||||||
|
if self
|
||||||
|
.notification_surface
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|entry| entry.id == layer_id) =>
|
||||||
|
{
|
||||||
|
let entry = self.notification_surface.as_mut().unwrap();
|
||||||
|
|
||||||
|
if entry.effects.is_none() {
|
||||||
|
match EffectsBinding::attach_role(&surface, None, 1.0) {
|
||||||
|
Ok(binding) => entry.effects = Some(binding),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("ferese-shell: notification effects unavailable: {error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id)))
|
||||||
|
if self.outputs.iter().any(|entry| entry.bar == layer_id) =>
|
||||||
|
{
|
||||||
|
self.attach_effects(layer_id, &surface);
|
||||||
|
self.attach_power_material(layer_id, &surface);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
_ => Task::none(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
|
||||||
|
let hidden = self.bar_hidden(id);
|
||||||
|
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if entry.effects.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
|
match EffectsBinding::attach(surface, !hidden) {
|
||||||
|
Ok(binding) => entry.effects = Some(binding),
|
||||||
|
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn view_wallpaper(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||||
|
let Some(handle) = self.wallpaper.as_ref() else {
|
||||||
|
return container(text(""))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.class(theme::Container::custom(wallpaper_fallback_style))
|
||||||
|
.into();
|
||||||
|
};
|
||||||
|
let content_fit = match self.config.wallpaper.mode {
|
||||||
|
WallpaperMode::Fill => ContentFit::Cover,
|
||||||
|
WallpaperMode::Fit => ContentFit::Contain,
|
||||||
|
};
|
||||||
|
|
||||||
|
container(
|
||||||
|
image(handle.clone())
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.content_fit(content_fit),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.class(theme::Container::custom(wallpaper_fallback_style))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a surface while iced guarantees the native handles are alive. Keep
|
||||||
|
// the borrowed-display connection alive until the effects binding takes over.
|
||||||
|
pub(super) fn native_wayland_surface(
|
||||||
|
window: &dyn cosmic::iced::window::Window,
|
||||||
|
) -> Result<(Connection, wl_surface::WlSurface), String> {
|
||||||
|
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||||
|
let display = window.display_handle().map_err(|error| error.to_string())?;
|
||||||
|
let handle = window.window_handle().map_err(|error| error.to_string())?;
|
||||||
|
let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) = (display.as_raw(), handle.as_raw())
|
||||||
|
else {
|
||||||
|
return Err("shell material requires a Wayland surface".to_owned());
|
||||||
|
};
|
||||||
|
// SAFETY: iced lends matching, live Wayland handles for this callback.
|
||||||
|
// This backend borrows the display; it never disconnects iced's connection.
|
||||||
|
// Both handles belong to the runtime, which outlives the shell bindings.
|
||||||
|
let backend = unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
|
||||||
|
let connection = Connection::from_backend(backend);
|
||||||
|
// SAFETY: raw-window-handle identifies this pointer as a live wl_surface.
|
||||||
|
let id = unsafe {
|
||||||
|
wayland_client::backend::ObjectId::from_ptr(wl_surface::WlSurface::interface(), handle.surface.as_ptr().cast())
|
||||||
|
}
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||||
|
Ok((connection, surface))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
|
||||||
|
let [red, green, blue] = config::default_background();
|
||||||
|
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color::from_rgb8(red, green, blue))),
|
||||||
|
..container::Style::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct EffectsBinding {
|
||||||
|
connection: Connection,
|
||||||
|
_manager: FereseEffectsManagerV1,
|
||||||
|
pub(super) surface: FereseSurfaceEffectsV1,
|
||||||
|
_queue: EventQueue<EffectsState>,
|
||||||
|
regions: std::cell::RefCell<Option<Vec<[i32; 5]>>>,
|
||||||
|
opacity: std::cell::Cell<Option<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EffectsBinding {
|
||||||
|
pub(super) fn attach(surface: &wl_surface::WlSurface, visible: bool) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
Self::attach_role(
|
||||||
|
surface,
|
||||||
|
visible.then_some(ferese_surface_effects_v1::Role::Panel),
|
||||||
|
if visible { 1.0 } else { 0.0 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn attach_role(
|
||||||
|
surface: &wl_surface::WlSurface,
|
||||||
|
role: Option<ferese_surface_effects_v1::Role>,
|
||||||
|
opacity: f32,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let backend = surface
|
||||||
|
.backend()
|
||||||
|
.upgrade()
|
||||||
|
.ok_or("libcosmic Wayland connection is no longer alive")?;
|
||||||
|
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..=3, ())?;
|
||||||
|
let effects = manager.get_surface_effects(surface, &qh, ());
|
||||||
|
|
||||||
|
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||||
|
if effects.version() >= 3 {
|
||||||
|
effects.set_opacity(opacity);
|
||||||
|
}
|
||||||
|
if let Some(role) = role {
|
||||||
|
effects.set_role(role);
|
||||||
|
} else {
|
||||||
|
effects.clear_role();
|
||||||
|
}
|
||||||
|
connection.flush()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
connection,
|
||||||
|
_manager: manager,
|
||||||
|
surface: effects,
|
||||||
|
_queue: queue,
|
||||||
|
regions: Default::default(),
|
||||||
|
opacity: std::cell::Cell::new(Some(opacity)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_material_regions(
|
||||||
|
&self,
|
||||||
|
regions: &[[i32; 5]],
|
||||||
|
role: ferese_surface_effects_v1::Role,
|
||||||
|
) -> 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(role);
|
||||||
|
}
|
||||||
|
self.connection.flush()?;
|
||||||
|
*self.regions.borrow_mut() = Some(regions.to_vec());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_visible(&self, visible: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.set_opacity(1.0)?;
|
||||||
|
if visible {
|
||||||
|
self.surface.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||||
|
} else {
|
||||||
|
self.surface.clear_role();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.connection.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||||
|
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
|
||||||
|
self.surface.set_opacity(opacity);
|
||||||
|
self.connection.flush()?;
|
||||||
|
self.opacity.set(Some(opacity));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EffectsState;
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
|
||||||
|
fn event(
|
||||||
|
_state: &mut Self,
|
||||||
|
_registry: &wl_registry::WlRegistry,
|
||||||
|
_event: wl_registry::Event,
|
||||||
|
_data: &GlobalListContents,
|
||||||
|
_connection: &Connection,
|
||||||
|
_qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
||||||
|
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
|
||||||
Reference in new issue
Block a user