Split shell, settings, and compositor into focused modules
This commit is contained in:
1 parent
2b9f30fa5d
commit
cbf048566a
38 files changed
+9998
-9817
No files matched your search
@@ -35,7 +35,7 @@ use smithay::wayland::presentation::Refresh;
|
||||
use crate::Ferese;
|
||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||
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 session: LibSeatSession,
|
||||
@@ -57,7 +57,7 @@ impl DirectBackendState {
|
||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||
output: &Output,
|
||||
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
|
||||
.devices
|
||||
.values_mut()
|
||||
@@ -65,7 +65,7 @@ impl DirectBackendState {
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
crate::winit::capture_resize_snapshot(
|
||||
crate::render::capture_resize_snapshot(
|
||||
&mut device.renderer,
|
||||
window,
|
||||
geometry,
|
||||
@@ -85,7 +85,7 @@ impl DirectBackendState {
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_buffer(
|
||||
crate::render::capture_window_buffer(
|
||||
&mut device.renderer,
|
||||
window,
|
||||
geometry,
|
||||
@@ -104,7 +104,7 @@ impl DirectBackendState {
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_frame(
|
||||
crate::render::capture_window_frame(
|
||||
&mut device.renderer,
|
||||
window,
|
||||
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 mut backend = backend.try_borrow_mut().ok()?;
|
||||
crate::winit::capture_window_frame(
|
||||
crate::render::capture_window_frame(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
geometry,
|
||||
|
||||
@@ -864,7 +864,7 @@ impl Ferese {
|
||||
};
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
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()),
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
|
||||
@@ -20,6 +20,7 @@ mod portal_shortcuts;
|
||||
mod presentation;
|
||||
mod private_client;
|
||||
mod reload;
|
||||
mod render;
|
||||
mod resize_transaction;
|
||||
mod session_lock;
|
||||
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();
|
||||
}
|
||||
}
|
||||
+16
-2901
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user