1
0
Fork 0
forked from wry/wry

Add open and close animations, xdg_popup blur pre-pass,

damage viz config option.
This commit is contained in:
entailz 2026-05-20 18:50:11 -07:00
parent 12adb678bb
commit e35dce433a
24 changed files with 1056 additions and 67 deletions

View file

@ -169,3 +169,55 @@ impl Default for BlurConfigIpc {
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
pub enum AnimationCurveIpc {
Linear,
EaseOut,
EaseInOut,
/// Standard CSS cubic-bezier(x1, y1, x2, y2). P0=(0,0), P3=(1,1) are fixed.
Bezier {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub struct AnimationsConfigIpc {
pub enabled: bool,
pub open_duration_ms: u32,
pub open_curve: AnimationCurveIpc,
pub close_duration_ms: u32,
pub close_curve: AnimationCurveIpc,
}
impl Default for AnimationsConfigIpc {
fn default() -> Self {
Self {
enabled: false,
open_duration_ms: 200,
open_curve: AnimationCurveIpc::EaseOut,
close_duration_ms: 200,
close_curve: AnimationCurveIpc::EaseOut,
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub struct DamageVisualizationIpc {
pub enabled: bool,
pub color: crate::theme::Color,
pub decay_millis: u64,
}
impl Default for DamageVisualizationIpc {
fn default() -> Self {
Self {
enabled: false,
color: crate::theme::Color::new_straight(255, 0, 0, 128),
decay_millis: 2000,
}
}
}

View file

@ -880,6 +880,14 @@ impl ConfigClient {
self.send(&ClientMessage::SetBlurConfig { config })
}
pub fn set_damage_visualization(&self, config: crate::_private::DamageVisualizationIpc) {
self.send(&ClientMessage::SetDamageVisualization { config })
}
pub fn set_animations_config(&self, config: crate::_private::AnimationsConfigIpc) {
self.send(&ClientMessage::SetAnimationsConfig { config })
}
pub fn switch_to_vt(&self, vtnr: u32) {
self.send(&ClientMessage::SwitchTo { vtnr })
}

View file

@ -1,8 +1,8 @@
use {
crate::{
_private::{
BlurConfigIpc, ClientCriterionIpc, LayerRuleIpc, PollableId, WindowCriterionIpc,
WireMode,
BlurConfigIpc, ClientCriterionIpc, DamageVisualizationIpc, LayerRuleIpc, PollableId,
WindowCriterionIpc, WireMode,
},
Axis, Direction, PciId, Workspace,
client::{Client, ClientCapabilities, ClientMatcher},
@ -925,6 +925,12 @@ pub enum ClientMessage<'a> {
SetBlurConfig {
config: BlurConfigIpc,
},
SetDamageVisualization {
config: DamageVisualizationIpc,
},
SetAnimationsConfig {
config: crate::_private::AnimationsConfigIpc,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -407,6 +407,34 @@ pub fn _set_blur_config(config: crate::_private::BlurConfigIpc) {
get!().set_blur_config(config)
}
#[doc(hidden)]
pub fn _set_damage_visualization(config: crate::_private::DamageVisualizationIpc) {
get!().set_damage_visualization(config)
}
#[doc(hidden)]
pub fn _set_animations_config(config: crate::_private::AnimationsConfigIpc) {
get!().set_animations_config(config)
}
/// Configures the damage region visualizer.
///
/// When enabled, every damaged screen region is overlaid with `color` and fades
/// out over `decay` (producing a "blink" effect as new damage accumulates).
/// Useful for debugging damage-tracked rendering paths.
pub fn set_damage_visualization(
enabled: bool,
color: crate::theme::Color,
decay: std::time::Duration,
) {
let decay_millis = decay.as_millis().min(u64::MAX as u128) as u64;
_set_damage_visualization(crate::_private::DamageVisualizationIpc {
enabled,
color,
decay_millis,
});
}
/// Returns the current corner radius for window borders.
pub fn get_corner_radius() -> f32 {
get!(0.0).get_corner_radius()

193
src/animation.rs Normal file
View file

@ -0,0 +1,193 @@
use {
crate::{
allocator::{BO_USE_RENDERING, BufferUsage},
format::ARGB8888,
gfx_api::{AcquireSync, GfxTexture, ReleaseSync, needs_render_usage},
rect::Rect,
renderer::Renderer,
state::State,
theme::Color,
tree::{OutputNode, ToplevelNode, Transform},
video::Modifier,
},
std::{cell::Cell, rc::Weak},
std::rc::Rc,
};
/// A captured snapshot of a toplevel's last rendered state, used to drive the
/// close animation after the toplevel itself has been torn down. Owns its own
/// GPU texture so the source client buffers can be released immediately.
pub struct Snapshot {
pub texture: Rc<dyn GfxTexture>,
/// The output the toplevel was on, used to schedule per-frame damage.
pub output: Weak<OutputNode>,
/// Logical absolute position the toplevel occupied, used to draw the
/// snapshot into the correct screen region during the close animation.
pub rect: Rect,
/// Slide-out direction in logical pixels. The snapshot moves from (0, 0)
/// at start to (slide_dx, slide_dy) at end.
pub slide_dx: f32,
pub slide_dy: f32,
pub start_nsec: Cell<u64>,
}
impl Snapshot {
/// Returns the eased close-animation progress in [0, 1], or None if the
/// animation has finished.
pub fn close_progress(&self, state: &State) -> Option<f32> {
let cfg = state.animations_config.get();
if !cfg.enabled || cfg.close_duration_ms == 0 {
return None;
}
let now = state.now_nsec();
let elapsed = now.saturating_sub(self.start_nsec.get());
let dur = (cfg.close_duration_ms as u64).saturating_mul(1_000_000);
if elapsed >= dur {
return None;
}
let t = (elapsed as f32) / (dur as f32);
let eased = match cfg.close_curve {
jay_config::_private::AnimationCurveIpc::Linear => t,
jay_config::_private::AnimationCurveIpc::EaseOut => {
let inv = 1.0 - t;
1.0 - inv * inv * inv
}
jay_config::_private::AnimationCurveIpc::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
let f = -2.0 * t + 2.0;
1.0 - f * f * f / 2.0
}
}
jay_config::_private::AnimationCurveIpc::Bezier { x1, y1, x2, y2 } => {
cubic_bezier_y_at_x(t, x1, y1, x2, y2)
}
};
Some(eased.clamp(0.0, 1.0))
}
}
fn cubic_bezier_y_at_x(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
fn bx(t: f32, x1: f32, x2: f32) -> f32 {
let it = 1.0 - t;
3.0 * it * it * t * x1 + 3.0 * it * t * t * x2 + t * t * t
}
fn dbx(t: f32, x1: f32, x2: f32) -> f32 {
let it = 1.0 - t;
3.0 * it * it * x1 + 6.0 * it * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2)
}
let mut t = x;
for _ in 0..8 {
let err = bx(t, x1, x2) - x;
if err.abs() < 1e-4 {
break;
}
let d = dbx(t, x1, x2);
if d.abs() < 1e-6 {
break;
}
t = (t - err / d).clamp(0.0, 1.0);
}
let it = 1.0 - t;
3.0 * it * it * t * y1 + 3.0 * it * t * t * y2 + t * t * t
}
/// Renders the toplevel into a private GPU texture and returns the texture.
/// Used at unmap time to capture the last-rendered state, so a close animation
/// can run after the toplevel itself has been destroyed. Returns None if the
/// render context is unavailable, the toplevel has no workspace, or
/// allocation/rendering fails.
///
/// Any open animation in flight on the toplevel is cleared before rendering so
/// the snapshot is at full opacity / final position.
pub fn capture_snapshot(state: &State, tl: &Rc<dyn ToplevelNode>) -> Option<Snapshot> {
let ctx = state.render_ctx.get()?;
let formats = ctx.formats();
let format_info = formats.get(&ARGB8888.drm)?;
let modifiers: Vec<Modifier> = format_info
.write_modifiers
.iter()
.filter(|(m, _)| format_info.read_modifiers.contains(*m))
.map(|(m, _)| *m)
.collect();
if modifiers.is_empty() {
return None;
}
let data = tl.tl_data();
data.anim_open_start_nsec.set(None);
let workspace = data.workspace.get()?;
let output = workspace.output.get();
let scale = output.global.persistent.scale.get();
let scalef = scale.to_f64();
let tl_rect = tl.node_absolute_position();
let pw = (tl_rect.width() as f64 * scalef).round() as i32;
let ph = (tl_rect.height() as f64 * scalef).round() as i32;
if pw <= 0 || ph <= 0 {
return None;
}
let allocator = ctx.allocator();
let mut usage = BO_USE_RENDERING;
if !needs_render_usage(format_info.write_modifiers.values()) {
usage = BufferUsage::none();
}
let bo = allocator
.create_bo(&state.dma_buf_ids, pw, ph, ARGB8888, &modifiers, usage)
.ok()?;
let img = ctx.clone().dmabuf_img(bo.dmabuf()).ok()?;
let fb = img.clone().to_framebuffer().ok()?;
let mut ops = vec![];
{
let mut renderer = Renderer {
base: fb.renderer_base(&mut ops, scale, Transform::None),
state,
logical_extents: tl_rect.at_point(0, 0),
pixel_extents: Rect::new_saturating(0, 0, pw, ph),
stretch: None,
corner_radius: None,
current_anim_node: None,
};
tl.clone().node_render(&mut renderer, 0, 0, None);
}
let cd = state.color_manager.srgb_gamma22();
fb.render(
AcquireSync::Unnecessary,
ReleaseSync::Implicit,
cd,
&ops,
Some(&Color::TRANSPARENT),
&cd.linear,
None,
cd,
)
.ok()?;
let texture = img.to_texture().ok()?;
// Slide-out direction: same closest-edge logic as the open animation, but
// the snapshot moves AWAY from its rect during close. Computed once here so
// we don't need the toplevel's tile lookup anymore once it's torn down.
let output_rect = output.global.pos.get();
let dl = (tl_rect.x1() - output_rect.x1()).max(0) as f32;
let dr = (output_rect.x2() - tl_rect.x2()).max(0) as f32;
let dt = (tl_rect.y1() - output_rect.y1()).max(0) as f32;
let db = (output_rect.y2() - tl_rect.y2()).max(0) as f32;
let mind = dl.min(dr).min(dt).min(db);
let (slide_dx, slide_dy) = if mind == dl {
(-(tl_rect.width() as f32), 0.0)
} else if mind == dr {
(tl_rect.width() as f32, 0.0)
} else if mind == dt {
(0.0, -(tl_rect.height() as f32))
} else {
(0.0, tl_rect.height() as f32)
};
Some(Snapshot {
texture,
output: Rc::downgrade(&output),
rect: tl_rect,
slide_dx,
slide_dy,
start_nsec: Cell::new(state.now_nsec()),
})
}

View file

@ -399,6 +399,10 @@ fn start_compositor2(
hyprland_global_shortcuts: Default::default(),
layer_rules: Default::default(),
blur_config: Default::default(),
blur_cache_epoch: Default::default(),
animations_config: Default::default(),
active_animations: Default::default(),
close_snapshots: Default::default(),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);

View file

@ -3537,6 +3537,20 @@ impl ConfigProxyHandler {
ClientMessage::SetBlurConfig { config } => {
self.state.blur_config.set(config);
}
ClientMessage::SetAnimationsConfig { config } => {
self.state.animations_config.set(config);
}
ClientMessage::SetDamageVisualization { config } => {
let [r, g, b, a] = config.color.to_u8_straight();
let color = crate::theme::Color::from_srgba_straight(r, g, b, a);
self.state.damage_visualizer.set_color(color);
self.state
.damage_visualizer
.set_decay(std::time::Duration::from_millis(config.decay_millis));
self.state
.damage_visualizer
.set_enabled(&self.state, config.enabled);
}
}
Ok(())
}

View file

@ -107,12 +107,36 @@ pub enum GfxApiOpt {
BlurBackdrop(BlurBackdrop),
}
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct BlurBackdrop {
pub rect: FramebufferRect,
pub passes: u8,
pub offset: f32,
pub mask: Option<BlurMask>,
pub cache: Option<Rc<std::cell::RefCell<Option<BlurCacheEntry>>>>,
pub cache_epoch: u64,
pub cache_pixel_rect: [i32; 4],
}
impl std::fmt::Debug for BlurBackdrop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlurBackdrop")
.field("rect", &self.rect)
.field("passes", &self.passes)
.field("offset", &self.offset)
.field("mask", &self.mask)
.field("cache_epoch", &self.cache_epoch)
.field("cache_pixel_rect", &self.cache_pixel_rect)
.finish()
}
}
pub struct BlurCacheEntry {
pub pixel_rect: [i32; 4],
pub passes: u8,
pub offset: f32,
pub epoch: u64,
pub image: Rc<dyn GfxTexture>,
}
#[derive(Clone)]
@ -120,6 +144,9 @@ pub struct BlurMask {
pub texture: Rc<dyn GfxTexture>,
pub source: SampleRect,
pub threshold: f32,
pub buffer_resv: Option<Rc<dyn BufferResv>>,
pub acquire_sync: AcquireSync,
pub release_sync: ReleaseSync,
}
impl std::fmt::Debug for BlurMask {
@ -785,6 +812,7 @@ impl dyn GfxFramebuffer {
},
stretch: None,
corner_radius: None,
current_anim_node: None,
};
cursor.render_hardware_cursor(&mut renderer);
self.render(
@ -1119,6 +1147,7 @@ pub fn create_render_pass(
},
stretch: None,
corner_radius: None,
current_anim_node: None,
};
node.node_render(&mut renderer, 0, 0, None);
if let Some(rect) = cursor_rect {
@ -1193,6 +1222,9 @@ pub fn renderer_base<'a>(
fb_width: width as _,
fb_height: height as _,
discard_alpha: None,
alpha_mul: 1.0,
translate_x: 0.0,
translate_y: 0.0,
}
}

View file

@ -1582,7 +1582,9 @@ impl WlSeatGlobal {
{
con.disconnect(TextDisconnectReason::FocusLost);
}
if let Some(tis) = self.text_inputs.borrow().get(&surface.client.id) {
if !surface.destroyed.get()
&& let Some(tis) = self.text_inputs.borrow().get(&surface.client.id)
{
for ti in tis.lock().values() {
ti.send_leave(surface);
ti.send_done();

View file

@ -26,7 +26,7 @@ use {
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
NodeLocation, NodeVisitor, OutputNode, StackedNode,
},
utils::{clonecell::CloneCell, smallmap::SmallMap},
utils::{clonecell::CloneCell, numcell::NumCell, smallmap::SmallMap},
wire::{XdgPopupId, xdg_popup::*},
},
std::{
@ -72,6 +72,7 @@ pub struct LayerPopupBlur {
}
pub struct XdgPopup {
pub blur_pre_rendered: Cell<bool>,
pub id: XdgPopupId,
node_id: PopupId,
pub xdg: Rc<XdgSurface>,
@ -79,6 +80,8 @@ pub struct XdgPopup {
relative_position: Cell<Rect>,
pos: RefCell<XdgPositioned>,
pub tracker: Tracker<Self>,
pub blur_cache: Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>,
pub blur_cache_epoch: NumCell<u64>,
seat_state: NodeSeatState,
set_visible_prepared: Cell<bool>,
jay_popup_ext: CloneCell<Option<Rc<JayPopupExtV1>>>,
@ -104,8 +107,11 @@ impl XdgPopup {
Ok(Self {
id,
node_id: xdg.surface.client.state.node_ids.next(),
blur_pre_rendered: Cell::new(false),
xdg: xdg.clone(),
parent: Default::default(),
blur_cache: Default::default(),
blur_cache_epoch: Default::default(),
relative_position: Cell::new(Default::default()),
pos: RefCell::new(pos),
tracker: Default::default(),
@ -314,6 +320,9 @@ impl XdgPopupRequestHandler for XdgPopup {
}
impl XdgPopup {
pub fn layer_blur_settings(&self) -> Option<LayerPopupBlur> {
self.parent.get()?.layer_blur_settings()
}
pub fn set_visible(&self, visible: bool) {
let surface = &self.xdg.surface;
let extents = surface.extents.get();
@ -418,33 +427,56 @@ impl Node for XdgPopup {
}
fn node_render(&self, renderer: &mut Renderer, x: i32, y: i32, bounds: Option<&Rect>) {
let settings = self.parent.get().and_then(|p| p.layer_blur_settings());
let settings = self.layer_blur_settings();
if let Some(s) = settings {
if s.blur {
if s.blur && !self.blur_pre_rendered.get() {
// Only push blur if it wasn't already pushed in the pre-pass
let extents = self.xdg.surface.extents.get();
let geo = self.xdg.geometry();
let (gx, gy) = geo.translate(x, y);
let rect = extents.move_(gx, gy);
let scaled = renderer.base.scale_rect(rect);
let popup_blur_rect = if let Some(parent) = self.parent.get() {
let parent_rect = parent.position();
if parent_rect.contains_rect(&rect) {
None
} else {
Some(rect)
}
} else {
Some(rect)
};
if let Some(blur_rect) = popup_blur_rect {
let scaled = renderer.base.scale_rect(blur_rect);
let cfg = renderer.state.blur_config.get();
let mask = s.ignore_alpha.and_then(|threshold| {
let buffer = self.xdg.surface.buffer.get()?;
let texture = buffer.buffer.buf.get_texture(&self.xdg.surface)?;
let source = *self.xdg.surface.buffer_points_norm.borrow();
let release_sync = buffer.release_sync;
Some(crate::gfx_api::BlurMask {
texture,
source,
threshold,
buffer_resv: Some(buffer),
acquire_sync: crate::gfx_api::AcquireSync::Unnecessary,
release_sync,
})
});
renderer
.base
.push_blur_backdrop(scaled, cfg.passes, cfg.size, mask);
renderer.base.push_blur_backdrop(
scaled,
cfg.passes,
cfg.size,
mask,
Some(self.blur_cache.clone()),
self.blur_cache_epoch.get(),
);
}
renderer.base.discard_alpha = s.ignore_alpha;
}
// Always clear the flag after node_render regardless of path
self.blur_pre_rendered.set(false);
renderer.render_xdg_surface(&self.xdg, x, y, bounds);
renderer.base.discard_alpha = None;
} else {
self.blur_pre_rendered.set(false);
renderer.render_xdg_surface(&self.xdg, x, y, bounds);
}
}

View file

@ -555,6 +555,7 @@ impl XdgToplevel {
self.state.tree_changed();
self.toplevel_data.mapped_source.trigger();
self.toplevel_data.broadcast(self.clone());
self.toplevel_data.start_open_animation();
}
self.toplevel_data
.set_content_type(self.xdg.surface.content_type.get());

View file

@ -53,6 +53,7 @@ pub struct ZwlrLayerSurfaceV1 {
pub client: Rc<Client>,
pub surface: Rc<WlSurface>,
pub output: Rc<OutputGlobalOpt>,
pub blur_cache_epoch: NumCell<u64>,
pub namespace: String,
pub tracker: Tracker<Self>,
output_extents: Cell<Rect>,
@ -62,6 +63,7 @@ pub struct ZwlrLayerSurfaceV1 {
pub blur: Cell<bool>,
pub blur_popups: Cell<bool>,
pub ignore_alpha: Cell<Option<f32>>,
pub blur_cache: Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>,
requested_serial: NumCell<u32>,
size: Cell<(i32, i32)>,
anchor: Cell<u32>,
@ -158,6 +160,7 @@ impl ZwlrLayerSurfaceV1 {
) -> Self {
Self {
id,
blur_cache_epoch: Default::default(),
node_id: shell.client.state.node_ids.next(),
shell: shell.clone(),
client: shell.client.clone(),
@ -172,6 +175,7 @@ impl ZwlrLayerSurfaceV1 {
blur: Cell::new(false),
blur_popups: Cell::new(false),
ignore_alpha: Cell::new(None),
blur_cache: Default::default(),
requested_serial: Default::default(),
size: Cell::new((0, 0)),
anchor: Cell::new(0),

View file

@ -48,6 +48,7 @@ mod leaks;
mod tracy;
mod acceptor;
mod allocator;
mod animation;
mod async_engine;
mod backend;
mod backends;

View file

@ -5,7 +5,7 @@ use {
ifs::wl_surface::{
SurfaceBuffer, WlSurface,
x_surface::xwindow::Xwindow,
xdg_surface::{XdgSurface, xdg_toplevel::XdgToplevel},
xdg_surface::{XdgSurface, xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel},
zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
},
rect::Rect,
@ -14,8 +14,9 @@ use {
state::State,
theme::{Color, CornerRadius},
tree::{
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
ContainerNode, DisplayNode, FloatNode, NodeId, OutputNode, PlaceholderNode,
StackedNode, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode,
tab_bar::TabBar,
},
},
std::{ops::Deref, rc::Rc, slice},
@ -30,9 +31,60 @@ pub struct Renderer<'a> {
pub pixel_extents: Rect,
pub stretch: Option<(i32, i32)>,
pub corner_radius: Option<CornerRadius>,
/// The toplevel whose open-animation transform is currently applied to the
/// renderer base. Used to prevent double-applying when a parent (container,
/// float) has already entered the animation scope before drawing its own
/// per-child decorations.
pub current_anim_node: Option<NodeId>,
}
#[must_use]
pub struct OpenAnimSaved {
alpha_mul: f32,
translate_x: f32,
translate_y: f32,
prev_node: Option<NodeId>,
}
impl Renderer<'_> {
pub fn render_layer_popup_blur_only(&mut self, popup: &XdgPopup, x: i32, y: i32) {
let Some(settings) = popup.layer_blur_settings() else {
return;
};
if !settings.blur {
return;
}
let extents = popup.xdg.surface.extents.get();
let geo = popup.xdg.geometry();
let (gx, gy) = geo.translate(x, y);
let rect = extents.move_(gx, gy);
let scaled = self.base.scale_rect(rect);
let cfg = self.state.blur_config.get();
let mask = settings.ignore_alpha.and_then(|threshold| {
let buffer = popup.xdg.surface.buffer.get()?;
let texture = buffer.buffer.buf.get_texture(&popup.xdg.surface)?;
let source = *popup.xdg.surface.buffer_points_norm.borrow();
let release_sync = buffer.release_sync;
Some(crate::gfx_api::BlurMask {
texture,
source,
threshold,
buffer_resv: Some(buffer),
acquire_sync: AcquireSync::Unnecessary,
release_sync,
})
});
popup.blur_pre_rendered.set(true);
self.base.push_blur_backdrop(
scaled,
cfg.passes,
cfg.size,
mask,
Some(popup.blur_cache.clone()),
popup.blur_cache_epoch.get(),
);
self.base.sync();
}
pub fn scale(&self) -> Scale {
self.base.scale
}
@ -215,14 +267,28 @@ impl Renderer<'_> {
};
}
render_stacked!(self.state.root.stacked);
// Flush RoundedFillRect ops from container/float borders so they don't
// sort after (and render on top of) layer-shell CopyTexture ops.
self.base.sync();
if fullscreen.is_none() {
// Pre-pass: push blur backdrops for layer-shell popups before
// the bar renders, so they sample the raw background rather than
// the already-composited bar content.
for stacked in self.state.root.stacked_above_layers.iter() {
if stacked.node_visible() {
let pos = stacked.node_absolute_position();
if pos.intersects(&opos) {
let (sx, sy) = opos.translate(pos.x1(), pos.y1());
let stacked_rc: Rc<dyn StackedNode> = stacked.deref().clone();
if let Some(popup) = stacked_rc.node_into_popup() {
self.render_layer_popup_blur_only(&popup, sx, sy);
}
}
}
}
render_layer!(output.layers[2]);
}
render_layer!(output.layers[3]);
render_stacked!(self.state.root.stacked_above_layers);
self.render_close_snapshots(output, x, y);
if let Some(ws) = output.workspace.get()
&& ws.render_highlight.get() > 0
{
@ -407,6 +473,7 @@ impl Renderer<'_> {
self.render_tab_bar(tb, x, y, container.width.get());
}
}
let saved_anim = self.enter_open_anim(&*child.node);
let mb = container.mono_body.get();
if self.state.theme.sizes.gap.get() != 0 {
let srgb_srgb = self.state.color_manager.srgb_gamma22();
@ -487,6 +554,7 @@ impl Renderer<'_> {
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
self.stretch = None;
self.corner_radius = None;
self.exit_open_anim(saved_anim);
} else {
let gap = self.state.theme.sizes.gap.get();
let (srgb_srgb, bw, border_color, focused_border_color) = if gap != 0 {
@ -504,6 +572,7 @@ impl Renderer<'_> {
if body.x1() >= container.width.get() || body.y1() >= container.height.get() {
break;
}
let saved_anim = self.enter_open_anim(&*child.node);
if let Some(srgb_srgb) = srgb_srgb {
let srgb = &srgb_srgb.linear;
let c = if child.border_color_is_focused.get() {
@ -574,6 +643,7 @@ impl Renderer<'_> {
.node_render(self, x + content.x1(), y + content.y1(), Some(&body));
self.stretch = None;
self.corner_radius = None;
self.exit_open_anim(saved_anim);
}
}
@ -581,13 +651,135 @@ impl Renderer<'_> {
}
pub fn render_xwindow(&mut self, tl: &Xwindow, x: i32, y: i32, bounds: Option<&Rect>) {
let saved = self.enter_open_anim(tl);
let bounds = if tl.tl_data().anim_open_alpha().is_some() {
None
} else {
bounds
};
self.render_surface(&tl.x.surface, x, y, bounds);
self.render_tl_aux(tl.tl_data(), bounds, true);
self.exit_open_anim(saved);
}
pub fn render_xdg_toplevel(&mut self, tl: &XdgToplevel, x: i32, y: i32, bounds: Option<&Rect>) {
let saved = self.enter_open_anim(tl);
let bounds = if tl.tl_data().anim_open_alpha().is_some() {
None
} else {
bounds
};
self.render_xdg_surface(&tl.xdg, x, y, bounds);
self.render_tl_aux(tl.tl_data(), bounds, true);
self.exit_open_anim(saved);
}
/// Enters open-animation scope for `tl`: applies its eased alpha + slide
/// translate to the renderer base. If a parent has already entered scope
/// for the same toplevel (so its borders/decorations slide too), this is a
/// no-op and returns `None`. Pair every `Some` return with `exit_open_anim`.
pub fn enter_open_anim(&mut self, tl: &dyn ToplevelNode) -> Option<OpenAnimSaved> {
let data = tl.tl_data();
let eased = data.anim_open_alpha()?;
if self.current_anim_node == Some(data.node_id) {
return None;
}
let saved = OpenAnimSaved {
alpha_mul: self.base.alpha_mul,
translate_x: self.base.translate_x,
translate_y: self.base.translate_y,
prev_node: self.current_anim_node,
};
self.current_anim_node = Some(data.node_id);
self.base.alpha_mul *= eased;
if let Some(ws) = data.workspace.get() {
let tl_rect = tl.node_absolute_position();
let output_rect = ws.output.get().global.pos.get();
let dl = (tl_rect.x1() - output_rect.x1()).max(0) as f32;
let dr = (output_rect.x2() - tl_rect.x2()).max(0) as f32;
let dt = (tl_rect.y1() - output_rect.y1()).max(0) as f32;
let db = (output_rect.y2() - tl_rect.y2()).max(0) as f32;
let mind = dl.min(dr).min(dt).min(db);
let (sx, sy) = if mind == dl {
(-(tl_rect.width() as f32), 0.0)
} else if mind == dr {
(tl_rect.width() as f32, 0.0)
} else if mind == dt {
(0.0, -(tl_rect.height() as f32))
} else {
(0.0, tl_rect.height() as f32)
};
let factor = (1.0 - eased) * self.base.scalef as f32;
self.base.translate_x += sx * factor;
self.base.translate_y += sy * factor;
}
Some(saved)
}
pub fn exit_open_anim(&mut self, saved: Option<OpenAnimSaved>) {
if let Some(s) = saved {
self.base.alpha_mul = s.alpha_mul;
self.base.translate_x = s.translate_x;
self.base.translate_y = s.translate_y;
self.current_anim_node = s.prev_node;
}
}
/// Renders any active close-animation snapshots that belong to this output.
/// Each snapshot fades out and slides toward its closest output edge —
/// mirroring the open animation in reverse. Finished snapshots stay in the
/// list until `tick_animations` cleans them up.
fn render_close_snapshots(&mut self, output: &OutputNode, x: i32, y: i32) {
let snaps = self.state.close_snapshots.borrow();
if snaps.is_empty() {
return;
}
let output_pos = output.global.pos.get();
for snap in snaps.iter() {
let Some(snap_output) = snap.output.upgrade() else {
continue;
};
if !std::ptr::eq(&*snap_output, output) {
continue;
}
let Some(progress) = snap.close_progress(self.state) else {
continue;
};
let alpha = (1.0 - progress).clamp(0.0, 1.0);
let prev_alpha = self.base.alpha_mul;
let prev_tx = self.base.translate_x;
let prev_ty = self.base.translate_y;
self.base.alpha_mul *= alpha;
self.base.translate_x += snap.slide_dx * progress * self.base.scalef as f32;
self.base.translate_y += snap.slide_dy * progress * self.base.scalef as f32;
let local_x = x + snap.rect.x1() - output_pos.x1();
let local_y = y + snap.rect.y1() - output_pos.y1();
let (sx, sy) = self.base.scale_point(local_x, local_y);
let scalef = self.base.scalef;
let tw = (snap.rect.width() as f64 * scalef).round() as i32;
let th = (snap.rect.height() as f64 * scalef).round() as i32;
let cd = self.state.color_manager.srgb_gamma22();
self.base.render_texture(
&snap.texture,
None,
sx,
sy,
None,
Some((tw, th)),
self.base.scale,
None,
None,
AcquireSync::Unnecessary,
ReleaseSync::Implicit,
false,
cd,
RenderIntent::Perceptual,
AlphaMode::PremultipliedElectrical,
);
self.base.alpha_mul = prev_alpha;
self.base.translate_x = prev_tx;
self.base.translate_y = prev_ty;
}
}
pub fn render_xdg_surface(
@ -804,6 +996,7 @@ impl Renderer<'_> {
Some(c) => c,
_ => return,
};
let saved_anim = self.enter_open_anim(&*child);
let pos = floating.position.get();
let theme = &self.state.theme;
let bw = theme.sizes.border_width.get();
@ -848,11 +1041,13 @@ impl Renderer<'_> {
}
child.node_render(self, body.x1(), body.y1(), Some(&scissor_body));
self.corner_radius = None;
self.exit_open_anim(saved_anim);
}
pub fn render_layer_surface(&mut self, surface: &ZwlrLayerSurfaceV1, x: i32, y: i32) {
let (dx, dy) = surface.surface.extents.get().position();
let blur = surface.blur.get();
let ignore_alpha = surface.ignore_alpha.get();
if blur {
let extents = surface.surface.extents.get();
@ -863,18 +1058,22 @@ impl Renderer<'_> {
let buffer = surface.surface.buffer.get()?;
let texture = buffer.buffer.buf.get_texture(&surface.surface)?;
let source = *surface.surface.buffer_points_norm.borrow();
let release_sync = buffer.release_sync;
Some(crate::gfx_api::BlurMask {
texture,
source,
threshold,
buffer_resv: Some(buffer),
acquire_sync: AcquireSync::Unnecessary,
release_sync,
})
});
let cache_epoch = surface.blur_cache_epoch.get();
let cache = Some(surface.blur_cache.clone());
self.base
.push_blur_backdrop(scaled, cfg.passes, cfg.size, mask);
.push_blur_backdrop(scaled, cfg.passes, cfg.size, mask, cache, cache_epoch);
}
self.base.discard_alpha = ignore_alpha;
self.render_surface(&surface.surface, x - dx, y - dy, None);
self.base.discard_alpha = None;
}
fn bounds_are_opaque(

View file

@ -26,6 +26,9 @@ pub struct RendererBase<'a> {
pub fb_width: f32,
pub fb_height: f32,
pub discard_alpha: Option<f32>,
pub alpha_mul: f32,
pub translate_x: f32,
pub translate_y: f32,
}
impl RendererBase<'_> {
@ -33,6 +36,26 @@ impl RendererBase<'_> {
self.scale
}
fn apply_alpha_mul(&self, alpha: Option<f32>) -> Option<f32> {
if self.alpha_mul >= 1.0 {
alpha
} else {
Some(alpha.unwrap_or(1.0) * self.alpha_mul)
}
}
fn fb_rect(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> FramebufferRect {
FramebufferRect::new(
x1 + self.translate_x,
y1 + self.translate_y,
x2 + self.translate_x,
y2 + self.translate_y,
self.transform,
self.fb_width,
self.fb_height,
)
}
pub fn scale_point(&self, mut x: i32, mut y: i32) -> (i32, i32) {
if self.scaled {
[x, y] = self.scale.pixel_size([x, y]);
@ -123,17 +146,14 @@ impl RendererBase<'_> {
true => bx,
};
self.ops.push(GfxApiOpt::FillRect(FillRect {
rect: FramebufferRect::new(
rect: self.fb_rect(
bx.x1() as f32,
bx.y1() as f32,
bx.x2() as f32,
bx.y2() as f32,
self.transform,
self.fb_width,
self.fb_height,
),
color: *color,
alpha,
alpha: self.apply_alpha_mul(alpha),
render_intent,
cd: cd.clone(),
}));
@ -166,17 +186,9 @@ impl RendererBase<'_> {
for bx in boxes {
let (x1, y1, x2, y2) = self.scale_rect_f(*bx);
self.ops.push(GfxApiOpt::FillRect(FillRect {
rect: FramebufferRect::new(
x1 + dx,
y1 + dy,
x2 + dx,
y2 + dy,
self.transform,
self.fb_width,
self.fb_height,
),
rect: self.fb_rect(x1 + dx, y1 + dy, x2 + dx, y2 + dy),
color: *color,
alpha: None,
alpha: self.apply_alpha_mul(None),
render_intent,
cd: cd.clone(),
}));
@ -227,21 +239,20 @@ impl RendererBase<'_> {
return;
}
let target = FramebufferRect::new(
let target = self.fb_rect(
target_x[0] as f32,
target_y[0] as f32,
target_x[1] as f32,
target_y[1] as f32,
self.transform,
self.fb_width,
self.fb_height,
);
let new_alpha = self.apply_alpha_mul(alpha);
let opaque = opaque && new_alpha == alpha;
self.ops.push(GfxApiOpt::CopyTexture(CopyTexture {
tex: texture.clone(),
source: texcoord,
target,
alpha,
alpha: new_alpha,
buffer_resv,
acquire_sync,
release_sync,
@ -296,17 +307,14 @@ impl RendererBase<'_> {
let fitted = corner_radius.fit_to(width, height);
let cr: [f32; 4] = fitted.into();
self.ops.push(GfxApiOpt::RoundedFillRect(RoundedFillRect {
rect: FramebufferRect::new(
rect: self.fb_rect(
rect.x1() as f32,
rect.y1() as f32,
rect.x2() as f32,
rect.y2() as f32,
self.transform,
self.fb_width,
self.fb_height,
),
color: *color,
alpha,
alpha: self.apply_alpha_mul(alpha),
render_intent,
cd: cd.clone(),
size: [width, height],
@ -358,14 +366,11 @@ impl RendererBase<'_> {
return;
}
let target = FramebufferRect::new(
let target = self.fb_rect(
target_x[0] as f32,
target_y[0] as f32,
target_x[1] as f32,
target_y[1] as f32,
self.transform,
self.fb_width,
self.fb_height,
);
let width = (target_x[1] - target_x[0]) as f32;
@ -379,7 +384,7 @@ impl RendererBase<'_> {
tex: texture.clone(),
source: texcoord,
target,
alpha,
alpha: self.apply_alpha_mul(alpha),
buffer_resv,
acquire_sync,
release_sync,
@ -404,6 +409,8 @@ impl RendererBase<'_> {
passes: u8,
offset: f32,
mask: Option<BlurMask>,
cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64,
) {
let target = FramebufferRect::new(
rect.x1() as f32,
@ -414,11 +421,15 @@ impl RendererBase<'_> {
self.fb_width,
self.fb_height,
);
let cache_pixel_rect = [rect.x1(), rect.y1(), rect.x2(), rect.y2()];
self.ops.push(GfxApiOpt::BlurBackdrop(BlurBackdrop {
rect: target,
passes,
offset,
mask,
cache,
cache_epoch,
cache_pixel_rect,
}));
}
}

View file

@ -306,6 +306,10 @@ pub struct State {
pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>,
pub layer_rules: RefCell<Vec<jay_config::_private::LayerRuleIpc>>,
pub blur_config: Cell<jay_config::_private::BlurConfigIpc>,
pub blur_cache_epoch: NumCell<u64>,
pub animations_config: Cell<jay_config::_private::AnimationsConfigIpc>,
pub active_animations: RefCell<Vec<std::rc::Weak<dyn ToplevelNode>>>,
pub close_snapshots: RefCell<Vec<Rc<crate::animation::Snapshot>>>,
}
// impl Drop for State {
@ -1049,6 +1053,25 @@ impl State {
if rect.is_empty() {
return;
}
if !cursor {
for output in self.root.outputs.lock().values() {
for layer in &output.layers {
for surface in layer.iter() {
if surface.blur.get() && surface.node_absolute_position().intersects(&rect)
{
surface.blur_cache_epoch.fetch_add(1);
}
if surface.blur.get() && surface.blur_popups.get() {
surface.for_each_popup(|popup| {
if popup.node_absolute_position().intersects(&rect) {
popup.blur_cache_epoch.fetch_add(1);
}
});
}
}
}
}
}
self.damage_visualizer.add(rect);
for output in self.root.outputs.lock().values() {
if output.global.pos.get().intersects(&rect) {
@ -1290,6 +1313,7 @@ impl State {
},
stretch: None,
corner_radius: None,
current_anim_node: None,
};
let mut sample_rect = SampleRect::identity();
sample_rect.buffer_transform = transform;
@ -1464,6 +1488,55 @@ impl State {
self.eng.now().msec()
}
/// Walks the active-animations list, damages each toplevel's slide region
/// (so the next frame re-renders it), and removes any whose animation is
/// done. Also ticks close-animation snapshots: damages their output and
/// drops finished ones. Intended to be called once per output present cycle.
pub fn tick_animations(&self) {
{
let mut animations = self.active_animations.borrow_mut();
if !animations.is_empty() {
animations.retain(|weak| {
let Some(tl) = weak.upgrade() else {
return false;
};
let data = tl.tl_data();
if data.anim_open_alpha().is_none() {
return false;
}
// Damage the entire output the toplevel is on: the slide
// can render outside the toplevel's nominal rect, so the
// narrow rect alone would leave the slid-out portion
// unredrawn.
if let Some(ws) = data.workspace.get() {
self.damage(ws.output.get().global.pos.get());
} else {
self.damage(tl.node_absolute_position());
}
true
});
}
}
let mut snapshots = self.close_snapshots.borrow_mut();
if snapshots.is_empty() {
return;
}
snapshots.retain(|snap| {
if snap.close_progress(self).is_none() {
// Final damage so the snapshot's last-rendered position gets
// repainted (clearing any leftover pixels).
if let Some(output) = snap.output.upgrade() {
self.damage(output.global.pos.get());
}
return false;
}
if let Some(output) = snap.output.upgrade() {
self.damage(output.global.pos.get());
}
true
});
}
pub fn output_extents_changed(&self) {
self.root.update_extents();
for seat in self.globals.seats.lock().values() {

View file

@ -236,6 +236,7 @@ impl OutputNode {
for listener in self.presentation_event.iter() {
listener.presented(self, tv_sec, tv_nsec, refresh, seq, flags, vrr);
}
self.state.tick_animations();
if locked && let Some(lock) = self.state.lock.lock.get() {
lock.check_locked()
}

View file

@ -428,6 +428,7 @@ pub struct ToplevelData {
pub property_changed_source: OnceCell<Rc<LazyEventSource>>,
pub mapped_source: Rc<LazyEventSource>,
pub unmapped_source: Rc<LazyEventSource>,
pub anim_open_start_nsec: Cell<Option<u64>>,
}
impl ToplevelData {
@ -485,6 +486,7 @@ impl ToplevelData {
property_changed_source: Default::default(),
mapped_source: state.lazy_event_sources.create_source(),
unmapped_source: state.lazy_event_sources.create_source(),
anim_open_start_nsec: Cell::new(None),
}
}
@ -531,6 +533,63 @@ impl ToplevelData {
}
}
/// Returns the eased alpha multiplier for the open animation, or None if no
/// animation is active. When the animation has finished, clears the start
/// time and returns None.
pub fn anim_open_alpha(&self) -> Option<f32> {
let start = self.anim_open_start_nsec.get()?;
let cfg = self.state.animations_config.get();
if !cfg.enabled || cfg.open_duration_ms == 0 {
self.anim_open_start_nsec.set(None);
return None;
}
let now = self.state.now_nsec();
let elapsed_ns = now.saturating_sub(start);
let dur_ns = (cfg.open_duration_ms as u64).saturating_mul(1_000_000);
if elapsed_ns >= dur_ns {
self.anim_open_start_nsec.set(None);
return None;
}
let t = (elapsed_ns as f32) / (dur_ns as f32);
let eased = match cfg.open_curve {
jay_config::_private::AnimationCurveIpc::Linear => t,
jay_config::_private::AnimationCurveIpc::EaseOut => {
let inv = 1.0 - t;
1.0 - inv * inv * inv
}
jay_config::_private::AnimationCurveIpc::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
let f = -2.0 * t + 2.0;
1.0 - f * f * f / 2.0
}
}
jay_config::_private::AnimationCurveIpc::Bezier { x1, y1, x2, y2 } => {
cubic_bezier_y_at_x(t, x1, y1, x2, y2)
}
};
Some(eased.clamp(0.0, 1.0))
}
/// Starts the open animation if animations are enabled. Inserts the
/// toplevel into the state's active-animations list so the present loop
/// can drive redraws.
pub fn start_open_animation(&self) {
let cfg = self.state.animations_config.get();
if !cfg.enabled || cfg.open_duration_ms == 0 {
return;
}
if self.anim_open_start_nsec.get().is_some() {
return;
}
self.anim_open_start_nsec.set(Some(self.state.now_nsec()));
self.state
.active_animations
.borrow_mut()
.push(self.slf.clone());
}
pub fn property_changed(&self, change: TlMatcherChange) {
self.trigger_property_source();
let mgr = &self.state.tl_matcher_manager;
@ -1108,3 +1167,31 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
tl.tl_set_fullscreen(true, Some(ws.clone()));
}
}
/// Evaluates a cubic Bezier easing curve `cubic-bezier(x1, y1, x2, y2)` at the
/// given input time `x`. P0=(0,0) and P3=(1,1) are fixed. Uses Newton-Raphson
/// to invert the x(t) parametric form, then evaluates y(t).
fn cubic_bezier_y_at_x(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
fn bx(t: f32, x1: f32, x2: f32) -> f32 {
let it = 1.0 - t;
3.0 * it * it * t * x1 + 3.0 * it * t * t * x2 + t * t * t
}
fn dbx(t: f32, x1: f32, x2: f32) -> f32 {
let it = 1.0 - t;
3.0 * it * it * x1 + 6.0 * it * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2)
}
let mut t = x;
for _ in 0..8 {
let err = bx(t, x1, x2) - x;
if err.abs() < 1e-4 {
break;
}
let d = dbx(t, x1, x2);
if d.abs() < 1e-6 {
break;
}
t = (t - err / d).clamp(0.0, 1.0);
}
let it = 1.0 - t;
3.0 * it * it * t * y1 + 3.0 * it * t * t * y2 + t * t * t
}

View file

@ -393,6 +393,30 @@ pub struct BlurConfig {
pub size: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnimationCurve {
Linear,
EaseOut,
EaseInOut,
Bezier { x1: f32, y1: f32, x2: f32, y2: f32 },
}
#[derive(Debug, Clone, Copy)]
pub struct AnimationsConfig {
pub enabled: Option<bool>,
pub open_duration_ms: Option<u32>,
pub open_curve: Option<AnimationCurve>,
pub close_duration_ms: Option<u32>,
pub close_curve: Option<AnimationCurve>,
}
#[derive(Debug, Clone, Copy)]
pub struct DamageVisualization {
pub enabled: Option<bool>,
pub color: Option<jay_config::theme::Color>,
pub decay_ms: Option<u64>,
}
#[derive(Debug, Clone)]
pub enum DrmDeviceMatch {
Any(Vec<DrmDeviceMatch>),
@ -607,6 +631,8 @@ pub struct Config {
pub window_rules: Vec<WindowRule>,
pub layer_rules: Vec<LayerRule>,
pub blur: Option<BlurConfig>,
pub damage_visualization: Option<DamageVisualization>,
pub animations: Option<AnimationsConfig>,
pub pointer_revert_key: Option<KeySym>,
pub use_hardware_cursor: Option<bool>,
pub show_bar: Option<bool>,

View file

@ -8,6 +8,7 @@ use {
pub mod action;
mod actions;
mod animations;
mod blur;
mod capabilities;
mod clean_logs_older_than;
@ -19,6 +20,7 @@ pub mod config;
mod connector;
mod connector_match;
mod content_type;
mod damage_visualization;
mod drm_device;
mod drm_device_match;
mod env;

View file

@ -0,0 +1,73 @@
use {
crate::{
config::{
AnimationCurve, AnimationsConfig,
context::Context,
extractor::{Extractor, ExtractorError, bol, int, opt, recover, str},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::{
toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum AnimationsConfigParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error("unknown animation curve `{0}`; expected one of: linear, ease-out, ease-in-out")]
UnknownCurve(String),
}
pub struct AnimationsConfigParser<'a>(pub &'a Context<'a>);
impl Parser for AnimationsConfigParser<'_> {
type Value = AnimationsConfig;
type Error = AnimationsConfigParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (enabled_val, open_duration_val, open_curve_val, close_duration_val, close_curve_val) =
ext.extract((
recover(opt(bol("enabled"))),
recover(opt(int("open-duration-ms"))),
opt(str("open-curve")),
recover(opt(int("close-duration-ms"))),
opt(str("close-curve")),
))?;
let enabled = enabled_val.despan();
let open_duration_ms = open_duration_val.despan().and_then(|v| u32::try_from(v).ok());
let close_duration_ms = close_duration_val.despan().and_then(|v| u32::try_from(v).ok());
let parse_curve = |val: Option<Spanned<&str>>| match val {
Some(s) => match s.value {
"linear" => Ok(Some(AnimationCurve::Linear)),
"ease-out" => Ok(Some(AnimationCurve::EaseOut)),
"ease-in-out" => Ok(Some(AnimationCurve::EaseInOut)),
other => {
Err(AnimationsConfigParserError::UnknownCurve(other.to_string()).spanned(s.span))
}
},
None => Ok(None),
};
let open_curve = parse_curve(open_curve_val)?;
let close_curve = parse_curve(close_curve_val)?;
Ok(AnimationsConfig {
enabled,
open_duration_ms,
open_curve,
close_duration_ms,
close_curve,
})
}
}

View file

@ -8,8 +8,10 @@ use {
parsers::{
action::ActionParser,
actions::ActionsParser,
animations::AnimationsConfigParser,
blur::BlurConfigParser,
clean_logs_older_than::CleanLogsOlderThanParser,
damage_visualization::DamageVisualizationParser,
client_rule::ClientRulesParser,
color_management::ColorManagementParser,
connector::ConnectorsParser,
@ -157,7 +159,7 @@ impl Parser for ConfigParser<'_> {
mouse_follows_focus,
layer_rules_val,
),
(blur_val,),
(blur_val, damage_visualization_val, animations_val),
) = ext.extract((
(
opt(val("keymap")),
@ -219,7 +221,11 @@ impl Parser for ConfigParser<'_> {
recover(opt(bol("unstable-mouse-follows-focus"))),
opt(val("layers")),
),
(opt(val("blur")),),
(
opt(val("blur")),
opt(val("damage-visualization")),
opt(val("animations")),
),
))?;
let mut keymap = None;
if let Some(value) = keymap_val {
@ -515,6 +521,26 @@ impl Parser for ConfigParser<'_> {
Err(e) => log::warn!("Could not parse the blur config: {}", self.0.error(e)),
}
}
let mut damage_visualization = None;
if let Some(value) = damage_visualization_val {
match value.parse(&mut DamageVisualizationParser(self.0)) {
Ok(v) => damage_visualization = Some(v),
Err(e) => log::warn!(
"Could not parse the damage-visualization config: {}",
self.0.error(e)
),
}
}
let mut animations = None;
if let Some(value) = animations_val {
match value.parse(&mut AnimationsConfigParser(self.0)) {
Ok(v) => animations = Some(v),
Err(e) => log::warn!(
"Could not parse the animations config: {}",
self.0.error(e)
),
}
}
let mut pointer_revert_key = None;
if let Some(value) = pointer_revert_key_str {
match Keysym::from_str(value.value) {
@ -616,6 +642,8 @@ impl Parser for ConfigParser<'_> {
window_rules,
layer_rules,
blur,
damage_visualization,
animations,
pointer_revert_key,
use_hardware_cursor: use_hardware_cursor.despan(),
show_bar: show_bar.despan(),

View file

@ -0,0 +1,65 @@
use {
crate::{
config::{
DamageVisualization,
context::Context,
extractor::{Extractor, ExtractorError, bol, int, opt, recover, str},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::color::{ColorParser, ColorParserError},
},
toml::{
toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum DamageVisualizationParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Color(#[from] ColorParserError),
}
pub struct DamageVisualizationParser<'a>(pub &'a Context<'a>);
impl Parser for DamageVisualizationParser<'_> {
type Value = DamageVisualization;
type Error = DamageVisualizationParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (enabled_val, color_val, decay_val) = ext.extract((
recover(opt(bol("enabled"))),
opt(str("color")),
recover(opt(int("decay-ms"))),
))?;
let enabled = enabled_val.despan();
let color = match color_val {
Some(s) => match ColorParser.parse_string(s.span, s.value) {
Ok(c) => Some(c),
Err(e) => {
return Err(DamageVisualizationParserError::Color(e.value)
.spanned(s.span));
}
},
None => None,
};
let decay_ms = decay_val.despan().and_then(|v| u64::try_from(v).ok());
Ok(DamageVisualization {
enabled,
color,
decay_ms,
})
}
}

View file

@ -13,7 +13,8 @@ mod toml;
use {
crate::{
config::{
Action, BlurConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
Action, AnimationCurve, AnimationsConfig, BlurConfig, ClientRule, Config,
ConfigConnector, ConfigDrmDevice, ConfigKeymap,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, LayerKind, LayerRule, Output,
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
},
@ -23,8 +24,12 @@ use {
ahash::{AHashMap, AHashSet},
error_reporter::Report,
jay_config::{
_private::{BlurConfigIpc, LayerKindIpc, LayerMatchIpc, LayerRuleIpc},
_set_blur_config, _set_layer_rules, Axis,
_private::{
AnimationCurveIpc, AnimationsConfigIpc, BlurConfigIpc, DamageVisualizationIpc,
LayerKindIpc, LayerMatchIpc, LayerRuleIpc,
},
_set_animations_config, _set_blur_config, _set_damage_visualization, _set_layer_rules,
Axis,
client::Client,
config, config_dir,
exec::{Command, set_env, unset_env},
@ -1471,6 +1476,8 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
persistent.window_rules.set(window_rules);
push_layer_rules(&config.layer_rules);
push_blur_config(config.blur);
push_damage_visualization(config.damage_visualization);
push_animations_config(config.animations);
state.set_status(&config.status);
persistent.actions.borrow_mut().clear();
for a in config.named_actions {
@ -1720,6 +1727,33 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
}
}
fn push_animations_config(anim: Option<AnimationsConfig>) {
let default = AnimationsConfigIpc::default();
let to_ipc = |c: AnimationCurve| match c {
AnimationCurve::Linear => AnimationCurveIpc::Linear,
AnimationCurve::EaseOut => AnimationCurveIpc::EaseOut,
AnimationCurve::EaseInOut => AnimationCurveIpc::EaseInOut,
AnimationCurve::Bezier { x1, y1, x2, y2 } => AnimationCurveIpc::Bezier { x1, y1, x2, y2 },
};
let cfg = match anim {
Some(a) => AnimationsConfigIpc {
enabled: a.enabled.unwrap_or(default.enabled),
open_duration_ms: a
.open_duration_ms
.unwrap_or(default.open_duration_ms)
.clamp(0, 10_000),
open_curve: to_ipc(a.open_curve.unwrap_or(AnimationCurve::EaseOut)),
close_duration_ms: a
.close_duration_ms
.unwrap_or(default.close_duration_ms)
.clamp(0, 10_000),
close_curve: to_ipc(a.close_curve.unwrap_or(AnimationCurve::EaseOut)),
},
None => default,
};
_set_animations_config(cfg);
}
fn push_blur_config(blur: Option<BlurConfig>) {
let default = BlurConfigIpc::default();
let cfg = match blur {
@ -1732,6 +1766,19 @@ fn push_blur_config(blur: Option<BlurConfig>) {
_set_blur_config(cfg);
}
fn push_damage_visualization(dv: Option<crate::config::DamageVisualization>) {
let default = DamageVisualizationIpc::default();
let cfg = match dv {
Some(d) => DamageVisualizationIpc {
enabled: d.enabled.unwrap_or(default.enabled),
color: d.color.unwrap_or(default.color),
decay_millis: d.decay_ms.unwrap_or(default.decay_millis),
},
None => default,
};
_set_damage_visualization(cfg);
}
fn push_layer_rules(rules: &[LayerRule]) {
let ipc: Vec<LayerRuleIpc> = rules
.iter()