diff --git a/book/src/tiling.md b/book/src/tiling.md index 2ff61d5e..650cd73a 100644 --- a/book/src/tiling.md +++ b/book/src/tiling.md @@ -77,20 +77,6 @@ You can also right-click any title in a container to toggle mono mode. In mono mode, scroll over the title bar to cycle between windows in the container. -## Autotiling - -Autotiling makes newly tiled windows alternate split direction from the focused -tiled window. The first split uses the containing group direction, then later -windows wrap the focused tile in the opposite direction, producing a horizontal, -vertical, horizontal pattern as the layout grows. - -```toml -[shortcuts] -alt-a = "toggle-autotile" -``` - -Manual grouping and split commands still use the direction you request. - ## Fullscreen Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 151e7591..b48c6227 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -640,22 +640,6 @@ impl ConfigClient { self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); } - pub fn seat_send_to_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatSendToScratchpad { seat, name }); - } - - pub fn seat_toggle_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatToggleScratchpad { seat, name }); - } - - pub fn seat_cycle_scratchpad(&self, seat: Seat, name: &str) { - self.send(&ClientMessage::SeatCycleScratchpad { seat, name }); - } - - pub fn window_send_to_scratchpad(&self, window: Window, name: &str) { - self.send(&ClientMessage::WindowSendToScratchpad { window, name }); - } - pub fn seat_split(&self, seat: Seat) -> Axis { let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); @@ -1039,26 +1023,6 @@ impl ConfigClient { self.send(&ClientMessage::SetUiDragThreshold { threshold }); } - pub fn set_animations_enabled(&self, enabled: bool) { - self.send(&ClientMessage::SetAnimationsEnabled { enabled }); - } - - pub fn set_animation_duration_ms(&self, duration_ms: u32) { - self.send(&ClientMessage::SetAnimationDurationMs { duration_ms }); - } - - pub fn set_animation_curve(&self, curve: u32) { - self.send(&ClientMessage::SetAnimationCurve { curve }); - } - - pub fn set_animation_style(&self, style: u32) { - self.send(&ClientMessage::SetAnimationStyle { style }); - } - - pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) { - self.send(&ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 }); - } - pub fn set_color_management_enabled(&self, enabled: bool) { self.send(&ClientMessage::SetColorManagementEnabled { enabled }); } @@ -1363,6 +1327,14 @@ impl ConfigClient { self.send(&ClientMessage::SetIdle { timeout }) } + pub fn set_key_press_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetKeyPressEnablesDpms { enabled }) + } + + pub fn set_mouse_move_enables_dpms(&self, enabled: bool) { + self.send(&ClientMessage::SetMouseMoveEnablesDpms { enabled }) + } + pub fn set_idle_grace_period(&self, period: Duration) { self.send(&ClientMessage::SetIdleGracePeriod { period }) } @@ -2095,12 +2067,6 @@ impl ConfigClient { self.send(&ClientMessage::SetAutotile { enabled }); } - pub fn get_autotile(&self) -> bool { - let res = self.send_with_response(&ClientMessage::GetAutotile); - get_response!(res, false, GetAutotile { enabled }); - enabled - } - pub fn set_tab_title_align(&self, align: u32) { self.send(&ClientMessage::SetTabTitleAlign { align }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 743acc57..0a2b9491 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -286,18 +286,6 @@ pub enum ClientMessage<'a> { seat: Seat, workspace: Workspace, }, - SeatSendToScratchpad { - seat: Seat, - name: &'a str, - }, - SeatToggleScratchpad { - seat: Seat, - name: &'a str, - }, - SeatCycleScratchpad { - seat: Seat, - name: &'a str, - }, GetTimer { name: &'a str, }, @@ -487,6 +475,12 @@ pub enum ClientMessage<'a> { SetIdle { timeout: Duration, }, + SetKeyPressEnablesDpms { + enabled: bool, + }, + SetMouseMoveEnablesDpms { + enabled: bool, + }, MoveToOutput { workspace: WorkspaceSource, connector: Connector, @@ -557,24 +551,6 @@ pub enum ClientMessage<'a> { SetUiDragThreshold { threshold: i32, }, - SetAnimationsEnabled { - enabled: bool, - }, - SetAnimationDurationMs { - duration_ms: u32, - }, - SetAnimationCurve { - curve: u32, - }, - SetAnimationStyle { - style: u32, - }, - SetAnimationCubicBezier { - x1: f32, - y1: f32, - x2: f32, - y2: f32, - }, SetXScalingMode { mode: XScalingMode, }, @@ -699,10 +675,6 @@ pub enum ClientMessage<'a> { window: Window, workspace: Workspace, }, - WindowSendToScratchpad { - window: Window, - name: &'a str, - }, SetWindowFullscreen { window: Window, fullscreen: bool, @@ -939,7 +911,6 @@ pub enum ClientMessage<'a> { SetAutotile { enabled: bool, }, - GetAutotile, SetTabTitleAlign { align: u32, }, @@ -1206,9 +1177,6 @@ pub enum Response { GetCornerRadius { radius: f32, }, - GetAutotile { - enabled: bool, - }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 450597e2..dbdef1ba 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -466,33 +466,6 @@ impl Seat { get!().set_seat_workspace(self, workspace) } - /// Sends the currently focused window to a scratchpad. - /// - /// Use an empty string for the default scratchpad. - pub fn send_to_scratchpad(self, name: &str) { - get!().seat_send_to_scratchpad(self, name) - } - - /// Toggles a scratchpad. - /// - /// If the scratchpad has a visible window, that window is hidden. Otherwise, the - /// most recently hidden window in the scratchpad is shown on the current workspace. - /// Scratchpad windows are always shown floating. - /// Use an empty string for the default scratchpad. - pub fn toggle_scratchpad(self, name: &str) { - get!().seat_toggle_scratchpad(self, name) - } - - /// Cycles through the windows of a scratchpad, one at a time. - /// - /// With nothing shown, the first window is brought up; each further invocation - /// hides the current window and shows the next; after the last window the - /// scratchpad is hidden again. Scratchpad windows are always shown floating. - /// Use an empty string for the default scratchpad. - pub fn cycle_scratchpad(self, name: &str) { - get!().seat_cycle_scratchpad(self, name) - } - /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { let c = get!(); diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index fff94506..dcc4e346 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -103,27 +103,6 @@ impl Axis { } } -/// The curve used for tiled window animations. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct AnimationCurve(pub u32); - -impl AnimationCurve { - pub const LINEAR: Self = Self(0); - pub const EASE: Self = Self(1); - pub const EASE_IN: Self = Self(2); - pub const EASE_OUT: Self = Self(3); - pub const EASE_IN_OUT: Self = Self(4); -} - -/// The presentation style used for tiled window movement animations. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct AnimationStyle(pub u32); - -impl AnimationStyle { - pub const PLAIN: Self = Self(0); - pub const MULTIPHASE: Self = Self(1); -} - /// Exits the compositor. pub fn quit() { get!().quit() @@ -273,6 +252,20 @@ pub fn set_idle(timeout: Option) { get!().set_idle(timeout.unwrap_or_default()) } +/// Configures whether a key press turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_key_press_enables_dpms(enabled: bool) { + get!().set_key_press_enables_dpms(enabled) +} + +/// Configures whether mouse movement turns monitors back on after `jay dpms off`. +/// +/// The default is `false`. +pub fn set_mouse_move_enables_dpms(enabled: bool) { + get!().set_mouse_move_enables_dpms(enabled) +} + /// Configures the idle grace period. /// /// The grace period starts after the idle timeout expires. During the grace period, the @@ -308,42 +301,6 @@ pub fn set_ui_drag_threshold(threshold: i32) { get!().set_ui_drag_threshold(threshold); } -/// Enables or disables tiled window animations. -/// -/// The default is `false`. -pub fn set_animations_enabled(enabled: bool) { - get!().set_animations_enabled(enabled); -} - -/// Sets the duration of tiled window animations in milliseconds. -/// -/// The default is `160`. -pub fn set_animation_duration_ms(duration_ms: u32) { - get!().set_animation_duration_ms(duration_ms); -} - -/// Sets the curve used by tiled window animations. -/// -/// The default is [`AnimationCurve::EASE_OUT`]. -pub fn set_animation_curve(curve: AnimationCurve) { - get!().set_animation_curve(curve.0); -} - -/// Sets the presentation style used for tiled window movement animations. -/// -/// The default is [`AnimationStyle::MULTIPHASE`]. -pub fn set_animation_style(style: AnimationStyle) { - get!().set_animation_style(style.0); -} - -/// Sets a custom cubic-bezier curve used by tiled window animations. -/// -/// `x1` and `x2` must be between `0.0` and `1.0`. The curve starts at `(0, 0)` -/// and ends at `(1, 1)`. -pub fn set_animation_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) { - get!().set_animation_cubic_bezier(x1, y1, x2, y2); -} - /// Enables or disables the color-management protocol. /// /// The default is `false`. @@ -453,21 +410,14 @@ pub fn get_corner_radius() -> f32 { /// Enables or disables autotiling. /// -/// When enabled, newly tiled windows alternate split orientation from the -/// focused tiled window: the first split uses the containing group's direction, -/// then subsequent splits wrap the focused window in the perpendicular -/// direction. +/// When enabled, new windows are automatically placed in a perpendicular +/// sub-container if the predicted body would be narrower than tall (or vice versa). /// /// The default is `false`. pub fn set_autotile(enabled: bool) { get!().set_autotile(enabled) } -/// Returns whether autotiling is enabled. -pub fn get_autotile() -> bool { - get!(false).get_autotile() -} - /// Sets the horizontal alignment of title text within tab buttons. /// /// - `"start"` — left-aligned (default) diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 96e4d3b1..662cda44 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -205,13 +205,6 @@ impl Window { get!().set_window_workspace(self, workspace) } - /// Sends the window to a scratchpad. - /// - /// Use an empty string for the default scratchpad. - pub fn send_to_scratchpad(self, name: &str) { - get!().window_send_to_scratchpad(self, name) - } - /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { self.set_fullscreen(!self.fullscreen()) diff --git a/src/animation.rs b/src/animation.rs deleted file mode 100644 index e76e030b..00000000 --- a/src/animation.rs +++ /dev/null @@ -1,1233 +0,0 @@ -use { - crate::{ - cmm::{cmm_description::ColorDescription, cmm_render_intent::RenderIntent}, - gfx_api::{GfxTexture, SampleRect}, - ifs::wl_surface::{SurfaceBuffer, WlSurface}, - rect::Rect, - state::State, - theme::Color, - tree::{LatchListener, NodeId, OutputNode}, - utils::{clonecell::CloneCell, event_listener::EventListener}, - }, - ahash::AHashMap, - std::{ - cell::{Cell, RefCell}, - collections::VecDeque, - rc::{Rc, Weak}, - }, -}; - -pub mod multiphase; - -const DEFAULT_DURATION_MS: u32 = 160; -const CURVE_MAX_POINTS: usize = 33; -const CURVE_FLATNESS_EPSILON: f32 = 0.001; -const CURVE_MAX_DEPTH: u8 = 8; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum AnimationCurve { - Linear, - Piecewise(PiecewiseCurve), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum AnimationStyle { - Plain, - Multiphase, -} - -impl AnimationStyle { - pub fn from_config(value: u32) -> Option { - match value { - 0 => Some(Self::Plain), - 1 => Some(Self::Multiphase), - _ => None, - } - } -} - -impl AnimationCurve { - pub fn from_config(value: u32) -> Self { - match value { - 0 => Self::Linear, - 1 => Self::from_cubic_bezier(0.25, 0.1, 0.25, 1.0).unwrap(), - 2 => Self::from_cubic_bezier(0.42, 0.0, 1.0, 1.0).unwrap(), - 4 => Self::from_cubic_bezier(0.42, 0.0, 0.58, 1.0).unwrap(), - _ => Self::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap(), - } - } - - pub fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Option { - if !x1.is_finite() - || !y1.is_finite() - || !x2.is_finite() - || !y2.is_finite() - || !(0.0..=1.0).contains(&x1) - || !(0.0..=1.0).contains(&x2) - { - return None; - } - Some(Self::Piecewise(PiecewiseCurve::from_cubic_bezier( - x1, y1, x2, y2, - ))) - } - - fn sample(self, t: f64) -> f64 { - let t = t.clamp(0.0, 1.0); - match self { - Self::Linear => t, - Self::Piecewise(curve) => curve.sample(t as f32) as f64, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct PiecewiseCurve { - len: u8, - points: [CurvePoint; CURVE_MAX_POINTS], -} - -impl PiecewiseCurve { - fn from_cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { - let mut points = Vec::with_capacity(CURVE_MAX_POINTS); - let p0 = cubic_bezier_point(x1, y1, x2, y2, 0.0); - let p1 = cubic_bezier_point(x1, y1, x2, y2, 1.0); - points.push(p0); - flatten_cubic_bezier(&mut points, (x1, y1, x2, y2), 0.0, p0, 1.0, p1, 0); - let mut array = [CurvePoint::default(); CURVE_MAX_POINTS]; - let len = points.len().min(CURVE_MAX_POINTS); - array[..len].copy_from_slice(&points[..len]); - Self { - len: len as u8, - points: array, - } - } - - fn sample(self, x: f32) -> f32 { - let len = self.len as usize; - if len <= 1 { - return x; - } - let points = &self.points[..len]; - if x <= points[0].x { - return points[0].y; - } - if x >= points[len - 1].x { - return points[len - 1].y; - } - let mut lo = 0; - let mut hi = len - 1; - while lo + 1 < hi { - let mid = (lo + hi) / 2; - if points[mid].x <= x { - lo = mid; - } else { - hi = mid; - } - } - let from = points[lo]; - let to = points[hi]; - if to.x <= from.x { - return to.y; - } - let t = (x - from.x) / (to.x - from.x); - from.y + (to.y - from.y) * t - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -struct CurvePoint { - x: f32, - y: f32, -} - -pub struct AnimationState { - pub enabled: Cell, - pub duration_ms: Cell, - pub curve: Cell, - pub style: Cell, - windows: RefCell>, - phased: RefCell>, - exits: RefCell>, - tick: CloneCell>>, -} - -pub struct RetainedToplevel { - pub offset: (i32, i32), - pub surface: RetainedSurface, -} - -pub struct RetainedSurface { - pub offset: (i32, i32), - pub size: (i32, i32), - pub content: RetainedContent, - pub below: Vec, - pub above: Vec, -} - -pub enum RetainedContent { - Texture { - texture: Rc, - buffer: Rc, - source: SampleRect, - alpha: Option, - color_description: Rc, - render_intent: RenderIntent, - alpha_mode: crate::gfx_api::AlphaMode, - opaque: bool, - }, - Color { - color: Color, - alpha: Option, - color_description: Rc, - render_intent: RenderIntent, - }, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum RetainedExitLayer { - Tiled, - Floating, -} - -pub struct RetainedExitFrame { - pub rect: Rect, - pub retained: Rc, - pub frame_inset: i32, - pub source_body_size: (i32, i32), - pub active: bool, - pub layer: RetainedExitLayer, -} - -impl RetainedToplevel { - pub fn capture_surface(surface: &WlSurface, offset: (i32, i32)) -> Option> { - Some(Rc::new(Self { - offset, - surface: RetainedSurface::capture(surface, (0, 0))?, - })) - } -} - -impl RetainedSurface { - fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option { - let buffer = surface.buffer.get()?; - buffer.buffer.buf.update_texture_or_log(surface, true); - let size = surface.buffer_abs_pos.get().size(); - let source = *surface.buffer_points_norm.borrow(); - let color_description = surface.color_description(); - let render_intent = surface.render_intent(); - let alpha_mode = surface.alpha_mode(); - let alpha = surface.alpha(); - let content = match buffer.buffer.buf.get_texture(surface) { - Some(texture) => RetainedContent::Texture { - opaque: surface.opaque(), - texture, - buffer, - source, - alpha, - color_description, - render_intent, - alpha_mode, - }, - None => { - let color = buffer.buffer.buf.color?; - RetainedContent::Color { - color: Color::from_u32( - color_description.eotf, - alpha_mode, - color[0], - color[1], - color[2], - color[3], - ), - alpha, - color_description, - render_intent, - } - } - }; - let mut below = vec![]; - let mut above = vec![]; - if let Some(children) = surface.children.borrow().as_deref() { - for child in children.below.iter() { - if child.pending.get() { - continue; - } - let pos = child.sub_surface.position.get(); - if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) { - below.push(surface); - } - } - for child in children.above.iter() { - if child.pending.get() { - continue; - } - let pos = child.sub_surface.position.get(); - if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) { - above.push(surface); - } - } - } - Some(Self { - offset, - size, - content, - below, - above, - }) - } -} - -impl Default for AnimationState { - fn default() -> Self { - Self { - enabled: Cell::new(false), - duration_ms: Cell::new(DEFAULT_DURATION_MS), - curve: Cell::new(AnimationCurve::from_config(3)), - style: Cell::new(AnimationStyle::Multiphase), - windows: Default::default(), - phased: Default::default(), - exits: Default::default(), - tick: Default::default(), - } - } -} - -impl AnimationState { - pub fn clear(&self) { - self.windows.borrow_mut().clear(); - self.phased.borrow_mut().clear(); - self.exits.borrow_mut().clear(); - if let Some(tick) = self.tick.take() { - tick.detach(); - } - } - - pub fn set_target( - &self, - node_id: NodeId, - old: Rect, - new: Rect, - _retained: Option>, - now_nsec: u64, - duration_ms: u32, - curve: AnimationCurve, - ) -> bool { - if old == new || new.is_empty() || duration_ms == 0 { - self.windows.borrow_mut().remove(&node_id); - self.phased.borrow_mut().remove(&node_id); - return false; - } - let duration_nsec = duration_ms as u64 * 1_000_000; - let mut from = old; - { - let phased = self.phased.borrow(); - if let Some(anim) = phased.get(&node_id) { - if anim.final_rect == new { - return false; - } - from = anim.rect_at(now_nsec); - } - } - { - let windows = self.windows.borrow(); - if let Some(anim) = windows.get(&node_id) { - if anim.to == new { - return false; - } - from = anim.rect_at(now_nsec); - } - } - if from == new { - self.windows.borrow_mut().remove(&node_id); - self.phased.borrow_mut().remove(&node_id); - return false; - } - self.phased.borrow_mut().remove(&node_id); - self.windows.borrow_mut().insert( - node_id, - WindowAnimation { - from, - to: new, - start_nsec: now_nsec, - duration_nsec, - curve, - last_damage: from, - retained: None, - }, - ); - true - } - - pub fn set_phased_target( - &self, - node_id: NodeId, - phases: Vec<(Rect, Rect)>, - _retained: Option>, - now_nsec: u64, - duration_ms: u32, - curve: AnimationCurve, - ) -> bool { - if phases.is_empty() || duration_ms == 0 { - return false; - } - let Some((from, _)) = phases.first().copied() else { - return false; - }; - let Some((_, final_rect)) = phases.last().copied() else { - return false; - }; - if from.is_empty() || final_rect.is_empty() || from == final_rect { - return false; - } - let segments: Vec<_> = phases - .into_iter() - .map(|(from, to)| PhasedSegment { from, to }) - .collect(); - let mut route_edges = route_edges_from_segments(&segments); - if let Some(anim) = self.phased.borrow().get(&node_id) - && !anim.done(now_nsec) - { - for &(from, to) in &anim.route_edges { - push_unique_route_edge(&mut route_edges, from, to); - } - } - self.windows.borrow_mut().remove(&node_id); - self.phased.borrow_mut().insert( - node_id, - PhasedWindowAnimation { - segments, - start_nsec: now_nsec, - duration_nsec: duration_ms as u64 * 1_000_000, - curve, - last_damage: from, - final_rect, - route_edges, - retained: None, - }, - ); - true - } - - pub fn set_spawn_in( - &self, - node_id: NodeId, - target: Rect, - retained: Option>, - now_nsec: u64, - duration_ms: u32, - curve: AnimationCurve, - ) -> bool { - let start = spawn_in_start_rect(target); - self.set_target( - node_id, - start, - target, - retained, - now_nsec, - duration_ms, - curve, - ) - } - - pub fn set_spawn_out( - &self, - from: Rect, - frame_inset: i32, - retained: Rc, - active: bool, - layer: RetainedExitLayer, - now_nsec: u64, - duration_ms: u32, - curve: AnimationCurve, - ) -> bool { - if from.is_empty() || duration_ms == 0 { - return false; - } - let to = spawn_in_start_rect(from); - if to == from { - return false; - } - let source_body_size = body_size_for_frame(from, frame_inset); - if source_body_size.0 <= 0 || source_body_size.1 <= 0 { - return false; - } - self.exits.borrow_mut().push(ExitAnimation { - from, - to, - start_nsec: now_nsec, - duration_nsec: duration_ms as u64 * 1_000_000, - curve, - last_damage: from, - retained, - frame_inset, - source_body_size, - active, - layer, - }); - true - } - - pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect { - let phased = self.phased.borrow(); - if let Some(anim) = phased.get(&node_id) - && !anim.done(now_nsec) - { - return anim.rect_at(now_nsec); - } - drop(phased); - let windows = self.windows.borrow(); - match windows.get(&node_id) { - Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec), - _ => layout, - } - } - - pub fn retained_snapshot( - &self, - node_id: NodeId, - now_nsec: u64, - ) -> Option> { - let phased = self.phased.borrow(); - if let Some(anim) = phased.get(&node_id) - && !anim.done(now_nsec) - { - return anim.retained.clone(); - } - drop(phased); - let windows = self.windows.borrow(); - match windows.get(&node_id) { - Some(anim) if !anim.done(now_nsec) => anim.retained.clone(), - _ => None, - } - } - - pub fn phased_route_to( - &self, - node_id: NodeId, - target: Rect, - now_nsec: u64, - ) -> Option> { - let phased = self.phased.borrow(); - let anim = phased.get(&node_id)?; - if anim.done(now_nsec) { - return None; - } - anim.route_to(target, now_nsec) - } - - pub fn exit_frames(&self, now_nsec: u64) -> Vec { - self.exits - .borrow() - .iter() - .filter(|exit| !exit.done(now_nsec)) - .map(|exit| RetainedExitFrame { - rect: exit.rect_at(now_nsec), - retained: exit.retained.clone(), - frame_inset: exit.frame_inset, - source_body_size: exit.source_body_size, - active: exit.active, - layer: exit.layer, - }) - .collect() - } - - fn damage_active(&self, state: &State, now_nsec: u64) -> bool { - let mut damages = vec![]; - let mut any_active = false; - { - let mut windows = self.windows.borrow_mut(); - windows.retain(|_, anim| { - let current = anim.rect_at(now_nsec); - let damage = anim.last_damage.union(current).union(anim.to); - damages.push(expand_damage_rect( - damage, - state.theme.sizes.border_width.get().max(0), - )); - anim.last_damage = current; - let active = !anim.done(now_nsec); - any_active |= active; - active - }); - self.phased.borrow_mut().retain(|_, anim| { - let current = anim.rect_at(now_nsec); - let damage = anim.last_damage.union(current).union(anim.final_rect); - damages.push(expand_damage_rect( - damage, - state.theme.sizes.border_width.get().max(0), - )); - anim.last_damage = current; - let active = !anim.done(now_nsec); - any_active |= active; - active - }); - self.exits.borrow_mut().retain_mut(|exit| { - let current = exit.rect_at(now_nsec); - let damage = exit.last_damage.union(current).union(exit.to); - damages.push(expand_damage_rect( - damage, - state.theme.sizes.border_width.get().max(0), - )); - exit.last_damage = current; - let active = !exit.done(now_nsec); - any_active |= active; - active - }); - } - for damage in damages { - state.damage(damage); - } - any_active - } - - pub(crate) fn tick_is_active(&self) -> bool { - self.tick.is_some() - } - - pub(crate) fn set_tick(&self, tick: Rc) { - self.tick.set(Some(tick)); - } - - pub(crate) fn clear_tick(&self) { - self.tick.take(); - } -} - -struct WindowAnimation { - from: Rect, - to: Rect, - start_nsec: u64, - duration_nsec: u64, - curve: AnimationCurve, - last_damage: Rect, - retained: Option>, -} - -impl WindowAnimation { - fn done(&self, now_nsec: u64) -> bool { - now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec - } - - fn rect_at(&self, now_nsec: u64) -> Rect { - if self.duration_nsec == 0 { - return self.to; - } - let elapsed = now_nsec.saturating_sub(self.start_nsec); - let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0); - let t = self.curve.sample(t); - lerp_rect(self.from, self.to, t) - } -} - -struct PhasedWindowAnimation { - segments: Vec, - start_nsec: u64, - duration_nsec: u64, - curve: AnimationCurve, - last_damage: Rect, - final_rect: Rect, - route_edges: Vec<(Rect, Rect)>, - retained: Option>, -} - -struct PhasedSegment { - from: Rect, - to: Rect, -} - -impl PhasedWindowAnimation { - fn done(&self, now_nsec: u64) -> bool { - let total_duration = self - .duration_nsec - .saturating_mul(self.segments.len() as u64); - now_nsec.saturating_sub(self.start_nsec) >= total_duration - } - - fn rect_at(&self, now_nsec: u64) -> Rect { - if self.duration_nsec == 0 { - return self.final_rect; - } - let elapsed = now_nsec.saturating_sub(self.start_nsec); - let phase = (elapsed / self.duration_nsec) as usize; - let Some(segment) = self.segments.get(phase) else { - return self.final_rect; - }; - let phase_elapsed = elapsed % self.duration_nsec; - let t = (phase_elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0); - let t = self.curve.sample(t); - lerp_rect(segment.from, segment.to, t) - } - - fn phase_at(&self, now_nsec: u64) -> Option { - if self.duration_nsec == 0 || self.segments.is_empty() { - return None; - } - let elapsed = now_nsec.saturating_sub(self.start_nsec); - let phase = (elapsed / self.duration_nsec) as usize; - (phase < self.segments.len()).then_some(phase) - } - - fn route_to(&self, target: Rect, now_nsec: u64) -> Option> { - let phase = self.phase_at(now_nsec)?; - let current = self.rect_at(now_nsec); - if current == target { - return Some(vec![]); - } - let segment = self.segments.get(phase)?; - route_through_edges(current, target, segment.from, segment.to, &self.route_edges) - } -} - -fn route_edges_from_segments(segments: &[PhasedSegment]) -> Vec<(Rect, Rect)> { - let mut edges = vec![]; - for segment in segments { - push_unique_route_edge(&mut edges, segment.from, segment.to); - } - edges -} - -fn push_unique_route_edge(edges: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) { - if from == to { - return; - } - if edges - .iter() - .any(|&(a, b)| (a == from && b == to) || (a == to && b == from)) - { - return; - } - edges.push((from, to)); -} - -fn route_through_edges( - current: Rect, - target: Rect, - current_from: Rect, - current_to: Rect, - known_edges: &[(Rect, Rect)], -) -> Option> { - let mut edges = known_edges.to_vec(); - push_unique_route_edge(&mut edges, current, current_from); - push_unique_route_edge(&mut edges, current, current_to); - rect_graph_route(current, target, &edges) -} - -fn rect_graph_route( - start: Rect, - target: Rect, - edges: &[(Rect, Rect)], -) -> Option> { - let mut nodes = vec![]; - let mut adjacency: Vec> = vec![]; - let start_idx = rect_graph_node(&mut nodes, &mut adjacency, start); - let target_idx = rect_graph_node(&mut nodes, &mut adjacency, target); - for &(from, to) in edges { - let from_idx = rect_graph_node(&mut nodes, &mut adjacency, from); - let to_idx = rect_graph_node(&mut nodes, &mut adjacency, to); - if !adjacency[from_idx].contains(&to_idx) { - adjacency[from_idx].push(to_idx); - } - if !adjacency[to_idx].contains(&from_idx) { - adjacency[to_idx].push(from_idx); - } - } - - let mut previous = vec![None; nodes.len()]; - let mut queue = VecDeque::from([start_idx]); - previous[start_idx] = Some(start_idx); - while let Some(idx) = queue.pop_front() { - if idx == target_idx { - break; - } - for &next in &adjacency[idx] { - if previous[next].is_none() { - previous[next] = Some(idx); - queue.push_back(next); - } - } - } - previous[target_idx]?; - - let mut reversed_nodes = vec![target_idx]; - let mut idx = target_idx; - while idx != start_idx { - idx = previous[idx]?; - reversed_nodes.push(idx); - } - reversed_nodes.reverse(); - - let mut route = vec![]; - for pair in reversed_nodes.windows(2) { - push_non_empty_segment(&mut route, nodes[pair[0]], nodes[pair[1]]); - } - Some(route) -} - -fn rect_graph_node(nodes: &mut Vec, adjacency: &mut Vec>, rect: Rect) -> usize { - if let Some(idx) = nodes.iter().position(|&node| node == rect) { - return idx; - } - let idx = nodes.len(); - nodes.push(rect); - adjacency.push(vec![]); - idx -} - -fn push_non_empty_segment(route: &mut Vec<(Rect, Rect)>, from: Rect, to: Rect) { - if from != to { - route.push((from, to)); - } -} - -struct ExitAnimation { - from: Rect, - to: Rect, - start_nsec: u64, - duration_nsec: u64, - curve: AnimationCurve, - last_damage: Rect, - retained: Rc, - frame_inset: i32, - source_body_size: (i32, i32), - active: bool, - layer: RetainedExitLayer, -} - -impl ExitAnimation { - fn done(&self, now_nsec: u64) -> bool { - now_nsec.saturating_sub(self.start_nsec) >= self.duration_nsec - } - - fn rect_at(&self, now_nsec: u64) -> Rect { - if self.duration_nsec == 0 { - return self.to; - } - let elapsed = now_nsec.saturating_sub(self.start_nsec); - let t = (elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0); - let t = self.curve.sample(t); - lerp_rect(self.from, self.to, t) - } -} - -pub struct AnimationTick { - state: Weak, - slf: Weak, - latch_listeners: RefCell>>, -} - -impl AnimationTick { - pub fn new(state: &Rc, slf: &Weak) -> Self { - let slf: Weak = slf.clone(); - Self { - state: Rc::downgrade(state), - slf, - latch_listeners: Default::default(), - } - } - - pub fn attach(&self, output: &OutputNode) { - let listener = EventListener::new(self.slf.clone()); - listener.attach(&output.latch_event); - self.latch_listeners.borrow_mut().push(listener); - } - - pub fn detach(&self) { - for listener in self.latch_listeners.borrow_mut().drain(..) { - listener.detach(); - } - } -} - -impl LatchListener for AnimationTick { - fn after_latch(self: Rc, _on: &OutputNode, _tearing: bool) { - let Some(state) = self.state.upgrade() else { - self.detach(); - return; - }; - let active = state.animations.damage_active(&state, state.now_nsec()); - if !active { - self.detach(); - state.animations.clear_tick(); - } - } -} - -pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect { - let (cx, cy) = target.center(); - Rect::new_empty(cx, cy) -} - -fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) { - ( - rect.width().saturating_sub(2 * frame_inset), - rect.height().saturating_sub(2 * frame_inset), - ) -} - -fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect { - fn lerp(from: i32, to: i32, t: f64) -> i32 { - (from as f64 + (to as f64 - from as f64) * t).round() as i32 - } - Rect::new_saturating( - lerp(from.x1(), to.x1(), t), - lerp(from.y1(), to.y1(), t), - lerp(from.x2(), to.x2(), t), - lerp(from.y2(), to.y2(), t), - ) -} - -pub(crate) fn expand_damage_rect(rect: Rect, width: i32) -> Rect { - Rect::new_saturating( - rect.x1().saturating_sub(width), - rect.y1().saturating_sub(width), - rect.x2().saturating_add(width), - rect.y2().saturating_add(width), - ) -} - -fn flatten_cubic_bezier( - points: &mut Vec, - controls: (f32, f32, f32, f32), - t0: f32, - p0: CurvePoint, - t1: f32, - p1: CurvePoint, - depth: u8, -) { - let tm = (t0 + t1) * 0.5; - let pm = cubic_bezier_point(controls.0, controls.1, controls.2, controls.3, tm); - let projected_y = if p1.x <= p0.x { - (p0.y + p1.y) * 0.5 - } else { - let tx = (pm.x - p0.x) / (p1.x - p0.x); - p0.y + (p1.y - p0.y) * tx - }; - if (pm.y - projected_y).abs() > CURVE_FLATNESS_EPSILON - && depth < CURVE_MAX_DEPTH - && points.len() + 2 < CURVE_MAX_POINTS - { - flatten_cubic_bezier(points, controls, t0, p0, tm, pm, depth + 1); - flatten_cubic_bezier(points, controls, tm, pm, t1, p1, depth + 1); - } else { - points.push(p1); - } -} - -fn cubic_bezier_point(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> CurvePoint { - fn bezier(a: f32, b: f32, t: f32) -> f32 { - let inv = 1.0 - t; - 3.0 * inv * inv * t * a + 3.0 * inv * t * t * b + t * t * t - } - CurvePoint { - x: bezier(x1, x2, t), - y: bezier(y1, y2, t), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::cmm::cmm_manager::ColorManager; - - fn retained_for_tests() -> Rc { - let color_manager = ColorManager::new(); - Rc::new(RetainedToplevel { - offset: (0, 0), - surface: RetainedSurface { - offset: (0, 0), - size: (100, 100), - content: RetainedContent::Color { - color: Color::SOLID_BLACK, - alpha: None, - color_description: color_manager.srgb_gamma22().clone(), - render_intent: RenderIntent::Perceptual, - }, - below: vec![], - above: vec![], - }, - }) - } - - #[test] - fn linear_rect_interpolation_is_symmetric() { - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(100, 40, 200, 80); - assert_eq!(lerp_rect(a, b, 0.25), lerp_rect(b, a, 0.75)); - } - - #[test] - fn custom_cubic_bezier_curve_is_prepared() { - let curve = AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.0, 1.0).unwrap(); - assert_eq!(curve.sample(0.0), 0.0); - assert_eq!(curve.sample(1.0), 1.0); - assert!((curve.sample(0.5) - 0.5).abs() < 0.001); - - let ease_out = AnimationCurve::from_cubic_bezier(0.0, 0.0, 0.58, 1.0).unwrap(); - let mid = ease_out.sample(0.5); - assert!(mid > 0.5); - assert!(mid < 1.0); - } - - #[test] - fn invalid_custom_cubic_bezier_curve_is_rejected() { - assert!(AnimationCurve::from_cubic_bezier(-0.1, 0.0, 0.58, 1.0).is_none()); - assert!(AnimationCurve::from_cubic_bezier(0.0, 0.0, 1.1, 1.0).is_none()); - assert!(AnimationCurve::from_cubic_bezier(0.0, f32::NAN, 0.58, 1.0).is_none()); - } - - #[test] - fn spawn_out_frames_use_configured_curve_and_expire() { - let state = AnimationState::default(); - let retained = retained_for_tests(); - let from = Rect::new_sized_saturating(10, 20, 100, 80); - let to = spawn_in_start_rect(from); - let curve = AnimationCurve::from_config(3); - assert!(state.set_spawn_out( - from, - 2, - retained.clone(), - true, - RetainedExitLayer::Floating, - 0, - 160, - curve - )); - - let start = state.exit_frames(0); - assert_eq!(start.len(), 1); - assert_eq!(start[0].rect, from); - assert_eq!(start[0].source_body_size, (96, 76)); - assert!(start[0].active); - assert_eq!(start[0].layer, RetainedExitLayer::Floating); - assert!(Rc::ptr_eq(&start[0].retained, &retained)); - - let middle = state.exit_frames(80_000_000); - assert_eq!(middle.len(), 1); - assert_eq!(middle[0].rect, lerp_rect(from, to, curve.sample(0.5))); - assert_ne!(middle[0].rect, lerp_rect(from, to, 0.5)); - assert!(state.exit_frames(160_000_000).is_empty()); - } - - #[test] - fn normal_window_animations_do_not_retain_content() { - let state = AnimationState::default(); - let id = NodeId(1); - let from = Rect::new_sized_saturating(0, 0, 100, 100); - let to = Rect::new_sized_saturating(100, 0, 100, 100); - assert!(state.set_target( - id, - from, - to, - Some(retained_for_tests()), - 0, - 160, - AnimationCurve::Linear - )); - - assert!(state.retained_snapshot(id, 80_000_000).is_none()); - } - - #[test] - fn phased_window_animations_do_not_retain_content() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - Some(retained_for_tests()), - 0, - 100, - AnimationCurve::Linear - )); - - assert!(state.retained_snapshot(id, 50_000_000).is_none()); - } - - #[test] - fn phased_animation_uses_full_duration_per_phase() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - None, - 0, - 100, - AnimationCurve::Linear - )); - assert_eq!(state.visual_rect(id, c, 0), a); - assert_eq!(state.visual_rect(id, c, 50_000_000), lerp_rect(a, b, 0.5)); - assert_eq!(state.visual_rect(id, c, 100_000_000), b); - assert_eq!(state.visual_rect(id, c, 150_000_000), lerp_rect(b, c, 0.5)); - assert_eq!(state.visual_rect(id, c, 200_000_000), c); - } - - #[test] - fn phased_route_reverses_to_existing_endpoint() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - None, - 0, - 100, - AnimationCurve::Linear - )); - - let current = lerp_rect(b, c, 0.5); - assert_eq!( - state.phased_route_to(id, a, 150_000_000).unwrap(), - vec![(current, b), (b, a)] - ); - } - - #[test] - fn phased_route_continues_to_existing_endpoint() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - None, - 0, - 100, - AnimationCurve::Linear - )); - - let current = lerp_rect(a, b, 0.5); - assert_eq!( - state.phased_route_to(id, c, 50_000_000).unwrap(), - vec![(current, b), (b, c)] - ); - } - - #[test] - fn phased_route_remembers_original_path_after_retarget() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - None, - 0, - 100, - AnimationCurve::Linear - )); - - let current = lerp_rect(b, c, 0.5); - let reverse = state.phased_route_to(id, a, 150_000_000).unwrap(); - assert_eq!(reverse, vec![(current, b), (b, a)]); - assert!(state.set_phased_target( - id, - reverse, - None, - 150_000_000, - 100, - AnimationCurve::Linear - )); - - let current = lerp_rect(current, b, 0.5); - assert_eq!( - state.phased_route_to(id, c, 200_000_000).unwrap(), - vec![(current, b), (b, c)] - ); - } - - #[test] - fn linear_retarget_interrupts_phased_animation_from_current_rect() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(0, 0, 100, 50); - let c = Rect::new_sized_saturating(100, 0, 100, 50); - let d = Rect::new_sized_saturating(100, 100, 100, 50); - assert!(state.set_phased_target( - id, - vec![(a, b), (b, c)], - None, - 0, - 100, - AnimationCurve::Linear - )); - let current = lerp_rect(a, b, 0.5); - assert!(state.set_target(id, a, d, None, 50_000_000, 100, AnimationCurve::Linear)); - assert_eq!(state.visual_rect(id, d, 50_000_000), current); - assert_eq!( - state.visual_rect(id, d, 100_000_000), - lerp_rect(current, d, 0.5) - ); - } - - #[test] - fn unchanged_target_does_not_restart() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(100, 0, 100, 100); - assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear)); - assert!(!state.set_target(id, a, b, None, 80_000_000, 160, AnimationCurve::Linear)); - assert_eq!( - state.visual_rect(id, b, 80_000_000), - Rect::new_sized_saturating(50, 0, 100, 100) - ); - } - - #[test] - fn changed_target_restarts_from_current_visual_rect() { - let state = AnimationState::default(); - let id = NodeId(1); - let a = Rect::new_sized_saturating(0, 0, 100, 100); - let b = Rect::new_sized_saturating(100, 0, 100, 100); - let c = Rect::new_sized_saturating(200, 0, 100, 100); - assert!(state.set_target(id, a, b, None, 0, 160, AnimationCurve::Linear)); - assert!(state.set_target(id, a, c, None, 80_000_000, 160, AnimationCurve::Linear)); - assert_eq!( - state.visual_rect(id, c, 80_000_000), - Rect::new_sized_saturating(50, 0, 100, 100) - ); - assert_eq!( - state.visual_rect(id, c, 160_000_000), - Rect::new_sized_saturating(125, 0, 100, 100) - ); - } - - #[test] - fn spawn_in_start_rect_is_centered_and_empty() { - let target = Rect::new_sized_saturating(10, 20, 100, 50); - assert_eq!(spawn_in_start_rect(target), Rect::new_empty(60, 45)); - } - - #[test] - fn spawn_in_uses_configured_curve() { - let state = AnimationState::default(); - let id = NodeId(1); - let target = Rect::new_sized_saturating(10, 20, 100, 50); - let curve = AnimationCurve::from_config(3); - assert!(state.set_spawn_in(id, target, None, 0, 160, curve)); - assert_eq!( - state.visual_rect(id, target, 80_000_000), - lerp_rect(spawn_in_start_rect(target), target, curve.sample(0.5)) - ); - assert_ne!( - state.visual_rect(id, target, 80_000_000), - Rect::new_sized_saturating(35, 33, 50, 25) - ); - } -} diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs deleted file mode 100644 index cb067241..00000000 --- a/src/animation/multiphase.rs +++ /dev/null @@ -1,3405 +0,0 @@ -use {crate::rect::Rect, crate::tree::NodeId}; - -const MIN_SHRINK_DENOMINATOR: i32 = 8; -// Integer split remainders can make swapped siblings differ by one pixel. Do -// not spend a full animation phase on that imperceptible bookkeeping step. -const SWAP_AXIS_SNAP_PX: i32 = 1; - -#[derive(Clone, Debug)] -pub struct MultiphaseRequest { - pub bounds: Rect, - pub windows: Vec, - pub clearance: i32, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct MultiphaseWindow { - pub node_id: NodeId, - pub from: Rect, - pub to: Rect, - pub hierarchy: MultiphaseWindowHierarchy, -} - -impl MultiphaseWindow { - pub fn new(node_id: NodeId, from: Rect, to: Rect) -> Self { - Self { - node_id, - from, - to, - hierarchy: Default::default(), - } - } - - pub fn with_hierarchy( - node_id: NodeId, - from: Rect, - to: Rect, - hierarchy: MultiphaseWindowHierarchy, - ) -> Self { - Self { - node_id, - from, - to, - hierarchy, - } - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct MultiphaseWindowHierarchy { - pub source: MultiphaseHierarchyPosition, - pub target: MultiphaseHierarchyPosition, - pub transition: MultiphaseHierarchyTransition, -} - -impl MultiphaseWindowHierarchy { - pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self { - let transition = if !source.parent_is_mono && target.parent_is_mono { - MultiphaseHierarchyTransition::EnteringMono - } else if source.parent_is_mono && !target.parent_is_mono { - MultiphaseHierarchyTransition::ExitingMono - } else if source.parent.is_none() || target.parent.is_none() { - MultiphaseHierarchyTransition::Unknown - } else if target.depth < source.depth { - MultiphaseHierarchyTransition::Ascending - } else if target.depth > source.depth { - MultiphaseHierarchyTransition::Descending - } else { - MultiphaseHierarchyTransition::SameLevel - }; - Self { - source, - target, - transition, - } - } - - fn reversed(self) -> Self { - Self { - source: self.target, - target: self.source, - transition: self.transition.reversed(), - } - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct MultiphaseHierarchyPosition { - pub parent: Option, - pub depth: u16, - pub sibling_index: Option, - pub split_axis: Option, - pub nearest_horizontal_split_depth: Option, - pub nearest_vertical_split_depth: Option, - pub parent_is_mono: bool, - pub mono_active: bool, -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum MultiphaseHierarchyTransition { - #[default] - Unknown, - SameLevel, - Ascending, - Descending, - EnteringMono, - ExitingMono, -} - -impl MultiphaseHierarchyTransition { - fn reversed(self) -> Self { - match self { - Self::Unknown => Self::Unknown, - Self::SameLevel => Self::SameLevel, - Self::Ascending => Self::Descending, - Self::Descending => Self::Ascending, - Self::EnteringMono => Self::ExitingMono, - Self::ExitingMono => Self::EnteringMono, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MultiphasePlan { - pub phases: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MultiphasePlanned { - pub plan: MultiphasePlan, - pub explanation: MultiphasePlanExplanation, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MultiphasePlanExplanation { - pub strategy: PlanStrategy, - pub phases: Vec, - pub validation: ValidationExplanation, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PhaseExplanation { - pub action: MultiphasePhaseAction, - pub reason: PhaseReason, - pub nodes: Vec, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct ValidationExplanation { - pub continuous_overlap_passed: bool, - pub final_rects_matched: bool, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PlanStrategy { - NoOp, - SingleAction, - MixedSinglePhase, - HierarchyOrderedScales, - OrientationChange { from_axis: PhaseAxis }, - SwapLanes { axis: PhaseAxis }, - SpaceThenOrthogonalGrowth { axis: PhaseAxis }, - ReversedForwardPlan { original: Box }, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PlanDirection { - Forward, - Reverse, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct RejectedStrategy { - pub direction: PlanDirection, - pub strategy: PlanStrategy, - pub reason: MultiphasePlanFailure, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PhaseReason { - SingleAction, - SameAxisRedistribution, - MixedAxisActions, - ShrinkIntoLanes { - lane_axis: PhaseAxis, - }, - MoveThroughFreedSpace, - GrowOutOfLanes, - CreateSpaceForAscendingChild, - MoveAscendingChildAfterSpaceExists, - OrthogonalGrowthAfterMove, - ParentAxisBeforeChildAxis { - parent_axis: PhaseAxis, - parent_depth: u16, - child_axis: PhaseAxis, - child_depth: u16, - }, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MultiphasePhase { - pub action: MultiphasePhaseAction, - pub steps: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum MultiphasePhaseAction { - Uniform(PhaseAction), - Mixed(Vec), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct MultiphaseStep { - pub node_id: NodeId, - pub from: Rect, - pub to: Rect, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct PhaseAction { - pub kind: PhaseKind, - pub axis: PhaseAxis, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PhaseKind { - Move, - Scale, -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PhaseAxis { - Horizontal, - Vertical, -} - -impl MultiphasePhaseAction { - fn from_step_actions(actions: Vec) -> Self { - debug_assert!(!actions.is_empty()); - let first = actions[0]; - if actions.iter().all(|action| *action == first) { - Self::Uniform(first) - } else { - Self::Mixed(actions) - } - } - - fn action_for_step(&self, idx: usize) -> Option { - match self { - Self::Uniform(action) => Some(*action), - Self::Mixed(actions) => actions.get(idx).copied(), - } - } - - fn as_uniform(&self) -> Option { - match self { - Self::Uniform(action) => Some(*action), - Self::Mixed(_) => None, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum MultiphaseError { - EmptyBounds, - EmptyWindow, - DuplicateWindow, - InitialOverlap, - FinalOverlap, - NoPlan, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MultiphasePlanDiagnostic { - pub forward: MultiphasePlanFailure, - pub reverse: Option, - pub attempted: Vec, -} - -impl MultiphasePlanDiagnostic { - fn legacy_error(self) -> MultiphaseError { - match self.forward { - MultiphasePlanFailure::Request(error) => error, - _ => MultiphaseError::NoPlan, - } - } -} - -impl ValidationExplanation { - fn passed() -> Self { - Self { - continuous_overlap_passed: true, - final_rects_matched: true, - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum MultiphasePlanFailure { - Request(MultiphaseError), - NoPattern, - ShrinkBound { - axis: PhaseAxis, - available: i32, - required: i32, - }, - InvalidPhaseStep { - action: PhaseAction, - node_id: NodeId, - }, - Validation(MultiphaseValidationError), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum MultiphaseValidationError { - DuplicatePhaseStep { - phase: usize, - node_id: NodeId, - }, - PhaseActionCount { - phase: usize, - actions: usize, - steps: usize, - }, - UnknownPhaseStep { - phase: usize, - node_id: NodeId, - }, - StaleStepStart { - phase: usize, - node_id: NodeId, - }, - PhaseOverlap { - phase: usize, - a: NodeId, - b: NodeId, - }, - FinalMismatch { - node_id: NodeId, - }, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct PlanForwardFailure { - reason: MultiphasePlanFailure, - attempted: Vec, -} - -pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result { - plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error()) -} - -pub fn plan_no_overlap_with_diagnostics( - request: &MultiphaseRequest, -) -> Result { - plan_no_overlap_explained(request).map(|planned| planned.plan) -} - -pub fn plan_no_overlap_explained( - request: &MultiphaseRequest, -) -> Result { - if let Err(error) = validate_request(request) { - return Err(MultiphasePlanDiagnostic { - forward: MultiphasePlanFailure::Request(error), - reverse: None, - attempted: vec![], - }); - } - if request - .windows - .iter() - .all(|window| window.from == window.to) - { - return Ok(MultiphasePlanned { - plan: MultiphasePlan { phases: vec![] }, - explanation: MultiphasePlanExplanation { - strategy: PlanStrategy::NoOp, - phases: vec![], - validation: ValidationExplanation::passed(), - }, - }); - } - if let Some(failure) = target_shrink_bound_failure(request) { - return Err(MultiphasePlanDiagnostic { - forward: failure, - reverse: None, - attempted: vec![], - }); - } - let forward = match plan_forward(request, PlanDirection::Forward) { - Ok(plan) => return Ok(plan), - Err(error) => error, - }; - let reversed = reverse_request(request); - match plan_forward(&reversed, PlanDirection::Reverse) { - Ok(plan) => Ok(reverse_planned(plan)), - Err(reverse) => { - let mut attempted = forward.attempted; - attempted.extend(reverse.attempted); - Err(MultiphasePlanDiagnostic { - forward: forward.reason, - reverse: Some(reverse.reason), - attempted, - }) - } - } -} - -pub(crate) fn validate_phase_paths( - request: &MultiphaseRequest, - paths: &[Vec<(Rect, Rect)>], -) -> Result { - if paths.len() != request.windows.len() { - return Err(MultiphasePlanFailure::NoPattern); - } - let phase_count = paths.iter().map(Vec::len).max().unwrap_or(0); - if phase_count == 0 { - return Err(MultiphasePlanFailure::NoPattern); - } - let mut phases = vec![]; - for phase_idx in 0..phase_count { - let mut steps = vec![]; - let mut actions = vec![]; - for (window_idx, path) in paths.iter().enumerate() { - let Some((from, to)) = path.get(phase_idx).copied() else { - continue; - }; - if from == to { - continue; - } - let step = MultiphaseStep { - node_id: request.windows[window_idx].node_id, - from, - to, - }; - let Some(action) = classify_step(step) else { - return Err(MultiphasePlanFailure::NoPattern); - }; - steps.push(step); - actions.push(action); - } - if !steps.is_empty() { - phases.push(MultiphasePhase { - action: MultiphasePhaseAction::from_step_actions(actions), - steps, - }); - } - } - let plan = MultiphasePlan { phases }; - validate_plan_continuous_diagnostic(request, &plan) - .map(|_| plan) - .map_err(MultiphasePlanFailure::Validation) -} - -pub(crate) fn partition_motion_groups( - windows: &[MultiphaseWindow], - clearance: i32, -) -> Vec> { - let clearance = clearance.max(0); - let mut groups = vec![]; - let mut seen = vec![false; windows.len()]; - for start in 0..windows.len() { - if seen[start] { - continue; - } - seen[start] = true; - let mut group = vec![]; - let mut pending = vec![start]; - while let Some(idx) = pending.pop() { - group.push(idx); - let bounds = motion_bounds_with_clearance(windows[idx], clearance); - for other in 0..windows.len() { - if seen[other] - || !bounds.intersects(&motion_bounds_with_clearance(windows[other], clearance)) - { - continue; - } - seen[other] = true; - pending.push(other); - } - } - group.sort_unstable(); - groups.push(group); - } - groups -} - -fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> { - if request.bounds.is_empty() { - return Err(MultiphaseError::EmptyBounds); - } - for (idx, window) in request.windows.iter().enumerate() { - if window.from.is_empty() || window.to.is_empty() { - return Err(MultiphaseError::EmptyWindow); - } - for other in &request.windows[..idx] { - if other.node_id == window.node_id { - return Err(MultiphaseError::DuplicateWindow); - } - } - } - if overlaps(request.windows.iter().map(|window| window.from)) { - return Err(MultiphaseError::InitialOverlap); - } - if overlaps(request.windows.iter().map(|window| window.to)) { - return Err(MultiphaseError::FinalOverlap); - } - Ok(()) -} - -fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option { - let min_width = sane_min_size(request.bounds.width()); - let min_height = sane_min_size(request.bounds.height()); - for window in &request.windows { - if window.to.width() < min_width { - return Some(MultiphasePlanFailure::ShrinkBound { - axis: PhaseAxis::Horizontal, - available: window.to.width(), - required: min_width, - }); - } - if window.to.height() < min_height { - return Some(MultiphasePlanFailure::ShrinkBound { - axis: PhaseAxis::Vertical, - available: window.to.height(), - required: min_height, - }); - } - } - None -} - -fn plan_forward( - request: &MultiphaseRequest, - direction: PlanDirection, -) -> Result { - let mut rejection = None; - let mut attempted = vec![]; - match plan_single_action_phase(request) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - match plan_space_then_orthogonal_growth(request, axis) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection( - &mut attempted, - direction, - PlanStrategy::SpaceThenOrthogonalGrowth { axis }, - error, - ); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - } - match plan_hierarchy_ordered_axis_scales(request) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection( - &mut attempted, - direction, - PlanStrategy::HierarchyOrderedScales, - error, - ); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - match plan_orientation_change(request, axis) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection( - &mut attempted, - direction, - PlanStrategy::OrientationChange { from_axis: axis }, - error, - ); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - } - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - match plan_axis_crossing_lanes(request, axis) { - Ok(plan) => return Ok(plan), - Err(error) => { - record_rejection( - &mut attempted, - direction, - PlanStrategy::SwapLanes { axis }, - error, - ); - if error != MultiphasePlanFailure::NoPattern { - rejection.get_or_insert(error); - } - } - } - } - Err(PlanForwardFailure { - reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern), - attempted, - }) -} - -fn record_rejection( - attempted: &mut Vec, - direction: PlanDirection, - strategy: PlanStrategy, - reason: MultiphasePlanFailure, -) { - attempted.push(RejectedStrategy { - direction, - strategy, - reason, - }); -} - -fn plan_single_action_phase( - request: &MultiphaseRequest, -) -> Result { - let mut uniform_action = None; - let mut is_uniform = true; - let mut steps = vec![]; - let mut step_actions = vec![]; - for window in &request.windows { - if window.from == window.to { - continue; - } - let step = MultiphaseStep { - node_id: window.node_id, - from: window.from, - to: window.to, - }; - let Some(step_action) = classify_step(step) else { - return Err(MultiphasePlanFailure::NoPattern); - }; - if step_action.kind == PhaseKind::Scale { - let (available, required) = match step_action.axis { - PhaseAxis::Horizontal => (window.to.width(), sane_min_size(request.bounds.width())), - PhaseAxis::Vertical => (window.to.height(), sane_min_size(request.bounds.height())), - }; - if available < required { - return Err(MultiphasePlanFailure::ShrinkBound { - axis: step_action.axis, - available, - required, - }); - } - } - if uniform_action.is_some_and(|action| action != step_action) { - is_uniform = false; - } - uniform_action.get_or_insert(step_action); - steps.push(step); - step_actions.push(step_action); - } - if steps.is_empty() { - return Err(MultiphasePlanFailure::NoPattern); - } - if !is_uniform { - return build_validated_plan( - request, - PlanStrategy::MixedSinglePhase, - [phase_draft_mixed( - steps, - step_actions, - PhaseReason::MixedAxisActions, - )], - ); - } - let action = uniform_action.unwrap(); - build_validated_plan( - request, - PlanStrategy::SingleAction, - [phase_draft_uniform( - action, - steps, - single_action_reason(action), - )], - ) -} - -fn plan_hierarchy_ordered_axis_scales( - request: &MultiphaseRequest, -) -> Result { - let mut changed_axes = vec![]; - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - if request - .windows - .iter() - .any(|window| interval_changed(window.from, window.to, axis)) - { - changed_axes.push(axis); - } - } - let [first_axis, second_axis] = changed_axes - .try_into() - .map_err(|_| MultiphasePlanFailure::NoPattern)?; - let order = hierarchy_scale_axis_order(request, first_axis, second_axis) - .ok_or(MultiphasePlanFailure::NoPattern)?; - let mut current: Vec<_> = request - .windows - .iter() - .map(|window| (window.node_id, window.from)) - .collect(); - let mut phases = vec![]; - let reason = PhaseReason::ParentAxisBeforeChildAxis { - parent_axis: order.axes[0], - parent_depth: order.depths[0], - child_axis: order.axes[1], - child_depth: order.depths[1], - }; - for axis in order.axes { - let mut steps = vec![]; - for window in &request.windows { - let (_, rect) = current - .iter_mut() - .find(|(node_id, _)| *node_id == window.node_id) - .unwrap(); - let next = with_main_interval( - *rect, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ); - if next == *rect { - continue; - } - if main_size(*rect, axis) == main_size(next, axis) { - return Err(MultiphasePlanFailure::NoPattern); - } - steps.push(MultiphaseStep { - node_id: window.node_id, - from: *rect, - to: next, - }); - *rect = next; - } - if steps.is_empty() { - return Err(MultiphasePlanFailure::NoPattern); - } - phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason)); - } - let [first, second] = phases - .try_into() - .map_err(|_| MultiphasePlanFailure::NoPattern)?; - build_validated_plan( - request, - PlanStrategy::HierarchyOrderedScales, - [first, second], - ) -} - -fn hierarchy_scale_axis_order( - request: &MultiphaseRequest, - first_axis: PhaseAxis, - second_axis: PhaseAxis, -) -> Option { - let first_priority = hierarchy_axis_priority(request, first_axis)?; - let second_priority = hierarchy_axis_priority(request, second_axis)?; - match first_priority.cmp(&second_priority) { - std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder { - axes: [first_axis, second_axis], - depths: [first_priority, second_priority], - }), - std::cmp::Ordering::Greater => Some(HierarchyScaleAxisOrder { - axes: [second_axis, first_axis], - depths: [second_priority, first_priority], - }), - std::cmp::Ordering::Equal => None, - } -} - -#[derive(Copy, Clone)] -struct HierarchyScaleAxisOrder { - axes: [PhaseAxis; 2], - depths: [u16; 2], -} - -fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option { - request - .windows - .iter() - .filter(|window| interval_changed(window.from, window.to, axis)) - .flat_map(|window| { - [ - split_depth_for_axis(window.hierarchy.source, axis), - split_depth_for_axis(window.hierarchy.target, axis), - ] - }) - .flatten() - .min() -} - -fn plan_axis_crossing_lanes( - request: &MultiphaseRequest, - axis: PhaseAxis, -) -> Result { - let moving_windows: Vec<_> = request - .windows - .iter() - .copied() - .filter(|window| window.from != window.to) - .collect(); - if moving_windows.len() < 2 { - return Err(MultiphasePlanFailure::NoPattern); - } - let orth_min = request - .windows - .iter() - .map(|window| orth_start(window.from, axis)) - .min() - .ok_or(MultiphasePlanFailure::NoPattern)?; - let orth_max = request - .windows - .iter() - .map(|window| orth_end(window.from, axis)) - .max() - .ok_or(MultiphasePlanFailure::NoPattern)?; - if moving_windows.iter().any(|window| { - orth_start(window.from, axis) != orth_min - || orth_end(window.from, axis) != orth_max - || orth_start(window.to, axis) != orth_min - || orth_end(window.to, axis) != orth_max - || main_start(window.from, axis) == main_start(window.to, axis) - }) { - return Err(MultiphasePlanFailure::NoPattern); - } - let clearance = request.clearance.max(0); - let lane_count = moving_windows.len() as i32; - let available = (orth_max - orth_min) - clearance.saturating_mul(lane_count - 1); - if available <= 0 { - return Err(MultiphasePlanFailure::ShrinkBound { - axis: axis.other(), - available: 0, - required: sane_min_size(orth_max - orth_min), - }); - } - let lane_size = available / lane_count; - let mut lane_remainder = available % lane_count; - let required = sane_min_size(orth_max - orth_min); - if lane_size < required { - return Err(MultiphasePlanFailure::ShrinkBound { - axis: axis.other(), - available: lane_size, - required, - }); - } - - let mut windows = moving_windows; - windows.sort_by_key(|window| lane_sort_key(*window, axis)); - let mut phase1 = vec![]; - let mut phase2 = vec![]; - let mut phase3 = vec![]; - let mut phase4 = vec![]; - let mut lane_start = orth_min; - for (idx, window) in windows.iter().enumerate() { - let extra = if lane_remainder > 0 { - lane_remainder -= 1; - 1 - } else { - 0 - }; - let lane_end = lane_start + lane_size + extra; - let lane_from = with_orth_interval(window.from, axis, lane_start, lane_end); - let lane_to = with_main_interval( - lane_from, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ); - let mut lane_move = crossing_lane_move_rect(lane_from, window.to, axis); - if same_axis_delta_within(lane_move, lane_to, axis, SWAP_AXIS_SNAP_PX) { - lane_move = lane_to; - } - push_step(&mut phase1, window.node_id, window.from, lane_from); - push_step(&mut phase2, window.node_id, lane_from, lane_move); - push_step(&mut phase3, window.node_id, lane_move, lane_to); - push_step(&mut phase4, window.node_id, lane_to, window.to); - if idx + 1 < windows.len() { - lane_start = lane_end + clearance; - } - } - build_validated_plan( - request, - PlanStrategy::SwapLanes { axis }, - [ - phase_draft( - PhaseKind::Scale, - axis.other(), - phase1, - PhaseReason::ShrinkIntoLanes { - lane_axis: axis.other(), - }, - ), - phase_draft_classified( - phase2, - PhaseReason::MoveThroughFreedSpace, - )?, - phase_draft( - PhaseKind::Scale, - axis, - phase3, - PhaseReason::SameAxisRedistribution, - ), - phase_draft( - PhaseKind::Scale, - axis.other(), - phase4, - PhaseReason::GrowOutOfLanes, - ), - ], - ) -} - -fn phase_draft_classified( - steps: Vec, - reason: PhaseReason, -) -> Result { - let actions = steps - .iter() - .map(|step| classify_step(*step).ok_or(MultiphasePlanFailure::NoPattern)) - .collect::, _>>()?; - Ok(phase_draft_mixed(steps, actions, reason)) -} - -fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { - let size = main_size(from, axis); - if main_start(target, axis) > main_start(from, axis) { - let end = main_end(target, axis); - with_main_interval(from, axis, end - size, end) - } else { - let start = main_start(target, axis); - with_main_interval(from, axis, start, start + size) - } -} - -fn same_axis_delta_within(from: Rect, to: Rect, axis: PhaseAxis, max_delta: i32) -> bool { - let start_delta = (main_start(from, axis) - main_start(to, axis)).abs(); - let end_delta = (main_end(from, axis) - main_end(to, axis)).abs(); - start_delta.max(end_delta) <= max_delta -} - -fn lane_sort_key(window: MultiphaseWindow, axis: PhaseAxis) -> (usize, i32, i32, u32) { - let delta = main_start(window.to, axis) - main_start(window.from, axis); - let direction = match delta.cmp(&0) { - std::cmp::Ordering::Greater => 0, - std::cmp::Ordering::Less => 1, - std::cmp::Ordering::Equal => 2, - }; - ( - direction, - main_start(window.from, axis), - main_start(window.to, axis), - window.node_id.0, - ) -} - -fn plan_space_then_orthogonal_growth( - request: &MultiphaseRequest, - axis: PhaseAxis, -) -> Result { - if request.windows.len() < 2 { - return Err(MultiphasePlanFailure::NoPattern); - } - let orth_axis = axis.other(); - let min_width = sane_min_size(request.bounds.width()); - let min_height = sane_min_size(request.bounds.height()); - let mut phase1 = vec![]; - let mut phase2 = vec![]; - let mut phase3 = vec![]; - for window in &request.windows { - if window.to.width() < min_width { - return Err(MultiphasePlanFailure::ShrinkBound { - axis: PhaseAxis::Horizontal, - available: window.to.width(), - required: min_width, - }); - } - if window.to.height() < min_height { - return Err(MultiphasePlanFailure::ShrinkBound { - axis: PhaseAxis::Vertical, - available: window.to.height(), - required: min_height, - }); - } - let main_changes = main_start(window.from, axis) != main_start(window.to, axis) - || main_end(window.from, axis) != main_end(window.to, axis); - let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis) - || orth_end(window.from, axis) != orth_end(window.to, axis); - let mut orth_from = window.from; - if main_changes && main_size(window.from, axis) == main_size(window.to, axis) { - let after_move = with_main_interval( - window.from, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ); - push_step(&mut phase2, window.node_id, window.from, after_move); - orth_from = after_move; - } else if main_changes { - let target_size = main_size(window.to, axis); - let after_main_scale = if main_start(window.from, axis) == main_start(window.to, axis) - || main_end(window.from, axis) == main_end(window.to, axis) - { - with_main_interval( - window.from, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ) - } else if main_start(window.to, axis) < main_start(window.from, axis) { - with_main_interval( - window.from, - axis, - main_end(window.from, axis) - target_size, - main_end(window.from, axis), - ) - } else { - with_main_interval( - window.from, - axis, - main_start(window.from, axis), - main_start(window.from, axis) + target_size, - ) - }; - push_step(&mut phase1, window.node_id, window.from, after_main_scale); - orth_from = after_main_scale; - if main_start(after_main_scale, axis) != main_start(window.to, axis) - || main_end(after_main_scale, axis) != main_end(window.to, axis) - { - let after_move = with_main_interval( - after_main_scale, - axis, - main_start(window.to, axis), - main_end(window.to, axis), - ); - push_step(&mut phase2, window.node_id, after_main_scale, after_move); - orth_from = after_move; - } - } - if orth_changes { - push_step(&mut phase3, window.node_id, orth_from, window.to); - } - } - if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() { - return Err(MultiphasePlanFailure::NoPattern); - } - build_validated_plan( - request, - PlanStrategy::SpaceThenOrthogonalGrowth { axis }, - [ - phase_draft( - PhaseKind::Scale, - axis, - phase1, - PhaseReason::CreateSpaceForAscendingChild, - ), - phase_draft( - PhaseKind::Move, - axis, - phase2, - PhaseReason::MoveAscendingChildAfterSpaceExists, - ), - phase_draft( - PhaseKind::Scale, - orth_axis, - phase3, - PhaseReason::OrthogonalGrowthAfterMove, - ), - ], - ) -} - -fn plan_orientation_change( - request: &MultiphaseRequest, - from_axis: PhaseAxis, -) -> Result { - if request.windows.len() < 2 { - return Err(MultiphasePlanFailure::NoPattern); - } - let to_axis = from_axis.other(); - let min_lane_size = sane_min_size(main_size(request.bounds, to_axis)); - let target_start = request - .windows - .first() - .map(|window| main_start(window.to, from_axis)) - .ok_or(MultiphasePlanFailure::NoPattern)?; - let target_end = request - .windows - .first() - .map(|window| main_end(window.to, from_axis)) - .ok_or(MultiphasePlanFailure::NoPattern)?; - let source_start = request - .windows - .first() - .map(|window| main_start(window.from, to_axis)) - .ok_or(MultiphasePlanFailure::NoPattern)?; - let source_end = request - .windows - .first() - .map(|window| main_end(window.from, to_axis)) - .ok_or(MultiphasePlanFailure::NoPattern)?; - if request.windows.iter().any(|window| { - main_start(window.from, to_axis) != source_start - || main_end(window.from, to_axis) != source_end - || main_start(window.to, from_axis) != target_start - || main_end(window.to, from_axis) != target_end - || main_size(window.to, to_axis) < min_lane_size - }) { - return Err(MultiphasePlanFailure::NoPattern); - } - - let mut phase1 = vec![]; - let mut phase2 = vec![]; - let mut phase3 = vec![]; - for window in &request.windows { - let lane = with_main_interval( - window.from, - to_axis, - main_start(window.to, to_axis), - main_end(window.to, to_axis), - ); - let moved = with_main_interval( - lane, - from_axis, - main_start(window.to, from_axis), - main_start(window.to, from_axis) + main_size(lane, from_axis), - ); - push_step(&mut phase1, window.node_id, window.from, lane); - push_step(&mut phase2, window.node_id, lane, moved); - push_step(&mut phase3, window.node_id, moved, window.to); - } - if phase1.is_empty() || phase3.is_empty() { - return Err(MultiphasePlanFailure::NoPattern); - } - build_validated_plan( - request, - PlanStrategy::OrientationChange { from_axis }, - [ - phase_draft( - PhaseKind::Scale, - to_axis, - phase1, - PhaseReason::ShrinkIntoLanes { lane_axis: to_axis }, - ), - phase_draft( - PhaseKind::Move, - from_axis, - phase2, - PhaseReason::MoveThroughFreedSpace, - ), - phase_draft( - PhaseKind::Scale, - from_axis, - phase3, - PhaseReason::GrowOutOfLanes, - ), - ], - ) -} - -struct MultiphasePhaseDraft { - action: MultiphasePhaseActionDraft, - steps: Vec, - reason: PhaseReason, -} - -enum MultiphasePhaseActionDraft { - Uniform(PhaseAction), - Mixed(Vec), -} - -fn phase_draft_uniform( - action: PhaseAction, - steps: Vec, - reason: PhaseReason, -) -> MultiphasePhaseDraft { - MultiphasePhaseDraft { - action: MultiphasePhaseActionDraft::Uniform(action), - steps, - reason, - } -} - -fn phase_draft( - kind: PhaseKind, - axis: PhaseAxis, - steps: Vec, - reason: PhaseReason, -) -> MultiphasePhaseDraft { - phase_draft_uniform(PhaseAction { kind, axis }, steps, reason) -} - -fn phase_draft_mixed( - steps: Vec, - actions: Vec, - reason: PhaseReason, -) -> MultiphasePhaseDraft { - MultiphasePhaseDraft { - action: MultiphasePhaseActionDraft::Mixed(actions), - steps, - reason, - } -} - -fn build_validated_plan( - request: &MultiphaseRequest, - strategy: PlanStrategy, - phases: [MultiphasePhaseDraft; N], -) -> Result { - let mut explanations = vec![]; - let phases: Vec<_> = phases - .into_iter() - .filter_map(|draft| { - if draft.steps.is_empty() { - return None; - } - let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect(); - nodes.sort_by_key(|node_id| node_id.0); - let action = match draft.action { - MultiphasePhaseActionDraft::Uniform(action) => { - MultiphasePhaseAction::Uniform(action) - } - MultiphasePhaseActionDraft::Mixed(actions) => { - debug_assert_eq!(actions.len(), draft.steps.len()); - MultiphasePhaseAction::from_step_actions(actions) - } - }; - explanations.push(PhaseExplanation { - action: action.clone(), - reason: draft.reason, - nodes, - }); - Some(MultiphasePhase { - action, - steps: draft.steps, - }) - }) - .collect(); - for phase in &phases { - for (idx, step) in phase.steps.iter().enumerate() { - let action = phase.action.action_for_step(idx).unwrap(); - if classify_step(*step) != Some(action) { - return Err(MultiphasePlanFailure::InvalidPhaseStep { - action, - node_id: step.node_id, - }); - } - } - } - let plan = MultiphasePlan { phases }; - validate_plan_continuous_diagnostic(request, &plan) - .map(|_| MultiphasePlanned { - plan, - explanation: MultiphasePlanExplanation { - strategy, - phases: explanations, - validation: ValidationExplanation::passed(), - }, - }) - .map_err(MultiphasePlanFailure::Validation) -} - -fn validate_plan_continuous(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool { - validate_plan_continuous_diagnostic(request, plan).is_ok() -} - -fn validate_plan_continuous_diagnostic( - request: &MultiphaseRequest, - plan: &MultiphasePlan, -) -> Result<(), MultiphaseValidationError> { - let mut current: Vec<_> = request - .windows - .iter() - .map(|window| (window.node_id, window.from)) - .collect(); - for (phase_idx, phase) in plan.phases.iter().enumerate() { - if let MultiphasePhaseAction::Mixed(actions) = &phase.action - && actions.len() != phase.steps.len() - { - return Err(MultiphaseValidationError::PhaseActionCount { - phase: phase_idx, - actions: actions.len(), - steps: phase.steps.len(), - }); - } - for (idx, step) in phase.steps.iter().enumerate() { - if phase.steps[..idx] - .iter() - .any(|prev| prev.node_id == step.node_id) - { - return Err(MultiphaseValidationError::DuplicatePhaseStep { - phase: phase_idx, - node_id: step.node_id, - }); - } - let Some((_, rect)) = current.iter().find(|(node_id, _)| *node_id == step.node_id) - else { - return Err(MultiphaseValidationError::UnknownPhaseStep { - phase: phase_idx, - node_id: step.node_id, - }); - }; - if *rect != step.from { - return Err(MultiphaseValidationError::StaleStepStart { - phase: phase_idx, - node_id: step.node_id, - }); - } - } - let motions: Vec<_> = current - .iter() - .map(|(node_id, rect)| { - let to = phase - .steps - .iter() - .find(|step| step.node_id == *node_id) - .map(|step| step.to) - .unwrap_or(*rect); - RectMotion { from: *rect, to } - }) - .collect(); - for (idx, motion) in motions.iter().enumerate() { - if let Some((other_idx, _)) = motions[idx + 1..] - .iter() - .enumerate() - .find(|(_, other)| motions_overlap_during_phase(*motion, **other)) - { - return Err(MultiphaseValidationError::PhaseOverlap { - phase: phase_idx, - a: current[idx].0, - b: current[idx + 1 + other_idx].0, - }); - } - } - for step in &phase.steps { - let (_, rect) = current - .iter_mut() - .find(|(node_id, _)| *node_id == step.node_id) - .unwrap(); - *rect = step.to; - } - } - for window in &request.windows { - if !current - .iter() - .find(|(node_id, _)| *node_id == window.node_id) - .is_some_and(|(_, rect)| *rect == window.to) - { - return Err(MultiphaseValidationError::FinalMismatch { - node_id: window.node_id, - }); - } - } - Ok(()) -} - -#[derive(Copy, Clone)] -struct RectMotion { - from: Rect, - to: Rect, -} - -fn motions_overlap_during_phase(a: RectMotion, b: RectMotion) -> bool { - let mut interval = TimeInterval::unit(); - interval.intersect_less_than(edge_delta(a.from.x1(), a.to.x1(), b.from.x2(), b.to.x2())) - && interval.intersect_less_than(edge_delta(b.from.x1(), b.to.x1(), a.from.x2(), a.to.x2())) - && interval.intersect_less_than(edge_delta(a.from.y1(), a.to.y1(), b.from.y2(), b.to.y2())) - && interval.intersect_less_than(edge_delta(b.from.y1(), b.to.y1(), a.from.y2(), a.to.y2())) - && interval.is_non_empty() -} - -fn edge_delta(a0: i32, a1: i32, b0: i32, b1: i32) -> LinearDelta { - let from = a0 as i64 - b0 as i64; - let to = a1 as i64 - b1 as i64; - LinearDelta { - start: from, - velocity: to - from, - } -} - -#[derive(Copy, Clone)] -struct LinearDelta { - start: i64, - velocity: i64, -} - -#[derive(Copy, Clone)] -struct TimeInterval { - lower: Rational, - lower_open: bool, - upper: Rational, - upper_open: bool, -} - -impl TimeInterval { - fn unit() -> Self { - Self { - lower: Rational::new(0, 1), - lower_open: false, - upper: Rational::new(1, 1), - upper_open: false, - } - } - - fn intersect_less_than(&mut self, delta: LinearDelta) -> bool { - if delta.velocity == 0 { - return delta.start < 0; - } - let boundary = Rational::new(-delta.start, delta.velocity); - if delta.velocity > 0 { - self.tighten_upper(boundary, true); - } else { - self.tighten_lower(boundary, true); - } - self.is_non_empty() - } - - fn tighten_lower(&mut self, value: Rational, open: bool) { - match value.cmp(&self.lower) { - std::cmp::Ordering::Greater => { - self.lower = value; - self.lower_open = open; - } - std::cmp::Ordering::Equal => { - self.lower_open |= open; - } - std::cmp::Ordering::Less => {} - } - } - - fn tighten_upper(&mut self, value: Rational, open: bool) { - match value.cmp(&self.upper) { - std::cmp::Ordering::Less => { - self.upper = value; - self.upper_open = open; - } - std::cmp::Ordering::Equal => { - self.upper_open |= open; - } - std::cmp::Ordering::Greater => {} - } - } - - fn is_non_empty(&self) -> bool { - match self.lower.cmp(&self.upper) { - std::cmp::Ordering::Less => true, - std::cmp::Ordering::Equal => !self.lower_open && !self.upper_open, - std::cmp::Ordering::Greater => false, - } - } -} - -#[derive(Copy, Clone, Eq, PartialEq)] -struct Rational { - num: i64, - den: i64, -} - -impl Rational { - fn new(mut num: i64, mut den: i64) -> Self { - if den < 0 { - num = -num; - den = -den; - } - Self { num, den } - } - - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - (self.num as i128 * other.den as i128).cmp(&(other.num as i128 * self.den as i128)) - } -} - -fn classify_step(step: MultiphaseStep) -> Option { - let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2(); - let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2(); - let same_size = step.from.size() == step.to.size(); - match (same_x, same_y, same_size) { - (false, true, true) => Some(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }), - (true, false, true) => Some(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Vertical, - }), - (false, true, false) => Some(PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }), - (true, false, false) => Some(PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }), - _ => None, - } -} - -fn single_action_reason(action: PhaseAction) -> PhaseReason { - match action.kind { - PhaseKind::Move => PhaseReason::SingleAction, - PhaseKind::Scale => PhaseReason::SameAxisRedistribution, - } -} - -fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest { - MultiphaseRequest { - bounds: request.bounds, - clearance: request.clearance, - windows: request - .windows - .iter() - .map(|window| MultiphaseWindow { - node_id: window.node_id, - from: window.to, - to: window.from, - hierarchy: window.hierarchy.reversed(), - }) - .collect(), - } -} - -fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan { - MultiphasePlan { - phases: plan - .phases - .into_iter() - .rev() - .map(|phase| MultiphasePhase { - action: phase.action, - steps: phase - .steps - .into_iter() - .map(|step| MultiphaseStep { - node_id: step.node_id, - from: step.to, - to: step.from, - }) - .collect(), - }) - .collect(), - } -} - -fn reverse_planned(planned: MultiphasePlanned) -> MultiphasePlanned { - let mut phases = planned.explanation.phases; - phases.reverse(); - MultiphasePlanned { - plan: reverse_plan(planned.plan), - explanation: MultiphasePlanExplanation { - strategy: PlanStrategy::ReversedForwardPlan { - original: Box::new(planned.explanation.strategy), - }, - phases, - validation: planned.explanation.validation, - }, - } -} - -fn overlaps(rects: impl IntoIterator) -> bool { - let rects: Vec<_> = rects.into_iter().collect(); - for (idx, rect) in rects.iter().enumerate() { - if rects[idx + 1..].iter().any(|other| rect.intersects(other)) { - return true; - } - } - false -} - -fn motion_bounds(window: MultiphaseWindow) -> Rect { - window.from.union(window.to) -} - -fn motion_bounds_with_clearance(window: MultiphaseWindow, clearance: i32) -> Rect { - let bounds = motion_bounds(window); - Rect::new_saturating( - bounds.x1().saturating_sub(clearance), - bounds.y1().saturating_sub(clearance), - bounds.x2().saturating_add(clearance), - bounds.y2().saturating_add(clearance), - ) -} - -fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool { - main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis) -} - -fn split_depth_for_axis(position: MultiphaseHierarchyPosition, axis: PhaseAxis) -> Option { - match axis { - PhaseAxis::Horizontal => position.nearest_horizontal_split_depth, - PhaseAxis::Vertical => position.nearest_vertical_split_depth, - } -} - -fn push_step(steps: &mut Vec, node_id: NodeId, from: Rect, to: Rect) { - if from != to { - steps.push(MultiphaseStep { node_id, from, to }); - } -} - -fn sane_min_size(size: i32) -> i32 { - (size / MIN_SHRINK_DENOMINATOR).max(1) -} - -fn main_start(rect: Rect, axis: PhaseAxis) -> i32 { - match axis { - PhaseAxis::Horizontal => rect.x1(), - PhaseAxis::Vertical => rect.y1(), - } -} - -fn main_end(rect: Rect, axis: PhaseAxis) -> i32 { - match axis { - PhaseAxis::Horizontal => rect.x2(), - PhaseAxis::Vertical => rect.y2(), - } -} - -fn main_size(rect: Rect, axis: PhaseAxis) -> i32 { - main_end(rect, axis) - main_start(rect, axis) -} - -fn orth_start(rect: Rect, axis: PhaseAxis) -> i32 { - main_start(rect, axis.other()) -} - -fn orth_end(rect: Rect, axis: PhaseAxis) -> i32 { - main_end(rect, axis.other()) -} - -fn with_main_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { - match axis { - PhaseAxis::Horizontal => Rect::new_saturating(start, rect.y1(), end, rect.y2()), - PhaseAxis::Vertical => Rect::new_saturating(rect.x1(), start, rect.x2(), end), - } -} - -fn with_orth_interval(rect: Rect, axis: PhaseAxis, start: i32, end: i32) -> Rect { - with_main_interval(rect, axis.other(), start, end) -} - -impl PhaseAxis { - fn other(self) -> Self { - match self { - Self::Horizontal => Self::Vertical, - Self::Vertical => Self::Horizontal, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn id(raw: u32) -> NodeId { - NodeId(raw) - } - - fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect { - Rect::new_saturating(x1, y1, x2, y2) - } - - fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow { - MultiphaseWindow::new(id(raw), from, to) - } - - #[derive(Clone)] - enum TestTree { - Leaf(u32), - Split { - id: u32, - axis: PhaseAxis, - weights: Vec, - children: Vec, - }, - } - - struct TestLeaf { - node_id: NodeId, - rect: Rect, - hierarchy: MultiphaseHierarchyPosition, - } - - fn leaf(raw: u32) -> TestTree { - TestTree::Leaf(raw) - } - - fn split(id: u32, axis: PhaseAxis, weights: &[i32], children: Vec) -> TestTree { - TestTree::Split { - id, - axis, - weights: weights.to_vec(), - children, - } - } - - fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec { - let mut leaves = vec![]; - layout_tree_inner( - tree, - bounds, - TestHierarchy { - parent: None, - depth: 0, - sibling_index: None, - split_axis: None, - nearest_horizontal_split_depth: None, - nearest_vertical_split_depth: None, - }, - &mut leaves, - ); - leaves.sort_by_key(|leaf| leaf.node_id.0); - leaves - } - - #[derive(Copy, Clone)] - struct TestHierarchy { - parent: Option, - depth: u16, - sibling_index: Option, - split_axis: Option, - nearest_horizontal_split_depth: Option, - nearest_vertical_split_depth: Option, - } - - fn layout_tree_inner( - tree: &TestTree, - bounds: Rect, - hierarchy: TestHierarchy, - leaves: &mut Vec, - ) { - match tree { - TestTree::Leaf(raw) => leaves.push(TestLeaf { - node_id: id(*raw), - rect: bounds, - hierarchy: MultiphaseHierarchyPosition { - parent: hierarchy.parent, - depth: hierarchy.depth, - sibling_index: hierarchy.sibling_index, - split_axis: hierarchy.split_axis, - nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, - nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, - ..Default::default() - }, - }), - TestTree::Split { - id: split_id, - axis, - weights, - children, - } => { - assert_eq!(weights.len(), children.len()); - let rects = split_rect_by_weights(bounds, *axis, weights); - for (idx, (child, rect)) in children.iter().zip(rects).enumerate() { - let depth = hierarchy.depth.saturating_add(1); - let mut child_hierarchy = TestHierarchy { - parent: Some(id(*split_id)), - depth, - sibling_index: Some(idx.min(u16::MAX as usize) as u16), - split_axis: Some(*axis), - nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth, - nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth, - }; - match axis { - PhaseAxis::Horizontal => { - child_hierarchy.nearest_horizontal_split_depth = Some(depth); - } - PhaseAxis::Vertical => { - child_hierarchy.nearest_vertical_split_depth = Some(depth); - } - } - layout_tree_inner(child, rect, child_hierarchy, leaves); - } - } - } - } - - fn split_rect_by_weights(bounds: Rect, axis: PhaseAxis, weights: &[i32]) -> Vec { - let total_weight: i32 = weights.iter().sum(); - assert!(total_weight > 0); - let total_size = match axis { - PhaseAxis::Horizontal => bounds.width(), - PhaseAxis::Vertical => bounds.height(), - }; - let mut pos = match axis { - PhaseAxis::Horizontal => bounds.x1(), - PhaseAxis::Vertical => bounds.y1(), - }; - let mut remaining_size = total_size; - let mut remaining_weight = total_weight; - let mut rects = vec![]; - for (idx, weight) in weights.iter().enumerate() { - let size = if idx + 1 == weights.len() { - remaining_size - } else { - total_size * *weight / total_weight - }; - let rect = match axis { - PhaseAxis::Horizontal => { - Rect::new_sized_saturating(pos, bounds.y1(), size, bounds.height()) - } - PhaseAxis::Vertical => { - Rect::new_sized_saturating(bounds.x1(), pos, bounds.width(), size) - } - }; - rects.push(rect); - pos += size; - remaining_size -= size; - remaining_weight -= *weight; - if remaining_weight == 0 { - assert_eq!(remaining_size, 0); - } - } - rects - } - - fn generated_request(old: &TestTree, new: &TestTree, bounds: Rect) -> MultiphaseRequest { - let old_leaves = layout_tree(old, bounds); - let new_leaves = layout_tree(new, bounds); - assert_eq!(old_leaves.len(), new_leaves.len()); - let mut windows = vec![]; - for old_leaf in &old_leaves { - let new_leaf = new_leaves - .iter() - .find(|leaf| leaf.node_id == old_leaf.node_id) - .unwrap(); - windows.push(MultiphaseWindow::with_hierarchy( - old_leaf.node_id, - old_leaf.rect, - new_leaf.rect, - MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy), - )); - } - MultiphaseRequest { - bounds, - windows, - clearance: 0, - } - } - - fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) { - assert_generated_case_plans_deterministically(old, new, bounds); - } - - fn assert_generated_case_plans_deterministically( - old: &TestTree, - new: &TestTree, - bounds: Rect, - ) -> MultiphasePlanned { - let req = generated_request(old, new, bounds); - assert!(!overlaps(req.windows.iter().map(|window| window.from))); - assert!(!overlaps(req.windows.iter().map(|window| window.to))); - let first = plan_no_overlap_explained(&req).unwrap(); - let second = plan_no_overlap_explained(&req).unwrap(); - assert_eq!(first, second); - assert_eq!(first.plan.phases.len(), first.explanation.phases.len()); - assert_eq!( - first.explanation.validation, - ValidationExplanation::passed() - ); - for phase in &first.explanation.phases { - assert!(phase.nodes.windows(2).all(|pair| pair[0].0 < pair[1].0)); - } - assert!(validate_plan_continuous(&req, &first.plan)); - first - } - - fn bounds_for_axis(axis: PhaseAxis) -> Rect { - match axis { - PhaseAxis::Horizontal => rect(0, 0, 400, 100), - PhaseAxis::Vertical => rect(0, 0, 100, 400), - } - } - - fn push_generated_case_bidirectional( - cases: &mut Vec<(TestTree, TestTree, Rect)>, - old: TestTree, - new: TestTree, - bounds: Rect, - ) { - cases.push((old.clone(), new.clone(), bounds)); - cases.push((new, old, bounds)); - } - - fn request(windows: Vec) -> MultiphaseRequest { - let bounds = windows - .iter() - .map(|window| window.from.union(window.to)) - .reduce(|bounds, rect| bounds.union(rect)) - .unwrap_or_else(|| rect(0, 0, 1, 1)); - MultiphaseRequest { - bounds, - windows, - clearance: 0, - } - } - - fn actions(plan: &MultiphasePlan) -> Vec { - plan.phases - .iter() - .map(|phase| phase.action.as_uniform().unwrap()) - .collect() - } - - fn step_to(plan: &MultiphasePlan, phase: usize, node_id: NodeId) -> Rect { - plan.phases[phase] - .steps - .iter() - .find(|step| step.node_id == node_id) - .unwrap() - .to - } - - fn no_pattern_attempts(direction: PlanDirection) -> Vec { - vec![ - RejectedStrategy { - direction, - strategy: PlanStrategy::SingleAction, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Horizontal, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Vertical, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::HierarchyOrderedScales, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::OrientationChange { - from_axis: PhaseAxis::Horizontal, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::OrientationChange { - from_axis: PhaseAxis::Vertical, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - RejectedStrategy { - direction, - strategy: PlanStrategy::SwapLanes { - axis: PhaseAxis::Vertical, - }, - reason: MultiphasePlanFailure::NoPattern, - }, - ] - } - - #[test] - fn horizontal_swap_shrinks_moves_then_grows_without_overlap() { - let req = request(vec![ - window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), - MultiphaseWindow { - node_id: id(2), - from: rect(100, 0, 200, 100), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - ] - ); - assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50)); - assert_eq!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100)); - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal - } - ); - assert_eq!( - planned - .explanation - .phases - .iter() - .map(|phase| phase.reason) - .collect::>(), - vec![ - PhaseReason::ShrinkIntoLanes { - lane_axis: PhaseAxis::Vertical - }, - PhaseReason::MoveThroughFreedSpace, - PhaseReason::GrowOutOfLanes, - ] - ); - assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn horizontal_swap_reverse_uses_equivalent_lanes() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(100, 0, 200, 100), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(0, 0, 100, 100), - to: rect(100, 0, 200, 100), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); - assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn horizontal_swap_lanes_follow_motion_direction_not_node_id() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(100, 0, 200, 100), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(0, 0, 100, 100), - to: rect(100, 0, 200, 100), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 100, 50)); - assert_eq!(step_to(&plan, 0, id(1)), rect(100, 50, 200, 100)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn swap_lanes_respect_requested_clearance() { - let mut req = request(vec![ - window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), - window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), - ]); - req.clearance = 10; - - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 45)); - assert_eq!(step_to(&plan, 0, id(2)), rect(100, 55, 200, 100)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn swap_lanes_tolerate_stationary_siblings_in_request() { - let req = request(vec![ - window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), - window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)), - window(3, rect(200, 0, 300, 100), rect(200, 0, 300, 100)), - ]); - - let planned = plan_no_overlap_explained(&req).unwrap(); - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - } - ); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn one_pixel_uneven_swap_lanes_fold_remainder_into_crossing_phase() { - let req = request(vec![ - window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), - window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert_eq!(step_to(plan, 1, id(1)), rect(101, 0, 201, 50)); - assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 101, 100)); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn large_uneven_swap_lanes_split_move_and_same_axis_scale() { - let req = request(vec![ - window(1, rect(0, 0, 120, 100), rect(120, 0, 200, 100)), - window(2, rect(120, 0, 200, 100), rect(0, 0, 120, 100)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert_eq!(step_to(plan, 1, id(1)), rect(80, 0, 200, 50)); - assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 80, 100)); - assert_eq!(step_to(plan, 2, id(1)), rect(120, 0, 200, 50)); - assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 120, 100)); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn uneven_swap_lanes_tolerate_stationary_sibling_in_odd_layout() { - let req = request(vec![ - window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), - window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), - window(3, rect(201, 0, 300, 100), rect(201, 0, 300, 100)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(&planned.plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn horizontal_rotation_uses_crossing_lanes() { - let req = request(vec![ - window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)), - window(2, rect(100, 0, 200, 100), rect(200, 0, 300, 100)), - window(3, rect(200, 0, 300, 100), rect(0, 0, 100, 100)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SwapLanes { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(&planned.plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn vertical_swap_lanes_follow_motion_direction_not_node_id() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 100, 100, 200), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(0, 0, 100, 100), - to: rect(0, 100, 100, 200), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!(step_to(&plan, 0, id(2)), rect(0, 0, 50, 100)); - assert_eq!(step_to(&plan, 0, id(1)), rect(50, 100, 100, 200)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn generated_sibling_swaps_plan_for_both_axes() { - let bounds = rect(0, 0, 240, 240); - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - let old = split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]); - let new = split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]); - assert_generated_case_plans(&old, &new, bounds); - } - } - - #[test] - fn generated_size_redistributions_plan_as_single_axis_scale() { - let horizontal_old = split( - 10, - PhaseAxis::Horizontal, - &[1, 1, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let horizontal_new = split( - 10, - PhaseAxis::Horizontal, - &[1, 2, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let horizontal_req = - generated_request(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); - let horizontal_plan = plan_no_overlap(&horizontal_req).unwrap(); - assert_eq!( - actions(&horizontal_plan), - vec![PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }] - ); - assert!(validate_plan_continuous(&horizontal_req, &horizontal_plan)); - - let vertical_old = split( - 10, - PhaseAxis::Vertical, - &[1, 1, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let vertical_new = split( - 10, - PhaseAxis::Vertical, - &[1, 2, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let vertical_req = generated_request(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); - let vertical_plan = plan_no_overlap(&vertical_req).unwrap(); - assert_eq!( - actions(&vertical_plan), - vec![PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }] - ); - assert!(validate_plan_continuous(&vertical_req, &vertical_plan)); - } - - #[test] - fn mixed_single_phase_accepts_distinct_axis_actions_when_proven() { - let req = request(vec![ - window(1, rect(0, 0, 100, 100), rect(0, 0, 150, 100)), - window(2, rect(200, 0, 300, 100), rect(200, 0, 300, 150)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); - assert_eq!(planned.plan.phases.len(), 1); - assert_eq!( - planned.plan.phases[0].action, - MultiphasePhaseAction::Mixed(vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ]) - ); - assert_eq!( - planned.explanation.phases[0].reason, - PhaseReason::MixedAxisActions - ); - assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn mixed_single_phase_accepts_move_and_scale_when_proven() { - let req = request(vec![ - window(1, rect(0, 0, 80, 80), rect(40, 0, 120, 80)), - window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!(planned.explanation.strategy, PlanStrategy::MixedSinglePhase); - assert_eq!( - planned.plan.phases[0].action, - MultiphasePhaseAction::Mixed(vec![ - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ]) - ); - assert_eq!( - planned.explanation.phases[0].reason, - PhaseReason::MixedAxisActions - ); - assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn single_window_one_axis_group_is_still_multiphase_plannable() { - let req = request(vec![window(1, rect(0, 0, 100, 100), rect(40, 0, 140, 100))]); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!(planned.explanation.strategy, PlanStrategy::SingleAction); - assert_eq!( - planned.plan.phases[0].action, - MultiphasePhaseAction::Uniform(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }) - ); - assert_eq!(planned.explanation.phases[0].nodes, vec![id(1)]); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn mixed_single_phase_still_rejects_diagonal_per_window_motion() { - let req = request(vec![ - window(1, rect(0, 0, 80, 80), rect(40, 40, 120, 120)), - window(2, rect(200, 0, 280, 80), rect(200, 0, 280, 120)), - ]); - - assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); - let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); - let rejection = MultiphasePlanFailure::InvalidPhaseStep { - action: PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - node_id: id(1), - }; - assert_eq!(diagnostic.forward, rejection); - assert_eq!(diagnostic.reverse, Some(rejection)); - } - - #[test] - fn generated_nested_size_redistribution_scales_parent_axis_first() { - let old = split( - 10, - PhaseAxis::Horizontal, - &[1, 1], - vec![ - leaf(1), - split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ); - let new = split( - 10, - PhaseAxis::Horizontal, - &[1, 3], - vec![ - leaf(1), - split(11, PhaseAxis::Vertical, &[3, 1], vec![leaf(2), leaf(3)]), - ], - ); - let req = generated_request(&old, &new, rect(0, 0, 400, 100)); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 100)); - assert_eq!(step_to(plan, 0, id(2)), rect(100, 0, 400, 50)); - assert_eq!(step_to(plan, 0, id(3)), rect(100, 50, 400, 100)); - assert_eq!( - planned.explanation.strategy, - PlanStrategy::HierarchyOrderedScales - ); - assert_eq!( - planned.explanation.phases[0].reason, - PhaseReason::ParentAxisBeforeChildAxis { - parent_axis: PhaseAxis::Horizontal, - parent_depth: 1, - child_axis: PhaseAxis::Vertical, - child_depth: 2, - } - ); - assert_eq!( - planned.explanation.phases[0].nodes, - vec![id(1), id(2), id(3)] - ); - assert_eq!(planned.explanation.phases[1].nodes, vec![id(2), id(3)]); - assert_eq!( - planned.explanation.validation, - ValidationExplanation::passed() - ); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn orientation_change_shrinks_moves_then_grows() { - let req = request(vec![ - window(1, rect(0, 0, 200, 400), rect(0, 0, 400, 200)), - window(2, rect(200, 0, 400, 400), rect(0, 200, 400, 400)), - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::OrientationChange { - from_axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - ] - ); - assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 200, 200)); - assert_eq!(step_to(plan, 0, id(2)), rect(200, 200, 400, 400)); - assert_eq!(step_to(plan, 1, id(2)), rect(0, 200, 200, 400)); - assert_eq!(step_to(plan, 2, id(1)), rect(0, 0, 400, 200)); - assert_eq!(step_to(plan, 2, id(2)), rect(0, 200, 400, 400)); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn two_axis_redistribution_without_hierarchy_still_falls_back() { - let req = request(vec![ - window(1, rect(0, 0, 200, 100), rect(0, 0, 100, 100)), - window(2, rect(200, 0, 400, 50), rect(100, 0, 400, 75)), - window(3, rect(200, 50, 400, 100), rect(100, 75, 400, 100)), - ]); - assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); - } - - #[test] - fn generated_stack_extractions_plan_for_both_axes_and_directions() { - let horizontal_old = split( - 10, - PhaseAxis::Horizontal, - &[1, 1], - vec![ - leaf(1), - split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ); - let horizontal_new = split( - 10, - PhaseAxis::Horizontal, - &[1, 2, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - assert_generated_case_plans(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100)); - assert_generated_case_plans(&horizontal_new, &horizontal_old, rect(0, 0, 400, 100)); - - let vertical_old = split( - 20, - PhaseAxis::Vertical, - &[1, 1], - vec![ - leaf(1), - split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ); - let vertical_new = split( - 20, - PhaseAxis::Vertical, - &[1, 2, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - assert_generated_case_plans(&vertical_old, &vertical_new, rect(0, 0, 100, 400)); - assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400)); - } - - #[test] - fn stack_extraction_with_resized_moving_child_still_moves_before_growth() { - let old = split( - 10, - PhaseAxis::Horizontal, - &[1, 1], - vec![ - leaf(1), - split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ); - let new = split( - 10, - PhaseAxis::Horizontal, - &[1, 1, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let req = generated_request(&old, &new, rect(0, 0, 300, 120)); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 120)); - assert_eq!(step_to(plan, 0, id(2)), rect(200, 0, 300, 60)); - assert_eq!(step_to(plan, 0, id(3)), rect(200, 60, 300, 120)); - assert_eq!(step_to(plan, 1, id(2)), rect(100, 0, 200, 60)); - assert_eq!(step_to(plan, 2, id(2)), rect(100, 0, 200, 120)); - assert_eq!(step_to(plan, 2, id(3)), rect(200, 0, 300, 120)); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn three_child_stack_extraction_plans_without_linear_fallback() { - let old = split( - 10, - PhaseAxis::Horizontal, - &[1, 1], - vec![ - leaf(1), - split( - 11, - PhaseAxis::Vertical, - &[1, 1, 1], - vec![leaf(2), leaf(3), leaf(4)], - ), - ], - ); - let new = split( - 10, - PhaseAxis::Horizontal, - &[1, 1, 1], - vec![ - leaf(1), - leaf(3), - split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(4)]), - ], - ); - let req = generated_request(&old, &new, rect(0, 0, 600, 300)); - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Horizontal, - } - ); - assert_eq!( - actions(&planned.plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn validated_phase_paths_accept_interrupted_reverse_route() { - let a_current = rect(50, 0, 150, 50); - let b_current = rect(50, 50, 150, 100); - let req = request(vec![ - window(1, a_current, rect(0, 0, 100, 100)), - window(2, b_current, rect(100, 0, 200, 100)), - ]); - let paths = vec![ - vec![ - (a_current, rect(0, 0, 100, 50)), - (rect(0, 0, 100, 50), rect(0, 0, 100, 100)), - ], - vec![ - (b_current, rect(100, 50, 200, 100)), - (rect(100, 50, 200, 100), rect(100, 0, 200, 100)), - ], - ]; - - let plan = validate_phase_paths(&req, &paths).unwrap(); - assert_eq!( - actions(&plan), - vec![ - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical, - }, - ] - ); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn bounded_generated_supported_split_tree_corpus_is_deterministic() { - let mut cases = vec![]; - for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { - let child_axis = axis.other(); - let bounds = bounds_for_axis(axis); - - push_generated_case_bidirectional( - &mut cases, - split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]), - split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]), - bounds, - ); - push_generated_case_bidirectional( - &mut cases, - split(10, axis, &[1, 1, 1], vec![leaf(1), leaf(2), leaf(3)]), - split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), - bounds, - ); - push_generated_case_bidirectional( - &mut cases, - split( - 10, - axis, - &[1, 1], - vec![ - leaf(1), - split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ), - split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), - bounds, - ); - push_generated_case_bidirectional( - &mut cases, - split( - 10, - axis, - &[1, 1], - vec![ - split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), - leaf(3), - ], - ), - split(10, axis, &[1, 2, 1], vec![leaf(1), leaf(2), leaf(3)]), - bounds, - ); - push_generated_case_bidirectional( - &mut cases, - split( - 10, - axis, - &[1, 1], - vec![ - leaf(1), - split(11, child_axis, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ), - split( - 10, - axis, - &[1, 3], - vec![ - leaf(1), - split(11, child_axis, &[3, 1], vec![leaf(2), leaf(3)]), - ], - ), - bounds, - ); - push_generated_case_bidirectional( - &mut cases, - split( - 10, - axis, - &[1, 1], - vec![ - split(11, child_axis, &[1, 1], vec![leaf(1), leaf(2)]), - leaf(3), - ], - ), - split( - 10, - axis, - &[3, 1], - vec![ - split(11, child_axis, &[1, 3], vec![leaf(1), leaf(2)]), - leaf(3), - ], - ), - bounds, - ); - } - - assert_eq!(cases.len(), 24); - for (old, new, bounds) in cases { - assert_generated_case_plans_deterministically(&old, &new, bounds); - } - } - - #[test] - fn stack_extraction_creates_space_before_moving_child() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 200, 100), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(200, 0, 400, 50), - to: rect(100, 0, 300, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(200, 50, 400, 100), - to: rect(300, 0, 400, 100), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!( - actions(&plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - ] - ); - assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); - assert_eq!(plan.phases[0].steps[1].to, rect(300, 50, 400, 100)); - assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50)); - assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100)); - assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn stack_extraction_reverse_replays_phases_in_reverse() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(0, 0, 200, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(100, 0, 300, 100), - to: rect(200, 0, 400, 50), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(300, 0, 400, 100), - to: rect(200, 50, 400, 100), - hierarchy: Default::default(), - }, - ]); - let planned = plan_no_overlap_explained(&req).unwrap(); - let plan = &planned.plan; - assert_eq!( - actions(plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal - }, - ] - ); - assert_eq!( - planned.explanation.strategy, - PlanStrategy::ReversedForwardPlan { - original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Horizontal - }) - } - ); - assert!(validate_plan_continuous(&req, plan)); - } - - #[test] - fn vertical_stack_extraction_creates_space_before_moving_child() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 200), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(0, 200, 50, 400), - to: rect(0, 100, 100, 300), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(50, 200, 100, 400), - to: rect(0, 300, 100, 400), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!( - actions(&plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Vertical - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal - }, - ] - ); - assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100)); - assert_eq!(plan.phases[0].steps[1].to, rect(50, 300, 100, 400)); - assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300)); - assert_eq!(plan.phases[2].steps[0].to, rect(0, 100, 100, 300)); - assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400)); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn vertical_stack_extraction_with_clearance_still_plans() { - let old = split( - 20, - PhaseAxis::Vertical, - &[1, 1], - vec![ - leaf(1), - split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]), - ], - ); - let new = split( - 20, - PhaseAxis::Vertical, - &[1, 2, 1], - vec![leaf(1), leaf(2), leaf(3)], - ); - let mut req = generated_request(&old, &new, rect(0, 0, 100, 400)); - req.clearance = 10; - let planned = plan_no_overlap_explained(&req).unwrap(); - - assert_eq!( - planned.explanation.strategy, - PlanStrategy::SpaceThenOrthogonalGrowth { - axis: PhaseAxis::Vertical, - } - ); - assert!(validate_plan_continuous(&req, &planned.plan)); - } - - #[test] - fn vertical_stack_extraction_reverse_replays_phases_in_reverse() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(0, 0, 100, 200), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(0, 100, 100, 300), - to: rect(0, 200, 50, 400), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(0, 300, 100, 400), - to: rect(50, 200, 100, 400), - hierarchy: Default::default(), - }, - ]); - let plan = plan_no_overlap(&req).unwrap(); - assert_eq!( - actions(&plan), - vec![ - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Horizontal - }, - PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Vertical - }, - PhaseAction { - kind: PhaseKind::Scale, - axis: PhaseAxis::Vertical - }, - ] - ); - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn unsupported_diagonal_motion_falls_back_to_linear() { - let req = request(vec![MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(100, 100, 200, 200), - hierarchy: Default::default(), - }]); - assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); - let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); - assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern); - assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern)); - let mut expected = no_pattern_attempts(PlanDirection::Forward); - expected.extend(no_pattern_attempts(PlanDirection::Reverse)); - assert_eq!(diagnostic.attempted, expected); - } - - #[test] - fn diagnostics_report_shrink_bound_rejections() { - let req = MultiphaseRequest { - bounds: rect(0, 0, 400, 100), - clearance: 0, - windows: vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 200, 100), - to: rect(0, 0, 10, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(200, 0, 400, 100), - to: rect(10, 0, 400, 100), - hierarchy: Default::default(), - }, - ], - }; - - assert!(matches!( - plan_no_overlap_with_diagnostics(&req).unwrap_err().forward, - MultiphasePlanFailure::ShrinkBound { - axis: PhaseAxis::Horizontal, - available: 10, - required: 50, - } - )); - } - - #[test] - fn diagnostics_report_candidate_validation_rejections() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 60, 60), - to: rect(180, 0, 240, 60), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(90, 0, 150, 60), - to: rect(90, 0, 150, 60), - hierarchy: Default::default(), - }, - ]); - let rejection = - MultiphasePlanFailure::Validation(MultiphaseValidationError::PhaseOverlap { - phase: 0, - a: id(1), - b: id(2), - }); - let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err(); - - assert_eq!(diagnostic.forward, rejection); - assert_eq!(diagnostic.reverse, Some(rejection)); - assert_eq!( - diagnostic.attempted[0], - RejectedStrategy { - direction: PlanDirection::Forward, - strategy: PlanStrategy::SingleAction, - reason: rejection, - } - ); - assert!(diagnostic.attempted.iter().any(|attempt| *attempt - == RejectedStrategy { - direction: PlanDirection::Reverse, - strategy: PlanStrategy::SingleAction, - reason: rejection, - })); - } - - #[test] - fn hierarchy_metadata_classifies_depth_and_mono_transitions() { - let source = MultiphaseHierarchyPosition { - parent: Some(id(10)), - depth: 2, - sibling_index: Some(0), - split_axis: Some(PhaseAxis::Vertical), - nearest_horizontal_split_depth: Some(1), - nearest_vertical_split_depth: Some(2), - ..Default::default() - }; - let target = MultiphaseHierarchyPosition { - parent: Some(id(11)), - depth: 1, - sibling_index: Some(2), - split_axis: Some(PhaseAxis::Horizontal), - nearest_horizontal_split_depth: Some(1), - ..Default::default() - }; - assert_eq!( - MultiphaseWindowHierarchy::new(source, target).transition, - MultiphaseHierarchyTransition::Ascending - ); - assert_eq!(source.nearest_vertical_split_depth, Some(2)); - - let entering_mono = MultiphaseWindowHierarchy::new( - source, - MultiphaseHierarchyPosition { - parent_is_mono: true, - mono_active: true, - ..target - }, - ); - assert_eq!( - entering_mono.transition, - MultiphaseHierarchyTransition::EnteringMono - ); - assert_eq!( - entering_mono.reversed().transition, - MultiphaseHierarchyTransition::ExitingMono - ); - } - - #[test] - fn continuous_validation_rejects_narrow_mid_phase_overlap() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 10, 10), - to: rect(100, 0, 110, 10), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(13, 0, 14, 10), - to: rect(13, 0, 14, 10), - hierarchy: Default::default(), - }, - ]); - let plan = MultiphasePlan { - phases: vec![MultiphasePhase { - action: MultiphasePhaseAction::Uniform(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }), - steps: vec![MultiphaseStep { - node_id: id(1), - from: rect(0, 0, 10, 10), - to: rect(100, 0, 110, 10), - }], - }], - }; - - assert_eq!( - validate_plan_continuous_diagnostic(&req, &plan), - Err(MultiphaseValidationError::PhaseOverlap { - phase: 0, - a: id(1), - b: id(2), - }) - ); - } - - #[test] - fn continuous_validation_allows_edge_touching_motion() { - let req = request(vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 10, 10), - to: rect(10, 0, 20, 10), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(20, 0, 30, 10), - to: rect(20, 0, 30, 10), - hierarchy: Default::default(), - }, - ]); - let plan = MultiphasePlan { - phases: vec![MultiphasePhase { - action: MultiphasePhaseAction::Uniform(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }), - steps: vec![MultiphaseStep { - node_id: id(1), - from: rect(0, 0, 10, 10), - to: rect(10, 0, 20, 10), - }], - }], - }; - - assert!(validate_plan_continuous(&req, &plan)); - } - - #[test] - fn continuous_validation_rejects_mixed_phase_action_count_mismatch() { - let req = request(vec![ - window(1, rect(0, 0, 40, 40), rect(40, 0, 80, 40)), - window(2, rect(100, 0, 140, 40), rect(100, 0, 140, 80)), - ]); - let plan = MultiphasePlan { - phases: vec![MultiphasePhase { - action: MultiphasePhaseAction::Mixed(vec![PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }]), - steps: vec![ - MultiphaseStep { - node_id: id(1), - from: rect(0, 0, 40, 40), - to: rect(40, 0, 80, 40), - }, - MultiphaseStep { - node_id: id(2), - from: rect(100, 0, 140, 40), - to: rect(100, 0, 140, 80), - }, - ], - }], - }; - - assert_eq!( - validate_plan_continuous_diagnostic(&req, &plan), - Err(MultiphaseValidationError::PhaseActionCount { - phase: 0, - actions: 1, - steps: 2, - }) - ); - } - - #[test] - fn continuous_validation_rejects_stale_step_start_rect() { - let req = request(vec![MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 10, 10), - to: rect(20, 0, 30, 10), - hierarchy: Default::default(), - }]); - let plan = MultiphasePlan { - phases: vec![MultiphasePhase { - action: MultiphasePhaseAction::Uniform(PhaseAction { - kind: PhaseKind::Move, - axis: PhaseAxis::Horizontal, - }), - steps: vec![MultiphaseStep { - node_id: id(1), - from: rect(5, 0, 15, 10), - to: rect(20, 0, 30, 10), - }], - }], - }; - - assert_eq!( - validate_plan_continuous_diagnostic(&req, &plan), - Err(MultiphaseValidationError::StaleStepStart { - phase: 0, - node_id: id(1), - }) - ); - } - - #[test] - fn motion_groups_split_disjoint_layout_changes() { - let windows = vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(100, 0, 200, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(100, 0, 200, 100), - to: rect(0, 0, 100, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(300, 0, 400, 100), - to: rect(400, 0, 500, 100), - hierarchy: Default::default(), - }, - ]; - assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1], vec![2]]); - } - - #[test] - fn motion_groups_are_transitive() { - let windows = vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(80, 0, 180, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(170, 0, 270, 100), - to: rect(250, 0, 350, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(3), - from: rect(90, 0, 180, 100), - to: rect(180, 0, 260, 100), - hierarchy: Default::default(), - }, - ]; - assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1, 2]]); - } - - #[test] - fn motion_groups_join_across_animation_clearance() { - let windows = vec![ - MultiphaseWindow { - node_id: id(1), - from: rect(0, 0, 100, 100), - to: rect(0, 0, 80, 100), - hierarchy: Default::default(), - }, - MultiphaseWindow { - node_id: id(2), - from: rect(120, 0, 220, 100), - to: rect(110, 0, 210, 100), - hierarchy: Default::default(), - }, - ]; - assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0], vec![1]]); - assert_eq!(partition_motion_groups(&windows, 10), vec![vec![0, 1]]); - } -} diff --git a/src/cli.rs b/src/cli.rs index e3d8d74a..0b715bbb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,7 @@ mod color; mod color_management; mod config; mod damage_tracking; +mod dpms; mod duration; mod generate; mod idle; @@ -85,6 +86,8 @@ pub enum Cmd { Screenshot(ScreenshotArgs), /// Inspect/modify the idle (screensaver) settings. Idle(IdleArgs), + /// Turn monitors on or off. + Dpms(DpmsArgs), /// Run a privileged program. RunPrivileged(RunPrivilegedArgs), /// Run a program with a connection tag. @@ -131,6 +134,19 @@ pub struct IdleArgs { pub command: Option, } +#[derive(Args, Debug)] +pub struct DpmsArgs { + /// Whether monitors should be on or off. + #[clap(value_enum)] + pub state: DpmsState, +} + +#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] +pub enum DpmsState { + On, + Off, +} + #[derive(Args, Debug)] pub struct RunPrivilegedArgs { /// The program to run @@ -250,6 +266,7 @@ pub fn main() { Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a), Cmd::Screenshot(a) => screenshot::main(cli.global, a), Cmd::Idle(a) => idle::main(cli.global, a), + Cmd::Dpms(a) => dpms::main(cli.global, a), Cmd::Unlock => unlock::main(cli.global), Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::RunTagged(a) => run_tagged::main(cli.global, a), diff --git a/src/cli/dpms.rs b/src/cli/dpms.rs new file mode 100644 index 00000000..ec8fe577 --- /dev/null +++ b/src/cli/dpms.rs @@ -0,0 +1,23 @@ +use { + crate::{ + cli::{DpmsArgs, DpmsState, GlobalArgs}, + tools::tool_client::{ToolClient, with_tool_client}, + wire::jay_compositor::SetDpms, + }, + std::rc::Rc, +}; + +pub fn main(global: GlobalArgs, args: DpmsArgs) { + with_tool_client(global.log_level, |tc| async move { + run(tc, args).await; + }); +} + +async fn run(tc: Rc, args: DpmsArgs) { + let comp = tc.jay_compositor().await; + tc.send(SetDpms { + self_id: comp, + active: (args.state == DpmsState::On) as u32, + }); + tc.round_trip().await; +} diff --git a/src/compositor.rs b/src/compositor.rs index 4dd47342..e197353d 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -279,11 +279,14 @@ fn start_compositor2( change: Default::default(), timeout: Cell::new(Duration::from_secs(10 * 60)), grace_period: Cell::new(Duration::from_secs(5)), + key_press_enables_dpms: Cell::new(false), + mouse_move_enables_dpms: Cell::new(false), timeout_changed: Default::default(), inhibitors: Default::default(), inhibitors_changed: Default::default(), inhibited_idle_notifications: Default::default(), backend_idle: Cell::new(true), + dpms_off_by_command: Cell::new(false), in_grace_period: Cell::new(false), }, run_args, @@ -360,13 +363,6 @@ fn start_compositor2( cpu_worker, ui_drag_enabled: Cell::new(true), ui_drag_threshold_squared: Cell::new(10), - animations: Default::default(), - layout_animations_requested: Default::default(), - layout_animations_active: Default::default(), - layout_animation_curve_override: Default::default(), - layout_animation_style_override: Default::default(), - layout_animation_batch: Default::default(), - suppress_animations_for_next_layout: Default::default(), toplevels: Default::default(), const_40hz_latch: Default::default(), tray_item_ids: Default::default(), @@ -403,7 +399,6 @@ fn start_compositor2( bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), virtual_outputs: Default::default(), clean_logs_older_than: Default::default(), - scratchpads: 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 68ea93f5..90d65b9b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -658,23 +658,17 @@ impl ConfigProxyHandler { } fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.move_focused(direction.into()); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.move_focused(direction.into()); + Ok(()) } fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let window = self.get_window(window)?; - if let Some(float) = window.tl_data().float.get() { - float.move_by_direction(direction.into()); - } else if let Some(c) = toplevel_parent_container(&*window) { - c.move_child(window, direction.into()); - } - Ok(()) - }) + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.move_child(window, direction.into()); + } + Ok(()) } fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> { @@ -992,31 +986,6 @@ impl ConfigProxyHandler { self.state.set_ui_drag_threshold(threshold.max(1)); } - fn handle_set_animations_enabled(&self, enabled: bool) { - self.state.set_animations_enabled(enabled); - } - - fn handle_set_animation_duration_ms(&self, duration_ms: u32) { - self.state - .set_animation_duration_ms(duration_ms.min(10_000)); - } - - fn handle_set_animation_curve(&self, curve: u32) { - self.state.set_animation_curve(curve); - } - - fn handle_set_animation_style(&self, style: u32) { - if !self.state.set_animation_style(style) { - log::warn!("Ignoring invalid animation style"); - } - } - - fn handle_set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) { - if !self.state.set_animation_cubic_bezier(x1, y1, x2, y2) { - log::warn!("Ignoring invalid animation cubic-bezier curve"); - } - } - fn handle_set_direct_scanout_enabled( &self, device: Option, @@ -1100,32 +1069,6 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_seat_send_to_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() { - self.state.send_to_scratchpad(name, toplevel); - } - Ok(()) - }) - } - - fn handle_seat_toggle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - self.state.toggle_scratchpad(&seat, name); - Ok(()) - }) - } - - fn handle_seat_cycle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - self.state.cycle_scratchpad(&seat, name); - Ok(()) - }) - } - fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { let window = self.get_window(window)?; let name = self.get_workspace(ws)?; @@ -1140,14 +1083,6 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_window_send_to_scratchpad(&self, window: Window, name: &str) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let window = self.get_window(window)?; - self.state.send_to_scratchpad(name, window); - Ok(()) - }) - } - fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; let name = dev.device.name(); @@ -1199,6 +1134,14 @@ impl ConfigProxyHandler { self.state.idle.set_timeout(&self.state, timeout); } + fn handle_set_key_press_enables_dpms(&self, enabled: bool) { + self.state.idle.key_press_enables_dpms.set(enabled); + } + + fn handle_set_mouse_move_enables_dpms(&self, enabled: bool) { + self.state.idle.mouse_move_enables_dpms.set(enabled); + } + fn handle_set_idle_grace_period(&self, period: Duration) { self.state.idle.set_grace_period(&self.state, period); } @@ -1789,11 +1732,9 @@ impl ConfigProxyHandler { } fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.set_mono(mono); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.set_mono(mono); + Ok(()) } fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> { @@ -1807,13 +1748,11 @@ impl ConfigProxyHandler { } fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { - c.set_mono(mono.then_some(window.as_ref())); - } - Ok(()) - }) + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_mono(mono.then_some(window.as_ref())); + } + Ok(()) } fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { @@ -1828,19 +1767,15 @@ impl ConfigProxyHandler { } fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.set_split(axis.into()); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.set_split(axis.into()); + Ok(()) } fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.toggle_tab(); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.toggle_tab(); + Ok(()) } fn handle_seat_make_group( @@ -1849,35 +1784,27 @@ impl ConfigProxyHandler { axis: Axis, ephemeral: bool, ) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.make_group(axis.into(), ephemeral); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.make_group(axis.into(), ephemeral); + Ok(()) } fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.change_group_opposite(); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.change_group_opposite(); + Ok(()) } fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.equalize(recursive); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.equalize(recursive); + Ok(()) } fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.move_tab(right); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.move_tab(right); + Ok(()) } fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { @@ -1892,13 +1819,11 @@ impl ConfigProxyHandler { } fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { - c.set_split(axis.into()); - } - Ok(()) - }) + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_split(axis.into()); + } + Ok(()) } fn handle_add_shortcut( @@ -2038,11 +1963,9 @@ impl ConfigProxyHandler { } fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let seat = self.get_seat(seat)?; - seat.set_floating(floating); - Ok(()) - }) + let seat = self.get_seat(seat)?; + seat.set_floating(floating); + Ok(()) } fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { @@ -2054,11 +1977,9 @@ impl ConfigProxyHandler { } fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { - self.state.with_linear_layout_animations(|| { - let window = self.get_window(window)?; - toplevel_set_floating(&self.state, window, floating); - Ok(()) - }) + let window = self.get_window(window)?; + toplevel_set_floating(&self.state, window, floating); + Ok(()) } fn handle_add_pollable(self: &Rc, fd: i32) -> Result<(), CphError> { @@ -2808,10 +2729,8 @@ impl ConfigProxyHandler { dx2: i32, dy2: i32, ) -> Result<(), CphError> { - self.state.with_layout_animations(|| { - self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2); - Ok(()) - }) + self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2); + Ok(()) } fn handle_window_exists(&self, window: Window) { @@ -3023,15 +2942,6 @@ impl ConfigProxyHandler { ClientMessage::SetSeatWorkspace { seat, workspace } => self .handle_set_seat_workspace(seat, workspace) .wrn("set_seat_workspace")?, - ClientMessage::SeatSendToScratchpad { seat, name } => self - .handle_seat_send_to_scratchpad(seat, name) - .wrn("seat_send_to_scratchpad")?, - ClientMessage::SeatToggleScratchpad { seat, name } => self - .handle_seat_toggle_scratchpad(seat, name) - .wrn("seat_toggle_scratchpad")?, - ClientMessage::SeatCycleScratchpad { seat, name } => self - .handle_seat_cycle_scratchpad(seat, name) - .wrn("seat_cycle_scratchpad")?, ClientMessage::GetConnector { ty, idx } => { self.handle_get_connector(ty, idx).wrn("get_connector")? } @@ -3227,6 +3137,12 @@ impl ConfigProxyHandler { .handle_get_input_device_devnode(device) .wrn("get_input_device_devnode")?, ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout), + ClientMessage::SetKeyPressEnablesDpms { enabled } => { + self.handle_set_key_press_enables_dpms(enabled) + } + ClientMessage::SetMouseMoveEnablesDpms { enabled } => { + self.handle_set_mouse_move_enables_dpms(enabled) + } ClientMessage::MoveToOutput { workspace, connector, @@ -3291,17 +3207,6 @@ impl ConfigProxyHandler { ClientMessage::SetUiDragThreshold { threshold } => { self.handle_set_ui_drag_threshold(threshold) } - ClientMessage::SetAnimationsEnabled { enabled } => { - self.handle_set_animations_enabled(enabled) - } - ClientMessage::SetAnimationDurationMs { duration_ms } => { - self.handle_set_animation_duration_ms(duration_ms) - } - ClientMessage::SetAnimationCurve { curve } => self.handle_set_animation_curve(curve), - ClientMessage::SetAnimationStyle { style } => self.handle_set_animation_style(style), - ClientMessage::SetAnimationCubicBezier { x1, y1, x2, y2 } => { - self.handle_set_animation_cubic_bezier(x1, y1, x2, y2) - } ClientMessage::SetXScalingMode { mode } => self .handle_set_x_scaling_mode(mode) .wrn("set_x_scaling_mode")?, @@ -3416,9 +3321,6 @@ impl ConfigProxyHandler { ClientMessage::SetWindowWorkspace { window, workspace } => self .handle_set_window_workspace(window, workspace) .wrn("set_window_workspace")?, - ClientMessage::WindowSendToScratchpad { window, name } => self - .handle_window_send_to_scratchpad(window, name) - .wrn("window_send_to_scratchpad")?, ClientMessage::SetWindowFullscreen { window, fullscreen } => self .handle_set_window_fullscreen(window, fullscreen) .wrn("set_window_fullscreen")?, @@ -3633,11 +3535,6 @@ impl ConfigProxyHandler { ClientMessage::SetAutotile { enabled } => { self.state.theme.autotile_enabled.set(enabled); } - ClientMessage::GetAutotile => { - self.respond(Response::GetAutotile { - enabled: self.state.theme.autotile_enabled.get(), - }); - } ClientMessage::SeatToggleExpand { .. } => { // Removed feature; kept for binary protocol compatibility. } diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4373125e..4ccc45db 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -1,5 +1,6 @@ use { crate::{ + backend::transaction::BackendConnectorTransactionError, client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId}, compositor::LogLevel, globals::{Global, GlobalName}, @@ -78,7 +79,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 30 + 31 } fn required_caps(&self) -> ClientCaps { @@ -542,6 +543,14 @@ impl JayCompositorRequestHandler for JayCompositor { }); Ok(()) } + + fn set_dpms(&self, req: SetDpms, _slf: &Rc) -> Result<(), Self::Error> { + self.client + .state + .set_dpms_active(req.active != 0) + .map_err(JayCompositorError::SetDpms)?; + Ok(()) + } } object_base! { @@ -559,5 +568,7 @@ pub enum JayCompositorError { ClientError(Box), #[error("Unknown log level {0}")] UnknownLogLevel(u32), + #[error("Could not set DPMS state")] + SetDpms(#[source] BackendConnectorTransactionError), } efrom!(JayCompositorError, ClientError); diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 74ff4eda..5fba889c 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -936,9 +936,6 @@ impl WlSeatGlobal { { c.move_child(tl, direction); self.maybe_schedule_warp_mouse_to_focus(); - } else if let Some(float) = data.float.get() { - float.move_by_direction(direction); - self.maybe_schedule_warp_mouse_to_focus(); } } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 4224e727..547b7e2a 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -1520,25 +1520,25 @@ impl WlSurface { let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds()); let pos = self.buffer_abs_pos.get(); let apply_damage = |pos: Rect| { - let clip_damage = |mut damage: Rect| { - damage = damage.intersect(pos); + if pending.damage_full { + let mut damage = pos; if let Some(bounds) = bounds { damage = damage.intersect(bounds); } - damage - }; - if pending.damage_full { - self.client.state.damage(clip_damage(pos)); + self.client.state.damage(damage); } else { let matrix = self.damage_matrix.get(); if let Some(buffer) = self.buffer.get() { for damage in &pending.buffer_damage { - let damage = matrix.apply( + let mut damage = matrix.apply( pos.x1(), pos.y1(), damage.intersect(buffer.buffer.buf.rect), ); - self.client.state.damage(clip_damage(damage)); + if let Some(bounds) = bounds { + damage = damage.intersect(bounds); + } + self.client.state.damage(damage); } } for damage in &pending.surface_damage { @@ -1550,7 +1550,8 @@ impl WlSurface { let y2 = (damage.y2() + scale - 1) / scale; damage = Rect::new_saturating(x1, y1, x2, y2); } - self.client.state.damage(clip_damage(damage)); + damage = damage.intersect(bounds.unwrap_or(pos)); + self.client.state.damage(damage); } } }; diff --git a/src/ifs/wl_surface/commit_timeline.rs b/src/ifs/wl_surface/commit_timeline.rs index 93372993..80ac2b4f 100644 --- a/src/ifs/wl_surface/commit_timeline.rs +++ b/src/ifs/wl_surface/commit_timeline.rs @@ -628,11 +628,6 @@ fn schedule_async_upload( { back_tex_opt = None; } - if let Some(back_tex) = &back_tex_opt - && Rc::strong_count(back_tex) > 1 - { - back_tex_opt = None; - } let damage_full = || { back.damage.clear(); back.damage.damage(slice::from_ref(&buf.rect)); diff --git a/src/ifs/wl_surface/x_surface.rs b/src/ifs/wl_surface/x_surface.rs index 4f6db63c..1c3e295c 100644 --- a/src/ifs/wl_surface/x_surface.rs +++ b/src/ifs/wl_surface/x_surface.rs @@ -1,7 +1,7 @@ use { crate::{ ifs::wl_surface::{ - PendingState, SurfaceExt, WlSurface, WlSurfaceError, + SurfaceExt, WlSurface, WlSurfaceError, x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow}, }, leaks::Tracker, @@ -30,22 +30,6 @@ impl SurfaceExt for XSurface { win.node_layer() } - fn before_apply_commit( - self: Rc, - pending: &mut PendingState, - ) -> Result<(), WlSurfaceError> { - if pending - .buffer - .as_ref() - .is_some_and(|buffer| buffer.is_none()) - && self.surface.buffer.is_some() - && let Some(xwindow) = self.xwindow.get() - { - xwindow.queue_spawn_out(); - } - Ok(()) - } - fn after_apply_commit(self: Rc) { if let Some(xwindow) = self.xwindow.get() { xwindow.map_status_changed(); @@ -61,7 +45,6 @@ impl SurfaceExt for XSurface { } self.surface.unset_ext(); if let Some(xwindow) = self.xwindow.take() { - xwindow.queue_spawn_out(); xwindow.tl_destroy(); xwindow.data.window.set(None); xwindow.data.surface_id.set(None); diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 80ea8b1b..f1c68730 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -1,6 +1,5 @@ use { crate::{ - animation::RetainedToplevel, client::Client, cursor::KnownCursor, fixed::Fixed, @@ -253,11 +252,6 @@ impl Xwindow { self.x.surface.buffer.is_some() && self.data.info.mapped.get() } - pub fn queue_spawn_out(&self) { - self.toplevel_data - .queue_spawn_out(self, self.tl_animation_snapshot()); - } - fn map_change(&self) -> Change { match (self.may_be_mapped(), self.is_mapped()) { (true, false) => Change::Map, @@ -280,7 +274,6 @@ impl Xwindow { match map_change { Change::None => return, Change::Unmap => { - self.queue_spawn_out(); self.data .info .pending_extents @@ -521,10 +514,6 @@ impl ToplevelNodeBase for Xwindow { Some(self.x.surface.clone()) } - fn tl_animation_snapshot(&self) -> Option> { - RetainedToplevel::capture_surface(&self.x.surface, (0, 0)) - } - fn tl_admits_children(&self) -> bool { false } diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index ad87c951..9b5130d7 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -226,10 +226,6 @@ pub trait XdgSurfaceExt: Debug { // nothing } - fn prepare_unmap(&self) { - // nothing - } - fn extents_changed(&self) { // nothing } @@ -668,15 +664,6 @@ impl SurfaceExt for XdgSurface { if let Some(serial) = pending.serial.take() { self.applied_serial.set(serial); } - if pending - .buffer - .as_ref() - .is_some_and(|buffer| buffer.is_none()) - && self.surface.buffer.is_some() - && let Some(ext) = self.ext.get() - { - ext.prepare_unmap(); - } Ok(()) } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 6a7f395f..768a8367 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -2,7 +2,6 @@ pub mod xdg_dialog_v1; use { crate::{ - animation::RetainedToplevel, bugs, bugs::Bugs, client::{Client, ClientError}, @@ -260,7 +259,6 @@ impl XdgToplevelRequestHandler for XdgToplevel { type Error = XdgToplevelError; fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { - self.queue_spawn_out(); self.tl_destroy(); self.xdg.unset_ext(); { @@ -400,11 +398,6 @@ impl XdgToplevelRequestHandler for XdgToplevel { } impl XdgToplevel { - fn queue_spawn_out(&self) { - self.toplevel_data - .queue_spawn_out(self, self.tl_animation_snapshot()); - } - fn map( self: &Rc, parent: Option<&XdgToplevel>, @@ -786,11 +779,6 @@ impl ToplevelNodeBase for XdgToplevel { Some(self.xdg.surface.clone()) } - fn tl_animation_snapshot(&self) -> Option> { - let geo = self.xdg.geometry(); - RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1())) - } - fn tl_restack_popups(&self) { self.xdg.restack_popups(); } @@ -830,10 +818,6 @@ impl XdgSurfaceExt for XdgToplevel { self.after_commit(None); } - fn prepare_unmap(&self) { - self.queue_spawn_out(); - } - fn extents_changed(&self) { self.toplevel_data.pos.set(self.xdg.extents.get()); self.tl_extents_changed(); diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 8cb39935..56ee5272 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -284,27 +284,6 @@ impl TestConfig { }) } - pub fn send_to_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatSendToScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - - pub fn toggle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatToggleScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - - pub fn cycle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { - self.send(ClientMessage::SeatCycleScratchpad { - seat: Seat(seat.raw() as _), - name, - }) - } - fn clear(&self) { unsafe { if let Some(srv) = self.srv.take() { @@ -352,10 +331,6 @@ impl TestConfig { pub fn set_show_titles(&self, show: bool) -> TestResult { self.send(ClientMessage::SetShowTitles { show }) } - - pub fn set_autotile(&self, enabled: bool) -> TestResult { - self.send(ClientMessage::SetAutotile { enabled }) - } } impl Drop for TestConfig { diff --git a/src/it/test_ifs/test_viewport.rs b/src/it/test_ifs/test_viewport.rs index e08266de..b25105c8 100644 --- a/src/it/test_ifs/test_viewport.rs +++ b/src/it/test_ifs/test_viewport.rs @@ -29,17 +29,6 @@ impl TestViewport { Ok(()) } - pub fn unset_source(&self) -> Result<(), TestError> { - self.tran.send(SetSource { - self_id: self.id, - x: Fixed::from_int(-1), - y: Fixed::from_int(-1), - width: Fixed::from_int(-1), - height: Fixed::from_int(-1), - })?; - Ok(()) - } - pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> { self.tran.send(SetDestination { self_id: self.id, @@ -48,15 +37,6 @@ impl TestViewport { })?; Ok(()) } - - pub fn unset_destination(&self) -> Result<(), TestError> { - self.tran.send(SetDestination { - self_id: self.id, - width: -1, - height: -1, - })?; - Ok(()) - } } impl Drop for TestViewport { diff --git a/src/it/tests.rs b/src/it/tests.rs index 35b6be97..dc28888c 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -85,8 +85,6 @@ mod t0051_pointer_warp; mod t0052_bar; mod t0053_theme; mod t0054_subsurface_already_attached; -mod t0055_autotiling; -mod t0055_scratchpad; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -160,7 +158,5 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0052_bar, t0053_theme, t0054_subsurface_already_attached, - t0055_autotiling, - t0055_scratchpad, } } diff --git a/src/it/tests/t0002_window.rs b/src/it/tests/t0002_window.rs index 28ee359f..84571c57 100644 --- a/src/it/tests/t0002_window.rs +++ b/src/it/tests/t0002_window.rs @@ -1,6 +1,7 @@ use { crate::{ it::{test_error::TestError, testrun::TestRun}, + rect::Rect, tree::Node, }, std::rc::Rc, @@ -10,19 +11,29 @@ testcase!(); /// Create and map a single surface async fn test(run: Rc) -> Result<(), TestError> { - let ds = run.create_default_setup().await?; + run.backend.install_default()?; let client = run.create_client().await?; let window = client.create_window().await?; window.map().await?; - let workspace_rect = ds.output.workspace_rect.get(); + tassert_eq!(window.tl.core.width.get(), 800); + tassert_eq!( + window.tl.core.height.get(), + 600 - 2 * run.state.theme.title_plus_underline_height() + ); - tassert_eq!(window.tl.core.width.get(), workspace_rect.width()); - tassert_eq!(window.tl.core.height.get(), workspace_rect.height()); - - tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect); + tassert_eq!( + window.tl.server.node_absolute_position(), + Rect::new_sized( + 0, + 2 * run.state.theme.title_plus_underline_height(), + window.tl.core.width.get(), + window.tl.core.height.get(), + ) + .unwrap() + ); Ok(()) } diff --git a/src/it/tests/t0003_multi_window.rs b/src/it/tests/t0003_multi_window.rs index db726f90..3fbf599c 100644 --- a/src/it/tests/t0003_multi_window.rs +++ b/src/it/tests/t0003_multi_window.rs @@ -11,7 +11,7 @@ testcase!(); /// Create and map two surfaces async fn test(run: Rc) -> Result<(), TestError> { - let ds = run.create_default_setup().await?; + run.backend.install_default()?; let client = run.create_client().await?; @@ -21,30 +21,17 @@ async fn test(run: Rc) -> Result<(), TestError> { let window2 = client.create_window().await?; window2.map().await?; - let workspace_rect = ds.output.workspace_rect.get(); + let otop = 2 * run.state.theme.title_plus_underline_height(); let bw = run.state.theme.sizes.border_width.get(); - let child_width = (workspace_rect.width() - bw) / 2; tassert_eq!( window.tl.server.node_absolute_position(), - Rect::new_sized( - workspace_rect.x1(), - workspace_rect.y1(), - child_width, - workspace_rect.height(), - ) - .unwrap() + Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap() ); tassert_eq!( window2.tl.server.node_absolute_position(), - Rect::new_sized( - workspace_rect.x1() + child_width + bw, - workspace_rect.y1(), - child_width, - workspace_rect.height(), - ) - .unwrap() + Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap() ); Ok(()) diff --git a/src/it/tests/t0007_subsurface/screenshot_1.qoi b/src/it/tests/t0007_subsurface/screenshot_1.qoi index b5954651..230c0408 100644 Binary files a/src/it/tests/t0007_subsurface/screenshot_1.qoi and b/src/it/tests/t0007_subsurface/screenshot_1.qoi differ diff --git a/src/it/tests/t0007_subsurface/screenshot_2.qoi b/src/it/tests/t0007_subsurface/screenshot_2.qoi index 718d5c29..722271f6 100644 Binary files a/src/it/tests/t0007_subsurface/screenshot_2.qoi and b/src/it/tests/t0007_subsurface/screenshot_2.qoi differ diff --git a/src/it/tests/t0014_container_scroll_focus.rs b/src/it/tests/t0014_container_scroll_focus.rs index dccd1096..0186cbaf 100644 --- a/src/it/tests/t0014_container_scroll_focus.rs +++ b/src/it/tests/t0014_container_scroll_focus.rs @@ -48,18 +48,13 @@ async fn test(run: Rc) -> TestResult { let mono_container = w_mono2.tl.container_parent()?; let container_pos = mono_container.tl_data().pos.get(); - let (tab_x, tab_y) = { - let tab_bar = mono_container.tab_bar.borrow(); - let Some(tab_bar) = tab_bar.as_ref() else { - bail!("no tab bar"); - }; - let w_mono1_title = &tab_bar.entries[0]; - ( - container_pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2, - container_pos.y1() + tab_bar.height / 2, - ) - }; - ds.mouse.abs(&ds.connector, tab_x as _, tab_y as _); + let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0] + .move_(container_pos.x1(), container_pos.y1()); + ds.mouse.abs( + &ds.connector, + w_mono1_title.x1() as _, + w_mono1_title.y1() as _, + ); client.sync().await; tassert!(enters.next().is_err()); diff --git a/src/it/tests/t0015_scroll_partial.rs b/src/it/tests/t0015_scroll_partial.rs index f5cb6e3c..c6cf49b7 100644 --- a/src/it/tests/t0015_scroll_partial.rs +++ b/src/it/tests/t0015_scroll_partial.rs @@ -26,18 +26,12 @@ async fn test(run: Rc) -> TestResult { let container = w_mono2.tl.container_parent()?; let pos = container.tl_data().pos.get(); - let (tab_x, tab_y) = { - let tab_bar = container.tab_bar.borrow(); - let Some(tab_bar) = tab_bar.as_ref() else { - bail!("no tab bar"); - }; - let w_mono1_title = &tab_bar.entries[0]; - ( - pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2, - pos.y1() + tab_bar.height / 2, - ) - }; - ds.mouse.abs(&ds.connector, tab_x as f64, tab_y as f64); + let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1()); + ds.mouse.abs( + &ds.connector, + w_mono1_title.x1() as f64, + w_mono1_title.y1() as f64, + ); client.sync().await; let enters = dss.kb.enter.expect()?; diff --git a/src/it/tests/t0020_surface_offset/screenshot_1.qoi b/src/it/tests/t0020_surface_offset/screenshot_1.qoi index 4c826f86..eef5f37a 100644 Binary files a/src/it/tests/t0020_surface_offset/screenshot_1.qoi and b/src/it/tests/t0020_surface_offset/screenshot_1.qoi differ diff --git a/src/it/tests/t0020_surface_offset/screenshot_2.qoi b/src/it/tests/t0020_surface_offset/screenshot_2.qoi index 0fb763e2..7e8cf143 100644 Binary files a/src/it/tests/t0020_surface_offset/screenshot_2.qoi and b/src/it/tests/t0020_surface_offset/screenshot_2.qoi differ diff --git a/src/it/tests/t0022_toplevel_suspended.rs b/src/it/tests/t0022_toplevel_suspended.rs index 524856e3..1fdacb1a 100644 --- a/src/it/tests/t0022_toplevel_suspended.rs +++ b/src/it/tests/t0022_toplevel_suspended.rs @@ -2,7 +2,7 @@ use { crate::{ ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED, it::{ - test_error::{TestErrorExt, TestResult}, + test_error::TestResult, test_utils::{ test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt, }, @@ -10,7 +10,7 @@ use { }, }, isnt::std_1::collections::IsntHashSetExt, - std::{rc::Rc, time::Duration}, + std::rc::Rc, }; testcase!(); @@ -19,7 +19,6 @@ async fn test(run: Rc) -> TestResult { let ds = run.create_default_setup().await?; let client = run.create_client().await?; - let default_seat = client.get_default_seat().await?; let win1 = client.create_window().await?; win1.set_color(255, 0, 0, 255); @@ -45,23 +44,5 @@ async fn test(run: Rc) -> TestResult { client.sync().await; tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED)); - let leaves = default_seat.kb.leave.expect()?; - let enters = default_seat.kb.enter.expect()?; - - run.cfg.set_idle(Duration::from_micros(100))?; - run.cfg.set_idle_grace_period(Duration::from_secs(0))?; - run.state.wheel.timeout(3).await?; - - client.sync().await; - tassert!(win2.tl.core.states.borrow().contains(&STATE_SUSPENDED)); - let leave = leaves.next().with_context(|| "no leave on suspend")?; - tassert_eq!(leave.surface, win2.surface.id); - - ds.mouse.rel(1.0, 1.0); - client.sync().await; - tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED)); - let enter = enters.next().with_context(|| "no enter on restore")?; - tassert_eq!(enter.surface, win2.surface.id); - Ok(()) } diff --git a/src/it/tests/t0023_xdg_activation/screenshot_1.qoi b/src/it/tests/t0023_xdg_activation/screenshot_1.qoi index 960da20a..1fa8d204 100644 Binary files a/src/it/tests/t0023_xdg_activation/screenshot_1.qoi and b/src/it/tests/t0023_xdg_activation/screenshot_1.qoi differ diff --git a/src/it/tests/t0026_output_transform/screenshot_1.qoi b/src/it/tests/t0026_output_transform/screenshot_1.qoi index f11111bb..2206fc85 100644 Binary files a/src/it/tests/t0026_output_transform/screenshot_1.qoi and b/src/it/tests/t0026_output_transform/screenshot_1.qoi differ diff --git a/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi b/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi index 9f5fca3c..f7bf53bf 100644 Binary files a/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi and b/src/it/tests/t0028_top_level_restacking/screenshot_1.qoi differ diff --git a/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi b/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi index aaf1b108..b454acd3 100644 Binary files a/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi and b/src/it/tests/t0028_top_level_restacking/screenshot_2.qoi differ diff --git a/src/it/tests/t0029_double_click_float/screenshot_1.qoi b/src/it/tests/t0029_double_click_float/screenshot_1.qoi index e08dc525..dd974ccf 100644 Binary files a/src/it/tests/t0029_double_click_float/screenshot_1.qoi and b/src/it/tests/t0029_double_click_float/screenshot_1.qoi differ diff --git a/src/it/tests/t0029_double_click_float/screenshot_2.qoi b/src/it/tests/t0029_double_click_float/screenshot_2.qoi index e08dc525..f49edd4d 100644 Binary files a/src/it/tests/t0029_double_click_float/screenshot_2.qoi and b/src/it/tests/t0029_double_click_float/screenshot_2.qoi differ diff --git a/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi b/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi index 36c68e4e..b9826001 100644 Binary files a/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi and b/src/it/tests/t0037_toplevel_drag/screenshot_2.qoi differ diff --git a/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi b/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi index e6f6db74..988bc767 100644 Binary files a/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi and b/src/it/tests/t0038_subsurface_parent_state/screenshot_1.qoi differ diff --git a/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi b/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi index 9abc8de3..a7509404 100644 Binary files a/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi and b/src/it/tests/t0038_subsurface_parent_state/screenshot_2.qoi differ diff --git a/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi b/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi index 80a29c84..8fe5d0b2 100644 Binary files a/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi and b/src/it/tests/t0039_alpha_modifier/screenshot_1.qoi differ diff --git a/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi b/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi index 735af290..9874e2f5 100644 Binary files a/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi and b/src/it/tests/t0039_alpha_modifier/screenshot_2.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_1.qoi b/src/it/tests/t0041_input_method/screenshot_1.qoi index cd07ecd4..d25fcf64 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_1.qoi and b/src/it/tests/t0041_input_method/screenshot_1.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_2.qoi b/src/it/tests/t0041_input_method/screenshot_2.qoi index d76ea9a0..7f93231a 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_2.qoi and b/src/it/tests/t0041_input_method/screenshot_2.qoi differ diff --git a/src/it/tests/t0041_input_method/screenshot_3.qoi b/src/it/tests/t0041_input_method/screenshot_3.qoi index cd07ecd4..d25fcf64 100644 Binary files a/src/it/tests/t0041_input_method/screenshot_3.qoi and b/src/it/tests/t0041_input_method/screenshot_3.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_1.qoi b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi index 6d57d140..6423ef6d 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_1.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_2.qoi b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi index 478b3c43..823fd750 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_2.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_3.qoi b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi index 478b3c43..823fd750 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_3.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_4.qoi b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi index 07dd87fb..714222f1 100644 Binary files a/src/it/tests/t0042_toplevel_select/screenshot_4.qoi and b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi differ diff --git a/src/it/tests/t0047_surface_damage.rs b/src/it/tests/t0047_surface_damage.rs index c2d0d6dd..d9760bc8 100644 --- a/src/it/tests/t0047_surface_damage.rs +++ b/src/it/tests/t0047_surface_damage.rs @@ -308,8 +308,9 @@ async fn test(run: Rc) -> TestResult { let output_damage = connector_data.damage.borrow(); tassert!(!output_damage.is_empty()); - // The test window maps its 1x1 buffer through a viewport to the full window size. - let expected_buffer_damage = surface_pos; + // Buffer damage is transformed by the damage matrix which includes the surface position + // The buffer damage (0,0,1,1) should be transformed to surface coordinates + let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1()); // Find the exact output damage that matches our expected buffer damage let mut found_exact_buffer_damage = false; @@ -330,12 +331,10 @@ async fn test(run: Rc) -> TestResult { // Test 7: Check output damage from existing window's viewport (which already has scaling) connector_data.damage.borrow_mut().clear(); - // The existing window was created with create_surface_ext() which automatically creates a viewport. - // Commit the viewport size change separately; that commit intentionally damages the old/new extents. - window.surface.viewport.set_destination(150, 100)?; - window.surface.commit()?; - client.sync().await; - connector_data.damage.borrow_mut().clear(); + // The existing window was created with create_surface_ext() which automatically creates a viewport + // Let's verify that the viewport's existing scaling affects buffer damage correctly + // First, let's modify the viewport scaling that already exists on the window + window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100 // Add buffer damage to test viewport scaling coordinate transformation window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer @@ -347,8 +346,8 @@ async fn test(run: Rc) -> TestResult { let output_damage = connector_data.damage.borrow(); tassert!(!output_damage.is_empty()); - // With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination. - let surface_pos = window.surface.server.buffer_abs_pos.get(); + // With viewporter scaling, the 1x1 buffer damage should scale to 150x100 + // and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136) let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap(); let expected_output_damage = expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1()); @@ -403,9 +402,8 @@ async fn test(run: Rc) -> TestResult { rotation_window.map().await?; client.sync().await; - // Disable viewporter to rely purely on buffer dimensions. - rotation_window.surface.viewport.unset_source()?; - rotation_window.surface.viewport.unset_destination()?; + // Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions + rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter // Use a rectangular buffer (4x2) so rotation has a visible geometric effect // Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer diff --git a/src/it/tests/t0055_autotiling.rs b/src/it/tests/t0055_autotiling.rs deleted file mode 100644 index 4b3611c4..00000000 --- a/src/it/tests/t0055_autotiling.rs +++ /dev/null @@ -1,58 +0,0 @@ -use { - crate::{ - it::{test_error::TestResult, testrun::TestRun}, - tree::{ContainerSplit, Node, ToplevelNodeBase}, - }, - std::rc::Rc, -}; - -testcase!(); - -async fn test(run: Rc) -> TestResult { - run.backend.install_default()?; - run.cfg.set_autotile(true)?; - - let client = run.create_client().await?; - - let win1 = client.create_window().await?; - win1.map().await?; - let root = win1.tl.container_parent()?; - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - - let win2 = client.create_window().await?; - win2.map().await?; - client.sync().await; - - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - tassert_eq!(win1.tl.container_parent()?.node_id(), root.node_id()); - tassert_eq!(win2.tl.container_parent()?.node_id(), root.node_id()); - - let win3 = client.create_window().await?; - win3.map().await?; - client.sync().await; - - let v_group = win3.tl.container_parent()?; - tassert_eq!(root.split.get(), ContainerSplit::Horizontal); - tassert_eq!(v_group.split.get(), ContainerSplit::Vertical); - tassert_eq!(win2.tl.container_parent()?.node_id(), v_group.node_id()); - - let win4 = client.create_window().await?; - win4.map().await?; - client.sync().await; - - let h_group = win4.tl.container_parent()?; - tassert_eq!(h_group.split.get(), ContainerSplit::Horizontal); - tassert_eq!(win3.tl.container_parent()?.node_id(), h_group.node_id()); - let h_parent = match h_group - .tl_data() - .parent - .get() - .and_then(|p| p.node_into_container()) - { - Some(parent) => parent, - None => bail!("autotile group does not have a container parent"), - }; - tassert_eq!(h_parent.node_id(), v_group.node_id()); - - Ok(()) -} diff --git a/src/it/tests/t0055_scratchpad.rs b/src/it/tests/t0055_scratchpad.rs deleted file mode 100644 index 5abf2440..00000000 --- a/src/it/tests/t0055_scratchpad.rs +++ /dev/null @@ -1,107 +0,0 @@ -use { - crate::{ - it::{test_error::TestResult, testrun::TestRun}, - tree::{Node, ToplevelNodeBase}, - }, - std::rc::Rc, -}; - -testcase!(); - -async fn test(run: Rc) -> TestResult { - let ds = run.create_default_setup().await?; - - let client = run.create_client().await?; - let win1 = client.create_window().await?; - win1.map2().await?; - let win2 = client.create_window().await?; - win2.map2().await?; - - run.cfg.send_to_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win1.tl.server.node_visible()); - tassert!(!win2.tl.server.node_visible()); - - run.cfg.show_workspace(ds.seat.id(), "2")?; - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); - - run.cfg.show_workspace(ds.seat.id(), "3")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(win2.tl.server.node_visible()); - tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3"); - // Scratchpad windows are always shown floating. - tassert!(win2.tl.server.tl_data().parent_is_float.get()); - - // Park win2 again, then build a multi-window scratchpad and cycle it. - run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; - client.sync().await; - tassert!(!win2.tl.server.node_visible()); - - // Build a three-window scratchpad. Each window is focused right after it is - // mapped, so sending the focused window parks them in a known order. - let cyc1 = client.create_window().await?; - cyc1.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - let cyc2 = client.create_window().await?; - cyc2.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - let cyc3 = client.create_window().await?; - cyc3.map2().await?; - run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - // Nothing shown: cycle brings up the first window (insertion order: cyc1). - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - // Scratchpad windows are always shown floating. - tassert!(cyc1.tl.server.tl_data().parent_is_float.get()); - - // Cycle advances one at a time. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(cyc3.tl.server.node_visible()); - - // On the final window, the next cycle hides everything. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(!cyc1.tl.server.node_visible()); - tassert!(!cyc2.tl.server.node_visible()); - tassert!(!cyc3.tl.server.node_visible()); - - // And it wraps back to the first window. - run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?; - client.sync().await; - tassert!(cyc1.tl.server.node_visible()); - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 161d3d99..5a566f9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,7 +48,6 @@ 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 b80e3f18..e601a0e0 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,11 +1,7 @@ use { crate::{ - animation::{ - RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface, - RetainedToplevel, - }, cmm::cmm_render_intent::RenderIntent, - gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect}, + gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -18,8 +14,8 @@ use { state::State, theme::{Color, CornerRadius}, tree::{ - ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData, - ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, + ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, + ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, }, }, std::{ops::Deref, rc::Rc, slice}, @@ -204,22 +200,14 @@ impl Renderer<'_> { self.render_workspace(&ws, x, y); } } - let now = self.state.now_nsec(); - let exit_frames = self.state.animations.exit_frames(now); - self.render_exit_frames(&exit_frames, RetainedExitLayer::Tiled, &opos); macro_rules! render_stacked { ($stack:expr) => { for stacked in $stack.iter() { if stacked.node_visible() { self.base.sync(); let pos = stacked.node_absolute_position(); - let visual = self.state.animations.visual_rect( - stacked.node_id(), - pos, - self.state.now_nsec(), - ); - if visual.intersects(&opos) { - let (x, y) = opos.translate(visual.x1(), visual.y1()); + if pos.intersects(&opos) { + let (x, y) = opos.translate(pos.x1(), pos.y1()); stacked.node_render(self, x, y, None); } } @@ -227,7 +215,6 @@ impl Renderer<'_> { }; } render_stacked!(self.state.root.stacked); - self.render_exit_frames(&exit_frames, RetainedExitLayer::Floating, &opos); // 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(); @@ -466,265 +453,6 @@ impl Renderer<'_> { .fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y); } - fn presentation_child_body( - &self, - container: &ContainerNode, - child: &Rc, - body: Rect, - ) -> Rect { - let abs = body.move_(container.abs_x1.get(), container.abs_y1.get()); - let visual = self - .state - .animations - .visual_rect(child.node_id(), abs, self.state.now_nsec()); - visual.move_(-container.abs_x1.get(), -container.abs_y1.get()) - } - - fn render_child_or_snapshot( - &mut self, - child: &Rc, - x: i32, - y: i32, - bounds: Option<&Rect>, - ) { - if let Some(retained) = self - .state - .animations - .retained_snapshot(child.node_id(), self.state.now_nsec()) - { - self.render_retained_toplevel(&retained, x, y, bounds); - } else { - child.node_render(self, x, y, bounds); - } - } - - fn render_retained_toplevel( - &mut self, - retained: &RetainedToplevel, - x: i32, - y: i32, - bounds: Option<&Rect>, - ) { - let (x, y) = self - .base - .scale_point(x + retained.offset.0, y + retained.offset.1); - self.render_retained_surface_scaled(&retained.surface, x, y, None, bounds); - } - - fn render_exit_frames( - &mut self, - frames: &[RetainedExitFrame], - layer: RetainedExitLayer, - output_rect: &Rect, - ) { - for frame in frames { - if frame.layer != layer || !frame.rect.intersects(output_rect) { - continue; - } - self.render_exit_frame(frame, output_rect); - } - } - - fn render_exit_frame(&mut self, frame: &RetainedExitFrame, output_rect: &Rect) { - let (x, y) = output_rect.translate(frame.rect.x1(), frame.rect.y1()); - let inset = frame.frame_inset; - if inset > 0 { - let color = if frame.active { - self.state.theme.colors.active_border.get() - } else { - self.state.theme.colors.border.get() - }; - self.render_rounded_frame( - Rect::new_sized_saturating(0, 0, frame.rect.width(), frame.rect.height()), - &color, - self.state.theme.corner_radius.get(), - inset, - x, - y, - ); - } - let body = Rect::new_sized_saturating( - x + inset, - y + inset, - frame.rect.width() - 2 * inset, - frame.rect.height() - 2 * inset, - ); - if body.is_empty() { - return; - } - if inset > 0 && !self.state.theme.corner_radius.get().is_zero() { - let inner_cr = self.scale_corner_radius( - self.state - .theme - .corner_radius - .get() - .expanded_by(-(inset as f32)), - ); - self.corner_radius = Some(inner_cr); - } - self.render_window_body_background(body); - let bounds = self.base.scale_rect(body); - self.stretch = if frame.source_body_size != body.size() { - Some(self.base.scale_point(body.width(), body.height())) - } else { - None - }; - self.render_retained_toplevel(&frame.retained, body.x1(), body.y1(), Some(&bounds)); - self.stretch = None; - self.corner_radius = None; - } - - fn render_window_body_background(&mut self, body: Rect) { - if body.is_empty() { - return; - } - let color = self.state.theme.colors.background.get(); - let srgb_srgb = self.state.color_manager.srgb_gamma22(); - let srgb = &srgb_srgb.linear; - let perceptual = RenderIntent::Perceptual; - self.base.sync(); - if let Some(cr) = self.corner_radius - && !cr.is_zero() - { - self.base - .fill_rounded_rect(body, &color, None, srgb, perceptual, cr, 0.0); - } else { - let bounds = self.base.scale_rect(body); - self.base - .fill_scaled_boxes(slice::from_ref(&bounds), &color, None, srgb, perceptual); - } - } - - fn render_retained_surface_scaled( - &mut self, - retained: &RetainedSurface, - x: i32, - y: i32, - pos_rel: Option<(i32, i32)>, - bounds: Option<&Rect>, - ) { - let stretch = self.stretch.take(); - let corner_radius = self.corner_radius.take(); - let mut size = retained.size; - if let Some((x_rel, y_rel)) = pos_rel { - let (x, y) = self.base.scale_point(x_rel, y_rel); - let (w, h) = self.base.scale_point(x_rel + size.0, y_rel + size.1); - size = (w - x, h - y); - } else { - size = self.base.scale_point(size.0, size.1); - } - let mut stretched_source = None; - if let Some(s) = stretch { - if let RetainedContent::Texture { source, .. } = &retained.content { - let mut source = *source; - if size.0 > 0 && size.1 > 0 { - let sx = s.0 as f32 / size.0 as f32; - let sy = s.1 as f32 / size.1 as f32; - source.x2 *= sx; - source.y2 *= sy; - } - stretched_source = Some(source); - } - size = s; - } - for child in &retained.below { - let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1); - self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds); - } - self.corner_radius = corner_radius; - self.render_retained_content(retained, stretched_source, x, y, size, bounds); - for child in &retained.above { - let (x1, y1) = self.base.scale_point(child.offset.0, child.offset.1); - self.render_retained_surface_scaled(child, x + x1, y + y1, Some(child.offset), bounds); - } - } - - fn render_retained_content( - &mut self, - retained: &RetainedSurface, - stretched_source: Option, - x: i32, - y: i32, - size: (i32, i32), - bounds: Option<&Rect>, - ) { - let corner_radius = self.corner_radius.take(); - match &retained.content { - RetainedContent::Texture { - texture, - buffer, - source, - alpha, - color_description, - render_intent, - alpha_mode, - opaque, - } => { - let source = stretched_source.unwrap_or(*source); - if let Some(cr) = corner_radius { - self.base.render_rounded_texture( - texture, - *alpha, - x, - y, - Some(source), - Some(size), - self.base.scale, - bounds, - Some(buffer.clone() as Rc), - AcquireSync::Unnecessary, - buffer.release_sync, - color_description, - *render_intent, - *alpha_mode, - cr, - ); - } else { - self.base.render_texture( - texture, - *alpha, - x, - y, - Some(source), - Some(size), - self.base.scale, - bounds, - Some(buffer.clone() as Rc), - AcquireSync::Unnecessary, - buffer.release_sync, - *opaque, - color_description, - *render_intent, - *alpha_mode, - ); - } - } - RetainedContent::Color { - color, - alpha, - color_description, - render_intent, - } => { - if let Some(rect) = Rect::new_sized(x, y, size.0, size.1) { - let rect = match bounds { - None => rect, - Some(bounds) => rect.intersect(*bounds), - }; - if !rect.is_empty() { - self.base.sync(); - self.base.fill_scaled_boxes( - &[rect], - color, - *alpha, - &color_description.linear, - *render_intent, - ); - } - } - } - } - } - pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) { self.render_container_decorations(container, x, y); @@ -737,7 +465,6 @@ impl Renderer<'_> { } } let mb = container.mono_body.get(); - let visual_mb = self.presentation_child_body(container, &child.node, mb); if self.state.theme.sizes.gap.get() != 0 { let bw = self.state.theme.sizes.border_width.get(); let border_color = self.state.theme.colors.border.get(); @@ -749,10 +476,10 @@ impl Renderer<'_> { }; if !child.node.node_is_container() { let frame = Rect::new_sized_saturating( - visual_mb.x1() - bw, - visual_mb.y1() - bw, - visual_mb.width() + 2 * bw, - visual_mb.height() + 2 * bw, + mb.x1() - bw, + mb.y1() - bw, + mb.width() + 2 * bw, + mb.height() + 2 * bw, ); self.render_rounded_frame( frame, @@ -764,17 +491,14 @@ impl Renderer<'_> { ); } } - let body = visual_mb.move_(x, y); - let content = container - .mono_content - .get() - .at_point(visual_mb.x1(), visual_mb.y1()); - self.stretch = - if content.width() != visual_mb.width() || content.height() != visual_mb.height() { - Some(self.base.scale_point(visual_mb.width(), visual_mb.height())) - } else { - None - }; + let body = mb.move_(x, y); + let body = self.base.scale_rect(body); + let content = container.mono_content.get(); + self.stretch = if content.width() != mb.width() || content.height() != mb.height() { + Some(self.base.scale_point(mb.width(), mb.height())) + } else { + None + }; if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() { let cr = self.state.theme.corner_radius.get(); if !cr.is_zero() { @@ -783,16 +507,9 @@ impl Renderer<'_> { self.corner_radius = Some(inner_cr); } } - if !child.node.node_is_container() { - self.render_window_body_background(body); - } - let body = self.base.scale_rect(body); - self.render_child_or_snapshot( - &child.node, - x + content.x1(), - y + content.y1(), - Some(&body), - ); + child + .node + .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); self.stretch = None; self.corner_radius = None; } else { @@ -807,13 +524,10 @@ impl Renderer<'_> { }; let cr = self.state.theme.corner_radius.get(); for child in container.children.iter() { - let layout_body = child.body.get(); - if layout_body.x1() >= container.width.get() - || layout_body.y1() >= container.height.get() - { + let body = child.body.get(); + if body.x1() >= container.width.get() || body.y1() >= container.height.get() { break; } - let body = self.presentation_child_body(container, &child.node, layout_body); if gap != 0 { let c = if child.border_color_is_focused.get() { &focused_border_color @@ -830,7 +544,7 @@ impl Renderer<'_> { self.render_rounded_frame(frame, c, cr, bw, x, y); } } - let content = child.content.get().at_point(body.x1(), body.y1()); + let content = child.content.get(); self.stretch = if content.width() != body.width() || content.height() != body.height() { Some(self.base.scale_point(body.width(), body.height())) @@ -842,16 +556,10 @@ impl Renderer<'_> { self.corner_radius = Some(inner_cr); } let body = body.move_(x, y); - if !child.node.node_is_container() { - self.render_window_body_background(body); - } let body = self.base.scale_rect(body); - self.render_child_or_snapshot( - &child.node, - x + content.x1(), - y + content.y1(), - Some(&body), - ); + child + .node + .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); self.stretch = None; self.corner_radius = None; } @@ -1085,10 +793,6 @@ impl Renderer<'_> { _ => return, }; let pos = floating.position.get(); - let visual = - self.state - .animations - .visual_rect(floating.node_id(), pos, self.state.now_nsec()); let theme = &self.state.theme; let bw = theme.sizes.border_width.get(); let bc = if floating.active.get() { @@ -1097,27 +801,16 @@ impl Renderer<'_> { theme.colors.border.get() }; let cr = theme.corner_radius.get(); - let outer = Rect::new_sized_saturating(0, 0, visual.width(), visual.height()); + let outer = Rect::new_sized_saturating(0, 0, pos.width(), pos.height()); self.render_rounded_frame(outer, &bc, cr, bw, x, y); - let body = Rect::new_sized_saturating( - x + bw, - y + bw, - visual.width() - 2 * bw, - visual.height() - 2 * bw, - ); + let body = + Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw); let scissor_body = self.base.scale_rect(body); - self.stretch = if pos.width() != visual.width() || pos.height() != visual.height() { - Some(self.base.scale_point(body.width(), body.height())) - } else { - None - }; if !cr.is_zero() { let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32))); self.corner_radius = Some(inner_cr); } - self.render_window_body_background(body); - self.render_child_or_snapshot(&child, body.x1(), body.y1(), Some(&scissor_body)); - self.stretch = None; + child.node_render(self, body.x1(), body.y1(), Some(&scissor_body)); self.corner_radius = None; } diff --git a/src/state.rs b/src/state.rs index 74facaf1..1d2f7a08 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,23 +2,13 @@ use { crate::{ acceptor::Acceptor, allocator::BufferObject, - animation::{ - AnimationCurve, AnimationState, AnimationStyle, AnimationTick, RetainedExitLayer, - RetainedToplevel, - expand_damage_rect, - multiphase::{ - MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest, - MultiphaseWindow, MultiphaseWindowHierarchy, - partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths, - }, - spawn_in_start_rect, - }, async_engine::{AsyncEngine, SpawnedFuture}, backend::{ Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, BackendEvent, Connector, ConnectorId, ConnectorIds, DrmDeviceId, DrmDeviceIds, HardwareCursorUpdate, InputDevice, InputDeviceGroupIds, InputDeviceId, InputDeviceIds, - MonitorInfo, transaction::BackendConnectorTransactionError, + MonitorInfo, + transaction::{BackendConnectorTransactionError, ConnectorTransaction}, }, backends::dummy::DummyBackend, cli::RunArgs, @@ -113,12 +103,11 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode, - FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode, - PlaceholderNode, TearingMode, TileState, ToplevelData, - ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode, - WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig, - generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad, - toplevel_restore_from_scratchpad, toplevel_set_workspace, + FoundNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, + TearingMode, TileState, ToplevelData, ToplevelIdentifier, ToplevelNode, + ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, WorkspaceNode, + WorkspaceNodeId, + WsMoveConfig, generic_node_visitor, move_ws_to_output, }, udmabuf::UdmabufHolder, utils::{ @@ -166,98 +155,6 @@ use { uapi::{OwnedFd, c}, }; -#[derive(Clone)] -pub(crate) struct LayoutAnimationCandidate { - node_id: NodeId, - old: Rect, - new: Rect, - curve: AnimationCurve, - style: AnimationStyle, - hierarchy: MultiphaseWindowHierarchy, -} - -fn coalesce_layout_animation_candidates( - candidates: Vec, -) -> Vec { - let mut merged: Vec = vec![]; - for candidate in candidates { - if let Some(existing) = merged - .iter_mut() - .find(|existing| existing.node_id == candidate.node_id) - { - existing.new = candidate.new; - existing.curve = candidate.curve; - existing.style = candidate.style; - existing.hierarchy = MultiphaseWindowHierarchy::new( - existing.hierarchy.source, - candidate.hierarchy.target, - ); - } else { - merged.push(candidate); - } - } - merged -} - -fn layout_animation_group_uses_plain( - candidates: &[LayoutAnimationCandidate], - group: &[usize], -) -> bool { - group - .iter() - .any(|&idx| candidates[idx].style == AnimationStyle::Plain) -} - -fn bridged_retarget_plan( - request: &MultiphaseRequest, - candidates: &[LayoutAnimationCandidate], - group: &[usize], - bridge_paths: &[Vec<(Rect, Rect)>], - bridge_phase_count: usize, - follow_phases: &[MultiphasePhase], -) -> Result { - let mut paths = vec![]; - for (group_pos, &idx) in group.iter().enumerate() { - let candidate = &candidates[idx]; - let window = request.windows[group_pos]; - let Some(bridge_path) = bridge_paths.get(group_pos) else { - return Err(MultiphasePlanFailure::NoPattern); - }; - let mut path = bridge_path.clone(); - let mut current = path - .last() - .map(|(_, to)| *to) - .unwrap_or(window.from); - while path.len() < bridge_phase_count { - path.push((current, current)); - } - if current != candidate.old { - return Err(MultiphasePlanFailure::NoPattern); - } - for phase in follow_phases { - match phase - .steps - .iter() - .find(|step| step.node_id == candidate.node_id) - { - Some(step) => { - if step.from != current { - return Err(MultiphasePlanFailure::NoPattern); - } - path.push((step.from, step.to)); - current = step.to; - } - None => path.push((current, current)), - } - } - if current != window.to { - return Err(MultiphasePlanFailure::NoPattern); - } - paths.push(path); - } - validate_phase_paths(request, &paths) -} - pub struct State { pub pid: c::pid_t, pub kb_ctx: KbvmContext, @@ -368,13 +265,6 @@ pub struct State { pub cpu_worker: Rc, pub ui_drag_enabled: Cell, pub ui_drag_threshold_squared: Cell, - pub animations: AnimationState, - pub layout_animations_requested: Cell, - pub layout_animations_active: Cell, - pub layout_animation_curve_override: Cell>, - pub layout_animation_style_override: Cell>, - pub(crate) layout_animation_batch: RefCell>>, - pub suppress_animations_for_next_layout: Cell, pub toplevels: CopyHashMap>, pub const_40hz_latch: EventSource, pub tray_item_ids: TrayItemIds, @@ -414,7 +304,6 @@ pub struct State { pub bo_drop_queue: Rc>>, pub virtual_outputs: VirtualOutputs, pub clean_logs_older_than: Cell>, - pub scratchpads: RefCell>>>, } // impl Drop for State { @@ -453,36 +342,18 @@ pub struct IdleState { pub change: AsyncEvent, pub timeout: Cell, pub grace_period: Cell, + pub key_press_enables_dpms: Cell, + pub mouse_move_enables_dpms: Cell, pub timeout_changed: Cell, pub inhibitors: CopyHashMap>, pub inhibitors_changed: Cell, pub backend_idle: Cell, + pub dpms_off_by_command: Cell, pub inhibited_idle_notifications: CopyHashMap<(ClientId, ExtIdleNotificationV1Id), Rc>, pub in_grace_period: Cell, } -pub struct ScratchpadEntry { - node: Weak, - identifier: ToplevelIdentifier, - hidden: Cell, -} - -impl ScratchpadEntry { - fn alive(&self) -> bool { - self.node().is_some() - } - - fn node(&self) -> Option> { - let node = self.node.upgrade()?; - if node.tl_data().identifier.get() == self.identifier { - Some(node) - } else { - None - } - } -} - impl IdleState { pub fn set_timeout(&self, state: &State, timeout: Duration) { self.timeout.set(timeout); @@ -945,43 +816,16 @@ impl State { pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); - let animate_new_app_map = node.tl_data().parent.is_none() - && node.tl_data().kind.is_app_window() - && !node.tl_data().visible.get(); - if animate_new_app_map { - self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone(), true)); - } else { - self.do_map_tiled(seat.as_deref(), node.clone(), true); - } + self.do_map_tiled(seat.as_deref(), node.clone()); self.focus_after_map(node, seat.as_deref()); } - pub fn map_tiled_without_autotile(self: &Rc, node: Rc) { - let seat = self.seat_queue.last(); - self.do_map_tiled(seat.as_deref(), node.clone(), false); - self.focus_after_map(node, seat.as_deref()); - } - - fn do_map_tiled( - self: &Rc, - seat: Option<&Rc>, - node: Rc, - autotile: bool, - ) { + fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { let ws = self.ensure_map_workspace(seat); - self.map_tiled_on_(node, &ws, autotile); + self.map_tiled_on(node, &ws); } pub fn map_tiled_on(self: &Rc, node: Rc, ws: &Rc) { - self.map_tiled_on_(node, ws, false); - } - - fn map_tiled_on_( - self: &Rc, - node: Rc, - ws: &Rc, - autotile: bool, - ) { if let Some(c) = ws.container.get() { let la = c.clone().tl_last_active_child(); let lap = la @@ -990,11 +834,7 @@ impl State { .get() .and_then(|n| n.node_into_container()); if let Some(lap) = lap { - if autotile { - lap.add_tiled_child_after(&*la, node); - } else { - lap.add_child_after(&*la, node); - } + lap.add_child_after(&*la, node); } else { c.append_child(node); } @@ -1011,7 +851,7 @@ impl State { mut height: i32, workspace: &Rc, abs_pos: Option<(i32, i32)>, - ) -> Rc { + ) { width += 2 * self.theme.sizes.border_width.get(); height += 2 * self.theme.sizes.border_width.get() + self.theme.title_plus_underline_height(); @@ -1042,149 +882,8 @@ impl State { } Rect::new_sized_saturating(x1, y1, width, height) }; - let float = FloatNode::new(self, workspace, position, node.clone()); + FloatNode::new(self, workspace, position, node.clone()); self.focus_after_map(node, self.seat_queue.last().as_deref()); - float - } - - pub fn send_to_scratchpad(self: &Rc, name: &str, node: Rc) { - if node.node_is_placeholder() { - return; - } - let identifier = node.tl_data().identifier.get(); - if !toplevel_hide_for_scratchpad(node.clone()) { - return; - } - let entry = Rc::new(ScratchpadEntry { - node: Rc::downgrade(&node), - identifier, - hidden: Cell::new(true), - }); - { - let mut scratchpads = self.scratchpads.borrow_mut(); - for entries in scratchpads.values_mut() { - entries.retain(|entry| entry.alive() && entry.identifier != identifier); - } - scratchpads - .entry(name.to_string()) - .or_default() - .push(entry); - } - self.tree_changed(); - } - - pub fn toggle_scratchpad(self: &Rc, seat: &Rc, name: &str) { - let entry = { - let mut scratchpads = self.scratchpads.borrow_mut(); - let Some(entries) = scratchpads.get_mut(name) else { - return; - }; - entries.retain(|entry| entry.alive()); - // Prefer the currently-shown window; otherwise act on the most recent. - entries - .iter() - .rev() - .find(|entry| !entry.hidden.get()) - .or_else(|| entries.last()) - .cloned() - }; - let Some(entry) = entry else { - return; - }; - if entry.hidden.get() { - self.show_scratchpad_entry(seat, name, &entry); - } else if entry.node().is_some_and(|node| !node.node_visible()) { - self.move_scratchpad_entry_to_current_workspace(seat, &entry); - } else { - self.hide_scratchpad_entry(&entry); - } - } - - /// Cycles through the windows of a scratchpad, one at a time: - /// nothing shown -> first window -> ... -> last window -> nothing shown. - pub fn cycle_scratchpad(self: &Rc, seat: &Rc, name: &str) { - let (current, next) = { - let mut scratchpads = self.scratchpads.borrow_mut(); - let Some(entries) = scratchpads.get_mut(name) else { - return; - }; - entries.retain(|entry| entry.alive()); - match entries.iter().position(|entry| !entry.hidden.get()) { - // Nothing shown yet: bring up the first window. - None => (None, entries.first().cloned()), - // Hide the shown window and advance; on the last window, `next` - // is `None`, so the scratchpad toggles off. - Some(i) => (entries.get(i).cloned(), entries.get(i + 1).cloned()), - } - }; - if let Some(current) = ¤t { - self.hide_scratchpad_entry(current); - } - if let Some(next) = &next { - self.show_scratchpad_entry(seat, name, next); - } - } - - fn hide_scratchpad_entry(self: &Rc, entry: &Rc) { - if entry.hidden.get() { - return; - } - let Some(node) = entry.node() else { - return; - }; - if toplevel_hide_for_scratchpad(node) { - entry.hidden.set(true); - self.tree_changed(); - } - } - - fn show_scratchpad_entry( - self: &Rc, - seat: &Rc, - name: &str, - entry: &Rc, - ) { - if !entry.hidden.get() { - return; - } - let Some(node) = entry.node() else { - return; - }; - // Only one window of a scratchpad is visible at a time. - let siblings: Vec<_> = { - let scratchpads = self.scratchpads.borrow(); - scratchpads - .get(name) - .into_iter() - .flatten() - .filter(|sibling| !Rc::ptr_eq(sibling, entry) && !sibling.hidden.get()) - .cloned() - .collect() - }; - for sibling in siblings { - self.hide_scratchpad_entry(&sibling); - } - let ws = seat.get_fallback_output().ensure_workspace(); - toplevel_restore_from_scratchpad(self, node.clone(), &ws); - entry.hidden.set(false); - node.node_do_focus(seat, Direction::Unspecified); - seat.maybe_schedule_warp_mouse_to_focus(); - self.tree_changed(); - } - - fn move_scratchpad_entry_to_current_workspace( - self: &Rc, - seat: &Rc, - entry: &Rc, - ) { - let Some(node) = entry.node() else { - return; - }; - let ws = seat.get_fallback_output().ensure_workspace(); - toplevel_set_workspace(self, node.clone(), &ws); - node.node_do_focus(seat, Direction::Unspecified); - seat.maybe_schedule_warp_mouse_to_focus(); - self.tree_changed(); } fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { @@ -1279,7 +978,14 @@ impl State { } } - pub fn input_occurred(&self) { + pub fn input_occurred(self: &Rc, key_press: bool, mouse_move: bool) { + if self.idle.dpms_off_by_command.get() { + let enable_dpms = key_press && self.idle.key_press_enables_dpms.get() + || mouse_move && self.idle.mouse_move_enables_dpms.get(); + if enable_dpms && let Err(e) = self.set_dpms_active(true) { + log::error!("Could not enable DPMS after input: {}", ErrorFmt(e)); + } + } if !self.idle.input.replace(true) { self.idle.change.trigger(); } @@ -1420,12 +1126,6 @@ impl State { self.pending_screencast_reallocs_or_reconfigures.clear(); self.pending_placeholder_render_textures.clear(); self.pending_container_tab_render_textures.clear(); - self.animations.clear(); - self.layout_animations_requested.set(false); - self.layout_animations_active.set(false); - self.layout_animation_curve_override.set(None); - self.layout_animation_style_override.set(None); - self.suppress_animations_for_next_layout.set(false); self.render_ctx_watchers.clear(); self.workspace_watchers.clear(); self.toplevel_lists.clear(); @@ -1462,7 +1162,6 @@ impl State { self.node_at_tree.borrow_mut().clear(); self.position_hint_requests.clear(); self.pending_warp_mouse_to_focus.clear(); - self.scratchpads.borrow_mut().clear(); self.head_managers.clear(); self.head_managers_async.clear(); self.const_40hz_latch.clear(); @@ -1716,6 +1415,30 @@ impl State { } } + pub fn set_connectors_active( + self: &Rc, + active: bool, + ) -> Result<(), BackendConnectorTransactionError> { + let mut tran = ConnectorTransaction::new(self); + for connector in self.connectors.lock().values() { + let mut state = connector.state.borrow().clone(); + state.active = active; + tran.add(&connector.connector, state)?; + } + tran.prepare()?.apply()?.commit(); + self.set_backend_idle(!active); + Ok(()) + } + + pub fn set_dpms_active( + self: &Rc, + active: bool, + ) -> Result<(), BackendConnectorTransactionError> { + self.set_connectors_active(active)?; + self.idle.dpms_off_by_command.set(!active); + Ok(()) + } + pub fn root_visible(&self) -> bool { !self.idle.backend_idle.get() } @@ -1773,532 +1496,6 @@ impl State { self.eng.now().msec() } - pub fn queue_tiled_animation( - self: &Rc, - node_id: NodeId, - old: Rect, - new: Rect, - ) { - let curve = self - .layout_animation_curve_override - .get() - .unwrap_or_else(|| self.animations.curve.get()); - self.queue_layout_animation( - node_id, - old, - new, - curve, - MultiphaseWindowHierarchy::default(), - ); - } - - pub fn queue_tiled_animation_with_hierarchy( - self: &Rc, - node_id: NodeId, - old: Rect, - new: Rect, - hierarchy: MultiphaseWindowHierarchy, - ) { - let curve = self - .layout_animation_curve_override - .get() - .unwrap_or_else(|| self.animations.curve.get()); - self.queue_layout_animation(node_id, old, new, curve, hierarchy); - } - - pub fn queue_linear_layout_animation( - self: &Rc, - node_id: NodeId, - old: Rect, - new: Rect, - ) { - self.queue_layout_animation( - node_id, - old, - new, - AnimationCurve::Linear, - MultiphaseWindowHierarchy::default(), - ); - } - - fn queue_layout_animation( - self: &Rc, - node_id: NodeId, - old: Rect, - new: Rect, - curve: AnimationCurve, - hierarchy: MultiphaseWindowHierarchy, - ) { - if !self.animations.enabled.get() - || !self.layout_animations_active.get() - || self.suppress_animations_for_next_layout.get() - { - return; - } - let (old_output, old_scale) = { - let (x, y) = old.center(); - let (output, _, _) = self.find_closest_output(x, y); - (output.id, output.global.persistent.scale.get()) - }; - let (new_output, new_scale) = { - let (x, y) = new.center(); - let (output, _, _) = self.find_closest_output(x, y); - (output.id, output.global.persistent.scale.get()) - }; - if old_output != new_output || old_scale != new_scale { - return; - } - let candidate = LayoutAnimationCandidate { - node_id, - old, - new, - curve, - style: self - .layout_animation_style_override - .get() - .unwrap_or_else(|| self.animations.style.get()), - hierarchy, - }; - if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() { - batch.push(candidate); - return; - } - self.start_layout_animation_candidate(candidate, self.now_nsec()); - } - - fn start_layout_animation_candidate( - self: &Rc, - candidate: LayoutAnimationCandidate, - now_nsec: u64, - ) { - let started = self.animations.set_target( - candidate.node_id, - candidate.old, - candidate.new, - None, - now_nsec, - self.animations.duration_ms.get(), - candidate.curve, - ); - if started { - self.damage(expand_damage_rect( - candidate.old.union(candidate.new), - self.theme.sizes.border_width.get().max(0), - )); - self.ensure_animation_tick(); - } - } - - pub fn begin_layout_animation_batch(&self) { - self.layout_animation_batch - .borrow_mut() - .get_or_insert_with(Vec::new); - } - - pub fn finish_layout_animation_batch(self: &Rc) { - let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else { - return; - }; - let candidates = coalesce_layout_animation_candidates(candidates); - if candidates.is_empty() { - return; - } - let now = self.now_nsec(); - let windows: Vec<_> = candidates - .iter() - .map(|candidate| { - MultiphaseWindow::with_hierarchy( - candidate.node_id, - self.animations - .visual_rect(candidate.node_id, candidate.old, now), - candidate.new, - candidate.hierarchy, - ) - }) - .collect(); - for group in partition_motion_groups(&windows, self.layout_animation_clearance()) { - if layout_animation_group_uses_plain(&candidates, &group) { - for idx in group { - self.start_layout_animation_candidate(candidates[idx].clone(), now); - } - continue; - } - if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) { - continue; - } - for idx in group { - self.start_layout_animation_candidate(candidates[idx].clone(), now); - } - } - } - - fn layout_animation_clearance(&self) -> i32 { - let border = self.theme.sizes.border_width.get().max(0); - let gap = self.theme.sizes.gap.get().max(0); - if gap == 0 { border } else { gap + 2 * border } - } - - fn start_multiphase_layout_animation( - self: &Rc, - candidates: &[LayoutAnimationCandidate], - windows: &[MultiphaseWindow], - group: &[usize], - now_nsec: u64, - ) -> bool { - let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect(); - let Some(first) = request_windows.first() else { - return false; - }; - let mut bounds = first.from.union(first.to); - for window in &request_windows[1..] { - bounds = bounds.union(window.from).union(window.to); - } - let request = MultiphaseRequest { - bounds, - windows: request_windows, - clearance: self.layout_animation_clearance(), - }; - if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) { - return true; - } - if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) { - return true; - } - let plan = match plan_no_overlap_with_diagnostics(&request) { - Ok(plan) => plan, - Err(diagnostic) => { - log::debug!( - "falling back to plain layout animation for group {:?}: {:?}", - group, - diagnostic - ); - return false; - } - }; - self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) - } - - fn start_existing_phased_retarget( - self: &Rc, - candidates: &[LayoutAnimationCandidate], - windows: &[MultiphaseWindow], - group: &[usize], - request: &MultiphaseRequest, - now_nsec: u64, - ) -> bool { - let mut paths = vec![]; - for &idx in group { - let candidate = &candidates[idx]; - let window = windows[idx]; - let Some(path) = - self.animations - .phased_route_to(candidate.node_id, window.to, now_nsec) - else { - return false; - }; - paths.push(path); - } - let plan = match validate_phase_paths(request, &paths) { - Ok(plan) => plan, - Err(error) => { - log::debug!( - "existing phased retarget rejected for group {:?}: {:?}", - group, - error - ); - return false; - } - }; - log::debug!("retargeting active phased animation for group {:?}", group); - self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) - } - - fn start_bridged_phased_retarget( - self: &Rc, - candidates: &[LayoutAnimationCandidate], - windows: &[MultiphaseWindow], - group: &[usize], - request: &MultiphaseRequest, - now_nsec: u64, - ) -> bool { - let mut bridge_paths = vec![]; - let mut bridge_phase_count = 0; - let mut has_bridge = false; - for &idx in group { - let candidate = &candidates[idx]; - let window = windows[idx]; - if window.from == candidate.old { - bridge_paths.push(vec![]); - continue; - } - let Some(path) = - self.animations - .phased_route_to(candidate.node_id, candidate.old, now_nsec) - else { - return false; - }; - if !path.is_empty() { - has_bridge = true; - bridge_phase_count = bridge_phase_count.max(path.len()); - } - bridge_paths.push(path); - } - if !has_bridge { - return false; - } - - let settled_windows: Vec<_> = group - .iter() - .map(|&idx| { - let candidate = &candidates[idx]; - MultiphaseWindow::with_hierarchy( - candidate.node_id, - candidate.old, - candidate.new, - candidate.hierarchy, - ) - }) - .collect(); - let Some(first) = settled_windows.first() else { - return false; - }; - let mut bounds = first.from.union(first.to); - for window in &settled_windows[1..] { - bounds = bounds.union(window.from).union(window.to); - } - let settled_request = MultiphaseRequest { - bounds, - windows: settled_windows, - clearance: self.layout_animation_clearance(), - }; - let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) { - Ok(plan) => plan, - Err(diagnostic) => { - log::debug!( - "bridged phased retarget follow-up rejected for group {:?}: {:?}", - group, - diagnostic - ); - return false; - } - }; - let plan = match bridged_retarget_plan( - request, - candidates, - group, - &bridge_paths, - bridge_phase_count, - &follow_plan.phases, - ) { - Ok(plan) => plan, - Err(error) => { - log::debug!( - "bridged phased retarget rejected for group {:?}: {:?}", - group, - error - ); - return false; - } - }; - log::debug!("bridging active phased animation for group {:?}", group); - self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec) - } - - fn start_multiphase_plan( - self: &Rc, - candidates: &[LayoutAnimationCandidate], - windows: &[MultiphaseWindow], - group: &[usize], - plan_phases: &[crate::animation::multiphase::MultiphasePhase], - now_nsec: u64, - ) -> bool { - if plan_phases.is_empty() { - return false; - } - let mut entries = vec![]; - for &idx in group { - let candidate = &candidates[idx]; - let window = windows[idx]; - let mut current = window.from; - let mut damage = current.union(window.to); - let mut phases = vec![]; - for phase in plan_phases { - match phase - .steps - .iter() - .find(|step| step.node_id == candidate.node_id) - { - Some(step) => { - phases.push((step.from, step.to)); - damage = damage.union(step.from).union(step.to); - current = step.to; - } - None => phases.push((current, current)), - } - } - if current != window.to { - return false; - } - entries.push((candidate.clone(), phases, damage)); - } - let mut started_any = false; - for (candidate, phases, damage) in entries { - if self.animations.set_phased_target( - candidate.node_id, - phases, - None, - now_nsec, - self.animations.duration_ms.get(), - candidate.curve, - ) { - started_any = true; - self.damage(expand_damage_rect( - damage, - self.theme.sizes.border_width.get().max(0), - )); - } - } - if started_any { - self.ensure_animation_tick(); - } - started_any - } - - pub fn queue_spawn_in_animation( - self: &Rc, - node_id: NodeId, - target: Rect, - ) { - if !self.animations.enabled.get() || target.is_empty() { - return; - } - let start = spawn_in_start_rect(target); - let now = self.now_nsec(); - let started = self.animations.set_spawn_in( - node_id, - target, - None, - now, - self.animations.duration_ms.get(), - self.animations.curve.get(), - ); - if started { - self.damage(expand_damage_rect( - start.union(target), - self.theme.sizes.border_width.get().max(0), - )); - self.ensure_animation_tick(); - } - } - - pub fn queue_spawn_out_animation( - self: &Rc, - from: Rect, - frame_inset: i32, - retained: Rc, - active: bool, - layer: RetainedExitLayer, - ) { - if !self.animations.enabled.get() || from.is_empty() { - return; - } - let now = self.now_nsec(); - let started = self.animations.set_spawn_out( - from, - frame_inset, - retained, - active, - layer, - now, - self.animations.duration_ms.get(), - self.animations.curve.get(), - ); - if started { - self.damage(expand_damage_rect( - from, - self.theme.sizes.border_width.get().max(0), - )); - self.ensure_animation_tick(); - } - } - - pub fn set_animations_enabled(&self, enabled: bool) { - if self.animations.enabled.replace(enabled) && !enabled { - self.animations.clear(); - self.damage(self.root.extents.get()); - } - } - - pub fn set_animation_duration_ms(&self, duration_ms: u32) { - self.animations.duration_ms.set(duration_ms); - } - - pub fn set_animation_curve(&self, curve: u32) { - self.animations - .curve - .set(AnimationCurve::from_config(curve)); - } - - pub fn set_animation_style(&self, style: u32) -> bool { - let Some(style) = AnimationStyle::from_config(style) else { - return false; - }; - self.animations.style.set(style); - true - } - - pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool { - let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else { - return false; - }; - self.animations.curve.set(curve); - true - } - - pub fn with_layout_animations(&self, f: impl FnOnce() -> T) -> T { - let prev_requested = self.layout_animations_requested.replace(true); - let prev_active = self.layout_animations_active.replace(true); - let res = f(); - self.layout_animations_requested.set(prev_requested); - self.layout_animations_active.set(prev_active); - res - } - - pub fn with_linear_layout_animations(&self, f: impl FnOnce() -> T) -> T { - let prev_requested = self.layout_animations_requested.replace(true); - let prev_active = self.layout_animations_active.replace(true); - let prev_curve = self - .layout_animation_curve_override - .replace(Some(AnimationCurve::Linear)); - let prev_style = self - .layout_animation_style_override - .replace(Some(AnimationStyle::Plain)); - let res = f(); - self.layout_animations_requested.set(prev_requested); - self.layout_animations_active.set(prev_active); - self.layout_animation_curve_override.set(prev_curve); - self.layout_animation_style_override.set(prev_style); - res - } - - fn ensure_animation_tick(self: &Rc) { - if self.animations.tick_is_active() { - return; - } - let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect(); - if outputs.is_empty() { - return; - } - let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak)); - for output in &outputs { - tick.attach(output); - } - self.animations.set_tick(tick); - for output in &outputs { - self.damage(output.global.pos.get()); - } - } - pub fn output_extents_changed(&self) { self.root.update_extents(); for seat in self.globals.seats.lock().values() { @@ -2827,227 +2024,6 @@ impl State { } } -#[cfg(test)] -mod tests { - use { - super::*, - crate::animation::multiphase::MultiphaseHierarchyPosition, - }; - - fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect { - Rect::new_saturating(x1, y1, x2, y2) - } - - fn hierarchy( - source: MultiphaseHierarchyPosition, - target: MultiphaseHierarchyPosition, - ) -> MultiphaseWindowHierarchy { - MultiphaseWindowHierarchy::new(source, target) - } - - fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate { - candidate_rects( - node_id, - rect(0, 0, 100, 100), - rect(100, 0, 200, 100), - style, - ) - } - - fn candidate_rects( - node_id: u32, - old: Rect, - new: Rect, - style: AnimationStyle, - ) -> LayoutAnimationCandidate { - LayoutAnimationCandidate { - node_id: NodeId(node_id), - old, - new, - curve: AnimationCurve::Linear, - style, - hierarchy: MultiphaseWindowHierarchy::default(), - } - } - - #[test] - fn plain_style_candidate_forces_group_plain() { - let candidates = vec![ - candidate(1, AnimationStyle::Multiphase), - candidate(2, AnimationStyle::Plain), - ]; - - assert!(!layout_animation_group_uses_plain(&candidates, &[0])); - assert!(layout_animation_group_uses_plain(&candidates, &[0, 1])); - } - - #[test] - fn bridged_retarget_handles_second_rotation_interrupt() { - let a_left = rect(0, 0, 100, 100); - let c_mid = rect(100, 0, 200, 100); - let c_left = a_left; - let a_mid = c_mid; - let c_current = rect(150, 50, 250, 100); - let c_mid_lane = rect(100, 50, 200, 100); - let candidates = vec![ - candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase), - candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase), - ]; - let request = MultiphaseRequest { - bounds: rect(0, 0, 250, 100), - windows: vec![ - MultiphaseWindow::new(NodeId(1), a_left, a_mid), - MultiphaseWindow::new(NodeId(3), c_current, c_left), - ], - clearance: 0, - }; - let settled_request = MultiphaseRequest { - bounds: rect(0, 0, 200, 100), - windows: vec![ - MultiphaseWindow::new(NodeId(1), a_left, a_mid), - MultiphaseWindow::new(NodeId(3), c_mid, c_left), - ], - clearance: 0, - }; - let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap(); - let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]]; - - let plan = bridged_retarget_plan( - &request, - &candidates, - &[0, 1], - &bridge_paths, - 2, - &follow_plan.phases, - ) - .unwrap(); - - assert!(plan - .phases - .iter() - .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1)))); - assert!(plan - .phases - .iter() - .any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3)))); - } - - #[test] - fn layout_animation_candidates_coalesce_duplicate_nodes() { - let source = MultiphaseHierarchyPosition { - parent: Some(NodeId(10)), - depth: 2, - sibling_index: Some(1), - ..Default::default() - }; - let intermediate = MultiphaseHierarchyPosition { - parent: Some(NodeId(11)), - depth: 1, - sibling_index: Some(0), - ..Default::default() - }; - let target = MultiphaseHierarchyPosition { - parent: Some(NodeId(12)), - depth: 0, - sibling_index: Some(2), - ..Default::default() - }; - let second_source = MultiphaseHierarchyPosition { - parent: Some(NodeId(20)), - depth: 1, - sibling_index: Some(0), - ..Default::default() - }; - let second_target = MultiphaseHierarchyPosition { - parent: Some(NodeId(20)), - depth: 1, - sibling_index: Some(1), - ..Default::default() - }; - - let candidates = vec![ - LayoutAnimationCandidate { - node_id: NodeId(1), - old: rect(0, 0, 100, 100), - new: rect(0, 0, 80, 100), - curve: AnimationCurve::Linear, - style: AnimationStyle::Multiphase, - hierarchy: hierarchy(source, intermediate), - }, - LayoutAnimationCandidate { - node_id: NodeId(2), - old: rect(100, 0, 200, 100), - new: rect(120, 0, 220, 100), - curve: AnimationCurve::Linear, - style: AnimationStyle::Multiphase, - hierarchy: hierarchy(second_source, second_target), - }, - LayoutAnimationCandidate { - node_id: NodeId(1), - old: rect(0, 0, 80, 100), - new: rect(0, 0, 60, 100), - curve: AnimationCurve::from_config(4), - style: AnimationStyle::Plain, - hierarchy: hierarchy(intermediate, target), - }, - ]; - - let merged = coalesce_layout_animation_candidates(candidates); - - assert_eq!(merged.len(), 2); - assert_eq!(merged[0].node_id, NodeId(1)); - assert_eq!(merged[0].old, rect(0, 0, 100, 100)); - assert_eq!(merged[0].new, rect(0, 0, 60, 100)); - assert_eq!(merged[0].curve, AnimationCurve::from_config(4)); - assert_eq!(merged[0].style, AnimationStyle::Plain); - assert_eq!(merged[0].hierarchy, hierarchy(source, target)); - assert_eq!(merged[1].node_id, NodeId(2)); - assert_eq!(merged[1].old, rect(100, 0, 200, 100)); - assert_eq!(merged[1].new, rect(120, 0, 220, 100)); - assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target)); - } - - #[test] - fn layout_animation_candidates_keep_coalesced_layout_noops() { - let hierarchy = MultiphaseWindowHierarchy::default(); - let candidates = vec![ - LayoutAnimationCandidate { - node_id: NodeId(1), - old: rect(0, 0, 100, 100), - new: rect(0, 0, 80, 100), - curve: AnimationCurve::Linear, - style: AnimationStyle::Multiphase, - hierarchy, - }, - LayoutAnimationCandidate { - node_id: NodeId(1), - old: rect(0, 0, 80, 100), - new: rect(0, 0, 100, 100), - curve: AnimationCurve::Linear, - style: AnimationStyle::Plain, - hierarchy, - }, - LayoutAnimationCandidate { - node_id: NodeId(2), - old: rect(100, 0, 200, 100), - new: rect(120, 0, 220, 100), - curve: AnimationCurve::Linear, - style: AnimationStyle::Multiphase, - hierarchy, - }, - ]; - - let merged = coalesce_layout_animation_candidates(candidates); - - assert_eq!(merged.len(), 2); - assert_eq!(merged[0].node_id, NodeId(1)); - assert_eq!(merged[0].old, rect(0, 0, 100, 100)); - assert_eq!(merged[0].new, rect(0, 0, 100, 100)); - assert_eq!(merged[0].style, AnimationStyle::Plain); - assert_eq!(merged[1].node_id, NodeId(2)); - } -} - #[derive(Debug, Error)] pub enum ShmScreencopyError { #[error("There is no render context")] diff --git a/src/tasks/idle.rs b/src/tasks/idle.rs index 5561a263..228f2a33 100644 --- a/src/tasks/idle.rs +++ b/src/tasks/idle.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::transaction::{BackendConnectorTransactionError, ConnectorTransaction}, + backend::transaction::BackendConnectorTransactionError, state::State, utils::{ errorfmt::ErrorFmt, @@ -136,15 +136,7 @@ impl Idle { } fn try_set_idle(&self, idle: bool) -> Result<(), BackendConnectorTransactionError> { - let mut tran = ConnectorTransaction::new(&self.state); - for connector in self.state.connectors.lock().values() { - let mut state = connector.state.borrow().clone(); - state.active = !idle; - tran.add(&connector.connector, state)?; - } - tran.prepare()?.apply()?.commit(); - self.state.set_backend_idle(idle); - Ok(()) + self.state.set_connectors_active(!idle) } } diff --git a/src/tasks/input_device.rs b/src/tasks/input_device.rs index 61550def..55afc5f9 100644 --- a/src/tasks/input_device.rs +++ b/src/tasks/input_device.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::{InputDevice, InputDeviceCapability}, + backend::{InputDevice, InputDeviceCapability, InputEvent, KeyState}, ifs::wl_seat::PX_PER_SCROLL, state::{DeviceHandlerData, InputDeviceData, State}, tasks::udev_utils::{UdevProps, udev_props}, @@ -80,13 +80,21 @@ impl DeviceHandler { } if let Some(seat) = self.data.seat.get() { let mut any_events = false; + let mut key_press = false; + let mut mouse_move = false; while let Some(event) = self.dev.event() { + let (is_key_press, is_mouse_move) = dpms_wake_triggers_for(&event); + key_press |= is_key_press; + mouse_move |= is_mouse_move; + if is_key_press || is_mouse_move { + self.state.input_occurred(is_key_press, is_mouse_move); + } seat.event(&self.data, event); any_events = true; } if any_events { seat.mark_last_active(); - self.state.input_occurred(); + self.state.input_occurred(key_press, mouse_move); } } else { while self.dev.event().is_some() { @@ -105,3 +113,16 @@ impl DeviceHandler { self.data.set_seat(&self.state, None); } } + +fn dpms_wake_triggers_for(event: &InputEvent) -> (bool, bool) { + match event { + InputEvent::Key { + state: KeyState::Pressed, + .. + } => (true, false), + InputEvent::ConnectorPosition { .. } + | InputEvent::Motion { .. } + | InputEvent::MotionAbsolute { .. } => (false, true), + _ => (false, false), + } +} diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 3cb77655..b62ce0b7 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -330,7 +330,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(30), + version: s.jay_compositor.1.min(31), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/tree/container.rs b/src/tree/container.rs index 44a6a778..61ec00d1 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -32,7 +32,6 @@ use { numcell::NumCell, on_drop_event::OnDropEvent, rc_eq::rc_eq, - scroller::Scroller, threshold_counter::ThresholdCounter, }, }, @@ -132,8 +131,6 @@ pub struct ContainerNode { pub content_height: Cell, pub sum_factors: Cell, pub layout_scheduled: Cell, - animate_next_layout: Cell, - pub mono_transition_animation_pending: Cell, compute_render_positions_scheduled: Cell, num_children: NumCell, pub children: LinkedList, @@ -151,7 +148,6 @@ pub struct ContainerNode { pub child_removed: Rc, pub all_children_resized: Rc, pub tab_bar: RefCell>, - scroll: Scroller, pub update_tab_textures_scheduled: Cell, pub ephemeral: Cell, } @@ -242,8 +238,6 @@ impl ContainerNode { content_height: Cell::new(0), sum_factors: Cell::new(1.0), layout_scheduled: Cell::new(false), - animate_next_layout: Cell::new(false), - mono_transition_animation_pending: Cell::new(false), compute_render_positions_scheduled: Cell::new(false), num_children: NumCell::new(1), children, @@ -268,7 +262,6 @@ impl ContainerNode { child_removed: state.lazy_event_sources.create_source(), all_children_resized: state.post_layout_event_sources.create_source(), tab_bar: RefCell::new(None), - scroll: Default::default(), update_tab_textures_scheduled: Cell::new(false), ephemeral: Cell::new(Ephemeral::Off), }); @@ -293,47 +286,6 @@ impl ContainerNode { self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new)); } - pub fn add_tiled_child_after(self: &Rc, prev: &dyn Node, new: Rc) { - if !self.state.theme.autotile_enabled.get() - || self.mono_child.is_some() - || self.num_children.get() <= 1 - { - self.add_child_after(prev, new); - return; - } - let focused = self - .child_nodes - .borrow() - .get(&prev.node_id()) - .map(|n| n.to_ref()); - let Some(focused) = focused else { - log::error!( - "Tried to autotile a child into a container but the preceding node is not in the container" - ); - return; - }; - let focused_node = focused.node.clone(); - let focused_active = focused_node.tl_data().active(); - let sub = ContainerNode::new( - &self.state, - &self.workspace.get(), - focused_node.clone(), - self.split.get().other(), - ); - // Autotile-created groups are structural and collapse once only one - // child remains. Explicit make-group commands control their own - // grouping through the regular manual paths. - sub.ephemeral.set(Ephemeral::On); - sub.append_child(new); - let sub_id = sub.node_id(); - self.clone().cnode_replace_child(&*focused_node, sub); - if focused_active - && let Some(group) = self.child_nodes.borrow().get(&sub_id).map(|n| n.to_ref()) - { - self.update_child_active(&group, true, 1); - } - } - pub fn add_child_before(self: &Rc, prev: &dyn Node, new: Rc) { self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new)); } @@ -484,10 +436,6 @@ impl ContainerNode { } fn schedule_layout(self: &Rc) { - if self.state.layout_animations_requested.get() || self.state.layout_animations_active.get() - { - self.animate_next_layout.set(true); - } if !self.layout_scheduled.replace(true) { self.state.pending_container_layout.push(self.clone()); } @@ -519,7 +467,6 @@ impl ContainerNode { fn perform_layout(self: &Rc) { self.layout_scheduled.set(false); if self.num_children.get() == 0 { - self.mono_transition_animation_pending.set(false); return; } if let Some(child) = self.mono_child.get() { @@ -537,7 +484,6 @@ impl ContainerNode { self.damage(); } } - self.mono_transition_animation_pending.set(false); } fn perform_mono_layout(self: &Rc, child: &ContainerChild) { @@ -710,7 +656,6 @@ impl ContainerNode { op.child.factor.set(child_factor); self.sum_factors.set(sum_factors); // log::info!("pointer_move"); - self.state.suppress_animations_for_next_layout.set(true); self.schedule_layout_immediate(); } } @@ -796,18 +741,6 @@ impl ContainerNode { self.activate_child2(child, false); } - fn activate_child_from_input( - self: &Rc, - child: &NodeRef, - seat: &Rc, - ) { - self.activate_child(child); - child - .node - .clone() - .node_do_focus(seat, Direction::Unspecified); - } - fn activate_child2(self: &Rc, child: &NodeRef, preserve_focus: bool) { if let Some(mc) = self.mono_child.get() { if mc.node.node_id() == child.node.node_id() { @@ -883,7 +816,6 @@ impl ContainerNode { } } self.mono_child.set(child.clone()); - self.mono_transition_animation_pending.set(true); if child.is_some() { self.rebuild_tab_bar(); } else { @@ -1425,6 +1357,42 @@ impl ContainerNode { } pub fn insert_child(self: &Rc, node: Rc, direction: Direction) { + // Autotile: if the container would become too narrow/tall, wrap the + // focused child and new node in a perpendicular sub-container. + if self.state.theme.autotile_enabled.get() && self.mono_child.is_none() { + let (pw, ph) = self.predict_child_body_size(); + let opposite = match self.split.get() { + ContainerSplit::Horizontal if pw > 0 && ph > 0 && pw < ph => { + Some(ContainerSplit::Vertical) + } + ContainerSplit::Vertical if pw > 0 && ph > 0 && ph < pw => { + Some(ContainerSplit::Horizontal) + } + _ => None, + }; + if let Some(opp_split) = opposite { + if let Some(focused) = self.focus_history.last() { + if self.num_children.get() <= 1 { + // Single child, autotile not applicable. + } else { + let focused_node = focused.node.clone(); + let was_ephemeral = self.ephemeral.replace(Ephemeral::Off); + self.clone().cnode_remove_child2(&*focused_node, true); + self.ephemeral.set(was_ephemeral); + let sub = ContainerNode::new( + &self.state, + &self.workspace.get(), + focused_node, + opp_split, + ); + sub.ephemeral.set(Ephemeral::On); + sub.append_child(node); + self.append_child(sub); + return; + } + } + } + } let (split, right) = direction_to_split(direction); if split != self.split.get() || right { self.append_child(node); @@ -1534,7 +1502,7 @@ impl ContainerNode { fn button( self: Rc, id: CursorType, - seat: &Rc, + _seat: &Rc, _time_usec: u64, pressed: bool, button: u32, @@ -1564,7 +1532,7 @@ impl ContainerNode { if let Some(child) = children.get(&child_id) { let child_ref = child.to_ref(); drop(children); - self.activate_child_from_input(&child_ref, seat); + self.activate_child(&child_ref); } return; } @@ -1791,42 +1759,10 @@ enum SeatOpKind { pub async fn container_layout(state: Rc) { loop { - let first = state.pending_container_layout.pop().await; - let mut containers = vec![first]; - while let Some(container) = state.pending_container_layout.try_pop() { - containers.push(container); + let container = state.pending_container_layout.pop().await; + if container.layout_scheduled.get() { + container.perform_layout(); } - let mut animated = vec![]; - let mut immediate = vec![]; - for container in containers { - if !container.layout_scheduled.get() { - continue; - } - let animate = container.animate_next_layout.replace(false) - && !state.suppress_animations_for_next_layout.get(); - if animate { - animated.push(container); - } else { - immediate.push(container); - } - } - if !animated.is_empty() { - let prev_active = state.layout_animations_active.replace(true); - state.begin_layout_animation_batch(); - for container in animated { - container.perform_layout(); - } - state.finish_layout_animation_batch(); - state.layout_animations_active.set(prev_active); - } - if !immediate.is_empty() { - let prev_active = state.layout_animations_active.replace(false); - for container in immediate { - container.perform_layout(); - } - state.layout_animations_active.set(prev_active); - } - state.suppress_animations_for_next_layout.set(false); } } @@ -2081,33 +2017,31 @@ impl Node for ContainerNode { self.button(id, seat, time_usec, state == ButtonState::Pressed, button); } - fn node_on_axis_event(self: Rc, seat: &Rc, event: &PendingScroll) { + fn node_on_axis_event(self: Rc, _seat: &Rc, event: &PendingScroll) { if self.mono_child.is_none() { return; } - let steps = match self.scroll.handle(event) { - Some(steps) => steps, + // Use vertical scroll (index 1) to switch tabs. + let v = match event.v120[1].get() { + Some(v) if v != 0 => v, _ => return, }; - let mut target = match self.mono_child.get() { + let mono = match self.mono_child.get() { Some(m) => m, None => return, }; - let current_id = target.node.node_id(); - for _ in 0..steps.abs() { - let next = if steps > 0 { - target.next().or_else(|| self.children.first()) - } else { - target.prev().or_else(|| self.children.last()) - }; - match next { - Some(next) => target = next, - None => break, + let next = if v > 0 { + // Scroll down → next tab. + mono.next().or_else(|| self.children.first()) + } else { + // Scroll up → previous tab. + mono.prev().or_else(|| self.children.last()) + }; + if let Some(next) = next { + if next.node.node_id() != mono.node.node_id() { + self.activate_child(&next); } } - if target.node.node_id() != current_id { - self.activate_child_from_input(&target, seat); - } } fn node_on_leave(&self, seat: &WlSeatGlobal) { @@ -2325,11 +2259,6 @@ impl ContainingNode for ContainerNode { } // log::info!("cnode_remove_child2"); self.rebuild_tab_bar(); - if self.state.animations.enabled.get() - && !self.state.suppress_animations_for_next_layout.get() - { - self.animate_next_layout.set(true); - } self.schedule_layout(); self.cancel_seat_ops(); self.child_removed.trigger(); diff --git a/src/tree/display.rs b/src/tree/display.rs index 26b31a88..440916bf 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -8,25 +8,18 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, - NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, + OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, WorkspaceNodeId, walker::NodeVisitor, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, - std::{ - cell::{Cell, RefCell}, - mem, - ops::Deref, - rc::{Rc, Weak}, - }, + std::{cell::Cell, ops::Deref, rc::Rc}, }; pub struct DisplayNode { pub id: NodeId, pub extents: Cell, - visible: Cell, - suspend_restore_kb_foci: RefCell, Weak)>>, pub outputs: CopyHashMap>, pub stacked: Rc>>, pub stacked_above_layers: Rc>>, @@ -38,8 +31,6 @@ impl DisplayNode { let slf = Self { id, extents: Default::default(), - visible: Default::default(), - suspend_restore_kb_foci: Default::default(), outputs: Default::default(), stacked: Default::default(), stacked_above_layers: Default::default(), @@ -80,17 +71,6 @@ impl DisplayNode { pub fn update_visible(&self, state: &State) { let visible = state.root_visible(); - let was_visible = self.visible.replace(visible); - if !visible && was_visible { - let mut foci = self.suspend_restore_kb_foci.borrow_mut(); - foci.clear(); - for seat in state.globals.seats.lock().values() { - let node = seat.get_keyboard_node(); - if node.node_id() != self.id { - foci.push((seat.clone(), Rc::downgrade(&node))); - } - } - } for output in self.outputs.lock().values() { output.update_visible(); } @@ -102,20 +82,6 @@ impl DisplayNode { for seat in state.globals.seats.lock().values() { seat.set_visible(visible); } - if visible && !was_visible { - for (seat, node) in mem::take(&mut *self.suspend_restore_kb_foci.borrow_mut()) { - if seat.get_keyboard_node().node_id() == self.id { - if let Some(node) = node.upgrade() - && node.node_visible() - { - seat.focus_node(node); - } else { - seat.get_fallback_output() - .take_keyboard_navigation_focus(&seat, Direction::Unspecified); - } - } - } - } if visible { state.damage(self.extents.get()); } diff --git a/src/tree/float.rs b/src/tree/float.rs index a57c2b91..dc0b44f4 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -31,9 +31,6 @@ use { }; tree_id!(FloatNodeId); - -const COMMAND_MOVE_DELTA: i32 = 100; - pub struct FloatNode { pub id: FloatNodeId, pub state: Rc, @@ -156,13 +153,6 @@ impl FloatNode { _ => return, }; let pos = self.position.get(); - let spawn_in_pending = { - let data = child.tl_data(); - data.spawn_in_pending.get() && data.kind.is_app_window() && !data.is_fullscreen.get() - }; - if spawn_in_pending && self.visible.get() { - self.state.queue_spawn_in_animation(self.id.into(), pos); - } let theme = &self.state.theme; let bw = theme.sizes.border_width.get(); let cpos = Rect::new_sized_saturating( @@ -373,50 +363,6 @@ impl FloatNode { y2 += y1 - pos.y1(); } let new_pos = Rect::new_saturating(x1, y1, x2, y2); - self.set_position(new_pos); - } - - pub fn move_by_direction(self: &Rc, direction: Direction) { - let (dx, dy) = match direction { - Direction::Left => (-COMMAND_MOVE_DELTA, 0), - Direction::Down => (0, COMMAND_MOVE_DELTA), - Direction::Up => (0, -COMMAND_MOVE_DELTA), - Direction::Right => (COMMAND_MOVE_DELTA, 0), - Direction::Unspecified => return, - }; - self.set_position(self.position.get().move_(dx, dy)); - } - - fn body_for_outer(&self, outer: Rect) -> Rect { - let bw = self.state.theme.sizes.border_width.get(); - Rect::new_sized_saturating( - outer.x1() + bw, - outer.y1() + bw, - outer.width() - 2 * bw, - outer.height() - 2 * bw, - ) - } - - fn queue_position_animation(&self, old_pos: Rect, new_pos: Rect) { - self.state - .clone() - .queue_tiled_animation(self.id.into(), old_pos, new_pos); - let Some(child) = self.child.get() else { - return; - }; - self.state.clone().queue_tiled_animation( - child.node_id(), - self.body_for_outer(old_pos), - self.body_for_outer(new_pos), - ); - } - - fn set_position(self: &Rc, new_pos: Rect) { - let pos = self.position.get(); - if new_pos == pos { - return; - } - self.queue_position_animation(pos, new_pos); self.position.set(new_pos); if self.visible.get() { self.state.damage(pos); @@ -845,7 +791,13 @@ impl ContainingNode for FloatNode { let bw = theme.sizes.border_width.get(); let (x, y) = (x - bw, y - bw); let pos = self.position.get(); - self.set_position(pos.at_point(x, y)); + if pos.position() != (x, y) { + let new_pos = pos.at_point(x, y); + self.position.set(new_pos); + self.state.damage(pos); + self.state.damage(new_pos); + self.schedule_layout(); + } } fn cnode_resize_child( @@ -876,7 +828,14 @@ impl ContainingNode for FloatNode { y2 = (v + bw).max(y1 + bw + bw); } let new_pos = Rect::new_saturating(x1, y1, x2, y2); - self.set_position(new_pos); + if new_pos != pos { + self.position.set(new_pos); + if self.visible.get() { + self.state.damage(pos); + self.state.damage(new_pos); + } + self.schedule_layout(); + } } fn cnode_pinned(&self) -> bool { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index c0a2f013..02bba848 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1,12 +1,5 @@ use { crate::{ - animation::{ - RetainedExitLayer, RetainedToplevel, - multiphase::{ - MultiphaseHierarchyPosition, MultiphaseHierarchyTransition, - MultiphaseWindowHierarchy, PhaseAxis, - }, - }, client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, @@ -124,7 +117,6 @@ impl ToplevelNode for T { let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); if parent_was_none { data.mapped_during_iteration.set(data.state.eng.iteration()); - data.spawn_in_pending.set(data.kind.is_app_window()); data.property_changed(TL_CHANGED_NEW); } let was_floating = data.parent_is_float.get(); @@ -192,57 +184,6 @@ impl ToplevelNode for T { fn tl_change_extents(self: Rc, rect: &Rect) { let data = self.tl_data(); let prev = data.desired_extents.replace(*rect); - let target_hierarchy = self.tl_multiphase_hierarchy_position(); - let hierarchy = MultiphaseWindowHierarchy::new( - data.layout_animation_position.replace(target_hierarchy), - target_hierarchy, - ); - let spawn_in_pending = data.spawn_in_pending.get(); - let spawn_in_eligible = spawn_in_pending - && !rect.is_empty() - && data.visible.get() - && !data.is_fullscreen.get() - && data.kind.is_app_window() - && !self.node_is_container(); - let parent_container = data - .parent - .get() - .and_then(|parent| parent.node_into_container()); - let parent_is_mono = parent_container - .as_ref() - .is_some_and(|container| container.mono_child.is_some()); - let parent_mono_transition = parent_container - .as_ref() - .is_some_and(|container| container.mono_transition_animation_pending.get()); - let active_mono_boundary = matches!( - hierarchy.transition, - MultiphaseHierarchyTransition::EnteringMono - | MultiphaseHierarchyTransition::ExitingMono - ) && parent_mono_transition - && (hierarchy.source.mono_active || hierarchy.target.mono_active); - if prev != *rect - && !prev.is_empty() - && !rect.is_empty() - && data.visible.get() - && !data.parent_is_float.get() - && !self.node_is_container() - && (!parent_is_mono || active_mono_boundary) - { - data.state.clone().queue_tiled_animation_with_hierarchy( - data.node_id, - prev, - *rect, - hierarchy, - ); - } - if spawn_in_eligible { - data.state - .clone() - .queue_spawn_in_animation(data.node_id, *rect); - } - if spawn_in_eligible { - data.spawn_in_pending.set(false); - } if prev.size() != rect.size() { for sc in data.jay_screencasts.lock().values() { sc.schedule_realloc_or_reconfigure(); @@ -334,35 +275,6 @@ pub trait ToplevelNodeBase: Node { true } - fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition { - let data = self.tl_data(); - let Some(parent) = data.parent.get() else { - return Default::default(); - }; - let mut position = MultiphaseHierarchyPosition { - parent: Some(parent.node_id()), - ..Default::default() - }; - populate_multiphase_ancestor_splits(&mut position, Some(parent.clone())); - if let Some(container) = parent.node_into_container() { - position.split_axis = Some(match container.split.get() { - ContainerSplit::Horizontal => PhaseAxis::Horizontal, - ContainerSplit::Vertical => PhaseAxis::Vertical, - }); - if let Some(mono) = container.mono_child.get() { - position.parent_is_mono = true; - position.mono_active = mono.node.node_id() == data.node_id; - } - for (idx, child) in container.children.iter().enumerate() { - if child.node.node_id() == data.node_id { - position.sibling_index = Some(idx.min(u16::MAX as usize) as u16); - break; - } - } - } - position - } - fn tl_set_active(&self, active: bool) { let _ = active; } @@ -387,11 +299,6 @@ pub trait ToplevelNodeBase: Node { fn tl_scanout_surface(&self) -> Option> { None } - - fn tl_animation_snapshot(&self) -> Option> { - None - } - fn tl_restack_popups(&self) { // nothing } @@ -432,31 +339,6 @@ pub trait ToplevelNodeBase: Node { } } -fn populate_multiphase_ancestor_splits( - position: &mut MultiphaseHierarchyPosition, - mut parent: Option>, -) { - let mut depth = 0u16; - while let Some(node) = parent { - let Some(toplevel) = node.clone().node_into_toplevel() else { - break; - }; - depth = depth.saturating_add(1); - if let Some(container) = node.node_into_container() { - match container.split.get() { - ContainerSplit::Horizontal => { - position.nearest_horizontal_split_depth.get_or_insert(depth); - } - ContainerSplit::Vertical => { - position.nearest_vertical_split_depth.get_or_insert(depth); - } - } - } - parent = toplevel.tl_data().parent.get(); - } - position.depth = depth; -} - pub struct FullscreenedData { pub placeholder: Rc, pub workspace: Rc, @@ -495,13 +377,6 @@ impl ToplevelType { ToplevelType::XWindow { .. } => window::X_WINDOW, } } - - pub fn is_app_window(&self) -> bool { - matches!( - self, - ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_) - ) - } } pub struct ToplevelData { @@ -524,10 +399,8 @@ pub struct ToplevelData { pub title: RefCell, pub parent: CloneCell>>, pub mapped_during_iteration: Cell, - pub spawn_in_pending: Cell, pub pos: Cell, pub desired_extents: Cell, - pub layout_animation_position: Cell, pub seat_state: NodeSeatState, pub wants_attention: Cell, pub requested_attention: Cell, @@ -589,10 +462,8 @@ impl ToplevelData { title: RefCell::new(title), parent: Default::default(), mapped_during_iteration: Cell::new(0), - spawn_in_pending: Cell::new(false), pos: Default::default(), desired_extents: Default::default(), - layout_animation_position: Default::default(), seat_state: Default::default(), wants_attention: Cell::new(false), requested_attention: Cell::new(false), @@ -979,7 +850,7 @@ impl ToplevelData { } fd.workspace.remove_fullscreen_node(); if fd.placeholder.is_destroyed() { - state.map_tiled_without_autotile(node); + state.map_tiled(node); return; } let parent = fd.placeholder.tl_data().parent.take().unwrap(); @@ -1064,62 +935,6 @@ impl ToplevelData { self.mapped_during_iteration.get() == self.state.eng.iteration() } - pub fn queue_spawn_out(&self, node: &dyn ToplevelNode, retained: Option>) { - if !self.kind.is_app_window() - || !self.visible.get() - || self.is_fullscreen.get() - || node.node_is_container() - { - return; - } - let Some(retained) = retained else { - return; - }; - let bw = self.state.theme.sizes.border_width.get().max(0); - let now = self.state.now_nsec(); - let (outer, frame_inset, layer) = if self.parent_is_float.get() { - let Some(float) = self.float.get() else { - return; - }; - ( - self.state - .animations - .visual_rect(float.node_id(), float.position.get(), now), - bw, - RetainedExitLayer::Floating, - ) - } else { - let body = - self.state - .animations - .visual_rect(self.node_id, node.node_absolute_position(), now); - if body.is_empty() { - return; - } - if self.state.theme.sizes.gap.get() != 0 { - ( - Rect::new_sized_saturating( - body.x1() - bw, - body.y1() - bw, - body.width() + 2 * bw, - body.height() + 2 * bw, - ), - bw, - RetainedExitLayer::Tiled, - ) - } else { - (body, 0, RetainedExitLayer::Tiled) - } - }; - self.state.clone().queue_spawn_out_animation( - outer, - frame_inset, - retained, - self.active(), - layer, - ); - } - pub fn set_content_type(&self, content_type: Option) { if self.content_type.replace(content_type) != content_type { self.property_changed(TL_CHANGED_CONTENT_TY); @@ -1228,26 +1043,6 @@ pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: } } -fn float_outer_for_body(state: &State, body: Rect) -> Rect { - let bw = state.theme.sizes.border_width.get(); - Rect::new_sized_saturating( - body.x1() - bw, - body.y1() - bw, - body.width() + 2 * bw, - body.height() + 2 * bw, - ) -} - -fn float_body_for_outer(state: &State, outer: Rect) -> Rect { - let bw = state.theme.sizes.border_width.get(); - Rect::new_sized_saturating( - outer.x1() + bw, - outer.y1() + bw, - outer.width() - 2 * bw, - outer.height() - 2 * bw, - ) -} - pub fn toplevel_set_floating(state: &Rc, tl: Rc, floating: bool) { let data = tl.tl_data(); if data.is_fullscreen.get() { @@ -1262,21 +1057,11 @@ pub fn toplevel_set_floating(state: &Rc, tl: Rc, floati }; if !floating { parent.cnode_remove_child2(&*tl, true); - state.map_tiled_without_autotile(tl); + state.map_tiled(tl); } else if let Some(ws) = data.workspace.get() { - let node_id = data.node_id; - let old_body = - state - .animations - .visual_rect(node_id, tl.node_absolute_position(), state.now_nsec()); - let old_outer = float_outer_for_body(state, old_body); parent.cnode_remove_child2(&*tl, true); let (width, height) = data.float_size(&ws); - let floater = state.map_floating(tl, width, height, &ws, None); - let new_outer = floater.position.get(); - let new_body = float_body_for_outer(state, new_outer); - state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer); - state.queue_linear_layout_animation(node_id, old_body, new_body); + state.map_floating(tl, width, height, &ws, None); } } @@ -1323,54 +1108,3 @@ pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: & tl.tl_set_fullscreen(true, Some(ws.clone())); } } - -/// Removes a toplevel from the tree so it can be parked in a scratchpad. -/// -/// Returns `true` if the window was hidden. A placeholder, a window without a -/// parent, or a window that refuses to leave fullscreen cannot be parked. -pub fn toplevel_hide_for_scratchpad(tl: Rc) -> bool { - if tl.node_is_placeholder() { - return false; - } - let data = tl.tl_data(); - let workspace = data.workspace.get(); - if data.is_fullscreen.get() { - tl.clone().tl_set_fullscreen(false, None); - if data.is_fullscreen.get() { - return false; - } - } - let Some(parent) = data.parent.get() else { - return false; - }; - let kb_foci = collect_kb_foci(tl.clone()); - parent.cnode_remove_child2(&*tl, true); - data.parent.take(); - data.float.take(); - if data.parent_is_float.replace(false) { - data.property_changed(TL_CHANGED_FLOATING); - } - if data.workspace.take().is_some() { - data.property_changed(TL_CHANGED_WORKSPACE); - } - tl.tl_set_visible(false); - if let Some(workspace) = &workspace { - for seat in kb_foci { - workspace - .clone() - .node_do_focus(&seat, Direction::Unspecified); - } - } - true -} - -/// Maps a parked scratchpad window back onto `ws`. Scratchpad windows always -/// return floating, regardless of how they were laid out before parking. -pub fn toplevel_restore_from_scratchpad( - state: &Rc, - tl: Rc, - ws: &Rc, -) { - let (width, height) = tl.tl_data().float_size(ws); - state.map_floating(tl.clone(), width, height, ws, None); -} diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 5e31efe6..f60354a4 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -197,10 +197,10 @@ impl WorkspaceNode { } self.pull_child_properties(&**container); let pos = self.position.get(); + container.clone().tl_change_extents(&pos); container.tl_set_parent(self.clone()); container.tl_set_visible(self.container_visible()); self.container.set(Some(container.clone())); - container.clone().tl_change_extents(&pos); self.state.damage(self.position.get()); } diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index f94645fe..b55312fe 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -2034,7 +2034,6 @@ impl Wm { self.windows_by_surface_serial.remove(&serial); } if let Some(window) = data.window.take() { - window.queue_spawn_out(); window.destroy(); } if let Some(parent) = data.parent.take() { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index b57de5ad..75c24bf2 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -64,9 +64,6 @@ pub enum SimpleCommand { SetFloating(bool), ToggleFullscreen, SetFullscreen(bool), - SendToScratchpad, - ToggleScratchpad, - CycleScratchpad, Forward(bool), EnableWindowManagement(bool), SetFloatAboveFullscreen(bool), @@ -133,15 +130,6 @@ pub enum Action { MoveToWorkspace { name: String, }, - SendToScratchpad { - name: String, - }, - ToggleScratchpad { - name: String, - }, - CycleScratchpad { - name: String, - }, Multi { actions: Vec, }, @@ -278,20 +266,6 @@ pub struct UiDrag { pub threshold: Option, } -#[derive(Debug, Clone, Default)] -pub struct Animations { - pub enabled: Option, - pub duration_ms: Option, - pub style: Option, - pub curve: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum AnimationCurveConfig { - Preset(String), - CubicBezier([f32; 4]), -} - #[derive(Debug, Clone)] pub enum OutputMatch { Any(Vec), @@ -586,6 +560,8 @@ pub struct Config { pub inputs: Vec, pub idle: Option, pub grace_period: Option, + pub key_press_enables_dpms: Option, + pub mouse_move_enables_dpms: Option, pub explicit_sync_enabled: Option, pub focus_follows_mouse: bool, pub window_management_key: Option, @@ -593,7 +569,6 @@ pub struct Config { pub tearing: Option, pub libei: Libei, pub ui_drag: UiDrag, - pub animations: Animations, pub xwayland: Option, pub color_management: Option, pub float: Option, @@ -612,14 +587,6 @@ pub struct Config { pub simple_im: Option, pub fallback_output_mode: Option, pub mouse_follows_focus: Option, - pub scratchpads: Vec, - pub autotile: Option, -} - -#[derive(Debug, Clone)] -pub struct Scratchpad { - pub name: String, - pub exec: Option, } #[derive(Debug, Error)] @@ -686,26 +653,3 @@ fn default_config_parses() { let input = include_bytes!("default-config.toml"); parse_config(input, &Default::default(), |_| ()).unwrap(); } - -#[test] -fn custom_animation_curve_parses() { - let input = b" - [animations] - curve = [0.25, 0.1, 0.25, 1.0] - "; - let config = parse_config(input, &Default::default(), |_| ()).unwrap(); - assert_eq!( - config.animations.curve, - Some(AnimationCurveConfig::CubicBezier([0.25, 0.1, 0.25, 1.0])) - ); -} - -#[test] -fn animation_style_parses() { - let input = b" - [animations] - style = \"plain\" - "; - let config = parse_config(input, &Default::default(), |_| ()).unwrap(); - assert_eq!(config.animations.style.as_deref(), Some("plain")); -} diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 98d3ab73..4c5e337b 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -8,7 +8,6 @@ use { pub mod action; mod actions; -mod animations; mod capabilities; mod clean_logs_older_than; mod client_match; @@ -41,7 +40,6 @@ pub mod modified_keysym; mod output; mod output_match; mod repeat_rate; -mod scratchpad; pub mod shortcuts; mod simple_im; mod status; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 29fdc3e4..7581198d 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -117,9 +117,6 @@ impl ActionParser<'_> { "toggle-fullscreen" => ToggleFullscreen, "enter-fullscreen" => SetFullscreen(true), "exit-fullscreen" => SetFullscreen(false), - "send-to-scratchpad" => SendToScratchpad, - "toggle-scratchpad" => ToggleScratchpad, - "cycle-scratchpad" => CycleScratchpad, "focus-parent" => FocusParent, "close" => Close, "disable-pointer-constraint" => DisablePointerConstraint, @@ -225,33 +222,6 @@ impl ActionParser<'_> { Ok(Action::MoveToWorkspace { name }) } - fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::SendToScratchpad { name }) - } - - fn parse_toggle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::ToggleScratchpad { name }) - } - - fn parse_cycle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { - let name = ext - .extract(opt(str("name")))? - .map(|name| name.value) - .unwrap_or("") - .to_string(); - Ok(Action::CycleScratchpad { name }) - } - fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let con = ext .extract(val("connector"))? @@ -581,9 +551,6 @@ impl Parser for ActionParser<'_> { "switch-to-vt" => self.parse_switch_to_vt(&mut ext), "show-workspace" => self.parse_show_workspace(&mut ext), "move-to-workspace" => self.parse_move_to_workspace(&mut ext), - "send-to-scratchpad" => self.parse_send_to_scratchpad(&mut ext), - "toggle-scratchpad" => self.parse_toggle_scratchpad(&mut ext), - "cycle-scratchpad" => self.parse_cycle_scratchpad(&mut ext), "configure-connector" => self.parse_configure_connector(&mut ext), "configure-input" => self.parse_configure_input(&mut ext), "configure-output" => self.parse_configure_output(&mut ext), diff --git a/toml-config/src/config/parsers/animations.rs b/toml-config/src/config/parsers/animations.rs deleted file mode 100644 index cc5cb439..00000000 --- a/toml-config/src/config/parsers/animations.rs +++ /dev/null @@ -1,99 +0,0 @@ -use { - crate::{ - config::{ - AnimationCurveConfig, Animations, - context::Context, - extractor::{Extractor, ExtractorError, bol, n32, opt, recover, str, val}, - parser::{DataType, ParseResult, Parser, UnexpectedDataType}, - }, - toml::{ - toml_span::{DespanExt, Span, Spanned, SpannedExt}, - toml_value::Value, - }, - }, - indexmap::IndexMap, - thiserror::Error, -}; - -#[derive(Debug, Error)] -pub enum AnimationsParserError { - #[error(transparent)] - Expected(#[from] UnexpectedDataType), - #[error(transparent)] - Extract(#[from] ExtractorError), - #[error("Expected animation curve to be a string or an array")] - CurveType, - #[error("Cubic-bezier animation curves must contain exactly four values")] - CubicBezierLen, - #[error("Cubic-bezier animation curve entries must be finite floats or integers")] - CubicBezierValue, - #[error("Cubic-bezier x control points must be between 0 and 1")] - CubicBezierXRange, -} - -pub struct AnimationsParser<'a>(pub &'a Context<'a>); - -impl Parser for AnimationsParser<'_> { - type Value = Animations; - type Error = AnimationsParserError; - 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, duration_ms, style, curve) = ext.extract(( - recover(opt(bol("enabled"))), - recover(opt(n32("duration-ms"))), - recover(opt(str("style"))), - opt(val("curve")), - ))?; - let curve = match curve { - Some(curve) => Some(parse_curve(curve)?), - None => None, - }; - Ok(Animations { - enabled: enabled.despan(), - duration_ms: duration_ms.despan(), - style: style.despan().map(|style| style.to_string()), - curve, - }) - } -} - -fn parse_curve( - curve: Spanned<&Value>, -) -> Result> { - match curve.value { - Value::String(s) => Ok(AnimationCurveConfig::Preset(s.clone())), - Value::Array(values) => parse_cubic_bezier(curve.span, values), - _ => Err(AnimationsParserError::CurveType.spanned(curve.span)), - } -} - -fn parse_cubic_bezier( - span: Span, - values: &[Spanned], -) -> Result> { - if values.len() != 4 { - return Err(AnimationsParserError::CubicBezierLen.spanned(span)); - } - let mut points = [0.0; 4]; - for (idx, value) in values.iter().enumerate() { - let f = match value.value { - Value::Float(f) => f, - Value::Integer(i) => i as f64, - _ => return Err(AnimationsParserError::CubicBezierValue.spanned(value.span)), - }; - if !f.is_finite() { - return Err(AnimationsParserError::CubicBezierValue.spanned(value.span)); - } - points[idx] = f as f32; - } - if !(0.0..=1.0).contains(&points[0]) || !(0.0..=1.0).contains(&points[2]) { - return Err(AnimationsParserError::CubicBezierXRange.spanned(span)); - } - Ok(AnimationCurveConfig::CubicBezier(points)) -} diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 8e776860..45654007 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -1,14 +1,13 @@ use { crate::{ config::{ - Action, Animations, Config, Libei, Theme, UiDrag, + Action, Config, Libei, Theme, UiDrag, context::Context, extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::ActionParser, actions::ActionsParser, - animations::AnimationsParser, clean_logs_older_than::CleanLogsOlderThanParser, client_rule::ClientRulesParser, color_management::ColorManagementParser, @@ -28,7 +27,6 @@ use { log_level::LogLevelParser, output::OutputsParser, repeat_rate::RepeatRateParser, - scratchpad::ScratchpadsParser, shortcuts::{ ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError, parse_modified_keysym_str, @@ -155,9 +153,7 @@ impl Parser for ConfigParser<'_> { fallback_output_mode_val, clean_logs_older_than_val, mouse_follows_focus, - animations_val, ), - (scratchpads_val, autotile), ) = ext.extract(( ( opt(val("keymap")), @@ -217,9 +213,7 @@ impl Parser for ConfigParser<'_> { opt(val("fallback-output-mode")), opt(val("clean-logs-older-than")), recover(opt(bol("unstable-mouse-follows-focus"))), - opt(val("animations")), ), - (opt(val("scratchpads")), recover(opt(bol("autotile")))), ))?; let mut keymap = None; if let Some(value) = keymap_val { @@ -373,11 +367,15 @@ impl Parser for ConfigParser<'_> { } let mut idle = None; let mut grace_period = None; + let mut key_press_enables_dpms = None; + let mut mouse_move_enables_dpms = None; if let Some(value) = idle_val { match value.parse(&mut IdleParser(self.0)) { Ok(v) => { idle = v.timeout; grace_period = v.grace_period; + key_press_enables_dpms = v.key_press_enables_dpms; + mouse_move_enables_dpms = v.mouse_move_enables_dpms; } Err(e) => { log::warn!("Could not parse the idle timeout: {}", self.0.error(e)); @@ -435,15 +433,6 @@ impl Parser for ConfigParser<'_> { } } } - let mut animations = Animations::default(); - if let Some(value) = animations_val { - match value.parse(&mut AnimationsParser(self.0)) { - Ok(v) => animations = v, - Err(e) => { - log::warn!("Could not parse animations setting: {}", self.0.error(e)); - } - } - } let mut xwayland = None; if let Some(value) = xwayland_val { match value.parse(&mut XwaylandParser(self.0)) { @@ -571,13 +560,6 @@ impl Parser for ConfigParser<'_> { } } } - let mut scratchpads = vec![]; - if let Some(value) = scratchpads_val { - match value.parse(&mut ScratchpadsParser(self.0)) { - Ok(v) => scratchpads = v, - Err(e) => log::warn!("Could not parse the scratchpads: {}", self.0.error(e)), - } - } Ok(Config { keymap, repeat_rate, @@ -603,13 +585,14 @@ impl Parser for ConfigParser<'_> { inputs, idle, grace_period, + key_press_enables_dpms, + mouse_move_enables_dpms, focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), window_management_key, vrr, tearing, libei, ui_drag, - animations, xwayland, color_management, float, @@ -628,8 +611,6 @@ impl Parser for ConfigParser<'_> { simple_im, fallback_output_mode, mouse_follows_focus: mouse_follows_focus.despan(), - scratchpads, - autotile: autotile.despan(), }) } } diff --git a/toml-config/src/config/parsers/idle.rs b/toml-config/src/config/parsers/idle.rs index 57d03b36..5da15f8b 100644 --- a/toml-config/src/config/parsers/idle.rs +++ b/toml-config/src/config/parsers/idle.rs @@ -2,7 +2,7 @@ use { crate::{ config::{ context::Context, - extractor::{Extractor, ExtractorError, n64, opt, val}, + extractor::{Extractor, ExtractorError, bol, n64, opt, recover, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, }, toml::{ @@ -28,6 +28,8 @@ pub struct IdleParser<'a>(pub &'a Context<'a>); pub struct Idle { pub timeout: Option, pub grace_period: Option, + pub key_press_enables_dpms: Option, + pub mouse_move_enables_dpms: Option, } impl Parser for IdleParser<'_> { @@ -41,10 +43,18 @@ impl Parser for IdleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (minutes, seconds, grace_period_val) = ext.extract(( + let ( + minutes, + seconds, + grace_period_val, + key_press_enables_dpms, + mouse_move_enables_dpms, + ) = ext.extract(( opt(n64("minutes")), opt(n64("seconds")), opt(val("grace-period")), + recover(opt(bol("key-press-enables-dpms"))), + recover(opt(bol("mouse-move-enables-dpms"))), ))?; let mut timeout = None; if minutes.is_some() || seconds.is_some() { @@ -57,6 +67,8 @@ impl Parser for IdleParser<'_> { Ok(Idle { timeout, grace_period, + key_press_enables_dpms: key_press_enables_dpms.despan(), + mouse_move_enables_dpms: mouse_move_enables_dpms.despan(), }) } } diff --git a/toml-config/src/config/parsers/scratchpad.rs b/toml-config/src/config/parsers/scratchpad.rs deleted file mode 100644 index 17cc5238..00000000 --- a/toml-config/src/config/parsers/scratchpad.rs +++ /dev/null @@ -1,87 +0,0 @@ -use { - crate::{ - config::{ - Scratchpad, - context::Context, - extractor::{Extractor, ExtractorError, opt, str, val}, - parser::{DataType, ParseResult, Parser, UnexpectedDataType}, - parsers::exec::{ExecParser, ExecParserError}, - }, - toml::{ - toml_span::{Span, Spanned}, - toml_value::Value, - }, - }, - indexmap::IndexMap, - thiserror::Error, -}; - -#[derive(Debug, Error)] -pub enum ScratchpadParserError { - #[error(transparent)] - Expected(#[from] UnexpectedDataType), - #[error(transparent)] - Extract(#[from] ExtractorError), - #[error(transparent)] - Exec(#[from] ExecParserError), -} - -pub struct ScratchpadParser<'a>(pub &'a Context<'a>); - -impl Parser for ScratchpadParser<'_> { - type Value = Scratchpad; - type Error = ScratchpadParserError; - 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 (name, exec_val) = ext.extract((str("name"), opt(val("exec"))))?; - let exec = match exec_val { - None => None, - Some(e) => Some(e.parse_map(&mut ExecParser(self.0))?), - }; - Ok(Scratchpad { - name: name.value.to_string(), - exec, - }) - } -} - -pub struct ScratchpadsParser<'a>(pub &'a Context<'a>); - -impl Parser for ScratchpadsParser<'_> { - type Value = Vec; - type Error = ScratchpadParserError; - const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array]; - - fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { - let mut res = vec![]; - for el in array { - match el.parse(&mut ScratchpadParser(self.0)) { - Ok(o) => res.push(o), - Err(e) => { - log::warn!("Could not parse scratchpad: {}", self.0.error(e)); - } - } - } - Ok(res) - } - - fn parse_table( - &mut self, - span: Span, - table: &IndexMap, Spanned>, - ) -> ParseResult { - log::warn!( - "`scratchpads` value should be an array: {}", - self.0.error3(span) - ); - ScratchpadParser(self.0) - .parse_table(span, table) - .map(|v| vec![v]) - } -} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 6e3430f8..d39941d3 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -13,9 +13,9 @@ mod toml; use { crate::{ config::{ - Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice, - ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, - OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config, + Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, + ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, + SimpleCommand, Status, Theme, WindowRule, parse_config, }, rules::{MatcherTemp, RuleMapper}, shortcuts::ModeState, @@ -23,11 +23,11 @@ use { ahash::{AHashMap, AHashSet}, error_reporter::Report, jay_config::{ - AnimationCurve, AnimationStyle, Axis, + Axis, client::Client, config, config_dir, exec::{Command, set_env, unset_env}, - get_autotile, get_workspace, + get_workspace, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH, get_seat, input_devices, on_input_device_removed, on_new_input_device, @@ -37,13 +37,12 @@ use { is_reload, keyboard::Keymap, logging::{clean_logs_older_than, set_log_level}, - on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier, - set_animation_curve, set_animation_duration_ms, set_animation_style, - set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius, - set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, - set_floating_titles, set_idle, set_idle_grace_period, set_middle_click_paste_enabled, - set_show_bar, set_show_float_pin_icon, set_show_titles, set_tab_title_align, - set_ui_drag_enabled, set_ui_drag_threshold, + on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile, + set_color_management_enabled, set_corner_radius, set_default_workspace_capture, + set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, + set_idle_grace_period, set_key_press_enables_dpms, set_middle_click_paste_enabled, + set_mouse_move_enables_dpms, set_show_bar, set_show_float_pin_icon, set_show_titles, + set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, tasks::{self, JoinHandle}, @@ -173,9 +172,6 @@ impl Action { SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), - SimpleCommand::SendToScratchpad => window_or_seat!(s, s.send_to_scratchpad("")), - SimpleCommand::ToggleScratchpad => b.new(move || s.toggle_scratchpad("")), - SimpleCommand::CycleScratchpad => b.new(move || s.cycle_scratchpad("")), SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::Close => window_or_seat!(s, s.close()), SimpleCommand::DisablePointerConstraint => { @@ -272,7 +268,12 @@ impl Action { SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)), SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)), SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)), - SimpleCommand::ToggleAutotile => b.new(move || set_autotile(!get_autotile())), + SimpleCommand::ToggleAutotile => { + b.new(move || { + // Toggle not directly supported; set to true + set_autotile(true) + }) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -309,9 +310,6 @@ impl Action { let workspace = get_workspace(&name); window_or_seat!(s, s.set_workspace(workspace)) } - Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)), - Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)), - Action::CycleScratchpad { name } => b.new(move || s.cycle_scratchpad(&name)), Action::ConfigureConnector { con } => b.new(move || { for c in connectors() { if con.match_.matches(c) { @@ -1463,43 +1461,6 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc set_animation_style(AnimationStyle::PLAIN), - "multiphase" => set_animation_style(AnimationStyle::MULTIPHASE), - style_name => log::warn!("Unknown animation style: {style_name}"), - } - match config - .animations - .curve - .unwrap_or_else(|| AnimationCurveConfig::Preset("ease-out".to_string())) - { - AnimationCurveConfig::Preset(curve_name) => { - let curve = match curve_name.as_str() { - "linear" => Some(AnimationCurve::LINEAR), - "ease" => Some(AnimationCurve::EASE), - "ease-in" => Some(AnimationCurve::EASE_IN), - "ease-out" => Some(AnimationCurve::EASE_OUT), - "ease-in-out" => Some(AnimationCurve::EASE_IN_OUT), - _ => { - log::warn!("Unknown animation curve: {curve_name}"); - None - } - }; - if let Some(curve) = curve { - set_animation_curve(curve); - } - } - AnimationCurveConfig::CubicBezier([x1, y1, x2, y2]) => { - set_animation_cubic_bezier(x1, y1, x2, y2); - } - } if let Some(xwayland) = config.xwayland { if let Some(enabled) = xwayland.enabled { set_x_wayland_enabled(enabled); @@ -1728,6 +1657,8 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4469c157..930ad697 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -162,54 +162,6 @@ "name" ] }, - { - "description": "Sends the currently focused window to a scratchpad and hides it.\n\nA scratchpad can hold any number of windows. If `name` is omitted, the\ndefault scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-shift-minus = { type = \"send-to-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "send-to-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, - { - "description": "Toggles a scratchpad.\n\nIf the scratchpad has a visible window, that window is hidden. Otherwise, the\nmost recently hidden window in the scratchpad is shown on the current workspace.\nOnly one window of a scratchpad is shown at a time, and scratchpad windows are\nalways shown floating. If `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"toggle-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "toggle-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, - { - "description": "Cycles through the windows of a scratchpad, one at a time.\n\nWith no window shown, the first window is brought up. Each further invocation\nhides the current window and shows the next; after the last window the\nscratchpad is hidden again. Scratchpad windows are always shown floating.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"cycle-scratchpad\", name = \"terminal\" }\n ```\n", - "type": "object", - "properties": { - "type": { - "const": "cycle-scratchpad" - }, - "name": { - "type": "string", - "description": "The name of the scratchpad." - } - }, - "required": [ - "type" - ] - }, { "description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n", "type": "object", @@ -689,61 +641,6 @@ } ] }, - "AnimationCurve": { - "description": "Describes a window animation curve.\n", - "anyOf": [ - { - "type": "string", - "description": "One of the supported curve presets.\n", - "enum": [ - "linear", - "ease", - "ease-in", - "ease-out", - "ease-in-out" - ] - }, - { - "type": "array", - "description": "A custom CSS-style cubic-bezier curve as four numbers:\n`x1`, `y1`, `x2`, and `y2`.\n\nThe implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must\nbe between `0` and `1`.\n", - "items": { - "type": "number", - "description": "" - } - } - ] - }, - "AnimationStyle": { - "type": "string", - "description": "Describes a tiled window movement animation style.\n", - "enum": [ - "plain", - "multiphase" - ] - }, - "Animations": { - "description": "Describes window animation settings.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = [0.25, 0.1, 0.25, 1.0]\n ```\n", - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Enables or disables window animations.\n\nThe default is `false`.\n" - }, - "duration-ms": { - "type": "integer", - "description": "Sets the animation duration in milliseconds.\n\nThe default is `160`.\n" - }, - "style": { - "description": "Sets the animation style used for tiled window movement animations.\n\nThe default is `multiphase`.\n", - "$ref": "#/$defs/AnimationStyle" - }, - "curve": { - "description": "Sets the animation curve.\n\nThe default is `ease-out`.\n", - "$ref": "#/$defs/AnimationCurve" - } - }, - "required": [] - }, "BarPosition": { "type": "string", "description": "The position of the bar.", @@ -1188,10 +1085,6 @@ "description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n", "$ref": "#/$defs/UiDrag" }, - "animations": { - "description": "Configures window animations.\n\nAnimations are disabled by default.\n\n- Example:\n\n ```toml\n [animations]\n enabled = true\n duration-ms = 160\n style = \"multiphase\"\n curve = \"ease-out\"\n ```\n", - "$ref": "#/$defs/Animations" - }, "xwayland": { "description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n", "$ref": "#/$defs/Xwayland" @@ -1257,10 +1150,6 @@ "type": "boolean", "description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n" }, - "autotile": { - "type": "boolean", - "description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n" - }, "modes": { "description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n", "type": "object", @@ -1288,14 +1177,6 @@ "egui": { "description": "Sets the egui settings of the compositor.\n", "$ref": "#/$defs/Egui" - }, - "scratchpads": { - "type": "array", - "description": "An array of pre-configured scratchpads.\n\nEach entry launches a program when the graphics are first initialized and\nimmediately parks its window in the named scratchpad. The window is captured\nvia a unique tag attached to the spawned process, so other windows of the\nsame application are never affected.\n\nUse a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows\nup; they are always shown floating.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n\n [[scratchpads]]\n name = \"notes\"\n exec = [\"obsidian\"]\n ```\n", - "items": { - "description": "", - "$ref": "#/$defs/Scratchpad" - } } }, "required": [] @@ -2110,23 +1991,6 @@ }, "required": [] }, - "Scratchpad": { - "description": "A pre-configured scratchpad whose program is launched at startup and parked\nin the scratchpad.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n ```\n", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the scratchpad that the spawned window is parked in." - }, - "exec": { - "description": "The program to launch when the graphics are first initialized.\n\nIf omitted, no program is launched and the scratchpad is only created on\ndemand by `send-to-scratchpad`.\n", - "$ref": "#/$defs/Exec" - } - }, - "required": [ - "name" - ] - }, "SimpleActionName": { "type": "string", "description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", @@ -2145,15 +2009,9 @@ "make-group-tab", "change-group-opposite", "toggle-tab", - "enable-autotile", - "disable-autotile", - "toggle-autotile", "toggle-fullscreen", "enter-fullscreen", "exit-fullscreen", - "send-to-scratchpad", - "toggle-scratchpad", - "cycle-scratchpad", "focus-parent", "close", "disable-pointer-constraint", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 21682ada..43e9f20d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -286,76 +286,6 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. -- `send-to-scratchpad`: - - Sends the currently focused window to a scratchpad and hides it. - - A scratchpad can hold any number of windows. If `name` is omitted, the - default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - -- `toggle-scratchpad`: - - Toggles a scratchpad. - - If the scratchpad has a visible window, that window is hidden. Otherwise, the - most recently hidden window in the scratchpad is shown on the current workspace. - Only one window of a scratchpad is shown at a time, and scratchpad windows are - always shown floating. If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "toggle-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - -- `cycle-scratchpad`: - - Cycles through the windows of a scratchpad, one at a time. - - With no window shown, the first window is brought up. Each further invocation - hides the current window and shows the next; after the last window the - scratchpad is hidden again. Scratchpad windows are always shown floating. - If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "cycle-scratchpad", name = "terminal" } - ``` - - The table has the following fields: - - - `name` (optional): - - The name of the scratchpad. - - The value of this field should be a string. - - `move-to-output`: Moves a workspace to a different output. @@ -1012,126 +942,6 @@ This table is a tagged union. The variant is determined by the `type` field. It The numbers should be integers. - -### `AnimationCurve` - -Describes a window animation curve. - -Values of this type should have one of the following forms: - -#### A string - -One of the supported curve presets. - -The string should have one of the following values: - -- `linear`: - - No easing. - -- `ease`: - - The CSS `ease` curve. - -- `ease-in`: - - The CSS `ease-in` curve. - -- `ease-out`: - - The CSS `ease-out` curve. - -- `ease-in-out`: - - The CSS `ease-in-out` curve. - - -#### An array - -A custom CSS-style cubic-bezier curve as four numbers: -`x1`, `y1`, `x2`, and `y2`. - -The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must -be between `0` and `1`. - -Each element of this array should be a number. - - - -### `AnimationStyle` - -Describes a tiled window movement animation style. - -Values of this type should be strings. - -The string should have one of the following values: - -- `plain`: - - Uses a single interpolated movement from each window's current visual - rectangle to its destination rectangle. - -- `multiphase`: - - Uses the no-overlap multiphase planner for tiled window movement when a - supported plan exists. - - - - -### `Animations` - -Describes window animation settings. - -- Example: - - ```toml - [animations] - enabled = true - duration-ms = 160 - style = "multiphase" - curve = [0.25, 0.1, 0.25, 1.0] - ``` - -Values of this type should be tables. - -The table has the following fields: - -- `enabled` (optional): - - Enables or disables window animations. - - The default is `false`. - - The value of this field should be a boolean. - -- `duration-ms` (optional): - - Sets the animation duration in milliseconds. - - The default is `160`. - - The value of this field should be a number. - - The numbers should be integers. - -- `style` (optional): - - Sets the animation style used for tiled window movement animations. - - The default is `multiphase`. - - The value of this field should be a [AnimationStyle](#types-AnimationStyle). - -- `curve` (optional): - - Sets the animation curve. - - The default is `ease-out`. - - The value of this field should be a [AnimationCurve](#types-AnimationCurve). - - ### `BarPosition` @@ -2359,24 +2169,6 @@ The table has the following fields: The value of this field should be a [UiDrag](#types-UiDrag). -- `animations` (optional): - - Configures window animations. - - Animations are disabled by default. - - - Example: - - ```toml - [animations] - enabled = true - duration-ms = 160 - style = "multiphase" - curve = "ease-out" - ``` - - The value of this field should be a [Animations](#types-Animations). - - `xwayland` (optional): Configures the Xwayland settings. @@ -2560,18 +2352,6 @@ The table has the following fields: The value of this field should be a boolean. -- `autotile` (optional): - - Configures whether autotiling is enabled by default. - - When enabled, newly mapped tiled windows alternate their split - orientation automatically. This can also be toggled at runtime via the - `enable-autotile`, `disable-autotile`, and `toggle-autotile` actions. - - The default is `false`. - - The value of this field should be a boolean. - - `modes` (optional): Configures the input modes. @@ -2672,32 +2452,6 @@ The table has the following fields: The value of this field should be a [Egui](#types-Egui). -- `scratchpads` (optional): - - An array of pre-configured scratchpads. - - Each entry launches a program when the graphics are first initialized and - immediately parks its window in the named scratchpad. The window is captured - via a unique tag attached to the spawned process, so other windows of the - same application are never affected. - - Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows - up; they are always shown floating. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - - [[scratchpads]] - name = "notes" - exec = ["obsidian"] - ``` - - The value of this field should be an array of [Scratchpads](#types-Scratchpad). - ### `Connector` @@ -4631,40 +4385,6 @@ The table has the following fields: The value of this field should be a string. - -### `Scratchpad` - -A pre-configured scratchpad whose program is launched at startup and parked -in the scratchpad. - -- Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - ``` - -Values of this type should be tables. - -The table has the following fields: - -- `name` (required): - - The name of the scratchpad that the spawned window is parked in. - - The value of this field should be a string. - -- `exec` (optional): - - The program to launch when the graphics are first initialized. - - If omitted, no program is launched and the scratchpad is only created on - demand by `send-to-scratchpad`. - - The value of this field should be a [Exec](#types-Exec). - - ### `SimpleActionName` @@ -4756,18 +4476,6 @@ The string should have one of the following values: Toggles the current group between tabbed and split mode. -- `enable-autotile`: - - Enables alternating split orientation for newly tiled windows. - -- `disable-autotile`: - - Disables alternating split orientation for newly tiled windows. - -- `toggle-autotile`: - - Toggles alternating split orientation for newly tiled windows. - - `toggle-fullscreen`: Toggle the currently focused window between fullscreen and windowed. @@ -4780,18 +4488,6 @@ The string should have one of the following values: Makes the currently focused window windowed. -- `send-to-scratchpad`: - - Sends the currently focused window to the default scratchpad. - -- `toggle-scratchpad`: - - Toggles the default scratchpad. - -- `cycle-scratchpad`: - - Cycles through the windows of the default scratchpad. - - `focus-parent`: Focus the parent of the currently focused window. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 315c74b9..aa6789da 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -345,64 +345,6 @@ Action: description: The name of the workspace. required: true kind: string - send-to-scratchpad: - description: | - Sends the currently focused window to a scratchpad and hides it. - - A scratchpad can hold any number of windows. If `name` is omitted, the - default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string - toggle-scratchpad: - description: | - Toggles a scratchpad. - - If the scratchpad has a visible window, that window is hidden. Otherwise, the - most recently hidden window in the scratchpad is shown on the current workspace. - Only one window of a scratchpad is shown at a time, and scratchpad windows are - always shown floating. If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "toggle-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string - cycle-scratchpad: - description: | - Cycles through the windows of a scratchpad, one at a time. - - With no window shown, the first window is brought up. Each further invocation - hides the current window and shows the next; after the last window the - scratchpad is hidden again. Scratchpad windows are always shown floating. - If `name` is omitted, the default scratchpad is used. - - - Example: - - ```toml - [shortcuts] - alt-minus = { type = "cycle-scratchpad", name = "terminal" } - ``` - fields: - name: - description: The name of the scratchpad. - required: false - kind: string move-to-output: description: | Moves a workspace to a different output. @@ -1122,24 +1064,12 @@ SimpleActionName: description: Toggles the current group's direction. - value: toggle-tab description: Toggles the current group between tabbed and split mode. - - value: enable-autotile - description: Enables alternating split orientation for newly tiled windows. - - value: disable-autotile - description: Disables alternating split orientation for newly tiled windows. - - value: toggle-autotile - description: Toggles alternating split orientation for newly tiled windows. - value: toggle-fullscreen description: Toggle the currently focused window between fullscreen and windowed. - value: enter-fullscreen description: Makes the currently focused window fullscreen. - value: exit-fullscreen description: Makes the currently focused window windowed. - - value: send-to-scratchpad - description: Sends the currently focused window to the default scratchpad. - - value: toggle-scratchpad - description: Toggles the default scratchpad. - - value: cycle-scratchpad - description: Cycles through the windows of the default scratchpad. - value: focus-parent description: Focus the parent of the currently focused window. - value: close @@ -3012,23 +2942,6 @@ Config: ```toml ui-drag = { enabled = false, threshold = 20 } ``` - animations: - ref: Animations - required: false - description: | - Configures window animations. - - Animations are disabled by default. - - - Example: - - ```toml - [animations] - enabled = true - duration-ms = 160 - style = "multiphase" - curve = "ease-out" - ``` xwayland: ref: Xwayland required: false @@ -3199,21 +3112,10 @@ Config: required: false description: | Configures whether middle-click pasting is enabled. - + Changing this has no effect on running applications. The default is `true`. - autotile: - kind: boolean - required: false - description: | - Configures whether autotiling is enabled by default. - - When enabled, newly mapped tiled windows alternate their split - orientation automatically. This can also be toggled at runtime via the - `enable-autotile`, `disable-autotile`, and `toggle-autotile` actions. - - The default is `false`. modes: kind: map values: @@ -3310,61 +3212,6 @@ Config: required: false description: | Sets the egui settings of the compositor. - scratchpads: - kind: array - items: - ref: Scratchpad - required: false - description: | - An array of pre-configured scratchpads. - - Each entry launches a program when the graphics are first initialized and - immediately parks its window in the named scratchpad. The window is captured - via a unique tag attached to the spawned process, so other windows of the - same application are never affected. - - Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows - up; they are always shown floating. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - - [[scratchpads]] - name = "notes" - exec = ["obsidian"] - ``` - - -Scratchpad: - kind: table - description: | - A pre-configured scratchpad whose program is launched at startup and parked - in the scratchpad. - - - Example: - - ```toml - [[scratchpads]] - name = "term" - exec = "foot" - ``` - fields: - name: - kind: string - required: true - description: The name of the scratchpad that the spawned window is parked in. - exec: - ref: Exec - required: false - description: | - The program to launch when the graphics are first initialized. - - If omitted, no program is launched and the scratchpad is only created on - demand by `send-to-scratchpad`. Idle: @@ -3808,97 +3655,6 @@ UiDrag: The default is `10`. -Animations: - kind: table - description: | - Describes window animation settings. - - - Example: - - ```toml - [animations] - enabled = true - duration-ms = 160 - style = "multiphase" - curve = [0.25, 0.1, 0.25, 1.0] - ``` - fields: - enabled: - kind: boolean - required: false - description: | - Enables or disables window animations. - - The default is `false`. - duration-ms: - kind: number - integer_only: true - required: false - description: | - Sets the animation duration in milliseconds. - - The default is `160`. - style: - ref: AnimationStyle - required: false - description: | - Sets the animation style used for tiled window movement animations. - - The default is `multiphase`. - curve: - ref: AnimationCurve - required: false - description: | - Sets the animation curve. - - The default is `ease-out`. - - -AnimationStyle: - kind: string - description: | - Describes a tiled window movement animation style. - values: - - value: plain - description: | - Uses a single interpolated movement from each window's current visual - rectangle to its destination rectangle. - - value: multiphase - description: | - Uses the no-overlap multiphase planner for tiled window movement when a - supported plan exists. - - -AnimationCurve: - kind: variable - description: | - Describes a window animation curve. - variants: - - kind: string - description: | - One of the supported curve presets. - values: - - value: linear - description: No easing. - - value: ease - description: The CSS `ease` curve. - - value: ease-in - description: The CSS `ease-in` curve. - - value: ease-out - description: The CSS `ease-out` curve. - - value: ease-in-out - description: The CSS `ease-in-out` curve. - - kind: array - items: - kind: number - description: | - A custom CSS-style cubic-bezier curve as four numbers: - `x1`, `y1`, `x2`, and `y2`. - - The implicit endpoints are `(0, 0)` and `(1, 1)`. `x1` and `x2` must - be between `0` and `1`. - - Xwayland: kind: table description: | diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 019f9ea3..f22805d1 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -135,6 +135,10 @@ request get_pid (since = 27) { } +request set_dpms (since = 31) { + active: u32, +} + # events event client_id {