diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 9f0a92c5..83aeccad 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -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, + } + } +} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2717a3e4..493c09a9 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -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 }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 5130a98e..be54e511 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -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)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 4d83f1e9..220f0570 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -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() diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 00000000..ec08b1db --- /dev/null +++ b/src/animation.rs @@ -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, + /// The output the toplevel was on, used to schedule per-frame damage. + pub output: Weak, + /// 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, +} + +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 { + 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) -> Option { + let ctx = state.render_ctx.get()?; + let formats = ctx.formats(); + let format_info = formats.get(&ARGB8888.drm)?; + let modifiers: Vec = 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()), + }) +} diff --git a/src/compositor.rs b/src/compositor.rs index ebe6e599..c8fbb5e9 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -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); diff --git a/src/config/handler.rs b/src/config/handler.rs index 18dbef71..07734fe3 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -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(()) } diff --git a/src/gfx_api.rs b/src/gfx_api.rs index bbe9d222..61183b61 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -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, + pub cache: Option>>>, + 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, } #[derive(Clone)] @@ -120,6 +144,9 @@ pub struct BlurMask { pub texture: Rc, pub source: SampleRect, pub threshold: f32, + pub buffer_resv: Option>, + 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, } } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 877151c4..79a3f19b 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -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(); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 7f7d053b..e31f90b1 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -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, pub id: XdgPopupId, node_id: PopupId, pub xdg: Rc, @@ -79,6 +80,8 @@ pub struct XdgPopup { relative_position: Cell, pos: RefCell, pub tracker: Tracker, + pub blur_cache: Rc>>, + pub blur_cache_epoch: NumCell, seat_state: NodeSeatState, set_visible_prepared: Cell, jay_popup_ext: CloneCell>>, @@ -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 { + 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 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(); - Some(crate::gfx_api::BlurMask { - texture, - source, - threshold, - }) - }); - renderer - .base - .push_blur_backdrop(scaled, cfg.passes, cfg.size, mask); + 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, + 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); } } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 768a8367..28728e28 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -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()); diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index f3b1e45f..8829fc9d 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -53,6 +53,7 @@ pub struct ZwlrLayerSurfaceV1 { pub client: Rc, pub surface: Rc, pub output: Rc, + pub blur_cache_epoch: NumCell, pub namespace: String, pub tracker: Tracker, output_extents: Cell, @@ -62,6 +63,7 @@ pub struct ZwlrLayerSurfaceV1 { pub blur: Cell, pub blur_popups: Cell, pub ignore_alpha: Cell>, + pub blur_cache: Rc>>, requested_serial: NumCell, size: Cell<(i32, i32)>, anchor: Cell, @@ -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), diff --git a/src/main.rs b/src/main.rs index 5a566f9b..161d3d99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ mod leaks; mod tracy; mod acceptor; mod allocator; +mod animation; mod async_engine; mod backend; mod backends; diff --git a/src/renderer.rs b/src/renderer.rs index ddb5ab7c..c3a97c2f 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -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, + /// 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, +} + +#[must_use] +pub struct OpenAnimSaved { + alpha_mul: f32, + translate_x: f32, + translate_y: f32, + prev_node: Option, } 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 = 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 { + 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) { + 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( diff --git a/src/renderer/renderer_base.rs b/src/renderer/renderer_base.rs index f2c95265..467e25e8 100644 --- a/src/renderer/renderer_base.rs +++ b/src/renderer/renderer_base.rs @@ -26,6 +26,9 @@ pub struct RendererBase<'a> { pub fb_width: f32, pub fb_height: f32, pub discard_alpha: Option, + 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) -> Option { + 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, + cache: Option>>>, + 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, })); } } diff --git a/src/state.rs b/src/state.rs index 9decfcd9..136597d1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -306,6 +306,10 @@ pub struct State { pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc>, pub layer_rules: RefCell>, pub blur_config: Cell, + pub blur_cache_epoch: NumCell, + pub animations_config: Cell, + pub active_animations: RefCell>>, + pub close_snapshots: RefCell>>, } // 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() { diff --git a/src/tree/output.rs b/src/tree/output.rs index 611eb822..b18c6624 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -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() } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 02bba848..752d9d77 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -428,6 +428,7 @@ pub struct ToplevelData { pub property_changed_source: OnceCell>, pub mapped_source: Rc, pub unmapped_source: Rc, + pub anim_open_start_nsec: Cell>, } 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 { + 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, tl: Rc, 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 +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 66326d57..1e408803 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -393,6 +393,30 @@ pub struct BlurConfig { pub size: Option, } +#[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, + pub open_duration_ms: Option, + pub open_curve: Option, + pub close_duration_ms: Option, + pub close_curve: Option, +} + +#[derive(Debug, Clone, Copy)] +pub struct DamageVisualization { + pub enabled: Option, + pub color: Option, + pub decay_ms: Option, +} + #[derive(Debug, Clone)] pub enum DrmDeviceMatch { Any(Vec), @@ -607,6 +631,8 @@ pub struct Config { pub window_rules: Vec, pub layer_rules: Vec, pub blur: Option, + pub damage_visualization: Option, + pub animations: Option, pub pointer_revert_key: Option, pub use_hardware_cursor: Option, pub show_bar: Option, diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 766522b3..8e6a2404 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -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; diff --git a/toml-config/src/config/parsers/animations.rs b/toml-config/src/config/parsers/animations.rs new file mode 100644 index 00000000..747b98f5 --- /dev/null +++ b/toml-config/src/config/parsers/animations.rs @@ -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>, + ) -> ParseResult { + 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>| 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, + }) + } +} diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index ab169895..a3df8be3 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -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(), diff --git a/toml-config/src/config/parsers/damage_visualization.rs b/toml-config/src/config/parsers/damage_visualization.rs new file mode 100644 index 00000000..65feeb80 --- /dev/null +++ b/toml-config/src/config/parsers/damage_visualization.rs @@ -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>, + ) -> ParseResult { + 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, + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 85b92127..49b85f66 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -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) { + 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) { let default = BlurConfigIpc::default(); let cfg = match blur { @@ -1732,6 +1766,19 @@ fn push_blur_config(blur: Option) { _set_blur_config(cfg); } +fn push_damage_visualization(dv: Option) { + 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 = rules .iter()