From 2167484861759db35e6024bd2df56fe71f462a78 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Mon, 25 May 2026 21:56:48 -0400 Subject: [PATCH 001/124] add dpms on/off command --- src/cli.rs | 17 +++++++++++++++++ src/cli/dpms.rs | 23 +++++++++++++++++++++++ src/ifs/jay_compositor.rs | 13 ++++++++++++- src/state.rs | 18 +++++++++++++++++- src/tasks/idle.rs | 12 ++---------- src/tools/tool_client.rs | 2 +- wire/jay_compositor.txt | 4 ++++ 7 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 src/cli/dpms.rs 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/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4373125e..4abf04c3 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_connectors_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/state.rs b/src/state.rs index 4ae761a0..9349bbc7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,8 @@ use { 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, @@ -1404,6 +1405,21 @@ 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 root_visible(&self) -> bool { !self.idle.backend_idle.get() } 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/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/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 { From eece44a59c493cc7c084e9a0ad3d6e45e25f34a7 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Mon, 25 May 2026 22:57:29 -0400 Subject: [PATCH 002/124] add config options for waking dpms on mouse and keyboard interaction --- jay-config/src/_private/client.rs | 8 ++++++++ jay-config/src/_private/ipc.rs | 6 ++++++ jay-config/src/lib.rs | 14 +++++++++++++ src/compositor.rs | 3 +++ src/config/handler.rs | 14 +++++++++++++ src/ifs/jay_compositor.rs | 2 +- src/state.rs | 21 +++++++++++++++++++- src/tasks/input_device.rs | 25 ++++++++++++++++++++++-- toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/config.rs | 6 ++++++ toml-config/src/config/parsers/idle.rs | 16 +++++++++++++-- toml-config/src/lib.rs | 8 +++++--- 12 files changed, 116 insertions(+), 9 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 8ef87476..b48c6227 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1327,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 }) } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index acb5ad81..0a2b9491 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -475,6 +475,12 @@ pub enum ClientMessage<'a> { SetIdle { timeout: Duration, }, + SetKeyPressEnablesDpms { + enabled: bool, + }, + SetMouseMoveEnablesDpms { + enabled: bool, + }, MoveToOutput { workspace: WorkspaceSource, connector: Connector, diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index e25710f9..dcc4e346 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -252,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 diff --git a/src/compositor.rs b/src/compositor.rs index 45d2a018..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, diff --git a/src/config/handler.rs b/src/config/handler.rs index 526c1cde..90d65b9b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1134,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); } @@ -3129,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, diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4abf04c3..4ccc45db 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -547,7 +547,7 @@ impl JayCompositorRequestHandler for JayCompositor { fn set_dpms(&self, req: SetDpms, _slf: &Rc) -> Result<(), Self::Error> { self.client .state - .set_connectors_active(req.active != 0) + .set_dpms_active(req.active != 0) .map_err(JayCompositorError::SetDpms)?; Ok(()) } diff --git a/src/state.rs b/src/state.rs index 9349bbc7..1d2f7a08 100644 --- a/src/state.rs +++ b/src/state.rs @@ -342,10 +342,13 @@ 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, @@ -975,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,6 +1430,15 @@ impl State { 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() } 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/toml-config/src/config.rs b/toml-config/src/config.rs index ba71c585..75c24bf2 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -560,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, diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index b9d34e74..45654007 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -367,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)); @@ -581,6 +585,8 @@ 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, 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/lib.rs b/toml-config/src/lib.rs index 391bcee9..d39941d3 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -40,9 +40,9 @@ use { 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_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, + 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}, @@ -1657,6 +1657,8 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Date: Thu, 21 May 2026 15:20:46 +1000 Subject: [PATCH 003/124] feat: add window animations --- jay-config/src/_private/client.rs | 20 + jay-config/src/_private/ipc.rs | 18 + jay-config/src/lib.rs | 57 + src/animation.rs | 1233 ++++++ src/animation/multiphase.rs | 3405 +++++++++++++++++ src/compositor.rs | 7 + src/config/handler.rs | 160 +- src/ifs/wl_seat.rs | 3 + src/ifs/wl_surface/commit_timeline.rs | 5 + src/ifs/wl_surface/x_surface.rs | 19 +- src/ifs/wl_surface/x_surface/xwindow.rs | 11 + src/ifs/wl_surface/xdg_surface.rs | 13 + .../wl_surface/xdg_surface/xdg_toplevel.rs | 16 + src/main.rs | 1 + src/renderer.rs | 367 +- src/state.rs | 886 ++++- src/tree/container.rs | 55 +- src/tree/float.rs | 71 +- src/tree/toplevel.rs | 217 +- src/tree/workspace.rs | 2 +- src/xwayland/xwm.rs | 1 + toml-config/src/config.rs | 38 + toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/animations.rs | 99 + toml-config/src/config/parsers/config.rs | 15 +- toml-config/src/lib.rs | 46 +- toml-spec/spec/spec.generated.json | 59 + toml-spec/spec/spec.generated.md | 138 +- toml-spec/spec/spec.yaml | 108 + 29 files changed, 6957 insertions(+), 114 deletions(-) create mode 100644 src/animation.rs create mode 100644 src/animation/multiphase.rs create mode 100644 toml-config/src/config/parsers/animations.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index b48c6227..70245b21 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1023,6 +1023,26 @@ 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 }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 0a2b9491..93067d55 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -551,6 +551,24 @@ 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, }, diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index dcc4e346..769a5215 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -103,6 +103,27 @@ 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() @@ -301,6 +322,42 @@ 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`. diff --git a/src/animation.rs b/src/animation.rs new file mode 100644 index 00000000..e76e030b --- /dev/null +++ b/src/animation.rs @@ -0,0 +1,1233 @@ +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 new file mode 100644 index 00000000..cb067241 --- /dev/null +++ b/src/animation/multiphase.rs @@ -0,0 +1,3405 @@ +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/compositor.rs b/src/compositor.rs index e197353d..8f1f9a05 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -363,6 +363,13 @@ 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(), diff --git a/src/config/handler.rs b/src/config/handler.rs index 90d65b9b..3cb0a6b1 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -658,17 +658,23 @@ impl ConfigProxyHandler { } fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.move_focused(direction.into()); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.move_focused(direction.into()); + Ok(()) + }) } fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { - let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { - c.move_child(window, direction.into()); - } - Ok(()) + 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(()) + }) } fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> { @@ -986,6 +992,31 @@ 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, @@ -1732,9 +1763,11 @@ impl ConfigProxyHandler { } fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.set_mono(mono); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.set_mono(mono); + Ok(()) + }) } fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> { @@ -1748,11 +1781,13 @@ impl ConfigProxyHandler { } fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> { - let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { - c.set_mono(mono.then_some(window.as_ref())); - } - Ok(()) + 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(()) + }) } fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { @@ -1767,15 +1802,19 @@ impl ConfigProxyHandler { } fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.set_split(axis.into()); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.set_split(axis.into()); + Ok(()) + }) } fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.toggle_tab(); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.toggle_tab(); + Ok(()) + }) } fn handle_seat_make_group( @@ -1784,27 +1823,35 @@ impl ConfigProxyHandler { axis: Axis, ephemeral: bool, ) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.make_group(axis.into(), ephemeral); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.make_group(axis.into(), ephemeral); + Ok(()) + }) } fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.change_group_opposite(); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.change_group_opposite(); + Ok(()) + }) } fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.equalize(recursive); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.equalize(recursive); + Ok(()) + }) } fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.move_tab(right); - Ok(()) + self.state.with_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.move_tab(right); + Ok(()) + }) } fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { @@ -1819,11 +1866,13 @@ impl ConfigProxyHandler { } fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { - let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { - c.set_split(axis.into()); - } - Ok(()) + 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(()) + }) } fn handle_add_shortcut( @@ -1963,9 +2012,11 @@ impl ConfigProxyHandler { } fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.set_floating(floating); - Ok(()) + self.state.with_linear_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.set_floating(floating); + Ok(()) + }) } fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { @@ -1977,9 +2028,11 @@ impl ConfigProxyHandler { } fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { - let window = self.get_window(window)?; - toplevel_set_floating(&self.state, window, floating); - Ok(()) + self.state.with_linear_layout_animations(|| { + let window = self.get_window(window)?; + toplevel_set_floating(&self.state, window, floating); + Ok(()) + }) } fn handle_add_pollable(self: &Rc, fd: i32) -> Result<(), CphError> { @@ -2729,8 +2782,10 @@ impl ConfigProxyHandler { dx2: i32, dy2: i32, ) -> Result<(), CphError> { - self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2); - Ok(()) + self.state.with_layout_animations(|| { + self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2); + Ok(()) + }) } fn handle_window_exists(&self, window: Window) { @@ -3207,6 +3262,17 @@ 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")?, diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 5fba889c..74ff4eda 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -936,6 +936,9 @@ 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/commit_timeline.rs b/src/ifs/wl_surface/commit_timeline.rs index 80ac2b4f..93372993 100644 --- a/src/ifs/wl_surface/commit_timeline.rs +++ b/src/ifs/wl_surface/commit_timeline.rs @@ -628,6 +628,11 @@ 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 1c3e295c..4f6db63c 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::{ - SurfaceExt, WlSurface, WlSurfaceError, + PendingState, SurfaceExt, WlSurface, WlSurfaceError, x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow}, }, leaks::Tracker, @@ -30,6 +30,22 @@ 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(); @@ -45,6 +61,7 @@ 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 f1c68730..80ea8b1b 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -1,5 +1,6 @@ use { crate::{ + animation::RetainedToplevel, client::Client, cursor::KnownCursor, fixed::Fixed, @@ -252,6 +253,11 @@ 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, @@ -274,6 +280,7 @@ impl Xwindow { match map_change { Change::None => return, Change::Unmap => { + self.queue_spawn_out(); self.data .info .pending_extents @@ -514,6 +521,10 @@ 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 9b5130d7..ad87c951 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -226,6 +226,10 @@ pub trait XdgSurfaceExt: Debug { // nothing } + fn prepare_unmap(&self) { + // nothing + } + fn extents_changed(&self) { // nothing } @@ -664,6 +668,15 @@ 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 768a8367..6a7f395f 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -2,6 +2,7 @@ pub mod xdg_dialog_v1; use { crate::{ + animation::RetainedToplevel, bugs, bugs::Bugs, client::{Client, ClientError}, @@ -259,6 +260,7 @@ 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(); { @@ -398,6 +400,11 @@ 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>, @@ -779,6 +786,11 @@ 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(); } @@ -818,6 +830,10 @@ 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/main.rs b/src/main.rs index 5a566f9b..161d3d99 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ mod leaks; mod tracy; mod acceptor; mod allocator; +mod animation; mod async_engine; mod backend; mod backends; diff --git a/src/renderer.rs b/src/renderer.rs index e601a0e0..b80e3f18 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,7 +1,11 @@ use { crate::{ + animation::{ + RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface, + RetainedToplevel, + }, cmm::cmm_render_intent::RenderIntent, - gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect}, + gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, x_surface::xwindow::Xwindow, @@ -14,8 +18,8 @@ use { state::State, theme::{Color, CornerRadius}, tree::{ - ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, - ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, + ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData, + ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, }, }, std::{ops::Deref, rc::Rc, slice}, @@ -200,14 +204,22 @@ 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(); - if pos.intersects(&opos) { - let (x, y) = opos.translate(pos.x1(), pos.y1()); + 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()); stacked.node_render(self, x, y, None); } } @@ -215,6 +227,7 @@ 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(); @@ -453,6 +466,265 @@ 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); @@ -465,6 +737,7 @@ 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(); @@ -476,10 +749,10 @@ impl Renderer<'_> { }; if !child.node.node_is_container() { let frame = Rect::new_sized_saturating( - mb.x1() - bw, - mb.y1() - bw, - mb.width() + 2 * bw, - mb.height() + 2 * bw, + visual_mb.x1() - bw, + visual_mb.y1() - bw, + visual_mb.width() + 2 * bw, + visual_mb.height() + 2 * bw, ); self.render_rounded_frame( frame, @@ -491,14 +764,17 @@ impl Renderer<'_> { ); } } - 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 - }; + 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 + }; 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() { @@ -507,9 +783,16 @@ impl Renderer<'_> { self.corner_radius = Some(inner_cr); } } - child - .node - .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); + 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), + ); self.stretch = None; self.corner_radius = None; } else { @@ -524,10 +807,13 @@ impl Renderer<'_> { }; let cr = self.state.theme.corner_radius.get(); for child in container.children.iter() { - let body = child.body.get(); - if body.x1() >= container.width.get() || body.y1() >= container.height.get() { + let layout_body = child.body.get(); + if layout_body.x1() >= container.width.get() + || layout_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 @@ -544,7 +830,7 @@ impl Renderer<'_> { self.render_rounded_frame(frame, c, cr, bw, x, y); } } - let content = child.content.get(); + let content = child.content.get().at_point(body.x1(), body.y1()); self.stretch = if content.width() != body.width() || content.height() != body.height() { Some(self.base.scale_point(body.width(), body.height())) @@ -556,10 +842,16 @@ 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); - child - .node - .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); + self.render_child_or_snapshot( + &child.node, + x + content.x1(), + y + content.y1(), + Some(&body), + ); self.stretch = None; self.corner_radius = None; } @@ -793,6 +1085,10 @@ 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() { @@ -801,16 +1097,27 @@ impl Renderer<'_> { theme.colors.border.get() }; let cr = theme.corner_radius.get(); - let outer = Rect::new_sized_saturating(0, 0, pos.width(), pos.height()); + let outer = Rect::new_sized_saturating(0, 0, visual.width(), visual.height()); self.render_rounded_frame(outer, &bc, cr, bw, x, y); - let body = - Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw); + let body = Rect::new_sized_saturating( + x + bw, + y + bw, + visual.width() - 2 * bw, + visual.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); } - child.node_render(self, body.x1(), body.y1(), Some(&scissor_body)); + self.render_window_body_background(body); + self.render_child_or_snapshot(&child, body.x1(), body.y1(), Some(&scissor_body)); + self.stretch = None; self.corner_radius = None; } diff --git a/src/state.rs b/src/state.rs index 1d2f7a08..e92a0092 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,6 +2,17 @@ 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, @@ -103,11 +114,10 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode, - 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, + 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, }, udmabuf::UdmabufHolder, utils::{ @@ -155,6 +165,98 @@ 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, @@ -265,6 +367,13 @@ 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, @@ -816,7 +925,14 @@ impl State { pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); - self.do_map_tiled(seat.as_deref(), node.clone()); + 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())); + } else { + self.do_map_tiled(seat.as_deref(), node.clone()); + } self.focus_after_map(node, seat.as_deref()); } @@ -851,7 +967,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(); @@ -882,8 +998,9 @@ impl State { } Rect::new_sized_saturating(x1, y1, width, height) }; - FloatNode::new(self, workspace, position, node.clone()); + let float = FloatNode::new(self, workspace, position, node.clone()); self.focus_after_map(node, self.seat_queue.last().as_deref()); + float } fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { @@ -1126,6 +1243,12 @@ 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(); @@ -1496,6 +1619,532 @@ 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() { @@ -2024,6 +2673,227 @@ 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/tree/container.rs b/src/tree/container.rs index 61ec00d1..b8de7b25 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -131,6 +131,8 @@ 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, @@ -238,6 +240,8 @@ 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, @@ -436,6 +440,10 @@ 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()); } @@ -467,6 +475,7 @@ 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() { @@ -484,6 +493,7 @@ impl ContainerNode { self.damage(); } } + self.mono_transition_animation_pending.set(false); } fn perform_mono_layout(self: &Rc, child: &ContainerChild) { @@ -656,6 +666,7 @@ 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(); } } @@ -816,6 +827,7 @@ impl ContainerNode { } } self.mono_child.set(child.clone()); + self.mono_transition_animation_pending.set(true); if child.is_some() { self.rebuild_tab_bar(); } else { @@ -1759,10 +1771,42 @@ enum SeatOpKind { pub async fn container_layout(state: Rc) { loop { - let container = state.pending_container_layout.pop().await; - if container.layout_scheduled.get() { - container.perform_layout(); + 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 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); } } @@ -2259,6 +2303,11 @@ 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/float.rs b/src/tree/float.rs index dc0b44f4..a57c2b91 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -31,6 +31,9 @@ use { }; tree_id!(FloatNodeId); + +const COMMAND_MOVE_DELTA: i32 = 100; + pub struct FloatNode { pub id: FloatNodeId, pub state: Rc, @@ -153,6 +156,13 @@ 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( @@ -363,6 +373,50 @@ 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); @@ -791,13 +845,7 @@ impl ContainingNode for FloatNode { let bw = theme.sizes.border_width.get(); let (x, y) = (x - bw, y - bw); let pos = self.position.get(); - 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(); - } + self.set_position(pos.at_point(x, y)); } fn cnode_resize_child( @@ -828,14 +876,7 @@ impl ContainingNode for FloatNode { y2 = (v + bw).max(y1 + bw + bw); } let new_pos = Rect::new_saturating(x1, y1, x2, y2); - 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(); - } + self.set_position(new_pos); } fn cnode_pinned(&self) -> bool { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 02bba848..312b4ac6 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1,5 +1,12 @@ use { crate::{ + animation::{ + RetainedExitLayer, RetainedToplevel, + multiphase::{ + MultiphaseHierarchyPosition, MultiphaseHierarchyTransition, + MultiphaseWindowHierarchy, PhaseAxis, + }, + }, client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, @@ -117,6 +124,7 @@ 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(); @@ -184,6 +192,57 @@ 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(); @@ -275,6 +334,35 @@ 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; } @@ -299,6 +387,11 @@ pub trait ToplevelNodeBase: Node { fn tl_scanout_surface(&self) -> Option> { None } + + fn tl_animation_snapshot(&self) -> Option> { + None + } + fn tl_restack_popups(&self) { // nothing } @@ -339,6 +432,31 @@ 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, @@ -377,6 +495,13 @@ impl ToplevelType { ToplevelType::XWindow { .. } => window::X_WINDOW, } } + + pub fn is_app_window(&self) -> bool { + matches!( + self, + ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_) + ) + } } pub struct ToplevelData { @@ -399,8 +524,10 @@ 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, @@ -462,8 +589,10 @@ 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), @@ -935,6 +1064,62 @@ 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); @@ -1043,6 +1228,26 @@ 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() { @@ -1059,9 +1264,19 @@ pub fn toplevel_set_floating(state: &Rc, tl: Rc, floati parent.cnode_remove_child2(&*tl, true); 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); - state.map_floating(tl, width, height, &ws, None); + 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); } } diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index f60354a4..5e31efe6 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 b55312fe..f94645fe 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -2034,6 +2034,7 @@ 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 75c24bf2..f755cdad 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -266,6 +266,20 @@ 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), @@ -569,6 +583,7 @@ 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, @@ -653,3 +668,26 @@ 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 4c5e337b..e353a2f8 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -8,6 +8,7 @@ use { pub mod action; mod actions; +mod animations; mod capabilities; mod clean_logs_older_than; mod client_match; diff --git a/toml-config/src/config/parsers/animations.rs b/toml-config/src/config/parsers/animations.rs new file mode 100644 index 00000000..cc5cb439 --- /dev/null +++ b/toml-config/src/config/parsers/animations.rs @@ -0,0 +1,99 @@ +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 45654007..5b96e27c 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -1,13 +1,14 @@ use { crate::{ config::{ - Action, Config, Libei, Theme, UiDrag, + Action, Animations, 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, @@ -153,6 +154,7 @@ impl Parser for ConfigParser<'_> { fallback_output_mode_val, clean_logs_older_than_val, mouse_follows_focus, + animations_val, ), ) = ext.extract(( ( @@ -213,6 +215,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")), ), ))?; let mut keymap = None; @@ -433,6 +436,15 @@ 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)) { @@ -593,6 +605,7 @@ impl Parser for ConfigParser<'_> { tearing, libei, ui_drag, + animations, xwayland, color_management, float, diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index d39941d3..2766f733 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -13,9 +13,9 @@ mod toml; use { crate::{ config::{ - Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, - ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, - SimpleCommand, Status, Theme, WindowRule, parse_config, + Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice, + ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, + OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config, }, rules::{MatcherTemp, RuleMapper}, shortcuts::ModeState, @@ -23,7 +23,7 @@ use { ahash::{AHashMap, AHashSet}, error_reporter::Report, jay_config::{ - Axis, + AnimationCurve, AnimationStyle, Axis, client::Client, config, config_dir, exec::{Command, set_env, unset_env}, @@ -37,8 +37,10 @@ use { is_reload, keyboard::Keymap, logging::{clean_logs_older_than, set_log_level}, - on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile, - set_color_management_enabled, set_corner_radius, set_default_workspace_capture, + 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_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, @@ -1649,6 +1651,38 @@ 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); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 930ad697..50cc8887 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -641,6 +641,61 @@ } ] }, + "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.", @@ -1085,6 +1140,10 @@ "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" diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 43e9f20d..a31a3767 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -942,6 +942,125 @@ 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` @@ -2169,6 +2288,24 @@ 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. @@ -5670,4 +5807,3 @@ The table has the following fields: The value of this field should be a [XScalingMode](#types-XScalingMode). - diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index aa6789da..706c016a 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2942,6 +2942,23 @@ 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 @@ -3655,6 +3672,97 @@ 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: | From 657e7ce2f755efcc6cac756a2b1b2f9df464f93d Mon Sep 17 00:00:00 2001 From: kossLAN Date: Fri, 29 May 2026 09:14:53 -0400 Subject: [PATCH 004/124] all: split reusable components into workspace crates --- Cargo.lock | 95 + Cargo.toml | 29 +- README.md | 2 +- book/src/SUMMARY.md | 1 - book/src/cli.md | 30 - book/src/configuration/idle.md | 8 - book/src/configuration/misc.md | 5 +- book/src/configuration/shortcuts.md | 9 - book/src/configuration/startup.md | 7 - book/src/configuration/theme.md | 2 +- book/src/configuration/xwayland.md | 2 +- book/src/features.md | 28 - book/src/installation.md | 17 +- book/src/introduction.md | 9 +- book/src/mouse.md | 4 +- book/src/screen-sharing.md | 111 - book/src/troubleshooting.md | 90 - book/src/window-rules.md | 154 - book/src/workspaces.md | 9 +- cmm/Cargo.toml | 8 + {src/cmm => cmm/src}/cmm_description.rs | 16 +- {src/cmm => cmm/src}/cmm_eotf.rs | 2 +- {src/cmm => cmm/src}/cmm_luminance.rs | 9 +- {src/cmm => cmm/src}/cmm_manager.rs | 16 +- {src/cmm => cmm/src}/cmm_primaries.rs | 2 +- {src/cmm => cmm/src}/cmm_render_intent.rs | 21 - {src/cmm => cmm/src}/cmm_tests.rs | 4 +- {src/cmm => cmm/src}/cmm_transform.rs | 31 +- cmm/src/lib.rs | 53 + criteria/Cargo.toml | 12 + {src/criteria => criteria/src}/crit_graph.rs | 0 .../src}/crit_graph/crit_downstream.rs | 2 +- .../src}/crit_graph/crit_middle.rs | 2 +- .../src}/crit_graph/crit_root.rs | 2 +- .../src}/crit_graph/crit_target.rs | 8 +- .../src}/crit_graph/crit_upstream.rs | 16 +- {src/criteria => criteria/src}/crit_leaf.rs | 14 +- .../src}/crit_matchers.rs | 0 .../src}/crit_matchers/critm_any_or_all.rs | 2 +- .../src}/crit_matchers/critm_constant.rs | 2 +- .../src}/crit_matchers/critm_exactly.rs | 2 +- .../src}/crit_matchers/critm_string.rs | 2 +- .../src}/crit_per_target_data.rs | 4 +- criteria/src/lib.rs | 131 + edid/Cargo.toml | 11 + edid/src/lib.rs | 1312 +++++++ etc/jay-portals.conf | 6 - etc/jay.portal | 3 - formats/Cargo.toml | 13 + formats/src/lib.rs | 559 +++ geometry/Cargo.toml | 11 + geometry/src/lib.rs | 365 ++ {src/rect => geometry/src}/region.rs | 19 +- {src/rect => geometry/src}/tests.rs | 2 +- jay-config-schema/Cargo.toml | 9 + jay-config-schema/src/animations.rs | 13 + jay-config-schema/src/lib.rs | 9 + jay-config/src/_private.rs | 12 - jay-config/src/_private/client.rs | 71 +- jay-config/src/_private/ipc.rs | 21 +- jay-config/src/client.rs | 88 - jay-config/src/exec.rs | 23 - jay-config/src/input.rs | 2 - jay-config/src/lib.rs | 35 +- jay-config/src/macros.rs | 18 - layout-animation/Cargo.toml | 10 + layout-animation/src/lib.rs | 3410 +++++++++++++++++ src/acceptor.rs | 85 +- src/animation/multiphase.rs | 3406 +--------------- src/cli.rs | 36 +- src/cli/clients.rs | 6 - src/cli/json.rs | 2 - src/cli/run_privileged.rs | 35 - src/cli/run_tagged.rs | 70 - src/client.rs | 68 +- src/cmm.rs | 57 +- src/compositor.rs | 31 +- src/config.rs | 124 +- src/config/handler.rs | 152 +- src/criteria.rs | 94 +- src/criteria/clm.rs | 57 +- src/criteria/clm/clm_matchers.rs | 4 +- src/criteria/clm/clm_matchers/clmm_id.rs | 7 +- .../clm/clm_matchers/clmm_is_xwayland.rs | 5 +- src/criteria/clm/clm_matchers/clmm_pid.rs | 7 +- .../clm/clm_matchers/clmm_sandboxed.rs | 5 +- src/criteria/clm/clm_matchers/clmm_string.rs | 30 +- src/criteria/clm/clm_matchers/clmm_uid.rs | 7 +- src/criteria/tlm.rs | 18 +- src/criteria/tlm/tlm_matchers/tlmm_client.rs | 6 +- src/edid.rs | 1303 +------ src/fixed.rs | 138 +- src/format.rs | 584 +-- src/gfx_apis/gl/gl/sys.rs | 4 - src/gfx_apis/vulkan/renderer.rs | 21 +- src/globals.rs | 7 +- src/ifs.rs | 1 - src/ifs/jay_acceptor_request.rs | 60 - src/ifs/jay_client_query.rs | 12 +- src/ifs/jay_compositor.rs | 22 - src/ifs/wl_registry.rs | 8 +- .../wp_color_management_surface_v1.rs | 4 +- src/ifs/wp_security_context_v1.rs | 1 - src/it.rs | 2 +- src/macros.rs | 88 +- src/main.rs | 3 - src/pipewire.rs | 7 - src/pipewire/pw_con.rs | 453 --- src/pipewire/pw_formatter.rs | 312 -- src/pipewire/pw_ifs.rs | 4 - src/pipewire/pw_ifs/pw_client.rs | 61 - src/pipewire/pw_ifs/pw_client_node.rs | 892 ----- src/pipewire/pw_ifs/pw_core.rs | 186 - src/pipewire/pw_ifs/pw_registry.rs | 48 - src/pipewire/pw_mem.rs | 155 - src/pipewire/pw_object.rs | 52 - src/pipewire/pw_parser.rs | 312 -- src/pipewire/pw_pod.rs | 1451 ------- src/pipewire/pw_pod/pw_debug.rs | 464 --- src/portal.rs | 353 -- src/portal/ptl_display.rs | 562 --- src/portal/ptl_remote_desktop.rs | 328 -- .../ptl_remote_desktop/remote_desktop_gui.rs | 160 - src/portal/ptl_render_ctx.rs | 17 - src/portal/ptl_screencast.rs | 967 ----- src/portal/ptl_screencast/screencast_gui.rs | 297 -- src/portal/ptl_session.rs | 169 - src/portal/ptl_text.rs | 102 - src/portal/ptr_gui.rs | 947 ----- src/rect.rs | 369 +- src/scale.rs | 73 +- src/security_context_acceptor.rs | 13 +- src/state.rs | 35 +- src/tagged_acceptor.rs | 199 - src/time.rs | 120 +- src/tools/tool_client.rs | 7 +- src/tree/toplevel.rs | 2 +- src/utils.rs | 134 +- src/xwayland.rs | 45 +- time/Cargo.toml | 8 + time/src/lib.rs | 119 + toml-config/Cargo.toml | 4 +- toml-config/src/config.rs | 24 +- toml-config/src/config/parsers.rs | 1 - toml-config/src/config/parsers/action.rs | 1 - .../src/config/parsers/capabilities.rs | 70 - .../src/config/parsers/client_match.rs | 6 - toml-config/src/config/parsers/client_rule.rs | 39 +- toml-config/src/config/parsers/exec.rs | 23 +- toml-config/src/lib.rs | 13 +- toml-config/src/rules.rs | 8 - toml-spec/spec/spec.generated.json | 67 +- toml-spec/spec/spec.generated.md | 1130 +++--- toml-spec/spec/spec.yaml | 133 - units/Cargo.toml | 7 + units/src/fixed.rs | 137 + units/src/lib.rs | 4 + units/src/scale.rs | 72 + utils/Cargo.toml | 26 + {src/utils => utils/src}/array.rs | 0 {src/utils => utils/src}/array_to_tuple.rs | 0 {src/utils => utils/src}/asyncevent.rs | 2 +- {src/utils => utils/src}/atomic_enum.rs | 0 {src/utils => utils/src}/binary_search_map.rs | 2 +- {src/utils => utils/src}/bitfield.rs | 0 {src/utils => utils/src}/bitflags.rs | 0 {src/utils => utils/src}/buf.rs | 3 +- {src/utils => utils/src}/cell_ext.rs | 2 +- {src/utils => utils/src}/clonecell.rs | 12 +- {src/utils => utils/src}/compat.rs | 0 {src/utils => utils/src}/copyhashmap.rs | 2 +- {src/utils => utils/src}/double_buffered.rs | 0 {src/utils => utils/src}/errorfmt.rs | 0 {src/utils => utils/src}/fdcloser.rs | 0 {src/utils => utils/src}/free_list.rs | 4 +- {src/utils => utils/src}/free_list/tests.rs | 2 +- {src/utils => utils/src}/geometric_decay.rs | 0 {src/utils => utils/src}/hash_map_ext.rs | 0 utils/src/lib.rs | 56 + {src/utils => utils/src}/log_on_drop.rs | 1 - {src/utils => utils/src}/mmap.rs | 2 +- {src/utils => utils/src}/nice.rs | 12 - {src/utils => utils/src}/nonblock.rs | 2 +- {src/utils => utils/src}/num_cpus.rs | 3 +- {src/utils => utils/src}/numcell.rs | 0 {src/utils => utils/src}/on_change.rs | 2 +- {src/utils => utils/src}/on_drop_event.rs | 2 +- {src/utils => utils/src}/once.rs | 0 {src/utils => utils/src}/opaque.rs | 2 +- {src/utils => utils/src}/opaque/tests.rs | 2 +- {src/utils => utils/src}/opaque_cell.rs | 0 {src/utils => utils/src}/opt.rs | 2 +- {src/utils => utils/src}/option_ext.rs | 0 {src/utils => utils/src}/ordered_float.rs | 0 {src/utils => utils/src}/oserror.rs | 0 {src/utils => utils/src}/page_size.rs | 0 {src/utils => utils/src}/pid_info.rs | 2 +- {src/utils => utils/src}/pidfd_send_signal.rs | 2 +- {src/utils => utils/src}/pipe.rs | 2 +- {src/utils => utils/src}/process_name.rs | 0 {src/utils => utils/src}/ptr_ext.rs | 0 {src/utils => utils/src}/queue.rs | 2 +- {src/utils => utils/src}/rc_eq.rs | 0 {src/utils => utils/src}/refcounted.rs | 2 +- {src/utils => utils/src}/smallmap.rs | 2 +- {src/utils => utils/src}/stack.rs | 3 +- {src/utils => utils/src}/static_text.rs | 0 {src/utils => utils/src}/string_ext.rs | 0 {src/utils => utils/src}/syncqueue.rs | 2 +- {src/utils => utils/src}/threshold_counter.rs | 2 +- {src/utils => utils/src}/tri.rs | 1 - {src/utils => utils/src}/unlink_on_drop.rs | 0 {src/utils => utils/src}/vec_ext.rs | 0 {src/utils => utils/src}/vecdeque_ext.rs | 0 {src/utils => utils/src}/vecset.rs | 0 {src/utils => utils/src}/vecstorage.rs | 30 + {src/utils => utils/src}/windows.rs | 3 +- {src/utils => utils/src}/xrd.rs | 0 ....freedesktop.impl.portal.RemoteDesktop.txt | 41 - .../org.freedesktop.impl.portal.Request.txt | 1 - ...org.freedesktop.impl.portal.ScreenCast.txt | 34 - .../org.freedesktop.impl.portal.Session.txt | 9 - wire/jay_acceptor_request.txt | 9 - wire/jay_client_query.txt | 4 - wire/jay_compositor.txt | 5 - 225 files changed, 7422 insertions(+), 17602 deletions(-) delete mode 100644 book/src/screen-sharing.md create mode 100644 cmm/Cargo.toml rename {src/cmm => cmm/src}/cmm_description.rs (85%) rename {src/cmm => cmm/src}/cmm_eotf.rs (97%) rename {src/cmm => cmm/src}/cmm_luminance.rs (93%) rename {src/cmm => cmm/src}/cmm_manager.rs (94%) rename {src/cmm => cmm/src}/cmm_primaries.rs (98%) rename {src/cmm => cmm/src}/cmm_render_intent.rs (50%) rename {src/cmm => cmm/src}/cmm_tests.rs (98%) rename {src/cmm => cmm/src}/cmm_transform.rs (90%) create mode 100644 cmm/src/lib.rs create mode 100644 criteria/Cargo.toml rename {src/criteria => criteria/src}/crit_graph.rs (100%) rename {src/criteria => criteria/src}/crit_graph/crit_downstream.rs (98%) rename {src/criteria => criteria/src}/crit_graph/crit_middle.rs (99%) rename {src/criteria => criteria/src}/crit_graph/crit_root.rs (99%) rename {src/criteria => criteria/src}/crit_graph/crit_target.rs (82%) rename {src/criteria => criteria/src}/crit_graph/crit_upstream.rs (92%) rename {src/criteria => criteria/src}/crit_leaf.rs (91%) rename {src/criteria => criteria/src}/crit_matchers.rs (100%) rename {src/criteria => criteria/src}/crit_matchers/critm_any_or_all.rs (94%) rename {src/criteria => criteria/src}/crit_matchers/critm_constant.rs (98%) rename {src/criteria => criteria/src}/crit_matchers/critm_exactly.rs (93%) rename {src/criteria => criteria/src}/crit_matchers/critm_string.rs (97%) rename {src/criteria => criteria/src}/crit_per_target_data.rs (97%) create mode 100644 criteria/src/lib.rs create mode 100644 edid/Cargo.toml create mode 100644 edid/src/lib.rs delete mode 100644 etc/jay-portals.conf delete mode 100644 etc/jay.portal create mode 100644 formats/Cargo.toml create mode 100644 formats/src/lib.rs create mode 100644 geometry/Cargo.toml create mode 100644 geometry/src/lib.rs rename {src/rect => geometry/src}/region.rs (94%) rename {src/rect => geometry/src}/tests.rs (99%) create mode 100644 jay-config-schema/Cargo.toml create mode 100644 jay-config-schema/src/animations.rs create mode 100644 jay-config-schema/src/lib.rs create mode 100644 layout-animation/Cargo.toml create mode 100644 layout-animation/src/lib.rs delete mode 100644 src/cli/run_privileged.rs delete mode 100644 src/cli/run_tagged.rs delete mode 100644 src/ifs/jay_acceptor_request.rs delete mode 100644 src/pipewire.rs delete mode 100644 src/pipewire/pw_con.rs delete mode 100644 src/pipewire/pw_formatter.rs delete mode 100644 src/pipewire/pw_ifs.rs delete mode 100644 src/pipewire/pw_ifs/pw_client.rs delete mode 100644 src/pipewire/pw_ifs/pw_client_node.rs delete mode 100644 src/pipewire/pw_ifs/pw_core.rs delete mode 100644 src/pipewire/pw_ifs/pw_registry.rs delete mode 100644 src/pipewire/pw_mem.rs delete mode 100644 src/pipewire/pw_object.rs delete mode 100644 src/pipewire/pw_parser.rs delete mode 100644 src/pipewire/pw_pod.rs delete mode 100644 src/pipewire/pw_pod/pw_debug.rs delete mode 100644 src/portal.rs delete mode 100644 src/portal/ptl_display.rs delete mode 100644 src/portal/ptl_remote_desktop.rs delete mode 100644 src/portal/ptl_remote_desktop/remote_desktop_gui.rs delete mode 100644 src/portal/ptl_render_ctx.rs delete mode 100644 src/portal/ptl_screencast.rs delete mode 100644 src/portal/ptl_screencast/screencast_gui.rs delete mode 100644 src/portal/ptl_session.rs delete mode 100644 src/portal/ptl_text.rs delete mode 100644 src/portal/ptr_gui.rs delete mode 100644 src/tagged_acceptor.rs create mode 100644 time/Cargo.toml create mode 100644 time/src/lib.rs delete mode 100644 toml-config/src/config/parsers/capabilities.rs create mode 100644 units/Cargo.toml create mode 100644 units/src/fixed.rs create mode 100644 units/src/lib.rs create mode 100644 units/src/scale.rs create mode 100644 utils/Cargo.toml rename {src/utils => utils/src}/array.rs (100%) rename {src/utils => utils/src}/array_to_tuple.rs (100%) rename {src/utils => utils/src}/asyncevent.rs (97%) rename {src/utils => utils/src}/atomic_enum.rs (100%) rename {src/utils => utils/src}/binary_search_map.rs (99%) rename {src/utils => utils/src}/bitfield.rs (100%) rename {src/utils => utils/src}/bitflags.rs (100%) rename {src/utils => utils/src}/buf.rs (98%) rename {src/utils => utils/src}/cell_ext.rs (83%) rename {src/utils => utils/src}/clonecell.rs (89%) rename {src/utils => utils/src}/compat.rs (100%) rename {src/utils => utils/src}/copyhashmap.rs (99%) rename {src/utils => utils/src}/double_buffered.rs (100%) rename {src/utils => utils/src}/errorfmt.rs (100%) rename {src/utils => utils/src}/fdcloser.rs (100%) rename {src/utils => utils/src}/free_list.rs (94%) rename {src/utils => utils/src}/free_list/tests.rs (95%) rename {src/utils => utils/src}/geometric_decay.rs (100%) rename {src/utils => utils/src}/hash_map_ext.rs (100%) create mode 100644 utils/src/lib.rs rename {src/utils => utils/src}/log_on_drop.rs (86%) rename {src/utils => utils/src}/mmap.rs (92%) rename {src/utils => utils/src}/nice.rs (66%) rename {src/utils => utils/src}/nonblock.rs (88%) rename {src/utils => utils/src}/num_cpus.rs (83%) rename {src/utils => utils/src}/numcell.rs (100%) rename {src/utils => utils/src}/on_change.rs (93%) rename {src/utils => utils/src}/on_drop_event.rs (81%) rename {src/utils => utils/src}/once.rs (100%) rename {src/utils => utils/src}/opaque.rs (99%) rename {src/utils => utils/src}/opaque/tests.rs (80%) rename {src/utils => utils/src}/opaque_cell.rs (100%) rename {src/utils => utils/src}/opt.rs (88%) rename {src/utils => utils/src}/option_ext.rs (100%) rename {src/utils => utils/src}/ordered_float.rs (100%) rename {src/utils => utils/src}/oserror.rs (100%) rename {src/utils => utils/src}/page_size.rs (100%) rename {src/utils => utils/src}/pid_info.rs (96%) rename {src/utils => utils/src}/pidfd_send_signal.rs (90%) rename {src/utils => utils/src}/pipe.rs (93%) rename {src/utils => utils/src}/process_name.rs (100%) rename {src/utils => utils/src}/ptr_ext.rs (100%) rename {src/utils => utils/src}/queue.rs (98%) rename {src/utils => utils/src}/rc_eq.rs (100%) rename {src/utils => utils/src}/refcounted.rs (97%) rename {src/utils => utils/src}/smallmap.rs (99%) rename {src/utils => utils/src}/stack.rs (93%) rename {src/utils => utils/src}/static_text.rs (100%) rename {src/utils => utils/src}/string_ext.rs (100%) rename {src/utils => utils/src}/syncqueue.rs (97%) rename {src/utils => utils/src}/threshold_counter.rs (92%) rename {src/utils => utils/src}/tri.rs (97%) rename {src/utils => utils/src}/unlink_on_drop.rs (100%) rename {src/utils => utils/src}/vec_ext.rs (100%) rename {src/utils => utils/src}/vecdeque_ext.rs (100%) rename {src/utils => utils/src}/vecset.rs (100%) rename {src/utils => utils/src}/vecstorage.rs (62%) rename {src/utils => utils/src}/windows.rs (92%) rename {src/utils => utils/src}/xrd.rs (100%) delete mode 100644 wire-dbus/org.freedesktop.impl.portal.RemoteDesktop.txt delete mode 100644 wire-dbus/org.freedesktop.impl.portal.Request.txt delete mode 100644 wire-dbus/org.freedesktop.impl.portal.ScreenCast.txt delete mode 100644 wire-dbus/org.freedesktop.impl.portal.Session.txt delete mode 100644 wire/jay_acceptor_request.txt diff --git a/Cargo.lock b/Cargo.lock index defd9291..6071ac1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,6 +639,13 @@ dependencies = [ "libloading", ] +[[package]] +name = "jay-cmm" +version = "0.1.0" +dependencies = [ + "jay-utils", +] + [[package]] name = "jay-compositor" version = "1.12.0" @@ -665,8 +672,17 @@ dependencies = [ "isnt 0.2.0", "jay-algorithms", "jay-ash", + "jay-cmm", "jay-config", + "jay-criteria", + "jay-edid", + "jay-formats", + "jay-geometry", + "jay-layout-animation", + "jay-time", "jay-toml-config", + "jay-units", + "jay-utils", "kbvm", "libloading", "linearize", @@ -711,6 +727,60 @@ dependencies = [ "uapi", ] +[[package]] +name = "jay-config-schema" +version = "0.1.0" + +[[package]] +name = "jay-criteria" +version = "0.1.0" +dependencies = [ + "ahash", + "jay-utils", + "linearize", + "regex", +] + +[[package]] +name = "jay-edid" +version = "0.1.0" +dependencies = [ + "bstr", + "thiserror", +] + +[[package]] +name = "jay-formats" +version = "0.1.0" +dependencies = [ + "ahash", + "clap", + "jay-ash", + "jay-config", +] + +[[package]] +name = "jay-geometry" +version = "0.1.0" +dependencies = [ + "jay-algorithms", + "smallvec", +] + +[[package]] +name = "jay-layout-animation" +version = "0.1.0" +dependencies = [ + "jay-geometry", +] + +[[package]] +name = "jay-time" +version = "0.1.0" +dependencies = [ + "uapi", +] + [[package]] name = "jay-toml-config" version = "0.12.0" @@ -720,6 +790,7 @@ dependencies = [ "error_reporter", "indexmap", "jay-config", + "jay-config-schema", "kbvm", "log", "phf", @@ -731,6 +802,30 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jay-units" +version = "0.1.0" + +[[package]] +name = "jay-utils" +version = "0.1.0" +dependencies = [ + "ahash", + "arrayvec", + "bstr", + "cfg-if", + "isnt 0.2.0", + "jay-config", + "linearize", + "log", + "parking_lot", + "rand 0.10.0", + "serde", + "smallvec", + "thiserror", + "uapi", +] + [[package]] name = "js-sys" version = "0.3.91" diff --git a/Cargo.toml b/Cargo.toml index 9a1b38a3..6939361e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,25 @@ name = "jay" path = "src/main.rs" [workspace] -members = ["jay-config", "toml-config", "algorithms", "toml-spec", "wire-to-xml", "xml-to-wire"] +resolver = "3" +members = [ + "jay-config", + "jay-config-schema", + "geometry", + "layout-animation", + "formats", + "edid", + "units", + "utils", + "criteria", + "cmm", + "time", + "toml-config", + "algorithms", + "toml-spec", + "wire-to-xml", + "xml-to-wire", +] [profile.release] panic = "abort" @@ -26,6 +44,15 @@ panic = "abort" jay-config = { version = "1.10.0", path = "jay-config" } jay-toml-config = { version = "0.12.0", path = "toml-config" } jay-algorithms = { version = "0.4.0", path = "algorithms" } +jay-geometry = { version = "0.1.0", path = "geometry" } +jay-layout-animation = { version = "0.1.0", path = "layout-animation" } +jay-formats = { version = "0.1.0", path = "formats" } +jay-edid = { version = "0.1.0", path = "edid" } +jay-units = { version = "0.1.0", path = "units" } +jay-utils = { version = "0.1.0", path = "utils" } +jay-criteria = { version = "0.1.0", path = "criteria" } +jay-cmm = { version = "0.1.0", path = "cmm" } +jay-time = { version = "0.1.0", path = "time" } uapi = "0.2.13" thiserror = "2.0.11" diff --git a/README.md b/README.md index 14ae7ba4..56a5f577 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![crates.io](https://img.shields.io/crates/v/jay-compositor.svg)](http://crates.io/crates/jay-compositor) Jay is a Wayland compositor for Linux with an i3-like tiling layout, -Vulkan and OpenGL rendering, multi-GPU support, screen sharing, and more. +Vulkan and OpenGL rendering, multi-GPU support, and more. ![screenshot.png](static/screenshot.png) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 2bb5904a..29e51133 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -35,7 +35,6 @@ - [Mouse Interactions](mouse.md) - [Input Modes](input-modes.md) - [Window & Client Rules](window-rules.md) -- [Screen Sharing](screen-sharing.md) - [HDR & Color Management](hdr.md) # Reference diff --git a/book/src/cli.md b/book/src/cli.md index c7c222a6..e2b8697f 100644 --- a/book/src/cli.md +++ b/book/src/cli.md @@ -674,18 +674,6 @@ Show color management status: ## Other Commands -### `jay portal` - -Run the Jay desktop portal (provides screen sharing and other XDG desktop -portal interfaces): - -```shell -~$ jay portal -``` - -Normally the portal is started automatically. This command is for running it -manually or debugging. - ### `jay seat-test` Test input events from a seat. Prints all keyboard, pointer, touch, gesture, @@ -697,24 +685,6 @@ tablet, and switch events to stdout: ~$ jay seat-test -a # test all seats simultaneously ``` -### `jay run-privileged` - -Run a program with access to a privileged Wayland socket: - -```shell -~$ jay run-privileged my-program --arg1 -``` - -### `jay run-tagged` - -Run a program with a tagged Wayland connection. All Wayland connections from the -spawned process tree will carry the specified tag, which can be matched in -[client rules](window-rules.md): - -```shell -~$ jay run-tagged my-tag firefox -``` - ### `jay generate-completion` Generate shell completion scripts: diff --git a/book/src/configuration/idle.md b/book/src/configuration/idle.md index 033d1120..d67bfdc9 100644 --- a/book/src/configuration/idle.md +++ b/book/src/configuration/idle.md @@ -62,16 +62,10 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` -> [!IMPORTANT] -> Screen lockers that use the Wayland session lock protocol (like swaylock) -> need `privileged = true` in the exec configuration. This grants the process -> the necessary permissions to lock the session. - You can also combine multiple actions: ```toml @@ -80,7 +74,6 @@ on-idle = [ type = "exec", exec = { prog = "swaylock", - privileged = true, }, }, { type = "exec", exec = ["notify-send", "System locked"] }, @@ -100,7 +93,6 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` diff --git a/book/src/configuration/misc.md b/book/src/configuration/misc.md index 9974d3f7..33f0c8ba 100644 --- a/book/src/configuration/misc.md +++ b/book/src/configuration/misc.md @@ -30,9 +30,8 @@ the color management protocol. ## Libei [libei](https://gitlab.freedesktop.org/libinput/libei) allows applications to -emulate input events. By default, applications can only access libei through -the portal (which prompts the user for permission). Setting `enable-socket` -exposes an unauthenticated socket that any application can use without a prompt. +emulate input events. Setting `enable-socket` exposes an unauthenticated socket +that any application can use. ```toml libei.enable-socket = false # default diff --git a/book/src/configuration/shortcuts.md b/book/src/configuration/shortcuts.md index 6d372be6..b36f4857 100644 --- a/book/src/configuration/shortcuts.md +++ b/book/src/configuration/shortcuts.md @@ -145,7 +145,6 @@ alt-shift-r = "reload-config-toml" (the next pressed key identifies the mark). See [Marks](#marks) below. - `enable-window-management`, `disable-window-management` -- programmatically enable or disable [window management mode](../floating.md#window-management-mode) -- `reload-config-so` -- reload the shared-library configuration (`config.so`) See the [specification](https://github.com/mahkoh/jay/blob/master/toml-spec/spec/spec.generated.md) for the full list of simple actions. @@ -309,7 +308,6 @@ alt-s = { type = "exec", exec = { shell = "grim - | wl-copy", - privileged = true, }, } ``` @@ -328,12 +326,6 @@ Table fields: `env` : Per-process environment variables -`privileged` -: If `true`, grants access to privileged Wayland protocols (default: `false`) - -`tag` -: Tag to apply to all Wayland connections spawned by this process - ### Practical examples Volume control with `pactl`: @@ -362,7 +354,6 @@ Print = { type = "exec", exec = { shell = "grim - | wl-copy", - privileged = true, }, } ``` diff --git a/book/src/configuration/startup.md b/book/src/configuration/startup.md index 9b4c333a..93408650 100644 --- a/book/src/configuration/startup.md +++ b/book/src/configuration/startup.md @@ -63,15 +63,10 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` -> [!NOTE] -> Screen lockers need `privileged = true` to access the privileged Wayland -> protocols required for locking the session. - You can combine idle with a grace period. The idle timeout and grace period are configured separately in the `[idle]` section (see [Idle & Screen Locking](idle.md)): @@ -83,7 +78,6 @@ on-idle = { type = "exec", exec = { prog = "swaylock", - privileged = true, }, } ``` @@ -97,7 +91,6 @@ on-idle = [ type = "exec", exec = { prog = "swaylock", - privileged = true, }, }, ] diff --git a/book/src/configuration/theme.md b/book/src/configuration/theme.md index 69448f5d..efd62d48 100644 --- a/book/src/configuration/theme.md +++ b/book/src/configuration/theme.md @@ -73,7 +73,7 @@ The available color keys in the `[theme]` table are: "Focused-inactive" refers to a window that was most recently focused in its container but whose container is not the active one. The "captured" colors apply -when a window is being recorded (e.g. via screen sharing). +when a window is being captured. ### Example diff --git a/book/src/configuration/xwayland.md b/book/src/configuration/xwayland.md index 8f5817af..2814a3b7 100644 --- a/book/src/configuration/xwayland.md +++ b/book/src/configuration/xwayland.md @@ -87,5 +87,5 @@ Xwayland client itself: ```toml [[clients]] match.is-xwayland = true -# ... grant capabilities, etc. +# ... configure client-specific behavior ``` diff --git a/book/src/features.md b/book/src/features.md index 1e7c69a3..6e15b0aa 100644 --- a/book/src/features.md +++ b/book/src/features.md @@ -61,10 +61,7 @@ Commands: unlock Unlocks the compositor screenshot Take a screenshot idle Inspect/modify the idle (screensaver) settings - run-privileged Run a privileged program - run-tagged Run a program with a connection tag seat-test Tests the events produced by a seat - portal Run the desktop portal randr Inspect/modify graphics card and connector settings input Inspect/modify input settings xwayland Inspect/modify xwayland settings @@ -101,17 +98,6 @@ runtime. See [GPUs](configuration/gpu.md) for details. -## Screen Sharing - -Jay supports screen sharing via xdg-desktop-portal. Three capture modes are -available: - -- **Window capture** -- share a single window. -- **Output capture** -- share an entire monitor. -- **Workspace capture** -- like output capture, but only one workspace is shown. - -See [Screen Sharing](screen-sharing.md) for setup instructions. - ## Screen Locking Jay can automatically lock your screen and disable outputs after inactivity. @@ -154,20 +140,6 @@ Jay supports running X11 applications seamlessly through Xwayland. See Jay supports clipboard managers via the `zwlr_data_control_manager_v1` and `ext_data_control_manager_v1` protocols. -## Privilege Separation - -Jay splits protocols into unprivileged and privileged protocols. By default, -applications only have access to unprivileged protocols. This means that tools -like screen lockers, status bars, and clipboard managers need to be explicitly -granted access. - -Jay provides several ways to grant privileges, from giving a program full -access to all privileged protocols down to granting individual capabilities to -specific tagged processes. See -[Granting Privileges](window-rules.md#granting-privileges) for a detailed -guide and the [Protocol Support](#protocol-support) section below for the full -list of protocols and their privilege requirements. - ## Push to Talk Jay's shortcut system allows you to execute an action when a key is pressed and diff --git a/book/src/installation.md b/book/src/installation.md index e29ec0b2..f331b284 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -63,7 +63,6 @@ For Vulkan, you also need the driver for your GPU: - **Linux 6.7 or later** -- required for explicit sync (needed for Nvidia GPUs). - **Xwayland** -- required for running X11 applications. -- **PipeWire** -- required for screen sharing. - **logind** (part of systemd) -- required when running Jay from a virtual terminal or display manager. ## Building @@ -129,14 +128,7 @@ retains `CAP_SYS_NICE` solely for creating elevated Vulkan queues later. > [!NOTE] > You need to re-run the `setcap` command each time you update the Jay binary. -### SCHED_RR and config.so - -`SCHED_RR` and `config.so` are mutually exclusive: running untrusted code at -real-time priority would be a security risk. Jay enforces this as follows: - -- If `config.so` exists in the config directory, Jay skips the `SCHED_RR` - elevation (elevated Vulkan queues are still created). -- If Jay has already elevated to `SCHED_RR`, it refuses to load `config.so`. +### SCHED_RR You can also skip `SCHED_RR` explicitly by setting `JAY_NO_REALTIME=1`: @@ -144,11 +136,7 @@ You can also skip `SCHED_RR` explicitly by setting `JAY_NO_REALTIME=1`: ~$ JAY_NO_REALTIME=1 jay run ``` -This still allows elevated Vulkan queues and does not affect `config.so` -loading. - -The mutual exclusion can be overridden at compile time by building Jay with -`JAY_ALLOW_REALTIME_CONFIG_SO=1`. +This still allows elevated Vulkan queues. ## Recommended Applications @@ -156,7 +144,6 @@ The following applications work well with Jay: - **[Alacritty](https://alacritty.org/)** -- the default terminal emulator in the built-in configuration. - **[bemenu](https://github.com/Cloudef/bemenu)** -- the default application launcher in the built-in configuration. -- **[xdg-desktop-portal-gtk4](https://github.com/mahkoh/xdg-desktop-portal-gtk4)** -- a file-picker portal with thumbnail support. Used automatically when installed. - **[wl-tray-bridge](https://github.com/mahkoh/wl-tray-bridge)** -- shows D-Bus StatusNotifierItem applications as tray icons. - **[mako](https://github.com/emersion/mako)** -- a notification daemon. Launched automatically by the default configuration. - **[window-to-tray](https://github.com/mahkoh/wl-proxy/tree/master/apps/window-to-tray)** -- run most Wayland applications as tray applications (e.g. `window-to-tray pavucontrol-qt`). diff --git a/book/src/introduction.md b/book/src/introduction.md index 306a5a46..9333dcc6 100644 --- a/book/src/introduction.md +++ b/book/src/introduction.md @@ -22,12 +22,11 @@ Jay is a Wayland compositor for Linux with an i3-inspired tiling layout. It supports Vulkan and OpenGL rendering, multi-GPU setups, fractional scaling, -variable refresh rate (VRR), tearing presentation, HDR, and screen sharing via -xdg-desktop-portal. X11 applications are supported through Xwayland. +variable refresh rate (VRR), tearing presentation, and HDR. X11 applications +are supported through Xwayland. -Jay is configured through a declarative TOML file, with an optional advanced -mode that uses a shared library for programmatic control. A comprehensive -command-line interface makes scripting and automation straightforward. +Jay is configured through a declarative TOML file. A comprehensive command-line +interface makes scripting and automation straightforward. See the [Features](features.md) chapter for a comprehensive overview of what Jay can do, or jump straight to [Installation](installation.md) to get started. diff --git a/book/src/mouse.md b/book/src/mouse.md index bfa92cd5..1b01a054 100644 --- a/book/src/mouse.md +++ b/book/src/mouse.md @@ -84,8 +84,8 @@ This is especially useful for: ## Other -**Toplevel selection.** Some actions (like screen sharing) ask you to select a -window, indicated by a purple overlay. During this selection, right-click a +**Toplevel selection.** Some actions ask you to select a window, indicated by a +purple overlay. During this selection, right-click a tile's title to select the entire container instead of an individual tile. **Canceling interactions.** Press `Escape` to cancel any in-progress mouse diff --git a/book/src/screen-sharing.md b/book/src/screen-sharing.md deleted file mode 100644 index 096c67de..00000000 --- a/book/src/screen-sharing.md +++ /dev/null @@ -1,111 +0,0 @@ -# Screen Sharing - -Jay supports screen sharing via -[xdg-desktop-portal](https://github.com/flatpak/xdg-desktop-portal). Three -capture types are available: - -- **Window capture** -- share a single window. -- **Output capture** -- share an entire monitor. -- **Workspace capture** -- like output capture, but only a single workspace is - shown. - -## Requirements - -[PipeWire](https://pipewire.org/) must be installed and running. Verify with: - -```shell -~$ systemctl --user status pipewire -``` - -## Portal Setup - -Jay implements its own portal backend for the `ScreenCast` and `RemoteDesktop` -interfaces. Two configuration files must be installed so that -`xdg-desktop-portal` knows to use Jay's backend. - -### If the Repository is Checked Out - -```shell -~$ sudo cp etc/jay.portal /usr/share/xdg-desktop-portal/portals/jay.portal -~$ sudo cp etc/jay-portals.conf /usr/share/xdg-desktop-portal/jay-portals.conf -``` - -### If Installed via cargo install - -Create the files manually: - -```shell -~$ sudo tee /usr/share/xdg-desktop-portal/portals/jay.portal > /dev/null << 'EOF' -[portal] -DBusName=org.freedesktop.impl.portal.desktop.jay -Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; -EOF -``` - -```shell -~$ sudo tee /usr/share/xdg-desktop-portal/jay-portals.conf > /dev/null << 'EOF' -[preferred] -default=gtk -org.freedesktop.impl.portal.ScreenCast=jay -org.freedesktop.impl.portal.RemoteDesktop=jay -org.freedesktop.impl.portal.Inhibit=none -org.freedesktop.impl.portal.FileChooser=gtk4 -EOF -``` - -### Restart the Portal - -After installing the files, restart the portal service: - -```shell -~$ systemctl --user restart xdg-desktop-portal -``` - -## Configuration - -### workspace-capture - -The top-level `workspace-capture` setting controls whether newly created -workspaces can be captured via workspace capture. The default is `true`: - -```toml -workspace-capture = false -``` - -Set this to `false` if you want to prevent workspace-level capture by default. - -### Capture Indicator Colors - -When a window is being recorded, its title bar color changes to make the -capture visually obvious. You can customize these colors in the `[theme]` -table: - -```toml -[theme] -captured-focused-title-bg-color = "#900000" -captured-unfocused-title-bg-color = "#5f0000" -``` - -- `captured-focused-title-bg-color` -- background color of focused title bars - that are being recorded. -- `captured-unfocused-title-bg-color` -- background color of unfocused title - bars that are being recorded. - -## The jay portal Command - -Jay's portal backend is normally started automatically when a screen-sharing -request comes in via D-Bus activation. If you need to start it manually for -debugging purposes: - -```shell -~$ jay portal -``` - -## Troubleshooting - -If screen sharing does not work: - -1. Verify PipeWire is running: `systemctl --user status pipewire` -2. Verify the portal files are installed in `/usr/share/xdg-desktop-portal/`. -3. Restart the portal: `systemctl --user restart xdg-desktop-portal` -4. Check the Jay log for errors: `jay log` diff --git a/book/src/troubleshooting.md b/book/src/troubleshooting.md index 3f973bb5..a20a33fd 100644 --- a/book/src/troubleshooting.md +++ b/book/src/troubleshooting.md @@ -54,57 +54,6 @@ bindings. > when any config file exists. Always use `jay config init` to start with a > working configuration. -## Application doesn't have access to a protocol - -Jay splits Wayland protocols into unprivileged and privileged. By default, -applications only have access to unprivileged protocols. If a program like a -screen locker, status bar, clipboard manager, or screen-capture tool is not -working, it likely needs access to one or more privileged protocols. - -Common symptoms include: - -- **swaylock** does nothing or fails to lock the screen (needs `session-lock`). -- **waybar** or **i3bar** shows no workspace information (needs - `foreign-toplevel-list`). -- **wl-copy** / **cliphist** cannot access the clipboard (needs - `data-control`). -- **grim** or **slurp** cannot capture the screen (needs `screencopy`). - -**Quick fix -- grant all privileges:** - -The simplest approach is to launch the program with full access to all -privileged protocols. In your config, set `privileged = true` in the exec -action: - -```toml -on-idle = { - type = "exec", - exec = { - prog = "swaylock", - privileged = true, - }, -} -``` - -Or from the command line: - -```shell -~$ jay run-privileged waybar -``` - -**Better fix -- grant only the capabilities needed:** - -Use a client rule to grant specific capabilities: - -```toml -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - -See [Granting Privileges](window-rules.md#granting-privileges) for the full -list of capabilities and more advanced approaches using connection tags. - ## Wrong keyboard layout The default keyboard layout is US QWERTY. To change it: @@ -132,45 +81,6 @@ layout = "de" This takes effect immediately but does not persist across restarts unless configured in the config file. -## Screen sharing doesn't work - -Screen sharing requires PipeWire and the Jay desktop portal. - -**1. Check that PipeWire is running:** - -```shell -~$ systemctl --user status pipewire -``` - -If it is not running, start it: - -```shell -~$ systemctl --user start pipewire -``` - -**2. Check that the portal files are installed:** - -Jay needs two files to be found by the XDG desktop portal framework: - -- A portal definition file (e.g. `/usr/share/xdg-desktop-portal/portals/jay.portal`). -- A portal configuration file (e.g. `/usr/share/xdg-desktop-portal/jay-portals.conf`). - -These files are included in the Jay repository under `etc/`. If you built Jay -from source and did not install them, copy them manually: - -```shell -~$ sudo cp etc/jay.portal /usr/share/xdg-desktop-portal/portals/ -~$ sudo cp etc/jay-portals.conf /usr/share/xdg-desktop-portal/ -``` - -**3. Restart the portal:** - -```shell -~$ systemctl --user restart xdg-desktop-portal -``` - -See the [Screen Sharing](screen-sharing.md) chapter for more details. - ## X11 applications don't work Jay uses Xwayland to run X11 applications. diff --git a/book/src/window-rules.md b/book/src/window-rules.md index 43770b46..6d3cfca1 100644 --- a/book/src/window-rules.md +++ b/book/src/window-rules.md @@ -31,12 +31,6 @@ Each client rule can have the following fields: `latch` : An action to run when a client stops matching. -`capabilities` -: Wayland protocol access granted to matching clients. - -`sandbox-bounding-capabilities` -: Upper bounds for protocols available to child sandboxes. - ### Client Match Criteria All client match criteria are constant over the lifetime of a client. If no @@ -70,142 +64,6 @@ implicitly AND-combined. `exe` / `exe-regex` : The client's `/proc/pid/exe` path. -`tag` / `tag-regex` -: The connection tag of the client. - -### Granting Privileges - -Jay splits Wayland protocols into unprivileged and privileged. By default, -applications only have access to unprivileged protocols. This means that tools -like screen lockers, status bars, screen-capture utilities, and clipboard -managers will not work unless you explicitly grant them the necessary -privileges. - -See the [Protocol Support](features.md#protocol-support) table in the Features -chapter for the full list of protocols and whether they are privileged. - -There are three ways to grant privileges, from simplest to most fine-grained. - -#### 1. Grant all privileges via `privileged = true` (exec) or `jay run-privileged` - -The simplest approach gives a program access to **all** privileged protocols. -This is appropriate for trusted tools like screen lockers where you don't want -to think about which specific protocols they need. - -In the config, set `privileged = true` in the exec table: - -```toml -on-idle = { - type = "exec", - exec = { - prog = "swaylock", - privileged = true, - }, -} -``` - -From the command line, use `jay run-privileged`: - -```shell -~$ jay run-privileged waybar -``` - -Both methods connect the program to a privileged Wayland socket that grants -access to all privileged protocols. - -#### 2. Grant capabilities via connection tags - -Connection tags let you combine the CLI with client rules for precise control. -You tag a program at launch time, then write a client rule that matches -the tag and grants specific capabilities. - -First, launch the program with a tag -- either from the command line: - -```shell -~$ jay run-tagged bar waybar -``` - -Or from the config using the `tag` field in an exec action: - -```toml -[shortcuts] -alt-w = { - type = "exec", - exec = { - prog = "waybar", - tag = "bar", - }, -} -``` - -Then write a client rule that matches the tag and grants capabilities: - -```toml -[[clients]] -match.tag = "bar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - -This way, only the specific instance you launched with the tag receives the -privileges -- other programs with the same binary name do not. - -Available capability values: `none`, `all`, `data-control`, -`virtual-keyboard`, `foreign-toplevel-list`, `idle-notifier`, `session-lock`, -`layer-shell`, `screencopy`, `seat-manager`, `drm-lease`, `input-method`, -`workspace-manager`, `foreign-toplevel-manager`, `head-manager`, -`gamma-control-manager`, `virtual-pointer`. - -**Default capabilities:** unsandboxed clients receive `layer-shell` and -`drm-lease`. Sandboxed clients receive only `drm-lease`. If any client rule -matches, its capabilities **replace** the defaults entirely. If multiple rules -match, their capabilities are unioned together, but the defaults are not -included unless a matching rule also grants them. - -#### 3. Grant capabilities via client match rules - -Client rules can also match programs by properties like their executable name -instead of a tag. This is convenient when you always want a given program to -have certain capabilities, regardless of how it was launched: - -```toml -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] - -# Vim 9.2 uses the data-control protocol for seamless wayland integration. -[[clients]] -match.comm = "vim" -match.sandboxed = false -capabilities = "data-control" - -# Older versions use wl-copy and wl-paste. -[[clients]] -match.any = [ - { comm = "wl-copy" }, - { comm = "wl-paste" }, -] -match.sandboxed = false -capabilities = "data-control" -``` - -> [!NOTE] -> Client match criteria like `comm`, `exe`, and `pid` are checked when a -> client connects. Any process with a matching name receives the specified -> capabilities. If you need to restrict privileges to programs you launch -> yourself, use connection tags (method 2) instead. - -#### Bounding capabilities (sandboxes) - -Capabilities can never exceed the client's **bounding capabilities**. Use -`sandbox-bounding-capabilities` on a client rule to set the upper bound for -protocols available to sandboxes created by that client: - -```toml -[[clients]] -match.comm = "flatpak-portal" -sandbox-bounding-capabilities = ["drm-lease", "layer-shell"] -``` - ## Window Rules Window rules operate on individual windows. They are defined with `[[windows]]` @@ -456,18 +314,6 @@ action = { } ``` -### Grant Protocol Access to a Trusted App - -```toml -[[clients]] -match.comm = "swaylock" -capabilities = ["session-lock", "layer-shell"] - -[[clients]] -match.comm = "waybar" -capabilities = ["layer-shell", "foreign-toplevel-list"] -``` - ### Suppress Focus Stealing for Chromium Screen-Share Windows ```toml diff --git a/book/src/workspaces.md b/book/src/workspaces.md index 100e13b7..eb1bcfd3 100644 --- a/book/src/workspaces.md +++ b/book/src/workspaces.md @@ -123,16 +123,15 @@ laptop. ## Workspace Capture -By default, newly created workspaces can be captured for screen sharing. You -can disable this globally: +By default, newly created workspaces can be captured by capture clients. You can +disable this globally: ```toml workspace-capture = false ``` -When workspace capture is enabled, screen-sharing applications can share -individual workspaces (in addition to full outputs and individual windows). See -[Screen Sharing](screen-sharing.md) for more details. +When workspace capture is enabled, compositor-native capture clients may capture +individual workspaces instead of whole outputs. ## Matching Windows by Workspace diff --git a/cmm/Cargo.toml b/cmm/Cargo.toml new file mode 100644 index 00000000..e731ba18 --- /dev/null +++ b/cmm/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "jay-cmm" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" + +[dependencies] +jay-utils = { version = "0.1.0", path = "../utils" } diff --git a/src/cmm/cmm_description.rs b/cmm/src/cmm_description.rs similarity index 85% rename from src/cmm/cmm_description.rs rename to cmm/src/cmm_description.rs index 85a0a71a..dc8ecbd4 100644 --- a/src/cmm/cmm_description.rs +++ b/cmm/src/cmm_description.rs @@ -1,15 +1,13 @@ use { crate::{ - cmm::{ - cmm_eotf::Eotf, - cmm_luminance::{Luminance, TargetLuminance, white_balance}, - cmm_manager::Shared, - cmm_primaries::{NamedPrimaries, Primaries}, - cmm_render_intent::RenderIntent, - cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, - }, - utils::ordered_float::F64, + cmm_eotf::Eotf, + cmm_luminance::{Luminance, TargetLuminance, white_balance}, + cmm_manager::Shared, + cmm_primaries::{NamedPrimaries, Primaries}, + cmm_render_intent::RenderIntent, + cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, }, + jay_utils::ordered_float::F64, std::rc::Rc, }; diff --git a/src/cmm/cmm_eotf.rs b/cmm/src/cmm_eotf.rs similarity index 97% rename from src/cmm/cmm_eotf.rs rename to cmm/src/cmm_eotf.rs index 89e123aa..afc26d0d 100644 --- a/src/cmm/cmm_eotf.rs +++ b/cmm/src/cmm_eotf.rs @@ -1,4 +1,4 @@ -use crate::utils::ordered_float::F32; +use jay_utils::ordered_float::F32; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Eotf { diff --git a/src/cmm/cmm_luminance.rs b/cmm/src/cmm_luminance.rs similarity index 93% rename from src/cmm/cmm_luminance.rs rename to cmm/src/cmm_luminance.rs index 8371a33d..584120da 100644 --- a/src/cmm/cmm_luminance.rs +++ b/cmm/src/cmm_luminance.rs @@ -1,10 +1,8 @@ use crate::{ - cmm::{ - cmm_render_intent::RenderIntent, - cmm_transform::{ColorMatrix, Xyz}, - }, - utils::ordered_float::F64, + cmm_render_intent::RenderIntent, + cmm_transform::{ColorMatrix, Xyz}, }; +use jay_utils::ordered_float::F64; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct Luminance { @@ -38,7 +36,6 @@ impl Luminance { white: F64(203.0), }; - #[expect(dead_code)] pub const HLG: Self = Self { min: F64(0.005), max: F64(1000.0), diff --git a/src/cmm/cmm_manager.rs b/cmm/src/cmm_manager.rs similarity index 94% rename from src/cmm/cmm_manager.rs rename to cmm/src/cmm_manager.rs index f73e2c12..8826f9b5 100644 --- a/src/cmm/cmm_manager.rs +++ b/cmm/src/cmm_manager.rs @@ -1,16 +1,14 @@ use { crate::{ - cmm::{ - cmm_description::{ - ColorDescription, ColorDescriptionIds, LinearColorDescription, - LinearColorDescriptionId, LinearColorDescriptionIds, - }, - cmm_eotf::Eotf, - cmm_luminance::{Luminance, TargetLuminance}, - cmm_primaries::{NamedPrimaries, Primaries}, + cmm_description::{ + ColorDescription, ColorDescriptionIds, LinearColorDescription, + LinearColorDescriptionId, LinearColorDescriptionIds, }, - utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64}, + cmm_eotf::Eotf, + cmm_luminance::{Luminance, TargetLuminance}, + cmm_primaries::{NamedPrimaries, Primaries}, }, + jay_utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64}, std::rc::{Rc, Weak}, }; diff --git a/src/cmm/cmm_primaries.rs b/cmm/src/cmm_primaries.rs similarity index 98% rename from src/cmm/cmm_primaries.rs rename to cmm/src/cmm_primaries.rs index 5541d8ee..a39f7450 100644 --- a/src/cmm/cmm_primaries.rs +++ b/cmm/src/cmm_primaries.rs @@ -1,4 +1,4 @@ -use {crate::utils::ordered_float::F64, std::hash::Hash}; +use {jay_utils::ordered_float::F64, std::hash::Hash}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum NamedPrimaries { diff --git a/src/cmm/cmm_render_intent.rs b/cmm/src/cmm_render_intent.rs similarity index 50% rename from src/cmm/cmm_render_intent.rs rename to cmm/src/cmm_render_intent.rs index afb16a30..77fbcf51 100644 --- a/src/cmm/cmm_render_intent.rs +++ b/cmm/src/cmm_render_intent.rs @@ -1,11 +1,3 @@ -use crate::{ - ifs::color_management::{ - ABSOLUTE_NO_ADAPTATION_SINCE, RENDER_INTENT_ABSOLUTE_NO_ADAPTATION, - RENDER_INTENT_PERCEPTUAL, RENDER_INTENT_RELATIVE, RENDER_INTENT_RELATIVE_BPC, - }, - object::Version, -}; - #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] pub enum RenderIntent { #[default] @@ -16,19 +8,6 @@ pub enum RenderIntent { } impl RenderIntent { - pub fn from_wayland(intent: u32, version: Version) -> Option { - let res = match intent { - RENDER_INTENT_PERCEPTUAL => Self::Perceptual, - RENDER_INTENT_RELATIVE => Self::Relative, - RENDER_INTENT_RELATIVE_BPC => Self::RelativeBpc, - RENDER_INTENT_ABSOLUTE_NO_ADAPTATION if version >= ABSOLUTE_NO_ADAPTATION_SINCE => { - Self::AbsoluteNoAdaptation - } - _ => return None, - }; - Some(res) - } - pub fn black_point_compensation(self) -> bool { match self { RenderIntent::Perceptual => true, diff --git a/src/cmm/cmm_tests.rs b/cmm/src/cmm_tests.rs similarity index 98% rename from src/cmm/cmm_tests.rs rename to cmm/src/cmm_tests.rs index 5f3564df..84e5edd6 100644 --- a/src/cmm/cmm_tests.rs +++ b/cmm/src/cmm_tests.rs @@ -1,5 +1,5 @@ mod matrices { - use crate::{cmm::cmm_primaries::Primaries, utils::ordered_float::F64}; + use {crate::cmm_primaries::Primaries, jay_utils::ordered_float::F64}; fn check(primaries: Primaries, expected: [[f64; 4]; 3]) { let (ltg, gtl) = primaries.matrices(); @@ -134,7 +134,7 @@ mod matrices { } mod transforms { - use crate::cmm::{ + use crate::{ cmm_eotf::Eotf, cmm_luminance::Luminance, cmm_manager::ColorManager, cmm_primaries::Primaries, cmm_render_intent::RenderIntent, }; diff --git a/src/cmm/cmm_transform.rs b/cmm/src/cmm_transform.rs similarity index 90% rename from src/cmm/cmm_transform.rs rename to cmm/src/cmm_transform.rs index 64e576ca..eaab4087 100644 --- a/src/cmm/cmm_transform.rs +++ b/cmm/src/cmm_transform.rs @@ -1,10 +1,6 @@ use { - crate::{ - cmm::{cmm_eotf::Eotf, cmm_primaries::Primaries}, - gfx_api::AlphaMode, - theme::Color, - utils::ordered_float::F64, - }, + crate::cmm_primaries::Primaries, + jay_utils::ordered_float::F64, std::{ fmt, fmt::{Debug, Formatter}, @@ -129,29 +125,6 @@ impl Mul<[f64; 3]> for ColorMatrix { } } -impl Mul for ColorMatrix { - type Output = Color; - - fn mul(self, rhs: Color) -> Self::Output { - let mut rgba = rhs.to_array(Eotf::Linear); - let a = rgba[3]; - if a < 1.0 && a > 0.0 { - for c in &mut rgba[..3] { - *c /= a; - } - } - let [r, g, b] = self * [rgba[0] as f64, rgba[1] as f64, rgba[2] as f64]; - Color::new( - Eotf::Linear, - AlphaMode::Straight, - r as f32, - g as f32, - b as f32, - a, - ) - } -} - impl ColorMatrix { pub const fn new(m: [[f64; 4]; 3]) -> Self { let m = [ diff --git a/cmm/src/lib.rs b/cmm/src/lib.rs new file mode 100644 index 00000000..3bc9bf15 --- /dev/null +++ b/cmm/src/lib.rs @@ -0,0 +1,53 @@ +macro_rules! linear_ids { + ($ids:ident, $id:ident, $ty:ty $(,)?) => { + #[derive(Debug)] + pub struct $ids { + next: jay_utils::numcell::NumCell<$ty>, + } + + impl Default for $ids { + fn default() -> Self { + Self { + next: jay_utils::numcell::NumCell::new(1), + } + } + } + + impl $ids { + pub fn next(&self) -> $id { + $id(self.next.fetch_add(1)) + } + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub struct $id($ty); + + impl $id { + #[allow(dead_code)] + pub fn raw(&self) -> $ty { + self.0 + } + + #[allow(dead_code)] + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl std::fmt::Display for $id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + }; +} + +pub mod cmm_description; +pub mod cmm_eotf; +pub mod cmm_luminance; +pub mod cmm_manager; +pub mod cmm_primaries; +pub mod cmm_render_intent; +#[cfg(test)] +mod cmm_tests; +pub mod cmm_transform; diff --git a/criteria/Cargo.toml b/criteria/Cargo.toml new file mode 100644 index 00000000..2b1e06d4 --- /dev/null +++ b/criteria/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "jay-criteria" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" + +[dependencies] +jay-utils = { version = "0.1.0", path = "../utils" } + +ahash = "0.8.7" +linearize = { version = "0.1.3", features = ["derive"] } +regex = "1.11.1" diff --git a/src/criteria/crit_graph.rs b/criteria/src/crit_graph.rs similarity index 100% rename from src/criteria/crit_graph.rs rename to criteria/src/crit_graph.rs diff --git a/src/criteria/crit_graph/crit_downstream.rs b/criteria/src/crit_graph/crit_downstream.rs similarity index 98% rename from src/criteria/crit_graph/crit_downstream.rs rename to criteria/src/crit_graph/crit_downstream.rs index cf3f50be..939661f8 100644 --- a/src/criteria/crit_graph/crit_downstream.rs +++ b/criteria/src/crit_graph/crit_downstream.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, crit_graph::{CritTarget, crit_upstream::CritUpstreamNode}, }, diff --git a/src/criteria/crit_graph/crit_middle.rs b/criteria/src/crit_graph/crit_middle.rs similarity index 99% rename from src/criteria/crit_graph/crit_middle.rs rename to criteria/src/crit_graph/crit_middle.rs index f8e76041..924c7010 100644 --- a/src/criteria/crit_graph/crit_middle.rs +++ b/criteria/src/crit_graph/crit_middle.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritUpstreamNode, crit_graph::{ CritDownstream, CritDownstreamData, CritTarget, CritUpstreamData, diff --git a/src/criteria/crit_graph/crit_root.rs b/criteria/src/crit_graph/crit_root.rs similarity index 99% rename from src/criteria/crit_graph/crit_root.rs rename to criteria/src/crit_graph/crit_root.rs index 3c2c6f4c..8c6f2c2a 100644 --- a/src/criteria/crit_graph/crit_root.rs +++ b/criteria/src/crit_graph/crit_root.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, crit_graph::{ CritTarget, CritUpstreamData, diff --git a/src/criteria/crit_graph/crit_target.rs b/criteria/src/crit_graph/crit_target.rs similarity index 82% rename from src/criteria/crit_graph/crit_target.rs rename to criteria/src/crit_graph/crit_target.rs index 18974748..b77d6796 100644 --- a/src/criteria/crit_graph/crit_target.rs +++ b/criteria/src/crit_graph/crit_target.rs @@ -1,11 +1,9 @@ use { crate::{ - criteria::{ - CritDestroyListener, CritMatcherId, FixedRootMatcher, crit_leaf::CritLeafEvent, - crit_matchers::critm_constant::CritMatchConstant, - }, - utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, + CritDestroyListener, CritMatcherId, FixedRootMatcher, crit_leaf::CritLeafEvent, + crit_matchers::critm_constant::CritMatchConstant, }, + jay_utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, std::{ hash::Hash, rc::{Rc, Weak}, diff --git a/src/criteria/crit_graph/crit_upstream.rs b/criteria/src/crit_graph/crit_upstream.rs similarity index 92% rename from src/criteria/crit_graph/crit_upstream.rs rename to criteria/src/crit_graph/crit_upstream.rs index 096a5555..e7bded8e 100644 --- a/src/criteria/crit_graph/crit_upstream.rs +++ b/criteria/src/crit_graph/crit_upstream.rs @@ -1,16 +1,14 @@ use { crate::{ - criteria::{ - CritDestroyListener, CritMatcherId, - crit_graph::{ - WeakCritTargetOwner, - crit_downstream::CritDownstream, - crit_target::{CritTarget, CritTargetOwner}, - }, - crit_per_target_data::CritPerTargetData, + CritDestroyListener, CritMatcherId, + crit_graph::{ + WeakCritTargetOwner, + crit_downstream::CritDownstream, + crit_target::{CritTarget, CritTargetOwner}, }, - utils::copyhashmap::CopyHashMap, + crit_per_target_data::CritPerTargetData, }, + jay_utils::copyhashmap::CopyHashMap, std::{ cell::RefMut, mem, diff --git a/src/criteria/crit_leaf.rs b/criteria/src/crit_leaf.rs similarity index 91% rename from src/criteria/crit_leaf.rs rename to criteria/src/crit_leaf.rs index 72b74ccd..9e22ba47 100644 --- a/src/criteria/crit_leaf.rs +++ b/criteria/src/crit_leaf.rs @@ -1,12 +1,10 @@ use { crate::{ - criteria::{ - CritUpstreamNode, - crit_graph::{CritDownstream, CritDownstreamData, CritMgr, CritTarget}, - crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, - }, - utils::{cell_ext::CellExt, queue::AsyncQueue}, + CritUpstreamNode, + crit_graph::{CritDownstream, CritDownstreamData, CritMgr, CritTarget}, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, }, + jay_utils::{cell_ext::CellExt, queue::AsyncQueue}, std::{ cell::Cell, rc::{Rc, Weak}, @@ -24,7 +22,7 @@ where events: Rc>>, } -pub(in crate::criteria) struct NodeHolder +pub struct NodeHolder where Target: CritTarget, { @@ -77,7 +75,7 @@ impl CritLeafMatcher where Target: CritTarget, { - pub(in crate::criteria) fn new( + pub(crate) fn new( mgr: &Target::Mgr, upstream: &Rc>, on_match: impl Fn(Target::LeafData) -> Box + 'static, diff --git a/src/criteria/crit_matchers.rs b/criteria/src/crit_matchers.rs similarity index 100% rename from src/criteria/crit_matchers.rs rename to criteria/src/crit_matchers.rs diff --git a/src/criteria/crit_matchers/critm_any_or_all.rs b/criteria/src/crit_matchers/critm_any_or_all.rs similarity index 94% rename from src/criteria/crit_matchers/critm_any_or_all.rs rename to criteria/src/crit_matchers/critm_any_or_all.rs index 38c3eaaa..71bc8efe 100644 --- a/src/criteria/crit_matchers/critm_any_or_all.rs +++ b/criteria/src/crit_matchers/critm_any_or_all.rs @@ -1,5 +1,5 @@ use { - crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + crate::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, std::{marker::PhantomData, rc::Rc}, }; diff --git a/src/criteria/crit_matchers/critm_constant.rs b/criteria/src/crit_matchers/critm_constant.rs similarity index 98% rename from src/criteria/crit_matchers/critm_constant.rs rename to criteria/src/crit_matchers/critm_constant.rs index b45eea19..41d2cda3 100644 --- a/src/criteria/crit_matchers/critm_constant.rs +++ b/criteria/src/crit_matchers/critm_constant.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherIds, FixedRootMatcher, crit_graph::{ CritFixedRootCriterion, CritFixedRootCriterionBase, CritMgr, CritRoot, CritRootFixed, diff --git a/src/criteria/crit_matchers/critm_exactly.rs b/criteria/src/crit_matchers/critm_exactly.rs similarity index 93% rename from src/criteria/crit_matchers/critm_exactly.rs rename to criteria/src/crit_matchers/critm_exactly.rs index fe4c3e0a..883cb14a 100644 --- a/src/criteria/crit_matchers/critm_exactly.rs +++ b/criteria/src/crit_matchers/critm_exactly.rs @@ -1,5 +1,5 @@ use { - crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + crate::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, std::{marker::PhantomData, rc::Rc}, }; diff --git a/src/criteria/crit_matchers/critm_string.rs b/criteria/src/crit_matchers/critm_string.rs similarity index 97% rename from src/criteria/crit_matchers/critm_string.rs rename to criteria/src/crit_matchers/critm_string.rs index 1464e2d6..968ceaff 100644 --- a/src/criteria/crit_matchers/critm_string.rs +++ b/criteria/src/crit_matchers/critm_string.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritLiteralOrRegex, RootMatcherMap, crit_graph::{CritRootCriterion, CritTarget}, }, diff --git a/src/criteria/crit_per_target_data.rs b/criteria/src/crit_per_target_data.rs similarity index 97% rename from src/criteria/crit_per_target_data.rs rename to criteria/src/crit_per_target_data.rs index cf527167..26db1c2b 100644 --- a/src/criteria/crit_per_target_data.rs +++ b/criteria/src/crit_per_target_data.rs @@ -1,5 +1,5 @@ use { - crate::criteria::{ + crate::{ CritMatcherId, crit_graph::{CritTarget, CritTargetOwner, WeakCritTargetOwner}, }, @@ -28,7 +28,7 @@ where data: T, } -pub(super) trait CritDestroyListenerBase: 'static +pub trait CritDestroyListenerBase: 'static where Target: CritTarget, { diff --git a/criteria/src/lib.rs b/criteria/src/lib.rs new file mode 100644 index 00000000..f5369669 --- /dev/null +++ b/criteria/src/lib.rs @@ -0,0 +1,131 @@ +pub mod crit_graph; +pub mod crit_leaf; +pub mod crit_matchers; +pub mod crit_per_target_data; + +use { + crate::{ + crit_graph::{CritMgr, CritMiddle, CritRoot, CritRootCriterion, CritRootFixed}, + crit_leaf::CritLeafMatcher, + crit_matchers::{critm_any_or_all::CritMatchAnyOrAll, critm_exactly::CritMatchExactly}, + }, + jay_utils::{copyhashmap::CopyHashMap, numcell::NumCell}, + linearize::StaticMap, + regex::Regex, + std::rc::{Rc, Weak}, +}; +pub use { + crit_graph::{CritTarget, CritUpstreamNode}, + crit_per_target_data::CritDestroyListener, +}; + +#[derive(Debug)] +pub struct CritMatcherIds { + next: NumCell, +} + +impl Default for CritMatcherIds { + fn default() -> Self { + Self { + next: NumCell::new(1), + } + } +} + +impl CritMatcherIds { + pub fn next(&self) -> CritMatcherId { + CritMatcherId(self.next.fetch_add(1)) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct CritMatcherId(u64); + +impl CritMatcherId { + #[allow(clippy::allow_attributes, dead_code)] + pub fn raw(&self) -> u64 { + self.0 + } + + #[allow(clippy::allow_attributes, dead_code)] + pub fn from_raw(id: u64) -> Self { + Self(id) + } +} + +impl std::fmt::Display for CritMatcherId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +pub type RootMatcherMap = CopyHashMap>>; +pub type FixedRootMatcher = + StaticMap>>>; + +#[derive(Clone)] +pub enum CritLiteralOrRegex { + Literal(String), + Regex(Regex), +} + +impl CritLiteralOrRegex { + fn matches(&self, string: &str) -> bool { + match self { + CritLiteralOrRegex::Literal(p) => string == p, + CritLiteralOrRegex::Regex(r) => r.is_match(string), + } + } +} + +pub trait CritMgrExt: CritMgr { + fn list( + &self, + upstream: &[Rc>], + all: bool, + ) -> Rc> { + if upstream.is_empty() { + return self.match_constant()[all].clone(); + } + CritMiddle::new(self, upstream, CritMatchAnyOrAll::new(upstream, all)) + } + + fn exactly( + &self, + upstream: &[Rc>], + num: usize, + ) -> Rc> { + if num > upstream.len() { + return self.match_constant()[false].clone(); + } + if num == 0 { + let upstream: Vec<_> = upstream.iter().map(|u| u.not(self)).collect(); + return self.list(&upstream, true); + } + CritMiddle::new(self, upstream, CritMatchExactly::new(upstream, num)) + } + + fn leaf( + &self, + upstream: &Rc>, + on_match: impl Fn(::LeafData) -> Box + 'static, + ) -> Rc> { + CritLeafMatcher::new(self, upstream, on_match) + } + + fn not( + &self, + upstream: &Rc>, + ) -> Rc> { + upstream.not(self) + } + + fn root(&self, criterion: T) -> Rc> + where + T: CritRootCriterion, + { + CritRoot::new(self.roots(), self.id(), criterion) + } +} + +impl CritMgrExt for T where T: CritMgr {} diff --git a/edid/Cargo.toml b/edid/Cargo.toml new file mode 100644 index 00000000..1d1a2f22 --- /dev/null +++ b/edid/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jay-edid" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "EDID parsing for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +bstr = { version = "1.9.0", default-features = false, features = ["std"] } +thiserror = "2.0.11" diff --git a/edid/src/lib.rs b/edid/src/lib.rs new file mode 100644 index 00000000..1272e8a7 --- /dev/null +++ b/edid/src/lib.rs @@ -0,0 +1,1312 @@ +use { + bstr::{BString, ByteSlice}, + std::{ + cell::RefCell, + fmt::{Debug, Formatter}, + rc::Rc, + }, + thiserror::Error, +}; + +trait BitflagsExt { + fn contains(self, other: Self) -> bool; +} + +impl BitflagsExt for u8 { + fn contains(self, other: Self) -> bool { + self & other == other + } +} + +struct Stack(RefCell>); + +impl Default for Stack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Stack { + fn push(&self, v: T) { + self.0.borrow_mut().push(v); + } + + fn pop(&self) -> Option { + self.0.borrow_mut().pop() + } + + fn to_vec(&self) -> Vec + where + T: Clone, + { + self.0.borrow().clone() + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ColorBitDepth { + Undefined, + Bits6, + Bits8, + Bits10, + Bits12, + Bits14, + Bits16, + Reserved, +} + +#[derive(Copy, Clone, Debug)] +pub enum DigitalVideoInterfaceStandard { + Undefined, + Dvi, + HdmiA, + HdmiB, + MDDI, + DisplayPort, + Unknown(u8), +} + +#[derive(Copy, Clone)] +pub struct SignalLevelStandard(u8); + +impl Debug for SignalLevelStandard { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self.0 { + 0 => "+0.7/−0.3 V", + 1 => "+0.714/−0.286 V", + 2 => "+1.0/−0.4 V", + _ => "+0.7/0 V", + }; + Debug::fmt(s, f) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum VideoInputDefinition { + Analog { + signal_level_standard: SignalLevelStandard, + blank_to_black_setup_or_pedestal: bool, + separate_h_v_sync_supported: bool, + composite_sync_on_horizontal_supported: bool, + composite_sync_on_green_supported: bool, + serration_on_vertical_sync_supported: bool, + }, + Digital { + bit_depth: ColorBitDepth, + video_interface: DigitalVideoInterfaceStandard, + }, +} + +#[derive(Copy, Clone, Debug)] +pub struct ScreenDimensions { + pub horizontal_screen_size_cm: Option, + pub vertical_screen_size_cm: Option, + pub landscape_aspect_ration: Option, + pub portrait_aspect_ration: Option, +} + +#[derive(Copy, Clone, Debug)] +pub struct ChromaticityCoordinates { + pub red_x: u16, + pub red_y: u16, + pub green_x: u16, + pub green_y: u16, + pub blue_x: u16, + pub blue_y: u16, + pub white_x: u16, + pub white_y: u16, +} + +#[derive(Copy, Clone, Debug)] +pub struct EstablishedTimings { + pub s_720x400_70: bool, + pub s_720x400_88: bool, + pub s_640x480_60: bool, + pub s_640x480_67: bool, + pub s_640x480_72: bool, + pub s_640x480_75: bool, + pub s_800x600_56: bool, + pub s_800x600_60: bool, + pub s_800x600_72: bool, + pub s_800x600_75: bool, + pub s_832x624_75: bool, + pub s_1024x768_87: bool, + pub s_1024x768_60: bool, + pub s_1024x768_70: bool, + pub s_1024x768_75: bool, + pub s_1280x1024_75: bool, + pub s_1152x870_75: bool, +} + +#[derive(Copy, Clone, Debug)] +pub enum AspectRatio { + A1_1, + A16_10, + A4_3, + A5_4, + A16_9, +} + +#[derive(Copy, Clone, Debug)] +pub struct StandardTiming { + pub x_resolution: u16, + pub aspect_ratio: AspectRatio, + pub vertical_frequency: u8, +} + +#[derive(Copy, Clone, Debug)] +pub enum AnalogSyncType { + AnalogComposite, + BipolarAnalogComposite, +} + +#[derive(Copy, Clone, Debug)] +pub enum SyncSignal { + Analog { + ty: AnalogSyncType, + with_serrations: bool, + sync_on_all_signals: bool, + }, + DigitalComposite { + with_serration: bool, + horizontal_sync_is_positive: bool, + }, + DigitalSeparate { + vertical_sync_is_positive: bool, + horizontal_sync_is_positive: bool, + }, +} + +#[derive(Copy, Clone)] +pub enum StereoViewingSupport { + None, + FieldSequentialRightDuringStereoSync, + FieldSequentialLeftDuringStereoSync, + TwoWayInterleavedRightImageOnEvenLines, + TwoWayInterleavedLeftImageOnEvenLines, + FourWayInterleaved, + SideBySideInterleaved, +} + +impl Debug for StereoViewingSupport { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let msg = match *self { + StereoViewingSupport::None => "none", + StereoViewingSupport::FieldSequentialRightDuringStereoSync => { + "field sequential, right during stereo sync" + } + StereoViewingSupport::FieldSequentialLeftDuringStereoSync => { + "field sequential, left during stereo sync" + } + StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines => { + "2-way interleaved, right image on even lines" + } + StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines => { + "2-way interleaved, left image on even lines" + } + StereoViewingSupport::FourWayInterleaved => "4-way interleaved", + StereoViewingSupport::SideBySideInterleaved => "side-by-side interleaved", + }; + write!(f, "\"{}\"", msg) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct DisplayRangeLimitsAndAdditionalTiming { + pub vertical_field_rate_min: u16, + pub vertical_field_rate_max: u16, + pub horizontal_field_rate_min: u16, + pub horizontal_field_rate_max: u16, + pub maximum_pixel_clock_mhz: u16, + pub extended_timing_information: ExtendedTimingInformation, +} + +#[derive(Copy, Clone, Debug)] +pub enum AspectRatioPreference { + A4_3, + A16_9, + A16_10, + A5_4, + A15_9, + Unknown(u8), +} + +#[derive(Copy, Clone, Debug)] +pub enum ExtendedTimingInformation { + DefaultGtf, + NoTimingInformation, + SecondaryGtf { + start_frequency: u16, + c_value: u16, + m_value: u16, + k_value: u8, + j_value: u16, + }, + Cvt { + cvt_major_version: u8, + cvt_minor_version: u8, + additional_clock_precision: u8, + maximum_active_pixels_per_line: Option, + ar_4_3: bool, + ar_16_9: bool, + ar_16_10: bool, + ar_5_4: bool, + ar_15_9: bool, + ar_preference: AspectRatioPreference, + cvt_rb_reduced_blanking_preferred: bool, + cvt_standard_blanking: bool, + scaling_support_horizontal_shrink: bool, + scaling_support_horizontal_stretch: bool, + scaling_support_vertical_shrink: bool, + scaling_support_vertical_stretch: bool, + preferred_vertical_refresh_rate_hz: u8, + }, + Unknown(u8), +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct ColorPoint { + pub white_point_index: u8, + pub white_point_x: u16, + pub white_point_y: u16, + pub gamma: Option, +} + +#[derive(Copy, Clone, Debug)] +pub struct EstablishedTimings3 { + pub s640x350_85: bool, + pub s640x400_85: bool, + pub s720x400_85: bool, + pub s640x480_85: bool, + pub s848x480_60: bool, + pub s800x600_85: bool, + pub s1024x768_85: bool, + pub s1152x864_75: bool, + pub s1280x768_60_rb: bool, + pub s1280x768_60: bool, + pub s1280x768_75: bool, + pub s1280x768_85: bool, + pub s1280x960_60: bool, + pub s1280x960_85: bool, + pub s1280x1024_60: bool, + pub s1280x1024_85: bool, + pub s1360x768_60: bool, + pub s1440x900_60_rb: bool, + pub s1440x900_60: bool, + pub s1440x900_75: bool, + pub s1440x900_85: bool, + pub s1400x1050_60_rb: bool, + pub s1400x1050_60: bool, + pub s1400x1050_75: bool, + pub s1400x1050_85: bool, + pub s1680x1050_60_rb: bool, + pub s1680x1050_60: bool, + pub s1680x1050_75: bool, + pub s1680x1050_85: bool, + pub s1600x1200_60: bool, + pub s1600x1200_65: bool, + pub s1600x1200_70: bool, + pub s1600x1200_75: bool, + pub s1600x1200_85: bool, + pub s1792x1344_60: bool, + pub s1792x1344_75: bool, + pub s1856x1392_60: bool, + pub s1856x1392_75: bool, + pub s1920x1200_60_rb: bool, + pub s1920x1200_60: bool, + pub s1920x1200_75: bool, + pub s1920x1200_85: bool, + pub s1920x1440_60: bool, + pub s1920x1440_75: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct ColorManagementData { + pub red_a3: u16, + pub red_a2: u16, + pub green_a3: u16, + pub green_a2: u16, + pub blue_a3: u16, + pub blue_a2: u16, +} + +#[derive(Copy, Clone, Debug)] +pub enum CvtAspectRatio { + A4_3, + A16_9, + A16_10, + A15_9, +} + +#[derive(Copy, Clone, Debug)] +pub enum CvtPreferredVerticalRate { + R50, + R60, + R75, + R85, +} + +#[derive(Copy, Clone, Debug)] +pub struct Cvt3ByteCode { + pub addressable_lines_per_field: u16, + pub aspect_ration: CvtAspectRatio, + pub preferred_vertical_rate: CvtPreferredVerticalRate, + pub r50: bool, + pub r60: bool, + pub r75: bool, + pub r85: bool, + pub r60_reduced_blanking: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct DetailedTimingDescriptor { + pub pixel_clock_khz: u32, + pub horizontal_addressable_pixels: u16, + pub horizontal_blanking_pixels: u16, + pub vertical_addressable_lines: u16, + pub vertical_blanking_lines: u16, + pub horizontal_front_porch_pixels: u16, + pub horizontal_sync_pulse_pixels: u16, + pub vertical_front_porch_lines: u8, + pub vertical_sync_pulse_lines: u8, + pub horizontal_addressable_mm: u16, + pub vertical_addressable_mm: u16, + pub horizontal_left_border_pixels: u8, + pub vertical_top_border_pixels: u8, + pub interlaced: bool, + pub stereo_viewing_support: StereoViewingSupport, + pub sync: SyncSignal, +} + +#[derive(Clone, Debug)] +pub enum Descriptor { + Unknown(u8), + DetailedTimingDescriptor(DetailedTimingDescriptor), + DisplayProductSerialNumber(String), + AlphanumericDataString(String), + DisplayProductName(String), + DisplayRangeLimitsAndAdditionalTiming(DisplayRangeLimitsAndAdditionalTiming), + EstablishedTimings3(EstablishedTimings3), + ColorManagementData(ColorManagementData), + StandardTimingIdentifier([Option; 6]), + ColorPoint(ColorPoint, Option), + Cvt3ByteCode([Cvt3ByteCode; 4]), +} + +type EdidContext = (usize, EdidParseContext); + +struct EdidParser<'a> { + data: &'a [u8], + pos: usize, + context: Rc>, + saved_ctx: Vec, + errors: Vec<(EdidError, Vec)>, +} + +macro_rules! bail { + ($slf:expr, $err:expr) => {{ + $slf.saved_ctx = $slf.context.to_vec(); + return Err($err); + }}; +} + +#[derive(Clone, Debug)] +pub enum EdidParseContext { + ReadingBytes(usize), + BaseBlock, + Descriptors, + Descriptor, + ChromaticityCoordinates, + EstablishedTimings, + StandardTimings, + ScreenDimensions, + Gamma, + FeatureSupport, + Magic, + Extension, + IdManufacturerName, + VideoInputDefinition, +} + +struct EdidPushedContext { + stack: Rc>, +} + +impl Drop for EdidPushedContext { + fn drop(&mut self) { + self.stack.pop(); + } +} + +impl<'a> EdidParser<'a> { + fn push_ctx(&self, pc: EdidParseContext) -> EdidPushedContext { + self.context.push((self.pos, pc)); + EdidPushedContext { + stack: self.context.clone(), + } + } + + fn nest(&self, data: &'a [u8]) -> Self { + Self { + data, + pos: 0, + context: self.context.clone(), + saved_ctx: vec![], + errors: vec![], + } + } + + fn store_error(&mut self, error: EdidError) { + self.errors.push((error, self.saved_ctx.clone())); + } + + fn is_empty(&self) -> bool { + self.pos >= self.data.len() + } + + fn read_n(&mut self) -> Result<&'a [u8; N], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(N)); + if self.data.len() - self.pos < N { + bail!(self, EdidError::UnexpectedEof); + } + let v = self.data[self.pos..self.pos + N].try_into().unwrap(); + self.pos += N; + Ok(v) + } + + fn read_var_n(&mut self, n: usize) -> Result<&'a [u8], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(n)); + if self.data.len() - self.pos < n { + bail!(self, EdidError::UnexpectedEof); + } + let v = &self.data[self.pos..self.pos + n]; + self.pos += n; + Ok(v) + } + + fn read_u8(&mut self) -> Result { + let &[a] = self.read_n()?; + Ok(a) + } + + fn read_u16(&mut self) -> Result { + let &[lo, hi] = self.read_n()?; + Ok(((hi as u16) << 8) + lo as u16) + } + + fn read_u32(&mut self) -> Result { + let &[a, b, c, d] = self.read_n()?; + Ok(((d as u32) << 24) + ((c as u32) << 16) + ((b as u32) << 8) + a as u32) + } + + fn parse_magic(&mut self) -> Result<(), EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Magic); + let magic = self.read_n::<8>()?; + if magic != &[0, 255, 255, 255, 255, 255, 255, 0] { + bail!(self, EdidError::InvalidMagic(magic.as_bstr().to_owned())); + } + Ok(()) + } + + fn parse_id_manufacturer_name(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::IdManufacturerName); + let name = self.read_n::<2>()?; + let a = (name[0] >> 2) & 0b11111; + let b = ((name[0] & 0b11) << 3) | (name[1] >> 5); + let c = name[1] & 0b11111; + let name = [a + b'@', b + b'@', c + b'@'].as_bstr().to_owned(); + Ok(name) + } + + fn parse_video_input_definition(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::VideoInputDefinition); + let val = self.read_u8()?; + let res = if val.contains(0x80) { + VideoInputDefinition::Digital { + bit_depth: match (val >> 4) & 0b111 { + 0b000 => ColorBitDepth::Undefined, + 0b001 => ColorBitDepth::Bits6, + 0b010 => ColorBitDepth::Bits8, + 0b011 => ColorBitDepth::Bits10, + 0b100 => ColorBitDepth::Bits12, + 0b101 => ColorBitDepth::Bits14, + 0b110 => ColorBitDepth::Bits16, + _ => ColorBitDepth::Reserved, + }, + video_interface: match val & 0b1111 { + 0b0000 => DigitalVideoInterfaceStandard::Undefined, + 0b0001 => DigitalVideoInterfaceStandard::Dvi, + 0b0010 => DigitalVideoInterfaceStandard::HdmiA, + 0b0011 => DigitalVideoInterfaceStandard::HdmiB, + 0b0100 => DigitalVideoInterfaceStandard::MDDI, + 0b0101 => DigitalVideoInterfaceStandard::DisplayPort, + n => DigitalVideoInterfaceStandard::Unknown(n), + }, + } + } else { + VideoInputDefinition::Analog { + signal_level_standard: SignalLevelStandard((val >> 5) & 0b11), + blank_to_black_setup_or_pedestal: (val >> 4).contains(1), + separate_h_v_sync_supported: (val >> 3).contains(1), + composite_sync_on_horizontal_supported: (val >> 2).contains(1), + composite_sync_on_green_supported: (val >> 1).contains(1), + serration_on_vertical_sync_supported: (val >> 0).contains(1), + } + }; + Ok(res) + } + + fn parse_screen_dimensions(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::ScreenDimensions); + let &[hor, vert] = self.read_n()?; + let mut res = ScreenDimensions { + horizontal_screen_size_cm: None, + vertical_screen_size_cm: None, + landscape_aspect_ration: None, + portrait_aspect_ration: None, + }; + if hor != 0 && vert != 0 { + res.horizontal_screen_size_cm = Some(hor); + res.vertical_screen_size_cm = Some(vert); + } else if vert != 0 { + res.portrait_aspect_ration = Some(100.0 / (vert as f64 + 99.0)); + } else if hor != 0 { + res.landscape_aspect_ration = Some((hor as f64 + 99.0) / 100.0); + } + Ok(res) + } + + fn parse_gamma(&mut self) -> Result, EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Gamma); + let val = self.read_u8()?; + if val == 0xff { + Ok(None) + } else { + Ok(Some((val as f64 + 100.0) / 100.0)) + } + } + + fn parse_feature_support(&mut self, digital: bool) -> Result { + let _ctx = self.push_ctx(EdidParseContext::FeatureSupport); + let val = self.read_u8()?; + Ok(FeatureSupport { + standby_supported: val.contains(0x80), + suspend_supported: val.contains(0x40), + active_off_supported: val.contains(0x20), + features: if digital { + FeatureSupport2::Digital { + rgb444_supported: true, + ycrcb422_supported: val.contains(0x10), + ycrcb444_supported: val.contains(0x08), + } + } else { + FeatureSupport2::Analog { + display_color_type: match (val >> 3) & 0b11 { + 0b00 => DisplayColorType::Monochrome, + 0b01 => DisplayColorType::Rgb, + 0b10 => DisplayColorType::NonRgb, + _ => DisplayColorType::Undefined, + }, + } + }, + srgb_is_default_color_space: val.contains(0x04), + preferred_mode_is_native: val.contains(0x02), + display_is_continuous_frequency: val.contains(0x01), + }) + } + + fn parse_chromaticity_coordinates(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::ChromaticityCoordinates); + let b = self.read_n::<10>()?; + let rx = ((b[0] as u16 >> 6) & 0b11) + ((b[2] as u16) << 2); + let ry = ((b[0] as u16 >> 4) & 0b11) + ((b[3] as u16) << 2); + let gx = ((b[0] as u16 >> 2) & 0b11) + ((b[4] as u16) << 2); + let gy = ((b[0] as u16 >> 0) & 0b11) + ((b[5] as u16) << 2); + let bx = ((b[1] as u16 >> 6) & 0b11) + ((b[6] as u16) << 2); + let by = ((b[1] as u16 >> 4) & 0b11) + ((b[7] as u16) << 2); + let wx = ((b[1] as u16 >> 2) & 0b11) + ((b[8] as u16) << 2); + let wy = ((b[1] as u16 >> 0) & 0b11) + ((b[9] as u16) << 2); + Ok(ChromaticityCoordinates { + red_x: rx, + red_y: ry, + green_x: gx, + green_y: gy, + blue_x: bx, + blue_y: by, + white_x: wx, + white_y: wy, + }) + } + + fn parse_established_timings(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::EstablishedTimings); + let b = self.read_n::<3>()?; + Ok(EstablishedTimings { + s_720x400_70: b[0].contains(0x80), + s_720x400_88: b[0].contains(0x40), + s_640x480_60: b[0].contains(0x20), + s_640x480_67: b[0].contains(0x10), + s_640x480_72: b[0].contains(0x08), + s_640x480_75: b[0].contains(0x04), + s_800x600_56: b[0].contains(0x02), + s_800x600_60: b[0].contains(0x01), + s_800x600_72: b[0].contains(0x80), + s_800x600_75: b[0].contains(0x40), + s_832x624_75: b[0].contains(0x20), + s_1024x768_87: b[0].contains(0x10), + s_1024x768_60: b[0].contains(0x08), + s_1024x768_70: b[0].contains(0x04), + s_1024x768_75: b[0].contains(0x02), + s_1280x1024_75: b[0].contains(0x01), + s_1152x870_75: b[0].contains(0x80), + }) + } + + fn parse_standard_timing(&mut self, revision: u8, a: u8, b: u8) -> Option { + if a == 0 { + return None; + } + Some(StandardTiming { + x_resolution: (a as u16 + 31) * 8, + aspect_ratio: match b >> 6 { + 0b00 if revision < 3 => AspectRatio::A1_1, + 0b00 => AspectRatio::A16_10, + 0b01 => AspectRatio::A4_3, + 0b10 => AspectRatio::A5_4, + _ => AspectRatio::A16_9, + }, + vertical_frequency: 60 + (b & 0b111111), + }) + } + + fn parse_standard_timings2( + &mut self, + revision: u8, + b: &[u8; 18], + ) -> [Option; 6] { + let mut res = [None; 6]; + for i in 0..6 { + let x = b[5 + 2 * i]; + let y = b[5 + 2 * i + 1]; + res[i] = self.parse_standard_timing(revision, x, y); + } + res + } + + fn parse_color_point(&mut self, b: &[u8; 18]) -> (ColorPoint, Option) { + let mut res = [Default::default(); 2]; + for n in 0..2 { + let b = &b[5 * (n + 1)..]; + res[n] = ColorPoint { + white_point_index: b[0], + white_point_x: ((b[2] as u16) << 2) | ((b[1] as u16) >> 2), + white_point_y: ((b[3] as u16) << 2) | ((b[1] as u16) & 0b11), + gamma: if b[4] == 0xff { + None + } else { + Some((b[5] as f64 + 100.0) / 100.0) + }, + }; + } + let second = if res[1].white_point_index != 0 { + Some(res[1]) + } else { + None + }; + (res[0], second) + } + + fn parse_standard_timings( + &mut self, + revision: u8, + ) -> Result<[Option; 8], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::StandardTimings); + let bytes = self.read_n::<16>()?; + let mut res = [None; 8]; + for i in 0..8 { + let a = bytes[2 * i]; + let b = bytes[2 * i + 1]; + if (a, b) != (1, 1) { + res[i] = self.parse_standard_timing(revision, a, b); + } + } + Ok(res) + } + + fn parse_detailed_timing_descriptor(&self, b: &[u8; 18]) -> DetailedTimingDescriptor { + let l = b[17]; + DetailedTimingDescriptor { + pixel_clock_khz: u16::from_le_bytes([b[0], b[1]]) as u32 * 10_000, + horizontal_addressable_pixels: u16::from_le_bytes([b[2], b[4] >> 4]), + horizontal_blanking_pixels: u16::from_le_bytes([b[3], b[4] & 0b1111]), + vertical_addressable_lines: u16::from_le_bytes([b[5], b[7] >> 4]), + vertical_blanking_lines: u16::from_le_bytes([b[6], b[7] & 0b1111]), + horizontal_front_porch_pixels: u16::from_le_bytes([b[8], b[11] >> 6]), + horizontal_sync_pulse_pixels: u16::from_le_bytes([b[9], (b[11] >> 4) & 0b11]), + vertical_front_porch_lines: (b[10] >> 4) | ((b[11] & 0b1100) << 2), + vertical_sync_pulse_lines: (b[10] & 0b1111) | ((b[11] & 0b11) << 4), + horizontal_addressable_mm: u16::from_le_bytes([b[12], b[14] >> 4]), + vertical_addressable_mm: u16::from_le_bytes([b[13], b[14] & 0b1111]), + horizontal_left_border_pixels: b[15], + vertical_top_border_pixels: b[16], + interlaced: l.contains(0x80), + stereo_viewing_support: match ((l >> 4) & 0b110) | (l & 0b1) { + 0b010 => StereoViewingSupport::FieldSequentialRightDuringStereoSync, + 0b100 => StereoViewingSupport::FieldSequentialLeftDuringStereoSync, + 0b011 => StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines, + 0b101 => StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines, + 0b110 => StereoViewingSupport::FourWayInterleaved, + 0b111 => StereoViewingSupport::SideBySideInterleaved, + _ => StereoViewingSupport::None, + }, + sync: if l.contains(0b10000) { + if l.contains(0b01000) { + SyncSignal::DigitalSeparate { + vertical_sync_is_positive: l.contains(0b100), + horizontal_sync_is_positive: l.contains(0b10), + } + } else { + SyncSignal::DigitalComposite { + with_serration: l.contains(0b100), + horizontal_sync_is_positive: l.contains(0b10), + } + } + } else { + SyncSignal::Analog { + ty: if l.contains(0b1000) { + AnalogSyncType::BipolarAnalogComposite + } else { + AnalogSyncType::AnalogComposite + }, + with_serrations: l.contains(0b100), + sync_on_all_signals: l.contains(0b10), + } + }, + } + } + + fn parse_display_range_limits_and_additional_timing( + &self, + b: &[u8; 18], + ) -> DisplayRangeLimitsAndAdditionalTiming { + let min_vert_off = b[4].contains(0b0001); + let max_vert_off = min_vert_off || b[4].contains(0b0010); + let min_horz_off = b[4].contains(0b0100); + let max_horz_off = min_horz_off || b[4].contains(0b1000); + DisplayRangeLimitsAndAdditionalTiming { + vertical_field_rate_min: b[5] as u16 + if min_vert_off { 255 } else { 0 }, + vertical_field_rate_max: b[6] as u16 + if max_vert_off { 255 } else { 0 }, + horizontal_field_rate_min: b[7] as u16 + if min_horz_off { 255 } else { 0 }, + horizontal_field_rate_max: b[8] as u16 + if max_horz_off { 255 } else { 0 }, + maximum_pixel_clock_mhz: b[9] as u16 * 10, + extended_timing_information: match b[10] { + 0x0 => ExtendedTimingInformation::DefaultGtf, + 0x1 => ExtendedTimingInformation::NoTimingInformation, + 0x2 => ExtendedTimingInformation::SecondaryGtf { + start_frequency: b[12] as u16, + c_value: b[13] as u16, + m_value: u16::from_le_bytes([b[14], b[15]]), + k_value: b[16], + j_value: b[17] as u16, + }, + 0x4 => ExtendedTimingInformation::Cvt { + cvt_major_version: b[11] >> 4, + cvt_minor_version: b[11] & 0b1111, + additional_clock_precision: b[12] >> 2, + maximum_active_pixels_per_line: if b[13] == 0 { + None + } else { + Some((((b[12] as u16 & 0b11) << 8) | b[13] as u16) * 8) + }, + ar_4_3: b[14].contains(0x80), + ar_16_9: b[14].contains(0x40), + ar_16_10: b[14].contains(0x20), + ar_5_4: b[14].contains(0x10), + ar_15_9: b[14].contains(0x08), + ar_preference: match b[15] >> 5 { + 0b000 => AspectRatioPreference::A4_3, + 0b001 => AspectRatioPreference::A16_9, + 0b010 => AspectRatioPreference::A16_10, + 0b011 => AspectRatioPreference::A5_4, + 0b100 => AspectRatioPreference::A15_9, + n => AspectRatioPreference::Unknown(n), + }, + cvt_rb_reduced_blanking_preferred: b[15].contains(0b10000), + cvt_standard_blanking: b[15].contains(0b1000), + scaling_support_horizontal_shrink: b[16].contains(0x80), + scaling_support_horizontal_stretch: b[16].contains(0x40), + scaling_support_vertical_shrink: b[16].contains(0x20), + scaling_support_vertical_stretch: b[16].contains(0x10), + preferred_vertical_refresh_rate_hz: b[17], + }, + n => ExtendedTimingInformation::Unknown(n), + }, + } + } + + fn parse_established_timings3(&self, b: &[u8; 18]) -> EstablishedTimings3 { + EstablishedTimings3 { + s640x350_85: b[6].contains(0x80), + s640x400_85: b[6].contains(0x40), + s720x400_85: b[6].contains(0x20), + s640x480_85: b[6].contains(0x10), + s848x480_60: b[6].contains(0x08), + s800x600_85: b[6].contains(0x04), + s1024x768_85: b[6].contains(0x02), + s1152x864_75: b[6].contains(0x01), + s1280x768_60_rb: b[7].contains(0x80), + s1280x768_60: b[7].contains(0x40), + s1280x768_75: b[7].contains(0x20), + s1280x768_85: b[7].contains(0x10), + s1280x960_60: b[7].contains(0x08), + s1280x960_85: b[7].contains(0x04), + s1280x1024_60: b[7].contains(0x02), + s1280x1024_85: b[7].contains(0x01), + s1360x768_60: b[8].contains(0x80), + s1440x900_60_rb: b[8].contains(0x40), + s1440x900_60: b[8].contains(0x20), + s1440x900_75: b[8].contains(0x10), + s1440x900_85: b[8].contains(0x08), + s1400x1050_60_rb: b[8].contains(0x04), + s1400x1050_60: b[8].contains(0x02), + s1400x1050_75: b[8].contains(0x01), + s1400x1050_85: b[9].contains(0x80), + s1680x1050_60_rb: b[9].contains(0x40), + s1680x1050_60: b[9].contains(0x20), + s1680x1050_75: b[9].contains(0x10), + s1680x1050_85: b[9].contains(0x08), + s1600x1200_60: b[9].contains(0x04), + s1600x1200_65: b[9].contains(0x02), + s1600x1200_70: b[9].contains(0x01), + s1600x1200_75: b[10].contains(0x80), + s1600x1200_85: b[10].contains(0x40), + s1792x1344_60: b[10].contains(0x20), + s1792x1344_75: b[10].contains(0x10), + s1856x1392_60: b[10].contains(0x08), + s1856x1392_75: b[10].contains(0x04), + s1920x1200_60_rb: b[10].contains(0x02), + s1920x1200_60: b[10].contains(0x01), + s1920x1200_75: b[11].contains(0x80), + s1920x1200_85: b[11].contains(0x40), + s1920x1440_60: b[11].contains(0x20), + s1920x1440_75: b[11].contains(0x10), + } + } + + fn parse_color_management_data(&self, b: &[u8; 18]) -> ColorManagementData { + ColorManagementData { + red_a3: u16::from_le_bytes([b[6], b[7]]), + red_a2: u16::from_le_bytes([b[8], b[9]]), + green_a3: u16::from_le_bytes([b[10], b[11]]), + green_a2: u16::from_le_bytes([b[12], b[13]]), + blue_a3: u16::from_le_bytes([b[14], b[15]]), + blue_a2: u16::from_le_bytes([b[16], b[17]]), + } + } + + fn parse_cvt3_byte_codes(&self, b: &[u8; 18]) -> [Cvt3ByteCode; 4] { + let parse = |n: usize| { + let b = &b[6 + 3 * n..]; + Cvt3ByteCode { + addressable_lines_per_field: u16::from_le_bytes([b[0], b[1] >> 4]), + aspect_ration: match (b[1] >> 2) & 0b11 { + 0 => CvtAspectRatio::A4_3, + 1 => CvtAspectRatio::A16_9, + 2 => CvtAspectRatio::A16_10, + _ => CvtAspectRatio::A15_9, + }, + preferred_vertical_rate: match (b[2] >> 5) & 0b11 { + 0 => CvtPreferredVerticalRate::R50, + 1 => CvtPreferredVerticalRate::R60, + 2 => CvtPreferredVerticalRate::R75, + _ => CvtPreferredVerticalRate::R85, + }, + r50: b[2].contains(0b10000), + r60: b[2].contains(0b01000), + r75: b[2].contains(0b00100), + r85: b[2].contains(0b00010), + r60_reduced_blanking: b[2].contains(0b00001), + } + }; + [parse(0), parse(1), parse(2), parse(3)] + } + + fn parse_descriptor(&mut self, revision: u8) -> Result, EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Descriptor); + let b = self.read_n::<18>()?; + let str = || { + let mut s = &b[5..]; + if let Some(n) = s.find_byte(b'\n') { + s = &s[..n]; + }; + let mut res = String::new(); + for &b in s { + res.push_str(CP437[b as usize]); + } + res + }; + let res = if (b[0], b[1]) == (0, 0) { + match b[3] { + 0xff => Descriptor::DisplayProductSerialNumber(str()), + 0xfe => Descriptor::AlphanumericDataString(str()), + 0xfd => Descriptor::DisplayRangeLimitsAndAdditionalTiming( + self.parse_display_range_limits_and_additional_timing(b), + ), + 0xfc => Descriptor::DisplayProductName(str()), + 0xfb => { + let (first, second) = self.parse_color_point(b); + Descriptor::ColorPoint(first, second) + } + 0xfa => { + Descriptor::StandardTimingIdentifier(self.parse_standard_timings2(revision, b)) + } + 0xf9 => Descriptor::ColorManagementData(self.parse_color_management_data(b)), + 0xf8 => Descriptor::Cvt3ByteCode(self.parse_cvt3_byte_codes(b)), + 0xf7 => Descriptor::EstablishedTimings3(self.parse_established_timings3(b)), + 0x10 => return Ok(None), + n => Descriptor::Unknown(n), + } + } else { + Descriptor::DetailedTimingDescriptor(self.parse_detailed_timing_descriptor(b)) + }; + Ok(Some(res)) + } + + fn parse_descriptors(&mut self, revision: u8) -> Result<[Option; 4], EdidError> { + let _ctx = self.push_ctx(EdidParseContext::Descriptors); + let mut res = [None, None, None, None]; + for res in &mut res { + *res = self.parse_descriptor(revision)?; + } + Ok(res) + } + + fn parse_base_block(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::BaseBlock); + self.parse_magic()?; + let id_manufacturer_name = self.parse_id_manufacturer_name()?; + let id_product_code = self.read_u16()?; + let id_serial_number = self.read_u32()?; + let mut week_of_manufacture = None; + let mut model_year = None; + let mut year_of_manufacture = None; + { + let &[a, b] = self.read_n()?; + if matches!(a, 1..=0x36) { + week_of_manufacture = Some(a); + } + let year = b as u16 + 1990; + if a == 0xff { + model_year = Some(year); + } else { + year_of_manufacture = Some(year); + } + } + let &[edid_version, edid_revision] = self.read_n()?; + let video_input_definition = self.parse_video_input_definition()?; + let is_digital = matches!(video_input_definition, VideoInputDefinition::Digital { .. }); + let screen_dimensions = self.parse_screen_dimensions()?; + let gamma = self.parse_gamma()?; + let feature_support = self.parse_feature_support(is_digital)?; + let chromaticity_coordinates = self.parse_chromaticity_coordinates()?; + let established_timings = self.parse_established_timings()?; + let standard_timings = self.parse_standard_timings(edid_revision)?; + let descriptors = self.parse_descriptors(edid_revision)?; + let num_extensions = self.read_u8()?; + let _checksum = self.read_u8()?; + Ok(EdidBaseBlock { + id_manufacturer_name, + id_product_code, + id_serial_number, + week_of_manufacture, + model_year, + year_of_manufacture, + edid_version, + edid_revision, + video_input_definition, + screen_dimensions, + gamma, + feature_support, + chromaticity_coordinates, + established_timings, + standard_timings, + descriptors, + num_extensions, + }) + } + + fn parse_cta_amd_vendor_data_block(&mut self) -> Result { + let _ = self.read_n::<2>()?; + Ok(CtaDataBlock::VendorAmd(CtaAmdVendorDataBlock { + minimum_refresh_hz: self.read_u8()?, + maximum_refresh_hz: self.read_u8()?, + })) + } + + fn parse_cta_vendor_data_block(&mut self) -> Result { + match self.read_n::<3>()? { + [0x1A, 0x00, 0x00] => self.parse_cta_amd_vendor_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_colorimetry_data_block(&mut self) -> Result { + let [lo, hi] = *self.read_n::<2>()?; + Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock { + bt2020_rgb: lo.contains(0x80), + bt2020_ycc: lo.contains(0x40), + bt2020_cycc: lo.contains(0x20), + op_rgb: lo.contains(0x10), + op_ycc_601601: lo.contains(0x08), + s_ycc_601: lo.contains(0x04), + xv_ycc_709: lo.contains(0x02), + xv_ycc_601: lo.contains(0x01), + dci_p3: hi.contains(0x80), + })) + } + + fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result { + let et = self.read_u8()?; + let _ = self.read_u8()?; + let mut read_luminance = |min: bool| { + let v = self.read_u8().unwrap_or_default(); + if v == 0 { + None + } else if min { + Some((v as f64 / 255.0).powi(2) / 100.0) + } else { + Some(50.0 * 2.0f64.powf(v as f64 / 32.0)) + } + }; + Ok(CtaDataBlock::StaticHdrMetadata( + CtaStaticHdrMetadataDataBlock { + traditional_gamma_sdr_luminance: et.contains(0x01), + traditional_gamma_hdr_luminance: et.contains(0x02), + smpte_st_2084: et.contains(0x04), + hlg: et.contains(0x08), + max_luminance: read_luminance(false), + max_frame_average_luminance: read_luminance(false), + min_luminance: read_luminance(true), + }, + )) + } + + fn parse_cta_extended_data_block(&mut self) -> Result { + match self.read_u8()? { + 0x5 => self.parse_cta_colorimetry_data_block(), + 0x6 => self.parse_cta_hdr_static_metadata_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_data_block(&mut self, tag: u8) -> Result { + match tag { + 0x3 => self.parse_cta_vendor_data_block(), + 0x7 => self.parse_cta_extended_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + + fn parse_cta_extension_v3(&mut self) -> Result { + let detailed_timing_descriptors_offset = self.read_u8()? as usize; + let _ = self.read_u8()?; + let mut data_blocks = vec![]; + while self.pos < detailed_timing_descriptors_offset { + let b1 = self.read_u8()?; + let data = self.read_var_n(b1 as usize & 0x1f)?; + let mut parser = self.nest(data); + match parser.parse_cta_data_block(b1 >> 5) { + Ok(d) => data_blocks.push(d), + Err(e) => { + self.saved_ctx = parser.saved_ctx; + self.store_error(e); + } + } + } + Ok(EdidExtension::CtaV3(CtaExtensionV3 { data_blocks })) + } + + fn parse_cta_extension(&mut self) -> Result { + // https://web.archive.org/web/20171201033424/https://standards.cta.tech/kwspub/published_docs/CTA-861-G_FINAL_revised_2017.pdf + match self.read_u8()? { + 0x3 => self.parse_cta_extension_v3(), + _ => Ok(EdidExtension::Unknown), + } + } + + fn parse_extension_impl(&mut self) -> Result { + match self.read_u8()? { + 0x2 => self.parse_cta_extension(), + _ => Ok(EdidExtension::Unknown), + } + } + + fn parse_extension(&mut self) -> Result { + let _ctx = self.push_ctx(EdidParseContext::Extension); + let data = self.read_n::<128>()?; + let mut parser = self.nest(data); + let res = parser.parse_extension_impl(); + if res.is_err() { + self.saved_ctx = parser.saved_ctx; + } + res + } + + fn parse(&mut self) -> Result { + let bb = self.parse_base_block()?; + let mut exts = vec![]; + while !self.is_empty() { + match self.parse_extension() { + Ok(e) => exts.push(e), + Err(e) => self.store_error(e), + } + } + Ok(EdidFile { + base_block: bb, + extension_blocks: exts, + }) + } +} + +#[derive(Debug)] +pub enum DisplayColorType { + Monochrome, + Rgb, + NonRgb, + Undefined, +} + +#[derive(Debug)] +pub enum FeatureSupport2 { + Analog { + display_color_type: DisplayColorType, + }, + Digital { + rgb444_supported: bool, + ycrcb444_supported: bool, + ycrcb422_supported: bool, + }, +} + +#[derive(Debug)] +pub struct FeatureSupport { + pub standby_supported: bool, + pub suspend_supported: bool, + pub active_off_supported: bool, + pub features: FeatureSupport2, + pub srgb_is_default_color_space: bool, + pub preferred_mode_is_native: bool, + pub display_is_continuous_frequency: bool, +} + +#[derive(Debug)] +pub struct EdidBaseBlock { + pub id_manufacturer_name: BString, + pub id_product_code: u16, + pub id_serial_number: u32, + pub week_of_manufacture: Option, + pub model_year: Option, + pub year_of_manufacture: Option, + pub edid_version: u8, + pub edid_revision: u8, + pub video_input_definition: VideoInputDefinition, + pub screen_dimensions: ScreenDimensions, + pub gamma: Option, + pub feature_support: FeatureSupport, + pub chromaticity_coordinates: ChromaticityCoordinates, + pub established_timings: EstablishedTimings, + pub standard_timings: [Option; 8], + pub descriptors: [Option; 4], + pub num_extensions: u8, +} + +#[derive(Debug)] +pub enum EdidExtension { + Unknown, + CtaV3(CtaExtensionV3), +} + +#[derive(Debug)] +pub struct CtaExtensionV3 { + pub data_blocks: Vec, +} + +#[derive(Debug)] +pub enum CtaDataBlock { + Unknown, + VendorAmd(CtaAmdVendorDataBlock), + Colorimetry(CtaColorimetryDataBlock), + StaticHdrMetadata(CtaStaticHdrMetadataDataBlock), +} + +#[derive(Debug)] +pub struct CtaAmdVendorDataBlock { + pub minimum_refresh_hz: u8, + pub maximum_refresh_hz: u8, +} + +#[derive(Copy, Clone, Debug)] +pub struct CtaColorimetryDataBlock { + pub bt2020_rgb: bool, + pub bt2020_ycc: bool, + pub bt2020_cycc: bool, + pub op_rgb: bool, + pub op_ycc_601601: bool, + pub s_ycc_601: bool, + pub xv_ycc_709: bool, + pub xv_ycc_601: bool, + pub dci_p3: bool, +} + +#[derive(Copy, Clone, Debug)] +pub struct CtaStaticHdrMetadataDataBlock { + pub traditional_gamma_sdr_luminance: bool, + pub traditional_gamma_hdr_luminance: bool, + pub smpte_st_2084: bool, + pub hlg: bool, + pub max_luminance: Option, + pub max_frame_average_luminance: Option, + pub min_luminance: Option, +} + +#[derive(Debug)] +pub struct EdidFile { + pub base_block: EdidBaseBlock, + pub extension_blocks: Vec, +} + +#[derive(Debug, Error)] +pub enum EdidError { + #[error("Unexpected end-of-file")] + UnexpectedEof, + #[error("Invalid magic header")] + InvalidMagic(BString), +} + +pub fn parse(data: &[u8]) -> Result { + let mut parser = EdidParser { + data, + pos: 0, + context: Rc::new(Default::default()), + saved_ctx: vec![], + errors: vec![], + }; + parser.parse() +} + +const CP437: &[&str] = &[ + "\u{0}", "☺", "☻", "♥", "♦", "♣", "♠", "•", "◘", "○", "◙", "♂", "♀", "♪", "♫", "☼", "►", "◄", + "↕", "‼", "¶", "§", "▬", "↨", "↑", "↓", "→", "←", "∟", "↔", "▲", "▼", " ", "!", "\"", "#", "$", + "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", + "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", + "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", "Ç", "ü", "é", "â", + "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ", "ô", "ö", "ò", "û", + "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", + "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", + "╜", "╛", "┐", "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", + "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", "α", "ß", "Γ", + "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", "≡", "±", "≥", "≤", "⌠", "⌡", + "÷", "≈", "°", "∙", "·", "√", "ⁿ", "²", "■", "\u{a0}", +]; diff --git a/etc/jay-portals.conf b/etc/jay-portals.conf deleted file mode 100644 index 607e1691..00000000 --- a/etc/jay-portals.conf +++ /dev/null @@ -1,6 +0,0 @@ -[preferred] -default=gtk -org.freedesktop.impl.portal.ScreenCast=jay -org.freedesktop.impl.portal.RemoteDesktop=jay -org.freedesktop.impl.portal.Inhibit=none -org.freedesktop.impl.portal.FileChooser=gtk4 diff --git a/etc/jay.portal b/etc/jay.portal deleted file mode 100644 index c6d67365..00000000 --- a/etc/jay.portal +++ /dev/null @@ -1,3 +0,0 @@ -[portal] -DBusName=org.freedesktop.impl.portal.desktop.jay -Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; diff --git a/formats/Cargo.toml b/formats/Cargo.toml new file mode 100644 index 00000000..5f72e5ec --- /dev/null +++ b/formats/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jay-formats" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "Pixel format tables for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +ahash = "0.8.7" +ash = { package = "jay-ash", version = "0.3.0" } +clap = { version = "4.4.18", features = ["derive", "wrap_help"] } +jay-config = { version = "1.10.0", path = "../jay-config" } diff --git a/formats/src/lib.rs b/formats/src/lib.rs new file mode 100644 index 00000000..0599e878 --- /dev/null +++ b/formats/src/lib.rs @@ -0,0 +1,559 @@ +use { + ahash::AHashMap, + ash::vk, + clap::{ValueEnum, builder::PossibleValue}, + jay_config::video::Format as ConfigFormat, + std::{ + fmt::{self, Debug, Write}, + sync::LazyLock, + }, +}; + +pub type GLenum = u32; +pub type GLint = i32; + +const GL_RGBA: GLint = 0x1908; +const GL_RGBA8: GLenum = 0x8058; +const GL_BGRA_EXT: GLint = 0x80E1; +const GL_UNSIGNED_BYTE: GLint = 0x1401; + +#[derive(Copy, Clone, Debug)] +pub struct FormatShmInfo { + pub gl_format: GLint, + pub gl_internal_format: GLenum, + pub gl_type: GLint, +} + +#[derive(Copy, Clone, Debug)] +pub struct Format { + pub name: &'static str, + pub vk_format: vk::Format, + pub drm: u32, + pub wl_id: Option, + pub external_only_guess: bool, + pub has_alpha: bool, + pub opaque: Option<&'static Format>, + pub shm_info: Option, + pub config: ConfigFormat, + pub bpp: u32, +} + +const fn default(config: ConfigFormat) -> Format { + Format { + name: "", + vk_format: vk::Format::UNDEFINED, + drm: 0, + wl_id: None, + external_only_guess: false, + has_alpha: false, + opaque: None, + shm_info: None, + config, + bpp: 4, + } +} + +impl PartialEq for Format { + fn eq(&self, other: &Self) -> bool { + self.drm == other.drm + } +} + +impl Eq for Format {} + +impl ValueEnum for &'static Format { + fn value_variants<'a>() -> &'a [Self] { + ref_formats() + } + + fn to_possible_value(&self) -> Option { + Some(PossibleValue::new(self.name)) + } +} + +static FORMATS_MAP: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.drm, format).is_none()); + } + map +}); + +static FORMATS_REFS: LazyLock> = LazyLock::new(|| FORMATS.iter().collect()); + +static FORMATS_NAMES: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.name, format).is_none()); + } + map +}); + +static FORMATS_CONFIG: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + assert!(map.insert(format.config, format).is_none()); + } + map +}); + +#[test] +fn formats_dont_panic() { + formats(); + named_formats(); + config_formats(); +} + +pub fn formats() -> &'static AHashMap { + &FORMATS_MAP +} + +pub fn ref_formats() -> &'static [&'static Format] { + &FORMATS_REFS +} + +pub fn named_formats() -> &'static AHashMap<&'static str, &'static Format> { + &FORMATS_NAMES +} + +pub fn config_formats() -> &'static AHashMap { + &FORMATS_CONFIG +} + +const fn fourcc_code(a: char, b: char, c: char, d: char) -> u32 { + (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) +} + +pub fn debug(fourcc: u32) -> impl Debug { + fmt::from_fn(move |fmt| { + fmt.write_char(fourcc as u8 as char)?; + fmt.write_char((fourcc >> 8) as u8 as char)?; + fmt.write_char((fourcc >> 16) as u8 as char)?; + fmt.write_char((fourcc >> 24) as u8 as char)?; + Ok(()) + }) +} + +const ARGB8888_ID: u32 = 0; +const ARGB8888_DRM: u32 = fourcc_code('A', 'R', '2', '4'); + +const XRGB8888_ID: u32 = 1; +const XRGB8888_DRM: u32 = fourcc_code('X', 'R', '2', '4'); + +pub fn map_wayland_format_id(id: u32) -> u32 { + match id { + ARGB8888_ID => ARGB8888_DRM, + XRGB8888_ID => XRGB8888_DRM, + _ => id, + } +} + +pub static ARGB8888: &Format = &Format { + name: "argb8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_BGRA_EXT, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::B8G8R8A8_UNORM, + bpp: 4, + drm: ARGB8888_DRM, + wl_id: Some(ARGB8888_ID), + external_only_guess: false, + has_alpha: true, + opaque: Some(XRGB8888), + config: ConfigFormat::ARGB8888, +}; + +pub static XRGB8888: &Format = &Format { + name: "xrgb8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_BGRA_EXT, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::B8G8R8A8_UNORM, + bpp: 4, + drm: XRGB8888_DRM, + wl_id: Some(XRGB8888_ID), + external_only_guess: false, + has_alpha: false, + opaque: None, + config: ConfigFormat::XRGB8888, +}; + +static ABGR8888: &Format = &Format { + name: "abgr8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_RGBA, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::R8G8B8A8_UNORM, + bpp: 4, + drm: fourcc_code('A', 'B', '2', '4'), + wl_id: None, + external_only_guess: false, + has_alpha: true, + opaque: Some(XBGR8888), + config: ConfigFormat::ABGR8888, +}; + +static XBGR8888: &Format = &Format { + name: "xbgr8888", + shm_info: Some(FormatShmInfo { + gl_format: GL_RGBA, + gl_internal_format: GL_RGBA8, + gl_type: GL_UNSIGNED_BYTE, + }), + vk_format: vk::Format::R8G8B8A8_UNORM, + bpp: 4, + drm: fourcc_code('X', 'B', '2', '4'), + wl_id: None, + external_only_guess: false, + has_alpha: false, + opaque: None, + config: ConfigFormat::XBGR8888, +}; + +static R8: &Format = &Format { + name: "r8", + vk_format: vk::Format::R8_UNORM, + bpp: 1, + drm: fourcc_code('R', '8', ' ', ' '), + ..default(ConfigFormat::R8) +}; + +static GR88: &Format = &Format { + name: "gr88", + vk_format: vk::Format::R8G8_UNORM, + bpp: 2, + drm: fourcc_code('G', 'R', '8', '8'), + ..default(ConfigFormat::GR88) +}; + +static RGB888: &Format = &Format { + name: "rgb888", + vk_format: vk::Format::B8G8R8_UNORM, + bpp: 3, + drm: fourcc_code('R', 'G', '2', '4'), + ..default(ConfigFormat::RGB888) +}; + +static BGR888: &Format = &Format { + name: "bgr888", + vk_format: vk::Format::R8G8B8_UNORM, + bpp: 3, + drm: fourcc_code('B', 'G', '2', '4'), + ..default(ConfigFormat::BGR888) +}; + +static RGBA4444: &Format = &Format { + name: "rgba4444", + vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'A', '1', '2'), + has_alpha: true, + opaque: Some(RGBX4444), + ..default(ConfigFormat::RGBA4444) +}; + +static RGBX4444: &Format = &Format { + name: "rgbx4444", + vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'X', '1', '2'), + ..default(ConfigFormat::RGBX4444) +}; + +static BGRA4444: &Format = &Format { + name: "bgra4444", + vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'A', '1', '2'), + has_alpha: true, + opaque: Some(BGRX4444), + ..default(ConfigFormat::BGRA4444) +}; + +static BGRX4444: &Format = &Format { + name: "bgrx4444", + vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'X', '1', '2'), + ..default(ConfigFormat::BGRX4444) +}; + +static RGB565: &Format = &Format { + name: "rgb565", + vk_format: vk::Format::R5G6B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'G', '1', '6'), + ..default(ConfigFormat::RGB565) +}; + +static BGR565: &Format = &Format { + name: "bgr565", + vk_format: vk::Format::B5G6R5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'G', '1', '6'), + ..default(ConfigFormat::BGR565) +}; + +static RGBA5551: &Format = &Format { + name: "rgba5551", + vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'A', '1', '5'), + has_alpha: true, + opaque: Some(RGBX5551), + ..default(ConfigFormat::RGBA5551) +}; + +static RGBX5551: &Format = &Format { + name: "rgbx5551", + vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('R', 'X', '1', '5'), + ..default(ConfigFormat::RGBX5551) +}; + +static BGRA5551: &Format = &Format { + name: "bgra5551", + vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'A', '1', '5'), + has_alpha: true, + opaque: Some(BGRX5551), + ..default(ConfigFormat::BGRA5551) +}; + +static BGRX5551: &Format = &Format { + name: "bgrx5551", + vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('B', 'X', '1', '5'), + ..default(ConfigFormat::BGRX5551) +}; + +static ARGB1555: &Format = &Format { + name: "argb1555", + vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('A', 'R', '1', '5'), + has_alpha: true, + opaque: Some(XRGB1555), + ..default(ConfigFormat::ARGB1555) +}; + +static XRGB1555: &Format = &Format { + name: "xrgb1555", + vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, + bpp: 2, + drm: fourcc_code('X', 'R', '1', '5'), + ..default(ConfigFormat::XRGB1555) +}; + +static ARGB2101010: &Format = &Format { + name: "argb2101010", + vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('A', 'R', '3', '0'), + has_alpha: true, + opaque: Some(XRGB2101010), + ..default(ConfigFormat::ARGB2101010) +}; + +static XRGB2101010: &Format = &Format { + name: "xrgb2101010", + vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('X', 'R', '3', '0'), + ..default(ConfigFormat::XRGB2101010) +}; + +static ABGR2101010: &Format = &Format { + name: "abgr2101010", + vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('A', 'B', '3', '0'), + has_alpha: true, + opaque: Some(XBGR2101010), + ..default(ConfigFormat::ABGR2101010) +}; + +static XBGR2101010: &Format = &Format { + name: "xbgr2101010", + vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, + bpp: 4, + drm: fourcc_code('X', 'B', '3', '0'), + ..default(ConfigFormat::XBGR2101010) +}; + +static ABGR16161616: &Format = &Format { + name: "abgr16161616", + vk_format: vk::Format::R16G16B16A16_UNORM, + bpp: 8, + drm: fourcc_code('A', 'B', '4', '8'), + has_alpha: true, + opaque: Some(XBGR16161616), + ..default(ConfigFormat::ABGR16161616) +}; + +static XBGR16161616: &Format = &Format { + name: "xbgr16161616", + vk_format: vk::Format::R16G16B16A16_UNORM, + bpp: 8, + drm: fourcc_code('X', 'B', '4', '8'), + ..default(ConfigFormat::XBGR16161616) +}; + +pub static ABGR16161616F: &Format = &Format { + name: "abgr16161616f", + vk_format: vk::Format::R16G16B16A16_SFLOAT, + bpp: 8, + drm: fourcc_code('A', 'B', '4', 'H'), + has_alpha: true, + opaque: Some(XBGR16161616F), + ..default(ConfigFormat::ABGR16161616F) +}; + +static XBGR16161616F: &Format = &Format { + name: "xbgr16161616f", + vk_format: vk::Format::R16G16B16A16_SFLOAT, + bpp: 8, + drm: fourcc_code('X', 'B', '4', 'H'), + ..default(ConfigFormat::XBGR16161616F) +}; + +static BGR161616: &Format = &Format { + name: "bgr161616", + vk_format: vk::Format::R16G16B16_UNORM, + bpp: 6, + drm: fourcc_code('B', 'G', '4', '8'), + ..default(ConfigFormat::BGR161616) +}; + +static R16F: &Format = &Format { + name: "r16f", + vk_format: vk::Format::R16_SFLOAT, + bpp: 2, + drm: fourcc_code('R', ' ', ' ', 'H'), + ..default(ConfigFormat::R16F) +}; + +static GR1616F: &Format = &Format { + name: "gr1616f", + vk_format: vk::Format::R16G16_SFLOAT, + bpp: 4, + drm: fourcc_code('G', 'R', ' ', 'H'), + ..default(ConfigFormat::GR1616F) +}; + +static BGR161616F: &Format = &Format { + name: "bgr161616f", + vk_format: vk::Format::R16G16B16_SFLOAT, + bpp: 6, + drm: fourcc_code('B', 'G', 'R', 'H'), + ..default(ConfigFormat::BGR161616F) +}; + +static R32F: &Format = &Format { + name: "r32f", + vk_format: vk::Format::R32_SFLOAT, + bpp: 4, + drm: fourcc_code('R', ' ', ' ', 'F'), + ..default(ConfigFormat::R32F) +}; + +static GR3232F: &Format = &Format { + name: "gr3232f", + vk_format: vk::Format::R32G32_SFLOAT, + bpp: 8, + drm: fourcc_code('G', 'R', ' ', 'F'), + ..default(ConfigFormat::GR3232F) +}; + +static BGR323232F: &Format = &Format { + name: "bgr323232f", + vk_format: vk::Format::R32G32B32_SFLOAT, + bpp: 12, + drm: fourcc_code('B', 'G', 'R', 'F'), + ..default(ConfigFormat::BGR323232F) +}; + +static ABGR32323232F: &Format = &Format { + name: "abgr32323232f", + vk_format: vk::Format::R32G32B32A32_SFLOAT, + bpp: 16, + drm: fourcc_code('A', 'B', '8', 'F'), + has_alpha: true, + ..default(ConfigFormat::ABGR32323232F) +}; + +pub static FORMATS: &[Format] = &[ + *ARGB8888, + *XRGB8888, + *ABGR8888, + *XBGR8888, + *R8, + *GR88, + *RGB888, + *BGR888, + #[cfg(target_endian = "little")] + *RGBA4444, + #[cfg(target_endian = "little")] + *RGBX4444, + #[cfg(target_endian = "little")] + *BGRA4444, + #[cfg(target_endian = "little")] + *BGRX4444, + #[cfg(target_endian = "little")] + *RGB565, + #[cfg(target_endian = "little")] + *BGR565, + #[cfg(target_endian = "little")] + *RGBA5551, + #[cfg(target_endian = "little")] + *RGBX5551, + #[cfg(target_endian = "little")] + *BGRA5551, + #[cfg(target_endian = "little")] + *BGRX5551, + #[cfg(target_endian = "little")] + *ARGB1555, + #[cfg(target_endian = "little")] + *XRGB1555, + #[cfg(target_endian = "little")] + *ARGB2101010, + #[cfg(target_endian = "little")] + *XRGB2101010, + #[cfg(target_endian = "little")] + *ABGR2101010, + #[cfg(target_endian = "little")] + *XBGR2101010, + #[cfg(target_endian = "little")] + *ABGR16161616, + #[cfg(target_endian = "little")] + *XBGR16161616, + #[cfg(target_endian = "little")] + *ABGR16161616F, + #[cfg(target_endian = "little")] + *XBGR16161616F, + #[cfg(target_endian = "little")] + *BGR161616, + #[cfg(target_endian = "little")] + *R16F, + #[cfg(target_endian = "little")] + *GR1616F, + #[cfg(target_endian = "little")] + *BGR161616F, + #[cfg(target_endian = "little")] + *R32F, + #[cfg(target_endian = "little")] + *GR3232F, + #[cfg(target_endian = "little")] + *BGR323232F, + #[cfg(target_endian = "little")] + *ABGR32323232F, +]; diff --git a/geometry/Cargo.toml b/geometry/Cargo.toml new file mode 100644 index 00000000..aef81efb --- /dev/null +++ b/geometry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jay-geometry" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "Geometry primitives for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-algorithms = { version = "0.4.0", path = "../algorithms" } +smallvec = { version = "1.11.1", features = ["const_generics", "const_new", "union"] } diff --git a/geometry/src/lib.rs b/geometry/src/lib.rs new file mode 100644 index 00000000..e18ba86f --- /dev/null +++ b/geometry/src/lib.rs @@ -0,0 +1,365 @@ +mod region; + +#[cfg(test)] +mod tests; + +pub use region::{DamageQueue, RegionBuilder}; +use { + jay_algorithms::rect::{NoTag, RectRaw, Tag}, + smallvec::SmallVec, + std::fmt::{Debug, Formatter}, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Default)] +#[repr(transparent)] +pub struct Rect +where + T: Tag, +{ + raw: RectRaw, +} + +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct Region +where + T: Tag, +{ + rects: SmallVec<[RectRaw; 1]>, + extents: Rect, +} + +impl Debug for Rect { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.raw, f) + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct RectOverflow { + pub left: i32, + pub right: i32, + pub top: i32, + pub bottom: i32, +} + +impl RectOverflow { + pub fn is_contained(&self) -> bool { + self.left <= 0 && self.right <= 0 && self.top <= 0 && self.bottom <= 0 + } + + pub fn x_overflow(&self) -> bool { + self.left > 0 || self.right > 0 + } + + pub fn y_overflow(&self) -> bool { + self.top > 0 || self.bottom > 0 + } +} + +impl Rect +where + T: Tag, +{ + pub fn untag(&self) -> Rect { + Rect { + raw: RectRaw { + x1: self.raw.x1, + y1: self.raw.y1, + x2: self.raw.x2, + y2: self.raw.y2, + tag: NoTag, + }, + } + } +} + +impl Rect { + pub fn new_empty(x: i32, y: i32) -> Self { + Self { + raw: RectRaw { + x1: x, + y1: y, + x2: x, + y2: y, + tag: NoTag, + }, + } + } + + pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Option { + if x2 < x1 || y2 < y1 { + return None; + } + Some(Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + }) + } + + #[cfg_attr(not(test), expect(dead_code))] + fn new_unchecked_danger(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + } + } + + pub fn new_sized(x1: i32, y1: i32, width: i32, height: i32) -> Option { + if width < 0 || height < 0 { + return None; + } + Self::new(x1, y1, x1 + width, y1 + height) + } + + pub fn new_saturating(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x2.max(x1), + y2: y2.max(y1), + tag: NoTag, + }, + } + } + + pub fn new_sized_saturating(x1: i32, y1: i32, width: i32, height: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x1.saturating_add(width.max(0)), + y2: y1.saturating_add(height.max(0)), + tag: NoTag, + }, + } + } + + pub fn union(&self, other: Self) -> Self { + Self { + raw: RectRaw { + x1: self.raw.x1.min(other.raw.x1), + y1: self.raw.y1.min(other.raw.y1), + x2: self.raw.x2.max(other.raw.x2), + y2: self.raw.y2.max(other.raw.y2), + tag: NoTag, + }, + } + } + + pub fn intersect(&self, other: Self) -> Self { + let x1 = self.raw.x1.max(other.raw.x1); + let y1 = self.raw.y1.max(other.raw.y1); + let x2 = self.raw.x2.min(other.raw.x2).max(x1); + let y2 = self.raw.y2.min(other.raw.y2).max(y1); + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag: NoTag, + }, + } + } + + pub fn with_size_saturating(&self, width: i32, height: i32) -> Self { + Self::new_sized_saturating(self.raw.x1, self.raw.y1, width, height) + } + + pub fn with_tag(&self, tag: u32) -> Rect { + Rect { + raw: RectRaw { + x1: self.raw.x1, + y1: self.raw.y1, + x2: self.raw.x2, + y2: self.raw.y2, + tag, + }, + } + } +} + +impl Rect +where + T: Tag, +{ + #[cfg_attr(not(test), expect(dead_code))] + fn new_unchecked_danger_tagged(x1: i32, y1: i32, x2: i32, y2: i32, tag: T) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2, + y2, + tag, + }, + } + } + + pub fn intersects(&self, other: &Self) -> bool { + self.raw.x1 < other.raw.x2 + && other.raw.x1 < self.raw.x2 + && self.raw.y1 < other.raw.y2 + && other.raw.y1 < self.raw.y2 + } + + pub fn contains(&self, x: i32, y: i32) -> bool { + self.raw.x1 <= x && self.raw.y1 <= y && self.raw.x2 > x && self.raw.y2 > y + } + + pub fn not_contains(&self, x: i32, y: i32) -> bool { + !self.contains(x, y) + } + + pub fn dist_squared(&self, x: i32, y: i32) -> i128 { + let x = x as i64; + let y = y as i64; + let x1 = self.raw.x1 as i64; + let x2 = self.raw.x2 as i64; + let y1 = self.raw.y1 as i64; + let y2 = self.raw.y2 as i64; + let mut dx = 0; + if x1 > x { + dx = x1 - x; + } else if x2 < x { + dx = x - x2; + } + let mut dy = 0; + if y1 > y { + dy = y1 - y; + } else if y2 < y { + dy = y - y2; + } + let dx = dx as i128; + let dy = dy as i128; + dx * dx + dy * dy + } + + pub fn contains_rect(&self, rect: &Rect) -> bool + where + U: Tag, + { + self.raw.x1 <= rect.raw.x1 + && self.raw.y1 <= rect.raw.x1 + && rect.raw.x2 <= self.raw.x2 + && rect.raw.y2 <= self.raw.y2 + } + + pub fn get_overflow(&self, child: &Rect) -> RectOverflow + where + U: Tag, + { + RectOverflow { + left: self.raw.x1 - child.raw.x1, + right: child.raw.x2 - self.raw.x2, + top: self.raw.y1 - child.raw.y1, + bottom: child.raw.y2 - self.raw.y2, + } + } + + pub fn is_empty(&self) -> bool { + self.raw.x1 == self.raw.x2 || self.raw.y1 == self.raw.y2 + } + + pub fn is_not_empty(&self) -> bool { + !self.is_empty() + } + + pub fn to_origin(&self) -> Self { + Self { + raw: RectRaw { + x1: 0, + y1: 0, + x2: self.raw.x2 - self.raw.x1, + y2: self.raw.y2 - self.raw.y1, + tag: self.raw.tag, + }, + } + } + + pub fn move_(&self, dx: i32, dy: i32) -> Self { + Self { + raw: RectRaw { + x1: self.raw.x1.saturating_add(dx), + y1: self.raw.y1.saturating_add(dy), + x2: self.raw.x2.saturating_add(dx), + y2: self.raw.y2.saturating_add(dy), + tag: self.raw.tag, + }, + } + } + + pub fn at_point(&self, x1: i32, y1: i32) -> Self { + Self { + raw: RectRaw { + x1, + y1, + x2: x1 + self.raw.x2 - self.raw.x1, + y2: y1 + self.raw.y2 - self.raw.y1, + tag: self.raw.tag, + }, + } + } + + pub fn translate(&self, x: i32, y: i32) -> (i32, i32) { + (x.wrapping_sub(self.raw.x1), y.wrapping_sub(self.raw.y1)) + } + + pub fn translate_inv(&self, x: i32, y: i32) -> (i32, i32) { + (x.wrapping_add(self.raw.x1), y.wrapping_add(self.raw.y1)) + } + + pub fn x1(&self) -> i32 { + self.raw.x1 + } + + pub fn x2(&self) -> i32 { + self.raw.x2 + } + + pub fn y1(&self) -> i32 { + self.raw.y1 + } + + pub fn y2(&self) -> i32 { + self.raw.y2 + } + + pub fn width(&self) -> i32 { + self.raw.x2 - self.raw.x1 + } + + pub fn height(&self) -> i32 { + self.raw.y2 - self.raw.y1 + } + + pub fn position(&self) -> (i32, i32) { + (self.raw.x1, self.raw.y1) + } + + pub fn size(&self) -> (i32, i32) { + (self.width(), self.height()) + } + + pub fn center(&self) -> (i32, i32) { + ( + self.raw.x1 + self.width() / 2, + self.raw.y1 + self.height() / 2, + ) + } + + pub fn tag(&self) -> T { + self.raw.tag + } +} diff --git a/src/rect/region.rs b/geometry/src/region.rs similarity index 94% rename from src/rect/region.rs rename to geometry/src/region.rs index bc1613c4..32425f21 100644 --- a/src/rect/region.rs +++ b/geometry/src/region.rs @@ -1,11 +1,5 @@ use { - crate::{ - rect::{Rect, Region}, - utils::{ - array, - ptr_ext::{MutPtrExt, PtrExt}, - }, - }, + crate::{Rect, Region}, jay_algorithms::rect::{ RectRaw, Tag, region::{ @@ -15,6 +9,7 @@ use { }, smallvec::SmallVec, std::{ + array, borrow::Cow, cell::UnsafeCell, fmt::{Debug, Formatter}, @@ -176,7 +171,6 @@ where } } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn extents(&self) -> Rect { self.extents } @@ -274,7 +268,6 @@ impl RegionBuilder { self.base.clone() } - #[expect(dead_code)] pub fn clear(&mut self) { self.pending.clear(); self.base = Region::empty(); @@ -321,26 +314,26 @@ impl DamageQueue { } pub fn damage(&self, rects: &[Rect]) { - let datas = unsafe { self.datas.get().deref_mut() }; + let datas = unsafe { &mut *self.datas.get() }; for data in datas { data.extend(rects); } } pub fn clear(&self) { - let data = unsafe { &mut self.datas.get().deref_mut()[self.this] }; + let data = unsafe { &mut (&mut *self.datas.get())[self.this] }; data.clear(); } pub fn clear_all(&self) { - let datas = unsafe { self.datas.get().deref_mut() }; + let datas = unsafe { &mut *self.datas.get() }; for data in datas { data.clear(); } } pub fn get(&self) -> Region { - let data = unsafe { &self.datas.get().deref()[self.this] }; + let data = unsafe { &(&*self.datas.get())[self.this] }; Region::from_rects2(data) } } diff --git a/src/rect/tests.rs b/geometry/src/tests.rs similarity index 99% rename from src/rect/tests.rs rename to geometry/src/tests.rs index c673ef5b..1b2d205f 100644 --- a/src/rect/tests.rs +++ b/geometry/src/tests.rs @@ -1,5 +1,5 @@ use { - crate::rect::{Rect, Region}, + crate::{Rect, Region}, jay_algorithms::rect::{NoTag, RectRaw}, }; diff --git a/jay-config-schema/Cargo.toml b/jay-config-schema/Cargo.toml new file mode 100644 index 00000000..18285ff6 --- /dev/null +++ b/jay-config-schema/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "jay-config-schema" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "Shared configuration schema declarations for the Jay compositor" +repository = "https://github.com/mahkoh/jay" + +[dependencies] diff --git a/jay-config-schema/src/animations.rs b/jay-config-schema/src/animations.rs new file mode 100644 index 00000000..a60ba034 --- /dev/null +++ b/jay-config-schema/src/animations.rs @@ -0,0 +1,13 @@ +#[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]), +} diff --git a/jay-config-schema/src/lib.rs b/jay-config-schema/src/lib.rs new file mode 100644 index 00000000..959becfc --- /dev/null +++ b/jay-config-schema/src/lib.rs @@ -0,0 +1,9 @@ +//! Shared configuration schema declarations for Jay. +//! +//! This crate is the target home for option structs, defaults, validation +//! policy, and docs metadata that need to be consumed by TOML parsing, +//! generated config documentation, and compositor-side application code. + +pub mod animations; + +pub use animations::{AnimationCurveConfig, Animations}; diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index facb7c17..ba8d7d43 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -12,7 +12,6 @@ use { }, bincode::Options, serde::{Deserialize, Serialize}, - std::marker::PhantomData, }; pub const VERSION: u32 = 1; @@ -31,12 +30,6 @@ pub struct ConfigEntry { pub handle_msg: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), } -pub struct ConfigEntryGen { - _phantom: PhantomData, -} - -impl ConfigEntryGen {} - pub fn bincode_ops() -> impl Options { bincode::DefaultOptions::new() .with_fixint_encoding() @@ -44,10 +37,6 @@ pub fn bincode_ops() -> impl Options { .with_no_limit() } -pub trait Config { - extern "C" fn configure(); -} - #[derive(Serialize, Deserialize, Debug)] pub struct WireMode { pub width: i32, @@ -99,7 +88,6 @@ pub enum ClientCriterionStringField { SandboxInstanceId, Comm, Exe, - Tag, } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 70245b21..6610ad4d 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -3,16 +3,14 @@ use { crate::{ _private::{ - ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen, - GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, - WindowCriterionStringField, WireMode, bincode_ops, + ClientCriterionIpc, ClientCriterionStringField, GenericCriterionIpc, PollableId, + WindowCriterionIpc, WindowCriterionStringField, WireMode, bincode_ops, ipc::{ ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, }, - logging, }, Axis, Direction, ModifiedKeySym, PciId, Workspace, - client::{Client, ClientCapabilities, ClientCriterion, ClientMatcher, MatchedClient}, + client::{Client, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, @@ -199,35 +197,6 @@ unsafe fn with_client T>(data: *const u8, f: F) - }) } -impl ConfigEntryGen { - pub const ENTRY: ConfigEntry = ConfigEntry { - version: VERSION, - init: Self::init, - unref, - handle_msg, - }; - - pub unsafe extern "C" fn init( - srv_data: *const u8, - srv_unref: unsafe extern "C" fn(data: *const u8), - srv_handler: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), - init_data: *const u8, - size: usize, - ) -> *const u8 { - logging::init(); - unsafe { - init( - srv_data, - srv_unref, - srv_handler, - init_data, - size, - T::configure, - ) - } - } -} - pub unsafe extern "C" fn init( srv_data: *const u8, srv_unref: unsafe extern "C" fn(data: *const u8), @@ -348,15 +317,7 @@ impl ConfigClient { .drain() .map(|(a, b)| (a, b.into_raw_fd())) .collect(); - if command.tag.is_some() { - self.send(&ClientMessage::Run3 { - prog: &command.prog, - args: command.args.clone(), - env, - fds, - tag: command.tag.as_deref(), - }); - } else if fds.is_empty() { + if fds.is_empty() { self.send(&ClientMessage::Run { prog: &command.prog, args: command.args.clone(), @@ -1556,22 +1517,6 @@ impl ConfigClient { connector } - pub fn set_client_matcher_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) { - self.send(&ClientMessage::SetClientMatcherCapabilities { matcher, caps }); - } - - pub fn set_client_matcher_bounding_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) { - self.send(&ClientMessage::SetClientMatcherBoundingCapabilities { matcher, caps }); - } - pub fn latch(&self, seat: Seat, f: F) { if !self.feat_mod_mask.get() { log::error!("compositor does not support latching"); @@ -1673,12 +1618,6 @@ impl ConfigClient { }) } - pub fn get_socket_path(&self) -> Option { - let res = self.send_with_response(&ClientMessage::GetSocketPath); - get_response!(res, None, GetSocketPath { path }); - Some(path) - } - pub fn create_pollable(&self, fd: i32) -> Result { let res = self.send_with_response(&ClientMessage::AddPollable { fd }); get_response!( @@ -1867,8 +1806,6 @@ impl ConfigClient { ClientCriterion::CommRegex(t) => string!(t, Comm, true), ClientCriterion::Exe(t) => string!(t, Exe, false), ClientCriterion::ExeRegex(t) => string!(t, Exe, true), - ClientCriterion::Tag(t) => string!(t, Tag, false), - ClientCriterion::TagRegex(t) => string!(t, Tag, true), }; let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion }); get_response!( diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 93067d55..d381f423 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -2,7 +2,7 @@ use { crate::{ _private::{ClientCriterionIpc, PollableId, WindowCriterionIpc, WireMode}, Axis, Direction, PciId, Workspace, - client::{Client, ClientCapabilities, ClientMatcher}, + client::{Client, ClientMatcher}, input::{ FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, SwitchEvent, Timeline, acceleration::AccelProfile, capability::Capability, @@ -488,7 +488,6 @@ pub enum ClientMessage<'a> { SetExplicitSyncEnabled { enabled: bool, }, - GetSocketPath, DeviceSetKeymap { device: InputDevice, keymap: Keymap, @@ -806,14 +805,6 @@ pub enum ClientMessage<'a> { SetTitleFont { font: &'a str, }, - SetClientMatcherCapabilities { - matcher: ClientMatcher, - caps: ClientCapabilities, - }, - SetClientMatcherBoundingCapabilities { - matcher: ClientMatcher, - caps: ClientCapabilities, - }, ShowWorkspaceOn { seat: Seat, workspace: Workspace, @@ -868,13 +859,6 @@ pub enum ClientMessage<'a> { SetXWaylandEnabled { enabled: bool, }, - Run3 { - prog: &'a str, - args: Vec, - env: Vec<(String, String)>, - fds: Vec<(i32, i32)>, - tag: Option<&'a str>, - }, ConnectorSupportsArbitraryModes { connector: Connector, }, @@ -1081,9 +1065,6 @@ pub enum Response { GetInputDeviceDevnode { devnode: String, }, - GetSocketPath { - path: String, - }, GetFloatAboveFullscreen { above: bool, }, diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs index 38a82d42..5365329c 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -91,10 +91,6 @@ pub enum ClientCriterion<'a> { Exe(&'a str), /// Matches the `/proc/pid/exe` of the client with a regular expression. ExeRegex(&'a str), - /// Matches the tag of the client verbatim. - Tag(&'a str), - /// Matches the tag of the client with a regular expression. - TagRegex(&'a str), } impl ClientCriterion<'_> { @@ -110,19 +106,6 @@ impl ClientCriterion<'_> { self.to_matcher().bind(cb); } - /// Sets the capabilities granted to clients matching this matcher. - /// - /// This leaks the matcher. - pub fn set_capabilities(self, caps: ClientCapabilities) { - self.to_matcher().set_capabilities(caps); - } - - /// Sets the upper capability bounds for clients in sandboxes created by this client. - /// - /// This leaks the matcher. - pub fn set_sandbox_bounding_capabilities(self, caps: ClientCapabilities) { - self.to_matcher().set_sandbox_bounding_capabilities(caps); - } } impl ClientMatcher { @@ -140,35 +123,6 @@ impl ClientMatcher { get!().set_client_matcher_handler(self, cb); } - /// Sets the capabilities granted to clients matching this matcher. - /// - /// If multiple matchers match a client, the capabilities are added. - /// - /// If no matcher matches a client, it is granted the default capabilities depending - /// on whether it's sandboxed or not. If it is not sandboxed, it is granted the - /// capabilities [`CC_LAYER_SHELL`] and [`CC_DRM_LEASE`]. Otherwise it is granted the - /// capability [`CC_DRM_LEASE`]. - /// - /// Regardless of any capabilities set through this function, the capabilities of the - /// client can never exceed its bounding capabilities. - pub fn set_capabilities(self, caps: ClientCapabilities) { - get!().set_client_matcher_capabilities(self, caps); - } - - /// Sets the upper capability bounds for clients in sandboxes created by this client. - /// - /// If multiple matchers match a client, the capabilities are added. - /// - /// If no matcher matches a client, the bounding capabilities for sandboxes depend on - /// whether the client is itself sandboxed. If it is sandboxed, the bounding - /// capabilities are the effective capabilities of the client. Otherwise the bounding - /// capabilities are all capabilities. - /// - /// Regardless of any capabilities set through this function, the capabilities set - /// through this function can never exceed the client's bounding capabilities. - pub fn set_sandbox_bounding_capabilities(self, caps: ClientCapabilities) { - get!().set_client_matcher_bounding_capabilities(self, caps); - } } impl MatchedClient { @@ -195,45 +149,3 @@ impl Deref for MatchedClient { &self.client } } - -bitflags! { - /// Capabilities granted to a client. - #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)] - pub struct ClientCapabilities(pub u64) { - /// Grants access to the `ext_data_control_manager_v1` and - /// `zwlr_data_control_manager_v1` globals. - pub const CC_DATA_CONTROL = 1 << 0, - /// Grants access to the `zwp_virtual_keyboard_manager_v1` global. - pub const CC_VIRTUAL_KEYBOARD = 1 << 1, - /// Grants access to the `ext_foreign_toplevel_list_v1` global. - pub const CC_FOREIGN_TOPLEVEL_LIST = 1 << 2, - /// Grants access to the `ext_idle_notifier_v1` global. - pub const CC_IDLE_NOTIFIER = 1 << 3, - /// Grants access to the `ext_session_lock_manager_v1` global. - pub const CC_SESSION_LOCK = 1 << 4, - /// Grants access to the `zwlr_layer_shell_v1` global. - pub const CC_LAYER_SHELL = 1 << 6, - /// Grants access to the `ext_image_copy_capture_manager_v1` and - /// `zwlr_screencopy_manager_v1` globals. - pub const CC_SCREENCOPY = 1 << 7, - /// Grants access to the `ext_transient_seat_manager_v1` global. - pub const CC_SEAT_MANAGER = 1 << 8, - /// Grants access to the `wp_drm_lease_device_v1` global. - pub const CC_DRM_LEASE = 1 << 9, - /// Grants access to the `zwp_input_method_manager_v2` global. - pub const CC_INPUT_METHOD = 1 << 10, - /// Grants access to the `ext_workspace_manager_v1` global. - pub const CC_WORKSPACE_MANAGER = 1 << 11, - /// Grants access to the `zwlr_foreign_toplevel_manager_v1` global. - pub const CC_FOREIGN_TOPLEVEL_MANAGER = 1 << 12, - /// Grants access to the `jay_head_manager_v1` and `zwlr_output_manager_v1` - /// globals. - pub const CC_HEAD_MANAGER = 1 << 13, - /// Grants access to the `zwlr_gamma_control_manager_v1` global. - pub const CC_GAMMA_CONTROL_MANAGER = 1 << 14, - /// Grants access to the `zwlr_virtual_pointer_manager_v1` global. - pub const CC_VIRTUAL_POINTER = 1 << 15, - /// Grants access to the `ext_foreign_toplevel_geometry_tracking_manager_v1` global. - pub const CC_FOREIGN_TOPLEVEL_GEOMETRY_TRACKING = 1 << 16, - } -} diff --git a/jay-config/src/exec.rs b/jay-config/src/exec.rs index 61074167..4c858900 100644 --- a/jay-config/src/exec.rs +++ b/jay-config/src/exec.rs @@ -22,7 +22,6 @@ pub struct Command { pub(crate) args: Vec, pub(crate) env: HashMap, pub(crate) fds: RefCell>, - pub(crate) tag: Option, } impl Command { @@ -38,7 +37,6 @@ impl Command { args: vec![], env: Default::default(), fds: Default::default(), - tag: Default::default(), } } @@ -84,27 +82,6 @@ impl Command { self.fd(2, fd) } - /// Runs the application with access to privileged wayland protocols. - /// - /// The default is `false`. - pub fn privileged(&mut self) -> &mut Self { - match get!(self).get_socket_path() { - Some(path) => { - self.env("WAYLAND_DISPLAY", &format!("{path}.jay")); - } - _ => { - log::error!("Compositor did not send the socket path"); - } - } - self - } - - /// Adds a tag to Wayland connections created by the spawned command. - pub fn tag(&mut self, tag: &str) -> &mut Self { - self.tag = Some(tag.to_owned()); - self - } - /// Executes the command. /// /// This consumes all attached file descriptors. diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index dbdef1ba..a3229654 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -832,8 +832,6 @@ pub enum SwitchEvent { /// Enables or disables the unauthenticated libei socket. /// -/// Even if the socket is disabled, application can still request access via the portal. -/// /// The default is `false`. pub fn set_libei_socket_enabled(enabled: bool) { get!().set_ei_socket_enabled(enabled); diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 769a5215..039b4479 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -1,36 +1,5 @@ -//! This crate allows you to configure the Jay compositor. -//! -//! A minimal example configuration looks as follows: -//! -//! ```rust -//! use jay_config::{config, quit, reload}; -//! use jay_config::input::get_default_seat; -//! use jay_config::keyboard::mods::ALT; -//! use jay_config::keyboard::syms::{SYM_q, SYM_r}; -//! -//! fn configure() { -//! let seat = get_default_seat(); -//! // Create a key binding to exit the compositor. -//! seat.bind(ALT | SYM_q, || quit()); -//! // Reload the configuration. -//! seat.bind(ALT | SYM_r, || reload()); -//! } -//! -//! config!(configure); -//! ``` -//! -//! You should configure your crate to be compiled as a shared library: -//! -//! ```toml -//! [lib] -//! crate-type = ["cdylib"] -//! ``` -//! -//! After compiling it, copy the shared library to `$HOME/.config/jay/config.so` and restart -//! the compositor. It should then use your configuration file. -//! -//! Note that you do not have to restart the compositor every time you want to reload your -//! configuration afterwards. Instead, simply invoke the [`reload`] function via a shortcut. +//! Internal Rust configuration API used by Jay's built-in TOML configuration +//! implementation. #![allow( clippy::zero_prefixed_literal, diff --git a/jay-config/src/macros.rs b/jay-config/src/macros.rs index 03b87581..fca5db04 100644 --- a/jay-config/src/macros.rs +++ b/jay-config/src/macros.rs @@ -1,21 +1,3 @@ -/// Declares the entry point of the configuration. -#[macro_export] -macro_rules! config { - ($f:path) => { - #[unsafe(no_mangle)] - #[used] - pub static mut JAY_CONFIG_ENTRY_V1: $crate::_private::ConfigEntry = { - struct X; - impl $crate::_private::Config for X { - extern "C" fn configure() { - $f(); - } - } - $crate::_private::ConfigEntryGen::::ENTRY - }; - }; -} - macro_rules! try_get { () => {{ unsafe { diff --git a/layout-animation/Cargo.toml b/layout-animation/Cargo.toml new file mode 100644 index 00000000..fd836052 --- /dev/null +++ b/layout-animation/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "jay-layout-animation" +version = "0.1.0" +edition = "2024" +license = "GPL-3.0-only" +description = "Layout animation planning for Jay" +repository = "https://github.com/mahkoh/jay" + +[dependencies] +jay-geometry = { version = "0.1.0", path = "../geometry" } diff --git a/layout-animation/src/lib.rs b/layout-animation/src/lib.rs new file mode 100644 index 00000000..03360a8e --- /dev/null +++ b/layout-animation/src/lib.rs @@ -0,0 +1,3410 @@ +use jay_geometry::Rect; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct NodeId(pub u32); + +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: impl Into, from: Rect, to: Rect) -> Self { + Self { + node_id: node_id.into(), + from, + to, + hierarchy: Default::default(), + } + } + + pub fn with_hierarchy( + node_id: impl Into, + from: Rect, + to: Rect, + hierarchy: MultiphaseWindowHierarchy, + ) -> Self { + Self { + node_id: node_id.into(), + 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(), + } + } + + #[cfg_attr(not(test), expect(dead_code))] + 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 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 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) +} + +#[cfg_attr(not(test), expect(dead_code))] +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/acceptor.rs b/src/acceptor.rs index d221d32c..b23b1ade 100644 --- a/src/acceptor.rs +++ b/src/acceptor.rs @@ -1,7 +1,6 @@ use { crate::{ async_engine::SpawnedFuture, - client::ClientCaps, security_context_acceptor::AcceptorMetadata, state::State, utils::{ @@ -46,63 +45,49 @@ struct AllocatedSocket { name: String, // /run/user/1000/wayland-x path: Ustring, - insecure: Rc, + socket: Rc, // /run/user/1000/wayland-x.lock lock_path: Ustring, _lock_fd: OwnedFd, - // /run/user/1000/wayland-x.jay - secure_path: Ustring, - secure: Rc, } impl Drop for AllocatedSocket { fn drop(&mut self) { let _ = uapi::unlink(&self.path); let _ = uapi::unlink(&self.lock_path); - let _ = uapi::unlink(&self.secure_path); } } -fn bind_socket( - insecure: &Rc, - secure: &Rc, - xrd: &str, - id: u32, -) -> Result { +fn bind_socket(socket: &Rc, xrd: &str, id: u32) -> Result { let mut addr: c::sockaddr_un = uapi::pod_zeroed(); addr.sun_family = c::AF_UNIX as _; let name = format!("wayland-{}", id); let path = format_ustr!("{}/{}", xrd, name); - let jay_path = format_ustr!("{}.jay", path.display()); let lock_path = format_ustr!("{}.lock", path.display()); - if jay_path.len() + 1 > addr.sun_path.len() { + if path.len() + 1 > addr.sun_path.len() { return Err(AcceptorError::XrdTooLong(xrd.to_string())); } let lock_fd = uapi::open(&*lock_path, c::O_CREAT | c::O_CLOEXEC | c::O_RDWR, 0o644) .map_os_err(AcceptorError::OpenLockFile)?; uapi::flock(lock_fd.raw(), c::LOCK_EX | c::LOCK_NB).map_os_err(AcceptorError::LockLockFile)?; - for (name, fd) in [(&path, insecure), (&jay_path, secure)] { - match uapi::lstat(name).to_os_error() { - Ok(_) => { - log::info!("Unlinking {}", name.display()); - let _ = uapi::unlink(name); - } - Err(OsError(c::ENOENT)) => {} - Err(e) => return Err(AcceptorError::SocketStat(e)), + match uapi::lstat(&path).to_os_error() { + Ok(_) => { + log::info!("Unlinking {}", path.display()); + let _ = uapi::unlink(&path); } - let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); - sun_path[..name.len()].copy_from_slice(name.as_bytes()); - sun_path[name.len()] = 0; - uapi::bind(fd.raw(), &addr).map_os_err(AcceptorError::BindFailed)?; + Err(OsError(c::ENOENT)) => {} + Err(e) => return Err(AcceptorError::SocketStat(e)), } + let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); + sun_path[..path.len()].copy_from_slice(path.as_bytes()); + sun_path[path.len()] = 0; + uapi::bind(socket.raw(), &addr).map_os_err(AcceptorError::BindFailed)?; Ok(AllocatedSocket { name, path, - insecure: insecure.clone(), + socket: socket.clone(), lock_path, _lock_fd: lock_fd, - secure_path: jay_path, - secure: secure.clone(), }) } @@ -111,17 +96,11 @@ fn allocate_socket() -> Result { Some(d) => d, _ => return Err(AcceptorError::XrdNotSet), }; - let mut fds = [None, None]; - for fd in &mut fds { - let socket = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) - .map(Rc::new) - .map_os_err(AcceptorError::SocketFailed)?; - *fd = Some(socket); - } - let unsecure = fds[0].take().unwrap(); - let secure = fds[1].take().unwrap(); + let socket = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) + .map(Rc::new) + .map_os_err(AcceptorError::SocketFailed)?; for i in 1..1000 { - match bind_socket(&unsecure, &secure, &xrd, i) { + match bind_socket(&socket, &xrd, i) { Ok(s) => return Ok(s), Err(e) => { log::warn!("Cannot use the wayland-{} socket: {}", i, ErrorFmt(e)); @@ -137,19 +116,12 @@ impl Acceptor { ) -> Result<(Rc, Vec>), AcceptorError> { let socket = allocate_socket()?; log::info!("bound to socket {}", socket.path.display()); - for fd in [&socket.secure, &socket.insecure] { - uapi::listen(fd.raw(), 4096).map_os_err(AcceptorError::ListenFailed)?; - } + uapi::listen(socket.socket.raw(), 4096).map_os_err(AcceptorError::ListenFailed)?; let acc = Rc::new(Acceptor { socket }); let futures = vec![ - state.eng.spawn( - "secure acceptor", - accept(acc.socket.secure.clone(), state.clone(), true), - ), - state.eng.spawn( - "insecure acceptor", - accept(acc.socket.insecure.clone(), state.clone(), false), - ), + state + .eng + .spawn("client acceptor", accept(acc.socket.socket.clone(), state.clone())), ]; state.acceptor.set(Some(acc.clone())); Ok((acc, futures)) @@ -160,16 +132,13 @@ impl Acceptor { } #[cfg_attr(not(feature = "it"), expect(dead_code))] - pub fn secure_path(&self) -> &Ustr { - self.socket.secure_path.as_ustr() + pub fn socket_path(&self) -> &Ustr { + self.socket.path.as_ustr() } } -async fn accept(fd: Rc, state: Rc, secure: bool) { - let metadata = Rc::new(AcceptorMetadata { - secure, - ..Default::default() - }); +async fn accept(fd: Rc, state: Rc) { + let metadata = Rc::new(AcceptorMetadata::default()); loop { let fd = match state.ring.accept(&fd, c::SOCK_CLOEXEC).await { Ok(fd) => fd, @@ -181,7 +150,7 @@ async fn accept(fd: Rc, state: Rc, secure: bool) { let id = state.clients.id(); if let Err(e) = state .clients - .spawn(id, &state, fd, ClientCaps::all(), false, &metadata) + .spawn(id, &state, fd, &metadata) { log::error!("Could not spawn a client: {}", ErrorFmt(e)); break; diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index cb067241..64879f72 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -1,3405 +1,27 @@ -use {crate::rect::Rect, crate::tree::NodeId}; +pub use jay_layout_animation::*; -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; +use crate::tree::NodeId as TreeNodeId; -#[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, - }); +impl From for NodeId { + fn from(value: TreeNodeId) -> Self { + Self(value.0) } - - 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) +impl From for TreeNodeId { + fn from(value: NodeId) -> Self { + Self(value.0) } } -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)) +impl PartialEq for NodeId { + fn eq(&self, other: &TreeNodeId) -> bool { + self.0 == other.0 } } -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]]); +impl PartialEq for TreeNodeId { + fn eq(&self, other: &NodeId) -> bool { + self.0 == other.0 } } diff --git a/src/cli.rs b/src/cli.rs index 0b715bbb..21aa3ec6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,8 +14,6 @@ mod pid; mod quit; mod randr; mod reexec; -mod run_privileged; -mod run_tagged; pub mod screenshot; mod seat_test; mod set_log_level; @@ -29,15 +27,13 @@ use { cli::{ clients::ClientsArgs, color_management::ColorManagementArgs, config::ConfigArgs, damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, - json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, run_tagged::RunTaggedArgs, - tree::TreeArgs, xwayland::XwaylandArgs, + json::VERBOSE_JSON, randr::RandrArgs, reexec::ReexecArgs, tree::TreeArgs, + xwayland::XwaylandArgs, }, compositor::{LogLevel, start_compositor}, - format::{Format, ref_formats}, - portal, pr_caps::drop_all_pr_caps, }, - clap::{Args, Parser, Subcommand, ValueEnum, ValueHint, builder::PossibleValue}, + clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}, clap_complete::Shell, std::sync::atomic::Ordering::Relaxed, }; @@ -88,14 +84,8 @@ pub enum Cmd { Idle(IdleArgs), /// Turn monitors on or off. Dpms(DpmsArgs), - /// Run a privileged program. - RunPrivileged(RunPrivilegedArgs), - /// Run a program with a connection tag. - RunTagged(RunTaggedArgs), /// Tests the events produced by a seat. SeatTest(SeatTestArgs), - /// Run the desktop portal. - Portal, /// Inspect/modify graphics card and connector settings. Randr(RandrArgs), /// Inspect/modify input settings. @@ -147,13 +137,6 @@ pub enum DpmsState { Off, } -#[derive(Args, Debug)] -pub struct RunPrivilegedArgs { - /// The program to run - #[clap(required = true, trailing_var_arg = true, value_hint = ValueHint::CommandWithArguments)] - pub program: Vec, -} - #[derive(ValueEnum, Debug, Copy, Clone, Hash, Default, PartialEq)] pub enum ScreenshotFormat { /// The PNG image format. @@ -240,16 +223,6 @@ pub struct GenerateArgs { shell: Shell, } -impl ValueEnum for &'static Format { - fn value_variants<'a>() -> &'a [Self] { - ref_formats() - } - - fn to_possible_value(&self) -> Option { - Some(PossibleValue::new(self.name)) - } -} - pub fn main() { let cli = Jay::parse(); if not_matches!(cli.command, Cmd::Run(_)) { @@ -268,10 +241,7 @@ pub fn main() { 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), Cmd::SeatTest(a) => seat_test::main(cli.global, a), - Cmd::Portal => portal::run_freestanding(cli.global), Cmd::Randr(a) => randr::main(cli.global, a), Cmd::Input(a) => input::main(cli.global, a), Cmd::DamageTracking(a) => damage_tracking::main(cli.global, a), diff --git a/src/cli/clients.rs b/src/cli/clients.rs index 6af73f46..393c2ecd 100644 --- a/src/cli/clients.rs +++ b/src/cli/clients.rs @@ -167,7 +167,6 @@ pub struct Client { pub is_xwayland: bool, pub comm: Option, pub exe: Option, - pub tag: Option, } pub async fn handle_client_query( @@ -212,9 +211,6 @@ pub async fn handle_client_query( Exe::handle(tl, id, c.clone(), |c, event| { last!(c).exe = Some(event.exe.to_string()); }); - Tag::handle(tl, id, c.clone(), |c, event| { - last!(c).tag = Some(event.tag.to_string()); - }); tl.round_trip().await; mem::take(&mut *c.borrow_mut()) .into_iter() @@ -253,7 +249,6 @@ impl ClientPrinter<'_> { bol!(is_xwayland, "xwayland"); opt!(comm, "comm"); opt!(exe, "exe"); - opt!(tag, "tag"); } } @@ -269,6 +264,5 @@ pub fn make_json_client(client: &Client) -> JsonClient<'_> { is_xwayland: client.is_xwayland, comm: client.comm.as_deref(), exe: client.exe.as_deref(), - tag: client.tag.as_deref(), } } diff --git a/src/cli/json.rs b/src/cli/json.rs index f7369a54..66f02d3f 100644 --- a/src/cli/json.rs +++ b/src/cli/json.rs @@ -66,8 +66,6 @@ pub struct JsonClient<'a> { pub comm: Option<&'a str>, #[serde(skip_serializing_if = "is_none")] pub exe: Option<&'a str>, - #[serde(skip_serializing_if = "is_none")] - pub tag: Option<&'a str>, } #[derive(Serialize)] diff --git a/src/cli/run_privileged.rs b/src/cli/run_privileged.rs deleted file mode 100644 index 7b1b01b9..00000000 --- a/src/cli/run_privileged.rs +++ /dev/null @@ -1,35 +0,0 @@ -use { - crate::{ - cli::{GlobalArgs, RunPrivilegedArgs}, - compositor::WAYLAND_DISPLAY, - logger::Logger, - utils::{errorfmt::ErrorFmt, oserror::OsErrorExt, xrd::xrd}, - }, - std::path::PathBuf, - uapi::UstrPtr, -}; - -pub fn main(global: GlobalArgs, args: RunPrivilegedArgs) { - Logger::install_stderr(global.log_level); - if let Some(xrd) = xrd() { - let mut wd = match std::env::var(WAYLAND_DISPLAY) { - Ok(v) => v, - _ => fatal!("{} is not set", WAYLAND_DISPLAY), - }; - wd.push_str(".jay"); - let mut path = PathBuf::from(xrd); - path.push(&wd); - if path.exists() { - unsafe { - std::env::set_var(WAYLAND_DISPLAY, &wd); - } - } - } - let mut argv = UstrPtr::new(); - for arg in &args.program { - argv.push(arg.as_str()); - } - let program = args.program[0].as_str(); - let res = uapi::execvp(program, &argv).to_os_error().unwrap_err(); - fatal!("Could not execute `{}`: {}", program, ErrorFmt(res)); -} diff --git a/src/cli/run_tagged.rs b/src/cli/run_tagged.rs deleted file mode 100644 index de7ba2c4..00000000 --- a/src/cli/run_tagged.rs +++ /dev/null @@ -1,70 +0,0 @@ -use { - crate::{ - cli::GlobalArgs, - compositor::WAYLAND_DISPLAY, - tools::tool_client::{Handle, ToolClient, with_tool_client}, - utils::{errorfmt::ErrorFmt, oserror::OsErrorExt}, - wire::{jay_acceptor_request, jay_compositor}, - }, - clap::{Args, ValueHint}, - std::{cell::Cell, env, rc::Rc}, - uapi::UstrPtr, -}; - -#[derive(Args, Debug)] -pub struct RunTaggedArgs { - /// Specifies a tag to apply to all spawned wayland connections. - tag: String, - /// The program to run. - #[clap(required = true, trailing_var_arg = true, value_hint = ValueHint::CommandWithArguments)] - pub program: Vec, -} - -pub fn main(global: GlobalArgs, run_tagged_args: RunTaggedArgs) { - with_tool_client(global.log_level, |tc| async move { - let run_tagged = Rc::new(RunTagged { tc: tc.clone() }); - run_tagged.run(run_tagged_args).await; - }); -} - -struct RunTagged { - tc: Rc, -} - -impl RunTagged { - async fn run(&self, args: RunTaggedArgs) { - let tc = &self.tc; - let comp = tc.jay_compositor().await; - let req = tc.id(); - tc.send(jay_compositor::GetTaggedAcceptor { - self_id: comp, - id: req, - tag: &args.tag, - }); - let res = Rc::new(Cell::new(None)); - jay_acceptor_request::Done::handle(&tc, req, res.clone(), |res, ev| { - res.set(Some(Ok(ev.name.to_owned()))); - }); - jay_acceptor_request::Failed::handle(&tc, req, res.clone(), |res, ev| { - res.set(Some(Err(ev.msg.to_owned()))); - }); - tc.round_trip().await; - match res.take().unwrap() { - Ok(n) => { - unsafe { - env::set_var(WAYLAND_DISPLAY, &n); - } - let mut argv = UstrPtr::new(); - for arg in &args.program { - argv.push(arg.as_str()); - } - let program = args.program[0].as_str(); - let res = uapi::execvp(program, &argv).to_os_error().unwrap_err(); - fatal!("Could not execute `{}`: {}", program, ErrorFmt(res)); - } - Err(msg) => { - fatal!("Could not create acceptor: {}", msg); - } - } - } -} diff --git a/src/client.rs b/src/client.rs index e21665ba..b3a36bb9 100644 --- a/src/client.rs +++ b/src/client.rs @@ -25,7 +25,6 @@ use { pending_serial::PendingSerial, pid_info::{PidInfo, get_pid_info, get_socket_creds}, pidfd_send_signal::pidfd_send_signal, - static_text::StaticText, }, wire::WlRegistryId, }, @@ -71,35 +70,6 @@ bitflags! { CAP_FOREIGN_TOPLEVEL_GEOMETRY_TRACKING = 1 << 16, } -impl StaticText for ClientCapsEnum { - fn text(&self) -> &'static str { - match self { - ClientCapsEnum::CAP_DATA_CONTROL_MANAGER => "data-control", - ClientCapsEnum::CAP_VIRTUAL_KEYBOARD_MANAGER => "virtual-keyboard", - ClientCapsEnum::CAP_FOREIGN_TOPLEVEL_LIST => "foreign-toplevel-list", - ClientCapsEnum::CAP_IDLE_NOTIFIER => "idle-notifier", - ClientCapsEnum::CAP_SESSION_LOCK_MANAGER => "session-lock", - ClientCapsEnum::CAP_JAY_COMPOSITOR => "jay-compositor", - ClientCapsEnum::CAP_LAYER_SHELL => "layer-shell", - ClientCapsEnum::CAP_SCREENCOPY_MANAGER => "screencopy", - ClientCapsEnum::CAP_SEAT_MANAGER => "seat-manager", - ClientCapsEnum::CAP_DRM_LEASE => "drm-lease", - ClientCapsEnum::CAP_INPUT_METHOD => "input-method", - ClientCapsEnum::CAP_WORKSPACE => "workspace-manager", - ClientCapsEnum::CAP_FOREIGN_TOPLEVEL_MANAGER => "foreign-toplevel-manager", - ClientCapsEnum::CAP_HEAD_MANAGER => "head-manager", - ClientCapsEnum::CAP_GAMMA_CONTROL_MANAGER => "gamma-control-manager", - ClientCapsEnum::CAP_VIRTUAL_POINTER_MANAGER => "virtual-pointer", - ClientCapsEnum::CAP_FOREIGN_TOPLEVEL_GEOMETRY_TRACKING => { - "foreign-toplevel-geometry-tracking" - } - } - } -} - -pub const CAPS_DEFAULT: ClientCaps = ClientCaps(CAP_LAYER_SHELL.0 | CAP_DRM_LEASE.0); -pub const CAPS_DEFAULT_SANDBOXED: ClientCaps = ClientCaps(CAP_DRM_LEASE.0); - #[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] pub struct ClientId(u64); @@ -156,24 +126,12 @@ impl Clients { id: ClientId, global: &Rc, socket: Rc, - bounding_caps: ClientCaps, - set_bounding_caps_for_children: bool, acceptor: &Rc, ) -> Result<(), ClientError> { let Some((uid, pid)) = get_socket_creds(&socket) else { return Ok(()); }; - self.spawn2( - id, - global, - socket, - uid, - pid, - bounding_caps, - set_bounding_caps_for_children, - false, - acceptor, - )?; + self.spawn2(id, global, socket, uid, pid, false, acceptor)?; Ok(()) } @@ -184,15 +142,9 @@ impl Clients { socket: Rc, uid: c::uid_t, pid: c::pid_t, - bounding_caps: ClientCaps, - set_bounding_caps_for_children: bool, is_xwayland: bool, acceptor: &Rc, ) -> Result, ClientError> { - let effective_caps = match acceptor.sandboxed { - true => CAPS_DEFAULT_SANDBOXED, - false => CAPS_DEFAULT, - }; let data = Rc::new_cyclic(|slf| Client { id, state: global.clone(), @@ -204,8 +156,6 @@ impl Clients { shutdown: Default::default(), tracker: Default::default(), is_xwayland, - effective_caps: Cell::new(effective_caps & bounding_caps), - bounding_caps_for_children: Cell::new(bounding_caps), last_enter_serial: Default::default(), pid_info: get_pid_info(uid, pid), serials: Default::default(), @@ -226,10 +176,6 @@ impl Clients { acceptor: acceptor.clone(), }); track!(data, data); - global.update_capabilities(&data, bounding_caps, set_bounding_caps_for_children); - if acceptor.secure || is_xwayland { - data.effective_caps.set(ClientCaps::all()); - } let display = Rc::new(WlDisplay::new(&data)); track!(data, display); data.objects.display.set(Some(display.clone())); @@ -239,13 +185,12 @@ impl Clients { data: data.clone(), }; log::info!( - "Client {} connected, pid: {}, uid: {}, fd: {}, comm: {:?}, caps: {:?}", + "Client {} connected, pid: {}, uid: {}, fd: {}, comm: {:?}", id, pid, uid, client.data.socket.raw(), data.pid_info.comm, - data.effective_caps.get(), ); client.data.property_changed(CL_CHANGED_NEW); self.clients.borrow_mut().insert(client.data.id, client); @@ -274,9 +219,8 @@ impl Clients { { let clients = self.clients.borrow(); for client in clients.values() { - if client.data.effective_caps.get().contains(required_caps) - && (!xwayland_only || client.data.is_xwayland) - { + let _ = required_caps; + if !xwayland_only || client.data.is_xwayland { f(&client.data); } } @@ -336,8 +280,6 @@ pub struct Client { shutdown: AsyncEvent, pub tracker: Tracker, pub is_xwayland: bool, - pub effective_caps: Cell, - pub bounding_caps_for_children: Cell, pub last_enter_serial: Cell>, pub pid_info: PidInfo, pub serials: RefCell>, @@ -349,7 +291,7 @@ pub struct Client { pub wire_scale: Cell>, pub focus_stealing_serial: Cell>, pub changed_properties: Cell, - pub destroyed: CopyHashMap>>>, + pub destroyed: CopyHashMap>>, pub acceptor: Rc, } diff --git a/src/cmm.rs b/src/cmm.rs index 9c359c9d..7a67574d 100644 --- a/src/cmm.rs +++ b/src/cmm.rs @@ -1,9 +1,48 @@ -pub mod cmm_description; -pub mod cmm_eotf; -pub mod cmm_luminance; -pub mod cmm_manager; -pub mod cmm_primaries; -pub mod cmm_render_intent; -#[cfg(test)] -mod cmm_tests; -pub mod cmm_transform; +pub mod cmm_description { + pub use jay_cmm::cmm_description::*; +} + +pub mod cmm_eotf { + pub use jay_cmm::cmm_eotf::*; +} + +pub mod cmm_luminance { + pub use jay_cmm::cmm_luminance::*; +} + +pub mod cmm_manager { + pub use jay_cmm::cmm_manager::*; +} + +pub mod cmm_primaries { + pub use jay_cmm::cmm_primaries::*; +} + +pub mod cmm_render_intent { + use crate::{ + ifs::color_management::{ + ABSOLUTE_NO_ADAPTATION_SINCE, RENDER_INTENT_ABSOLUTE_NO_ADAPTATION, + RENDER_INTENT_PERCEPTUAL, RENDER_INTENT_RELATIVE, RENDER_INTENT_RELATIVE_BPC, + }, + object::Version, + }; + + pub use jay_cmm::cmm_render_intent::*; + + pub fn from_wayland(intent: u32, version: Version) -> Option { + let res = match intent { + RENDER_INTENT_PERCEPTUAL => RenderIntent::Perceptual, + RENDER_INTENT_RELATIVE => RenderIntent::Relative, + RENDER_INTENT_RELATIVE_BPC => RenderIntent::RelativeBpc, + RENDER_INTENT_ABSOLUTE_NO_ADAPTATION if version >= ABSOLUTE_NO_ADAPTATION_SINCE => { + RenderIntent::AbsoluteNoAdaptation + } + _ => return None, + }; + Some(res) + } +} + +pub mod cmm_transform { + pub use jay_cmm::cmm_transform::*; +} diff --git a/src/compositor.rs b/src/compositor.rs index 8f1f9a05..c9ca9269 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -50,7 +50,6 @@ use { leaks, logger::Logger, output_schedule::OutputSchedule, - portal::{self, PortalStartup}, pr_caps::{PrCapsThread, pr_caps}, scale::Scale, sighand::{self, SighandError}, @@ -121,19 +120,10 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { None }; let forker = create_forker(reaper_pid); - let portal = portal::run_from_compositor(global.log_level); enable_profiler(); let logger = Logger::install_compositor(global.log_level); - let portal = match portal { - Ok(p) => Some(p), - Err(e) => { - log::error!("Could not spawn portal: {}", ErrorFmt(e)); - None - } - }; let res = start_compositor2( Some(forker), - portal, Some(logger.clone()), args, None, @@ -152,7 +142,7 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { #[cfg(feature = "it")] pub fn start_compositor_for_test(future: TestFuture) -> Result<(), CompositorError> { - let res = start_compositor2(None, None, None, RunArgs::default(), Some(future), None); + let res = start_compositor2(None, None, RunArgs::default(), Some(future), None); leaks::log_leaked(); res } @@ -194,7 +184,6 @@ pub type TestFuture = Box) -> Box>>; fn start_compositor2( forker: Option>, - portal: Option, logger: Option>, run_args: RunArgs, test_future: Option, @@ -308,7 +297,6 @@ fn start_compositor2( idle_inhibitor_ids: Default::default(), run_toplevel, config_dir: explicit_config_dir.or_else(config_dir), - config_file_id: NumCell::new(1), tracker: Default::default(), data_offer_ids: Default::default(), data_source_ids: Default::default(), @@ -342,7 +330,6 @@ fn start_compositor2( keyboard_state_ids: Default::default(), physical_keyboard_ids: Default::default(), security_context_acceptors: Default::default(), - tagged_acceptors: Default::default(), cursor_user_group_ids: Default::default(), cursor_user_ids: Default::default(), cursor_user_groups: Default::default(), @@ -420,13 +407,6 @@ fn start_compositor2( forker.setenv(key.as_bytes(), val.as_bytes()); } } - let mut _portal = None; - if let (Some(portal), Some(logger)) = (portal, &logger) { - _portal = Some(engine.spawn( - "portal", - portal.spawn(engine.clone(), ring.clone(), logger.clone()), - )); - } let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); ring.run()?; state.clear(); @@ -487,14 +467,7 @@ fn load_config( if for_test { return ConfigProxy::for_test(state); } - match ConfigProxy::from_config_dir(state) { - Ok(c) => c, - Err(e) => { - log::warn!("Could not load config.so: {}", ErrorFmt(e)); - log::warn!("Using default config"); - ConfigProxy::default(state) - } - } + ConfigProxy::default(state) } fn start_global_event_handlers(state: &Rc) -> Vec> { diff --git a/src/config.rs b/src/config.rs index 64ff595f..89cdfe5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,18 +5,14 @@ use crate::it::test_config::TEST_CONFIG_ENTRY; use { crate::{ backend::{ConnectorId, DrmDeviceId, InputDeviceId}, - client::{Client, ClientCaps}, config::handler::ConfigProxyHandler, ifs::wl_seat::SeatId, state::State, tree::{TileState, ToplevelData, ToplevelIdentifier}, utils::{ clonecell::CloneCell, - nice::{JAY_NO_REALTIME, dont_allow_config_so}, numcell::NumCell, ptr_ext::PtrExt, - unlink_on_drop::UnlinkOnDrop, - xrd::xrd, }, }, bincode::Options, @@ -30,27 +26,9 @@ use { video::{Connector, DrmDevice}, window::{self}, }, - libloading::Library, - std::{cell::Cell, io, mem, path::Path, ptr, rc::Rc}, - thiserror::Error, + std::{cell::Cell, mem, ptr, rc::Rc}, }; -#[derive(Debug, Error)] -pub enum ConfigError { - #[error("Could not load the config library")] - CouldNotLoadLibrary(#[source] libloading::Error), - #[error("Config library does not contain the entry symbol")] - LibraryDoesNotContainEntry(#[source] libloading::Error), - #[error("Could not determine the config directory")] - ConfigDirNotSet, - #[error("Could not copy the config file")] - CopyConfigFile(#[source] io::Error), - #[error("XDG_RUNTIME_DIR is not set")] - XrdNotSet, - #[error("Custom config.so is not permitted")] - NotPermitted, -} - pub struct ConfigProxy { handler: CloneCell>>, } @@ -181,16 +159,6 @@ impl ConfigProxy { self.handler.get()?.initial_tile_state(data) } - pub fn update_capabilities( - &self, - data: &Rc, - bounding_caps: ClientCaps, - set_bounding_caps: bool, - ) { - if let Some(handler) = self.handler.get() { - handler.update_capabilities(data, bounding_caps, set_bounding_caps); - } - } } impl Drop for ConfigProxy { @@ -215,18 +183,11 @@ unsafe extern "C" fn default_client_init( } impl ConfigProxy { - fn new( - lib: Option, - entry: &ConfigEntry, - state: &Rc, - path: Option, - ) -> Self { + fn new(entry: &ConfigEntry, state: &Rc) -> Self { let version = entry.version.min(VERSION); let data = Rc::new(ConfigProxyHandler { - path, client_data: Cell::new(ptr::null()), dropped: Cell::new(false), - _lib: lib, _version: version, unref: entry.unref, handle_msg: entry.handle_msg, @@ -249,8 +210,6 @@ impl ConfigProxy { client_matchers: Default::default(), client_matcher_cache: Default::default(), client_matcher_leafs: Default::default(), - client_matcher_capabilities: Default::default(), - client_matcher_bounding_capabilities: Default::default(), window_matcher_ids: NumCell::new(1), window_matchers: Default::default(), window_matcher_cache: Default::default(), @@ -291,75 +250,12 @@ impl ConfigProxy { unref: jay_config::_private::client::unref, handle_msg: jay_config::_private::client::handle_msg, }; - Self::new(None, &entry, state, None) + Self::new(&entry, state) } #[cfg(feature = "it")] pub fn for_test(state: &Rc) -> Self { - Self::new(None, &TEST_CONFIG_ENTRY, state, None) - } - - pub fn from_config_dir(state: &Rc) -> Result { - if dont_allow_config_so() { - if have_config_so(state.config_dir.as_deref()) { - log::warn!("Not loading config.so because"); - log::warn!(" 1. Jay was started with CAP_SYS_NICE"); - log::warn!(" 2. Jay was not started with {}=1", JAY_NO_REALTIME); - log::warn!(" 3. The scheduler was elevated to SCHED_RR"); - log::warn!( - " 4. Jay was not compiled with {}=1", - jay_allow_realtime_config_so!(), - ); - } - return Err(ConfigError::NotPermitted); - } - let dir = match state.config_dir.as_deref() { - Some(d) => d, - _ => return Err(ConfigError::ConfigDirNotSet), - }; - let file = format!("{}/{CONFIG_SO}", dir); - unsafe { Self::from_file(&file, state) } - } - - pub unsafe fn from_file(path: &str, state: &Rc) -> Result { - // Here we have to do a bit of a dance to support reloading. glibc will - // never load a library twice unless it has been unloaded in between. - // glibc identifies libraries by their file path and by their inode - // number. If either of those match, glibc considers the libraries - // identical. If the inode has not changed then this is not a problem - // for us since we don't want glibc to do any unnecessary work. - // However, if the user has created a new config with a new inode, then - // glibc will still not reload the library if we try to load it from - // the canonical location ~/.config/jay/config.so since it already has - // a library with that path loaded. To work around this, create a - // temporary copy with an incrementing number and load the library - // from there. - let xrd = match xrd() { - Some(x) => x, - _ => return Err(ConfigError::XrdNotSet), - }; - let copy = format!( - "{}/.jay_config.so.{}.{}", - xrd, - uapi::getpid(), - state.config_file_id.fetch_add(1) - ); - let _ = uapi::unlink(copy.as_str()); - if let Err(e) = std::fs::copy(path, ©) { - return Err(ConfigError::CopyConfigFile(e)); - } - let unlink = UnlinkOnDrop(©); - let lib = match unsafe { Library::new(©) } { - Ok(l) => l, - Err(e) => return Err(ConfigError::CouldNotLoadLibrary(e)), - }; - let entry = unsafe { lib.get::<&'static ConfigEntry>(b"JAY_CONFIG_ENTRY_V1\0") }; - let entry = match entry { - Ok(e) => *e, - Err(e) => return Err(ConfigError::LibraryDoesNotContainEntry(e)), - }; - mem::forget(unlink); - Ok(Self::new(Some(lib), entry, state, Some(copy))) + Self::new(&TEST_CONFIG_ENTRY, state) } } @@ -388,15 +284,3 @@ pub struct InvokedShortcut { pub effective_mods: Modifiers, pub sym: KeySym, } - -const CONFIG_SO: &str = "config.so"; - -pub fn have_config_so(config_dir: Option<&str>) -> bool { - let Some(dir) = config_dir else { - return false; - }; - let mut dir = dir.to_owned(); - dir.push_str("/"); - dir.push_str(CONFIG_SO); - Path::new(&dir).exists() -} diff --git a/src/config/handler.rs b/src/config/handler.rs index 3cb0a6b1..ab68c3c9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -6,9 +6,9 @@ use { InputDeviceAccelProfile, InputDeviceCapability, InputDeviceClickMethod, InputDeviceId, transaction::BackendConnectorTransactionError, }, - client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientId}, + client::{Client, ClientId}, cmm::cmm_eotf::Eotf, - compositor::{MAX_EXTENTS, WAYLAND_DISPLAY}, + compositor::MAX_EXTENTS, criteria::{ CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, clm::ClmLeafMatcher, @@ -25,7 +25,6 @@ use { output_schedule::map_cursor_hz, scale::Scale, state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State}, - tagged_acceptor::TaggedAcceptorError, theme::{ThemeColor, ThemeSized}, tree::{ ContainerSplit, OutputNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier, @@ -37,7 +36,7 @@ use { copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, - oserror::{OsError, OsErrorExt}, + oserror::OsErrorExt, stack::Stack, timer::{TimerError, TimerFd}, }, @@ -50,7 +49,7 @@ use { ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, - client::{Client as ConfigClient, ClientCapabilities, ClientMatcher}, + client::{Client as ConfigClient, ClientMatcher}, input::{ FallbackOutputMode, FocusFollowsMouseMode, InputDevice, LayerDirection, Seat, Timeline, acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile}, @@ -76,7 +75,6 @@ use { xwayland::XScalingMode, }, kbvm::Keycode, - libloading::Library, log::Level, regex::Regex, std::{ @@ -92,10 +90,8 @@ use { }; pub(super) struct ConfigProxyHandler { - pub path: Option, pub client_data: Cell<*const u8>, pub dropped: Cell, - pub _lib: Option, pub _version: u32, pub unref: unsafe extern "C" fn(data: *const u8), pub handle_msg: unsafe extern "C" fn(data: *const u8, msg: *const u8, size: usize), @@ -121,24 +117,9 @@ pub(super) struct ConfigProxyHandler { pub client_matcher_ids: NumCell, pub client_matchers: - CopyHashMap>>>, - pub client_matcher_cache: CriterionCache>, + CopyHashMap>>, + pub client_matcher_cache: CriterionCache, pub client_matcher_leafs: CopyHashMap>, - pub client_matcher_capabilities: CopyHashMap< - ClientMatcher, - ( - Rc>>, - ClientCaps, - ), - >, - pub client_matcher_bounding_capabilities: CopyHashMap< - ClientMatcher, - ( - Rc>>, - ClientCaps, - ), - >, - pub window_matcher_ids: NumCell, pub window_matchers: CopyHashMap>>, @@ -218,11 +199,6 @@ impl ConfigProxyHandler { self.window_matcher_leafs.clear(); self.window_matchers.clear(); - if let Some(path) = &self.path - && let Err(e) = uapi::unlink(path.as_str()) - { - log::error!("Could not unlink {}: {}", path, ErrorFmt(OsError(e.0))); - } } pub fn send(&self, msg: &ServerMessage) { @@ -1185,19 +1161,6 @@ impl ConfigProxyHandler { self.state.set_color_management_enabled(enabled); } - fn handle_get_socket_path(&self) { - match self.state.acceptor.get() { - Some(a) => { - self.respond(Response::GetSocketPath { - path: a.socket_name().to_string(), - }); - } - _ => { - log::warn!("There is no acceptor"); - } - } - } - fn handle_connector_connected(&self, connector: Connector) -> Result<(), CphError> { let connector = self.get_connector(connector)?; self.respond(Response::ConnectorConnected { @@ -1937,18 +1900,9 @@ impl ConfigProxyHandler { &self, prog: &str, args: Vec, - mut env: Vec<(String, String)>, + env: Vec<(String, String)>, fds: Vec<(i32, i32)>, - tag: Option<&str>, ) -> Result<(), CphError> { - if let Some(tag) = tag { - let display = self - .state - .tagged_acceptors - .get(&self.state, tag) - .map_err(CphError::CreateTaggedAcceptor)?; - env.push((WAYLAND_DISPLAY.to_string(), display.to_string())); - } let fds: Vec<_> = fds .into_iter() .map(|(a, b)| (a, Rc::new(OwnedFd::new(b)))) @@ -2125,7 +2079,7 @@ impl ConfigProxyHandler { fn get_client_matcher( &self, matcher: ClientMatcher, - ) -> Result>>, CphError> { + ) -> Result>, CphError> { self.client_matchers .get(&matcher) .ok_or(CphError::ClientMatcherDoesNotExist(matcher)) @@ -2226,7 +2180,6 @@ impl ConfigProxyHandler { } ClientCriterionStringField::Comm => mgr.comm(needle), ClientCriterionStringField::Exe => mgr.exe(needle), - ClientCriterionStringField::Tag => mgr.tag(needle), } } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), @@ -2249,8 +2202,6 @@ impl ConfigProxyHandler { fn handle_destroy_client_matcher(&self, matcher: ClientMatcher) { self.client_matchers.remove(&matcher); self.client_matcher_leafs.remove(&matcher); - self.client_matcher_capabilities.remove(&matcher); - self.client_matcher_bounding_capabilities.remove(&matcher); } fn handle_enable_client_matcher_events( @@ -2862,28 +2813,6 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_client_matcher_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) -> Result<(), CphError> { - let m = self.get_client_matcher(matcher)?; - self.client_matcher_capabilities - .set(matcher, (m, caps.to_client_caps())); - Ok(()) - } - - fn handle_set_client_matcher_bounding_capabilities( - &self, - matcher: ClientMatcher, - caps: ClientCapabilities, - ) -> Result<(), CphError> { - let m = self.get_client_matcher(matcher)?; - self.client_matcher_bounding_capabilities - .set(matcher, (m, caps.to_client_caps())); - Ok(()) - } - pub fn handle_request(self: &Rc, msg: &[u8]) { if let Err(e) = self.handle_request_(msg) { log::error!("Could not handle client request: {}", ErrorFmt(e)); @@ -2946,7 +2875,7 @@ impl ConfigProxyHandler { ClientMessage::GetSeats => self.handle_get_seats(), ClientMessage::RemoveSeat { .. } => {} ClientMessage::Run { prog, args, env } => { - self.handle_run(prog, args, env, vec![], None).wrn("run")? + self.handle_run(prog, args, env, vec![]).wrn("run")? } ClientMessage::GrabKb { kb, grab } => self.handle_grab(kb, grab).wrn("grab")?, ClientMessage::SetColor { colorable, color } => { @@ -3154,7 +3083,7 @@ impl ConfigProxyHandler { args, env, fds, - } => self.handle_run(prog, args, env, fds, None).wrn("run")?, + } => self.handle_run(prog, args, env, fds).wrn("run")?, ClientMessage::DisableDefaultSeat => self.state.create_default_seat.set(false), ClientMessage::DestroyKeymap { keymap } => self.handle_destroy_keymap(keymap), ClientMessage::GetConnectorName { connector } => self @@ -3207,7 +3136,6 @@ impl ConfigProxyHandler { ClientMessage::SetExplicitSyncEnabled { enabled } => { self.handle_set_explicit_sync_enabled(enabled) } - ClientMessage::GetSocketPath => self.handle_get_socket_path(), ClientMessage::DeviceSetKeymap { device, keymap } => self .handle_set_device_keymap(device, keymap) .wrn("set_device_keymap")?, @@ -3508,12 +3436,6 @@ impl ConfigProxyHandler { .wrn("connector_set_blend_space")?, ClientMessage::SetBarFont { font } => self.handle_set_bar_font(font), ClientMessage::SetTitleFont { font } => self.handle_set_title_font(font), - ClientMessage::SetClientMatcherCapabilities { matcher, caps } => self - .handle_set_client_matcher_capabilities(matcher, caps) - .wrn("set_client_matcher_capabilities")?, - ClientMessage::SetClientMatcherBoundingCapabilities { matcher, caps } => self - .handle_set_client_matcher_bounding_capabilities(matcher, caps) - .wrn("set_client_matcher_bounding_capabilities")?, ClientMessage::ShowWorkspaceOn { seat, workspace, @@ -3559,13 +3481,6 @@ impl ConfigProxyHandler { ClientMessage::SetXWaylandEnabled { enabled } => self .handle_set_x_wayland_enabled(enabled) .wrn("set_x_wayland_enabled")?, - ClientMessage::Run3 { - prog, - args, - env, - fds, - tag, - } => self.handle_run(prog, args, env, fds, tag).wrn("run")?, ClientMessage::ConnectorSupportsArbitraryModes { connector } => self .handle_connector_supports_arbitrary_modes(connector) .wrn("connector_supports_arbitrary_modes")?, @@ -3638,41 +3553,6 @@ impl ConfigProxyHandler { None } - pub fn update_capabilities( - &self, - data: &Rc, - bounding_caps: ClientCaps, - set_bounding_caps: bool, - ) { - let mut have_caps = false; - let mut have_bounding_caps = false; - let mut caps = ClientCaps::none(); - let mut new_bounding_caps = ClientCaps::none(); - for (matcher, state) in self.client_matcher_capabilities.lock().values() { - if matcher.node.pull(data) { - have_caps = true; - caps |= *state; - } - } - for (matcher, state) in self.client_matcher_bounding_capabilities.lock().values() { - if matcher.node.pull(data) { - have_bounding_caps = true; - new_bounding_caps |= *state; - } - } - if have_caps { - caps &= bounding_caps; - data.effective_caps.set(caps); - } - if !have_bounding_caps && set_bounding_caps { - have_bounding_caps = true; - new_bounding_caps = data.effective_caps.get(); - } - if have_bounding_caps { - new_bounding_caps &= bounding_caps; - data.bounding_caps_for_children.set(new_bounding_caps); - } - } } #[derive(Debug, Error)] @@ -3771,8 +3651,6 @@ enum CphError { UnknownFallbackOutputMode(FallbackOutputMode), #[error("Unknown tile state {0:?}")] UnknownTileState(ConfigTileState), - #[error("Could not create a tagged acceptor")] - CreateTaggedAcceptor(#[source] TaggedAcceptorError), } trait WithRequestName { @@ -3784,13 +3662,3 @@ impl WithRequestName for Result<(), CphError> { self.map_err(move |e| CphError::FailedRequest(request, Box::new(e))) } } - -trait ClientCapabilitiesExt { - fn to_client_caps(self) -> ClientCaps; -} - -impl ClientCapabilitiesExt for ClientCapabilities { - fn to_client_caps(self) -> ClientCaps { - ClientCaps(self.0 as u32) & !CAP_JAY_COMPOSITOR & ClientCaps::all() - } -} diff --git a/src/criteria.rs b/src/criteria.rs index cf8125f2..2405bacb 100644 --- a/src/criteria.rs +++ b/src/criteria.rs @@ -1,96 +1,4 @@ pub mod clm; -mod crit_graph; -pub mod crit_leaf; -mod crit_matchers; -mod crit_per_target_data; pub mod tlm; -use { - crate::{ - criteria::{ - crit_graph::{CritMgr, CritMiddle, CritRoot, CritRootCriterion, CritRootFixed}, - crit_leaf::CritLeafMatcher, - crit_matchers::{critm_any_or_all::CritMatchAnyOrAll, critm_exactly::CritMatchExactly}, - }, - utils::copyhashmap::CopyHashMap, - }, - linearize::StaticMap, - regex::Regex, - std::rc::{Rc, Weak}, -}; -pub use { - crit_graph::{CritTarget, CritUpstreamNode}, - crit_per_target_data::CritDestroyListener, -}; - -linear_ids!(CritMatcherIds, CritMatcherId, u64); - -type RootMatcherMap = CopyHashMap>>; -type FixedRootMatcher = StaticMap>>>; - -#[derive(Clone)] -pub enum CritLiteralOrRegex { - Literal(String), - Regex(Regex), -} - -impl CritLiteralOrRegex { - fn matches(&self, string: &str) -> bool { - match self { - CritLiteralOrRegex::Literal(p) => string == p, - CritLiteralOrRegex::Regex(r) => r.is_match(string), - } - } -} - -pub trait CritMgrExt: CritMgr { - fn list( - &self, - upstream: &[Rc>], - all: bool, - ) -> Rc> { - if upstream.is_empty() { - return self.match_constant()[all].clone(); - } - CritMiddle::new(self, upstream, CritMatchAnyOrAll::new(upstream, all)) - } - - fn exactly( - &self, - upstream: &[Rc>], - num: usize, - ) -> Rc> { - if num > upstream.len() { - return self.match_constant()[false].clone(); - } - if num == 0 { - let upstream: Vec<_> = upstream.iter().map(|u| u.not(self)).collect(); - return self.list(&upstream, true); - } - CritMiddle::new(self, upstream, CritMatchExactly::new(upstream, num)) - } - - fn leaf( - &self, - upstream: &Rc>, - on_match: impl Fn(::LeafData) -> Box + 'static, - ) -> Rc> { - CritLeafMatcher::new(self, upstream, on_match) - } - - fn not( - &self, - upstream: &Rc>, - ) -> Rc> { - upstream.not(self) - } - - fn root(&self, criterion: T) -> Rc> - where - T: CritRootCriterion, - { - CritRoot::new(self.roots(), self.id(), criterion) - } -} - -impl CritMgrExt for T where T: CritMgr {} +pub use jay_criteria::*; diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 96e78a03..87f0a8db 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -13,7 +13,7 @@ use { clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine, - ClmMatchSandboxInstanceId, ClmMatchTag, + ClmMatchSandboxInstanceId, }, clmm_uid::ClmMatchUid, }, @@ -39,19 +39,19 @@ bitflags! { CL_CHANGED_NEW, } -type ClmFixedRootMatcher = FixedRootMatcher, T>; +type ClmFixedRootMatcher = FixedRootMatcher; pub struct ClMatcherManager { ids: Rc, changes: AsyncQueue>, - leaf_events: Rc>>>, - constant: ClmFixedRootMatcher>>, + leaf_events: Rc>>, + constant: ClmFixedRootMatcher>, sandboxed: ClmFixedRootMatcher, is_xwayland: ClmFixedRootMatcher, matchers: Rc, } -type ClmRootMatcherMap = RootMatcherMap, T>; +type ClmRootMatcherMap = RootMatcherMap; #[derive(Default)] pub struct RootMatchers { @@ -62,7 +62,6 @@ pub struct RootMatchers { pid: ClmRootMatcherMap, comm: ClmRootMatcherMap, exe: ClmRootMatcherMap, - tag: ClmRootMatcherMap, id: ClmRootMatcherMap, } @@ -75,7 +74,6 @@ impl RootMatchers { self.pid.clear(); self.comm.clear(); self.exe.clear(); - self.tag.clear(); self.id.clear(); } } @@ -98,8 +96,8 @@ pub async fn handle_cl_leaf_events(state: Rc) { } } -pub type ClmUpstreamNode = dyn CritUpstreamNode>; -pub type ClmLeafMatcher = CritLeafMatcher>; +pub type ClmUpstreamNode = dyn CritUpstreamNode; +pub type ClmLeafMatcher = CritLeafMatcher; impl ClMatcherManager { pub fn new(ids: &Rc) -> Self { @@ -146,6 +144,7 @@ impl ClMatcherManager { } fn update_matches(&self, data: &Rc) { + let data = data.as_ref(); let mut changed = data.changed_properties.take(); if changed.contains(CL_CHANGED_DESTROYED) { for destroyed in data.destroyed.lock().drain_values() { @@ -186,7 +185,6 @@ impl ClMatcherManager { unconditional!(pid); unconditional!(comm); unconditional!(exe); - unconditional!(tag); unconditional!(id); fixed!(sandboxed); fixed!(is_xwayland); @@ -230,20 +228,29 @@ impl ClMatcherManager { self.root(ClmMatchExe::new(string)) } - pub fn tag(&self, string: CritLiteralOrRegex) -> Rc { - self.root(ClmMatchTag::new(string)) - } } -impl CritTarget for Rc { +pub struct ClientTargetOwner { + client: Rc, +} + +pub struct WeakClientTargetOwner { + state: Weak, + id: ClientId, +} + +impl CritTarget for Client { type Id = ClientId; type Mgr = ClMatcherManager; type RootMatchers = RootMatchers; type LeafData = ClientId; - type Owner = Weak; + type Owner = WeakClientTargetOwner; fn owner(&self) -> Self::Owner { - Rc::downgrade(self) + WeakClientTargetOwner { + state: Rc::downgrade(&self.state), + id: self.id, + } } fn id(&self) -> Self::Id { @@ -259,25 +266,27 @@ impl CritTarget for Rc { } } -impl CritTargetOwner for Rc { - type Target = Rc; +impl CritTargetOwner for ClientTargetOwner { + type Target = Client; fn data(&self) -> &Self::Target { - self + &self.client } } -impl WeakCritTargetOwner for Weak { - type Target = Rc; - type Owner = Rc; +impl WeakCritTargetOwner for WeakClientTargetOwner { + type Target = Client; + type Owner = ClientTargetOwner; fn upgrade(&self) -> Option { - self.upgrade() + let state = self.state.upgrade()?; + let client = state.clients.get(self.id).ok()?; + Some(ClientTargetOwner { client }) } } impl CritMgr for ClMatcherManager { - type Target = Rc; + type Target = Client; fn id(&self) -> CritMatcherId { self.ids.next() diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs index f39837fc..793d9a14 100644 --- a/src/criteria/clm/clm_matchers.rs +++ b/src/criteria/clm/clm_matchers.rs @@ -1,6 +1,6 @@ macro_rules! fixed_root_criterion { ($ty:ty, $field:ident) => { - impl crate::criteria::crit_graph::CritFixedRootCriterionBase> + impl crate::criteria::crit_graph::CritFixedRootCriterionBase for $ty { fn constant(&self) -> bool { @@ -10,7 +10,7 @@ macro_rules! fixed_root_criterion { fn not<'a>( &self, mgr: &'a crate::criteria::clm::ClMatcherManager, - ) -> &'a crate::criteria::FixedRootMatcher, Self> { + ) -> &'a crate::criteria::FixedRootMatcher { &mgr.$field } } diff --git a/src/criteria/clm/clm_matchers/clmm_id.rs b/src/criteria/clm/clm_matchers/clmm_id.rs index 1cb347af..f8eece41 100644 --- a/src/criteria/clm/clm_matchers/clmm_id.rs +++ b/src/criteria/clm/clm_matchers/clmm_id.rs @@ -3,17 +3,16 @@ use { client::{Client, ClientId}, criteria::{RootMatcherMap, clm::RootMatchers, crit_graph::CritRootCriterion}, }, - std::rc::Rc, }; pub struct ClmMatchId(pub ClientId); -impl CritRootCriterion> for ClmMatchId { - fn matches(&self, data: &Rc) -> bool { +impl CritRootCriterion for ClmMatchId { + fn matches(&self, data: &Client) -> bool { data.id == self.0 } - fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap, Self>> { + fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap> { Some(&roots.id) } } diff --git a/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs b/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs index 4f71c47f..d8201b65 100644 --- a/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs +++ b/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs @@ -1,14 +1,13 @@ use { crate::{client::Client, criteria::crit_graph::CritFixedRootCriterion}, - std::rc::Rc, }; pub struct ClmMatchIsXwayland(pub bool); fixed_root_criterion!(ClmMatchIsXwayland, is_xwayland); -impl CritFixedRootCriterion> for ClmMatchIsXwayland { - fn matches(&self, data: &Rc) -> bool { +impl CritFixedRootCriterion for ClmMatchIsXwayland { + fn matches(&self, data: &Client) -> bool { data.is_xwayland } } diff --git a/src/criteria/clm/clm_matchers/clmm_pid.rs b/src/criteria/clm/clm_matchers/clmm_pid.rs index fc7ae8dc..74ef299c 100644 --- a/src/criteria/clm/clm_matchers/clmm_pid.rs +++ b/src/criteria/clm/clm_matchers/clmm_pid.rs @@ -3,18 +3,17 @@ use { client::Client, criteria::{RootMatcherMap, clm::RootMatchers, crit_graph::CritRootCriterion}, }, - std::rc::Rc, uapi::c, }; pub struct ClmMatchPid(pub c::pid_t); -impl CritRootCriterion> for ClmMatchPid { - fn matches(&self, data: &Rc) -> bool { +impl CritRootCriterion for ClmMatchPid { + fn matches(&self, data: &Client) -> bool { data.pid_info.pid == self.0 } - fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap, Self>> { + fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap> { Some(&roots.pid) } } diff --git a/src/criteria/clm/clm_matchers/clmm_sandboxed.rs b/src/criteria/clm/clm_matchers/clmm_sandboxed.rs index 4988e9b1..8e0d8f53 100644 --- a/src/criteria/clm/clm_matchers/clmm_sandboxed.rs +++ b/src/criteria/clm/clm_matchers/clmm_sandboxed.rs @@ -1,14 +1,13 @@ use { crate::{client::Client, criteria::crit_graph::CritFixedRootCriterion}, - std::rc::Rc, }; pub struct ClmMatchSandboxed(pub bool); fixed_root_criterion!(ClmMatchSandboxed, sandboxed); -impl CritFixedRootCriterion> for ClmMatchSandboxed { - fn matches(&self, data: &Rc) -> bool { +impl CritFixedRootCriterion for ClmMatchSandboxed { + fn matches(&self, data: &Client) -> bool { data.acceptor.sandboxed } } diff --git a/src/criteria/clm/clm_matchers/clmm_string.rs b/src/criteria/clm/clm_matchers/clmm_string.rs index a5877adc..8f7c3689 100644 --- a/src/criteria/clm/clm_matchers/clmm_string.rs +++ b/src/criteria/clm/clm_matchers/clmm_string.rs @@ -7,15 +7,14 @@ use { }, security_context_acceptor::AcceptorMetadata, }, - std::{marker::PhantomData, rc::Rc}, + std::marker::PhantomData, }; -pub type ClmMatchString = CritMatchString, T>; +pub type ClmMatchString = CritMatchString; pub type ClmMatchSandboxEngine = ClmMatchString>; pub type ClmMatchSandboxAppId = ClmMatchString>; pub type ClmMatchSandboxInstanceId = ClmMatchString>; -pub type ClmMatchTag = ClmMatchString>; pub type ClmMatchComm = ClmMatchString; pub type ClmMatchExe = ClmMatchString; @@ -33,13 +32,12 @@ trait AcceptorMetadataField: Sized + 'static { pub struct SandboxEngineField; pub struct SandboxAppIdField; pub struct SandboxInstanceIdField; -pub struct TagField; -impl StringAccess> for AcceptorMetadataAccess +impl StringAccess for AcceptorMetadataAccess where T: AcceptorMetadataField, { - fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { + fn with_string(data: &Client, f: impl FnOnce(&str) -> bool) -> bool { f(T::field(&data.acceptor).as_deref().unwrap_or_default()) } @@ -84,20 +82,8 @@ impl AcceptorMetadataField for SandboxInstanceIdField { } } -impl AcceptorMetadataField for TagField { - fn field(meta: &AcceptorMetadata) -> &Option { - &meta.tag - } - - fn nodes( - roots: &RootMatchers, - ) -> &ClmRootMatcherMap>> { - &roots.tag - } -} - -impl StringAccess> for CommAccess { - fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { +impl StringAccess for CommAccess { + fn with_string(data: &Client, f: impl FnOnce(&str) -> bool) -> bool { f(&data.pid_info.comm) } @@ -106,8 +92,8 @@ impl StringAccess> for CommAccess { } } -impl StringAccess> for ExeAccess { - fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { +impl StringAccess for ExeAccess { + fn with_string(data: &Client, f: impl FnOnce(&str) -> bool) -> bool { f(&data.pid_info.exe) } diff --git a/src/criteria/clm/clm_matchers/clmm_uid.rs b/src/criteria/clm/clm_matchers/clmm_uid.rs index 6056b955..b68d8bf0 100644 --- a/src/criteria/clm/clm_matchers/clmm_uid.rs +++ b/src/criteria/clm/clm_matchers/clmm_uid.rs @@ -3,18 +3,17 @@ use { client::Client, criteria::{RootMatcherMap, clm::RootMatchers, crit_graph::CritRootCriterion}, }, - std::rc::Rc, uapi::c, }; pub struct ClmMatchUid(pub c::uid_t); -impl CritRootCriterion> for ClmMatchUid { - fn matches(&self, data: &Rc) -> bool { +impl CritRootCriterion for ClmMatchUid { + fn matches(&self, data: &Client) -> bool { data.pid_info.uid == self.0 } - fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap, Self>> { + fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap> { Some(&roots.uid) } } diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index d7b5bece..b7a61170 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -408,10 +408,10 @@ impl CritTarget for ToplevelData { type Mgr = TlMatcherManager; type RootMatchers = RootMatchers; type LeafData = ToplevelIdentifier; - type Owner = Weak; + type Owner = WeakToplevelTargetOwner; fn owner(&self) -> Self::Owner { - self.slf.clone() + WeakToplevelTargetOwner(self.slf.clone()) } fn id(&self) -> Self::Id { @@ -427,20 +427,24 @@ impl CritTarget for ToplevelData { } } -impl CritTargetOwner for Rc { +pub struct ToplevelTargetOwner(Rc); + +pub struct WeakToplevelTargetOwner(Weak); + +impl CritTargetOwner for ToplevelTargetOwner { type Target = ToplevelData; fn data(&self) -> &Self::Target { - self.tl_data() + self.0.tl_data() } } -impl WeakCritTargetOwner for Weak { +impl WeakCritTargetOwner for WeakToplevelTargetOwner { type Target = ToplevelData; - type Owner = Rc; + type Owner = ToplevelTargetOwner; fn upgrade(&self) -> Option { - self.upgrade() + self.0.upgrade().map(ToplevelTargetOwner) } } diff --git a/src/criteria/tlm/tlm_matchers/tlmm_client.rs b/src/criteria/tlm/tlm_matchers/tlmm_client.rs index 13fd3a5c..c6db5163 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_client.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_client.rs @@ -21,7 +21,7 @@ pub struct TlmMatchClient { id: CritMatcherId, state: Rc, node: Rc, - upstream: CritDownstreamData>, + upstream: CritDownstreamData, downstream: CritUpstreamData, } @@ -73,8 +73,8 @@ impl CritUpstreamNodeBase for TlmMatchClient { } } -impl CritDownstream> for TlmMatchClient { - fn update_matched(self: Rc, target: &Rc, matched: bool) { +impl CritDownstream for TlmMatchClient { + fn update_matched(self: Rc, target: &Client, matched: bool) { let handle = |data: &ToplevelData| { let node = match matched { true => self.downstream.get_or_create(data), diff --git a/src/edid.rs b/src/edid.rs index 685ac0a1..e6b91739 100644 --- a/src/edid.rs +++ b/src/edid.rs @@ -1,1302 +1 @@ -use { - crate::utils::{ - bitflags::BitflagsExt, clonecell::UnsafeCellCloneSafe, ptr_ext::PtrExt, stack::Stack, - }, - bstr::{BString, ByteSlice}, - std::{ - fmt::{Debug, Formatter}, - rc::Rc, - }, - thiserror::Error, -}; - -#[derive(Copy, Clone, Debug)] -pub enum ColorBitDepth { - Undefined, - Bits6, - Bits8, - Bits10, - Bits12, - Bits14, - Bits16, - Reserved, -} - -#[derive(Copy, Clone, Debug)] -pub enum DigitalVideoInterfaceStandard { - Undefined, - Dvi, - HdmiA, - HdmiB, - MDDI, - DisplayPort, - #[expect(dead_code)] - Unknown(u8), -} - -#[derive(Copy, Clone)] -pub struct SignalLevelStandard(u8); - -impl Debug for SignalLevelStandard { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = match self.0 { - 0 => "+0.7/−0.3 V", - 1 => "+0.714/−0.286 V", - 2 => "+1.0/−0.4 V", - _ => "+0.7/0 V", - }; - Debug::fmt(s, f) - } -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub enum VideoInputDefinition { - Analog { - signal_level_standard: SignalLevelStandard, - blank_to_black_setup_or_pedestal: bool, - separate_h_v_sync_supported: bool, - composite_sync_on_horizontal_supported: bool, - composite_sync_on_green_supported: bool, - serration_on_vertical_sync_supported: bool, - }, - Digital { - bit_depth: ColorBitDepth, - video_interface: DigitalVideoInterfaceStandard, - }, -} - -#[derive(Copy, Clone, Debug)] -pub struct ScreenDimensions { - pub horizontal_screen_size_cm: Option, - pub vertical_screen_size_cm: Option, - pub landscape_aspect_ration: Option, - pub portrait_aspect_ration: Option, -} - -#[derive(Copy, Clone, Debug)] -pub struct ChromaticityCoordinates { - pub red_x: u16, - pub red_y: u16, - pub green_x: u16, - pub green_y: u16, - pub blue_x: u16, - pub blue_y: u16, - pub white_x: u16, - pub white_y: u16, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct EstablishedTimings { - pub s_720x400_70: bool, - pub s_720x400_88: bool, - pub s_640x480_60: bool, - pub s_640x480_67: bool, - pub s_640x480_72: bool, - pub s_640x480_75: bool, - pub s_800x600_56: bool, - pub s_800x600_60: bool, - pub s_800x600_72: bool, - pub s_800x600_75: bool, - pub s_832x624_75: bool, - pub s_1024x768_87: bool, - pub s_1024x768_60: bool, - pub s_1024x768_70: bool, - pub s_1024x768_75: bool, - pub s_1280x1024_75: bool, - pub s_1152x870_75: bool, -} - -#[derive(Copy, Clone, Debug)] -pub enum AspectRatio { - A1_1, - A16_10, - A4_3, - A5_4, - A16_9, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct StandardTiming { - pub x_resolution: u16, - pub aspect_ratio: AspectRatio, - pub vertical_frequency: u8, -} - -#[derive(Copy, Clone, Debug)] -pub enum AnalogSyncType { - AnalogComposite, - BipolarAnalogComposite, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub enum SyncSignal { - Analog { - ty: AnalogSyncType, - with_serrations: bool, - sync_on_all_signals: bool, - }, - DigitalComposite { - with_serration: bool, - horizontal_sync_is_positive: bool, - }, - DigitalSeparate { - vertical_sync_is_positive: bool, - horizontal_sync_is_positive: bool, - }, -} - -#[derive(Copy, Clone)] -pub enum StereoViewingSupport { - None, - FieldSequentialRightDuringStereoSync, - FieldSequentialLeftDuringStereoSync, - TwoWayInterleavedRightImageOnEvenLines, - TwoWayInterleavedLeftImageOnEvenLines, - FourWayInterleaved, - SideBySideInterleaved, -} - -impl Debug for StereoViewingSupport { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let msg = match *self { - StereoViewingSupport::None => "none", - StereoViewingSupport::FieldSequentialRightDuringStereoSync => { - "field sequential, right during stereo sync" - } - StereoViewingSupport::FieldSequentialLeftDuringStereoSync => { - "field sequential, left during stereo sync" - } - StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines => { - "2-way interleaved, right image on even lines" - } - StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines => { - "2-way interleaved, left image on even lines" - } - StereoViewingSupport::FourWayInterleaved => "4-way interleaved", - StereoViewingSupport::SideBySideInterleaved => "side-by-side interleaved", - }; - write!(f, "\"{}\"", msg) - } -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct DisplayRangeLimitsAndAdditionalTiming { - pub vertical_field_rate_min: u16, - pub vertical_field_rate_max: u16, - pub horizontal_field_rate_min: u16, - pub horizontal_field_rate_max: u16, - pub maximum_pixel_clock_mhz: u16, - pub extended_timing_information: ExtendedTimingInformation, -} - -#[derive(Copy, Clone, Debug)] -pub enum AspectRatioPreference { - A4_3, - A16_9, - A16_10, - A5_4, - A15_9, - #[expect(dead_code)] - Unknown(u8), -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub enum ExtendedTimingInformation { - DefaultGtf, - NoTimingInformation, - SecondaryGtf { - start_frequency: u16, - c_value: u16, - m_value: u16, - k_value: u8, - j_value: u16, - }, - Cvt { - cvt_major_version: u8, - cvt_minor_version: u8, - additional_clock_precision: u8, - maximum_active_pixels_per_line: Option, - ar_4_3: bool, - ar_16_9: bool, - ar_16_10: bool, - ar_5_4: bool, - ar_15_9: bool, - ar_preference: AspectRatioPreference, - cvt_rb_reduced_blanking_preferred: bool, - cvt_standard_blanking: bool, - scaling_support_horizontal_shrink: bool, - scaling_support_horizontal_stretch: bool, - scaling_support_vertical_shrink: bool, - scaling_support_vertical_stretch: bool, - preferred_vertical_refresh_rate_hz: u8, - }, - Unknown(u8), -} - -#[derive(Copy, Clone, Debug, Default)] -#[expect(dead_code)] -pub struct ColorPoint { - pub white_point_index: u8, - pub white_point_x: u16, - pub white_point_y: u16, - pub gamma: Option, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct EstablishedTimings3 { - pub s640x350_85: bool, - pub s640x400_85: bool, - pub s720x400_85: bool, - pub s640x480_85: bool, - pub s848x480_60: bool, - pub s800x600_85: bool, - pub s1024x768_85: bool, - pub s1152x864_75: bool, - pub s1280x768_60_rb: bool, - pub s1280x768_60: bool, - pub s1280x768_75: bool, - pub s1280x768_85: bool, - pub s1280x960_60: bool, - pub s1280x960_85: bool, - pub s1280x1024_60: bool, - pub s1280x1024_85: bool, - pub s1360x768_60: bool, - pub s1440x900_60_rb: bool, - pub s1440x900_60: bool, - pub s1440x900_75: bool, - pub s1440x900_85: bool, - pub s1400x1050_60_rb: bool, - pub s1400x1050_60: bool, - pub s1400x1050_75: bool, - pub s1400x1050_85: bool, - pub s1680x1050_60_rb: bool, - pub s1680x1050_60: bool, - pub s1680x1050_75: bool, - pub s1680x1050_85: bool, - pub s1600x1200_60: bool, - pub s1600x1200_65: bool, - pub s1600x1200_70: bool, - pub s1600x1200_75: bool, - pub s1600x1200_85: bool, - pub s1792x1344_60: bool, - pub s1792x1344_75: bool, - pub s1856x1392_60: bool, - pub s1856x1392_75: bool, - pub s1920x1200_60_rb: bool, - pub s1920x1200_60: bool, - pub s1920x1200_75: bool, - pub s1920x1200_85: bool, - pub s1920x1440_60: bool, - pub s1920x1440_75: bool, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct ColorManagementData { - pub red_a3: u16, - pub red_a2: u16, - pub green_a3: u16, - pub green_a2: u16, - pub blue_a3: u16, - pub blue_a2: u16, -} - -#[derive(Copy, Clone, Debug)] -pub enum CvtAspectRatio { - A4_3, - A16_9, - A16_10, - A15_9, -} - -#[derive(Copy, Clone, Debug)] -pub enum CvtPreferredVerticalRate { - R50, - R60, - R75, - R85, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct Cvt3ByteCode { - pub addressable_lines_per_field: u16, - pub aspect_ration: CvtAspectRatio, - pub preferred_vertical_rate: CvtPreferredVerticalRate, - pub r50: bool, - pub r60: bool, - pub r75: bool, - pub r85: bool, - pub r60_reduced_blanking: bool, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct DetailedTimingDescriptor { - pub pixel_clock_khz: u32, - pub horizontal_addressable_pixels: u16, - pub horizontal_blanking_pixels: u16, - pub vertical_addressable_lines: u16, - pub vertical_blanking_lines: u16, - pub horizontal_front_porch_pixels: u16, - pub horizontal_sync_pulse_pixels: u16, - pub vertical_front_porch_lines: u8, - pub vertical_sync_pulse_lines: u8, - pub horizontal_addressable_mm: u16, - pub vertical_addressable_mm: u16, - pub horizontal_left_border_pixels: u8, - pub vertical_top_border_pixels: u8, - pub interlaced: bool, - pub stereo_viewing_support: StereoViewingSupport, - pub sync: SyncSignal, -} - -#[derive(Clone, Debug)] -#[expect(dead_code)] -pub enum Descriptor { - Unknown(u8), - DetailedTimingDescriptor(DetailedTimingDescriptor), - DisplayProductSerialNumber(String), - AlphanumericDataString(String), - DisplayProductName(String), - DisplayRangeLimitsAndAdditionalTiming(DisplayRangeLimitsAndAdditionalTiming), - EstablishedTimings3(EstablishedTimings3), - ColorManagementData(ColorManagementData), - StandardTimingIdentifier([Option; 6]), - ColorPoint(ColorPoint, Option), - Cvt3ByteCode([Cvt3ByteCode; 4]), -} - -type EdidContext = (usize, EdidParseContext); - -struct EdidParser<'a> { - data: &'a [u8], - pos: usize, - context: Rc>, - saved_ctx: Vec, - errors: Vec<(EdidError, Vec)>, -} - -macro_rules! bail { - ($slf:expr, $err:expr) => {{ - $slf.saved_ctx = $slf.context.to_vec(); - return Err($err); - }}; -} - -#[derive(Clone, Debug)] -pub enum EdidParseContext { - #[expect(dead_code)] - ReadingBytes(usize), - BaseBlock, - Descriptors, - Descriptor, - ChromaticityCoordinates, - EstablishedTimings, - StandardTimings, - ScreenDimensions, - Gamma, - FeatureSupport, - Magic, - Extension, - IdManufacturerName, - VideoInputDefinition, -} - -unsafe impl UnsafeCellCloneSafe for EdidParseContext {} - -struct EdidPushedContext { - stack: Rc>, -} - -impl Drop for EdidPushedContext { - fn drop(&mut self) { - self.stack.pop(); - } -} - -impl<'a> EdidParser<'a> { - fn push_ctx(&self, pc: EdidParseContext) -> EdidPushedContext { - self.context.push((self.pos, pc)); - EdidPushedContext { - stack: self.context.clone(), - } - } - - fn nest(&self, data: &'a [u8]) -> Self { - Self { - data, - pos: 0, - context: self.context.clone(), - saved_ctx: vec![], - errors: vec![], - } - } - - fn store_error(&mut self, error: EdidError) { - self.errors.push((error, self.saved_ctx.clone())); - } - - fn is_empty(&self) -> bool { - self.pos >= self.data.len() - } - - fn read_n(&mut self) -> Result<&'a [u8; N], EdidError> { - let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(N)); - if self.data.len() - self.pos < N { - bail!(self, EdidError::UnexpectedEof); - } - let v = unsafe { self.data[self.pos..].as_ptr().cast::<[u8; N]>().deref() }; - self.pos += N; - Ok(v) - } - - fn read_var_n(&mut self, n: usize) -> Result<&'a [u8], EdidError> { - let _ctx = self.push_ctx(EdidParseContext::ReadingBytes(n)); - if self.data.len() - self.pos < n { - bail!(self, EdidError::UnexpectedEof); - } - let v = &self.data[self.pos..self.pos + n]; - self.pos += n; - Ok(v) - } - - fn read_u8(&mut self) -> Result { - let &[a] = self.read_n()?; - Ok(a) - } - - fn read_u16(&mut self) -> Result { - let &[lo, hi] = self.read_n()?; - Ok(((hi as u16) << 8) + lo as u16) - } - - fn read_u32(&mut self) -> Result { - let &[a, b, c, d] = self.read_n()?; - Ok(((d as u32) << 24) + ((c as u32) << 16) + ((b as u32) << 8) + a as u32) - } - - fn parse_magic(&mut self) -> Result<(), EdidError> { - let _ctx = self.push_ctx(EdidParseContext::Magic); - let magic = self.read_n::<8>()?; - if magic != &[0, 255, 255, 255, 255, 255, 255, 0] { - bail!(self, EdidError::InvalidMagic(magic.as_bstr().to_owned())); - } - Ok(()) - } - - fn parse_id_manufacturer_name(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::IdManufacturerName); - let name = self.read_n::<2>()?; - let a = (name[0] >> 2) & 0b11111; - let b = ((name[0] & 0b11) << 3) | (name[1] >> 5); - let c = name[1] & 0b11111; - let name = [a + b'@', b + b'@', c + b'@'].as_bstr().to_owned(); - Ok(name) - } - - fn parse_video_input_definition(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::VideoInputDefinition); - let val = self.read_u8()?; - let res = if val.contains(0x80) { - VideoInputDefinition::Digital { - bit_depth: match (val >> 4) & 0b111 { - 0b000 => ColorBitDepth::Undefined, - 0b001 => ColorBitDepth::Bits6, - 0b010 => ColorBitDepth::Bits8, - 0b011 => ColorBitDepth::Bits10, - 0b100 => ColorBitDepth::Bits12, - 0b101 => ColorBitDepth::Bits14, - 0b110 => ColorBitDepth::Bits16, - _ => ColorBitDepth::Reserved, - }, - video_interface: match val & 0b1111 { - 0b0000 => DigitalVideoInterfaceStandard::Undefined, - 0b0001 => DigitalVideoInterfaceStandard::Dvi, - 0b0010 => DigitalVideoInterfaceStandard::HdmiA, - 0b0011 => DigitalVideoInterfaceStandard::HdmiB, - 0b0100 => DigitalVideoInterfaceStandard::MDDI, - 0b0101 => DigitalVideoInterfaceStandard::DisplayPort, - n => DigitalVideoInterfaceStandard::Unknown(n), - }, - } - } else { - VideoInputDefinition::Analog { - signal_level_standard: SignalLevelStandard((val >> 5) & 0b11), - blank_to_black_setup_or_pedestal: (val >> 4).contains(1), - separate_h_v_sync_supported: (val >> 3).contains(1), - composite_sync_on_horizontal_supported: (val >> 2).contains(1), - composite_sync_on_green_supported: (val >> 1).contains(1), - serration_on_vertical_sync_supported: (val >> 0).contains(1), - } - }; - Ok(res) - } - - fn parse_screen_dimensions(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::ScreenDimensions); - let &[hor, vert] = self.read_n()?; - let mut res = ScreenDimensions { - horizontal_screen_size_cm: None, - vertical_screen_size_cm: None, - landscape_aspect_ration: None, - portrait_aspect_ration: None, - }; - if hor != 0 && vert != 0 { - res.horizontal_screen_size_cm = Some(hor); - res.vertical_screen_size_cm = Some(vert); - } else if vert != 0 { - res.portrait_aspect_ration = Some(100.0 / (vert as f64 + 99.0)); - } else if hor != 0 { - res.landscape_aspect_ration = Some((hor as f64 + 99.0) / 100.0); - } - Ok(res) - } - - fn parse_gamma(&mut self) -> Result, EdidError> { - let _ctx = self.push_ctx(EdidParseContext::Gamma); - let val = self.read_u8()?; - if val == 0xff { - Ok(None) - } else { - Ok(Some((val as f64 + 100.0) / 100.0)) - } - } - - fn parse_feature_support(&mut self, digital: bool) -> Result { - let _ctx = self.push_ctx(EdidParseContext::FeatureSupport); - let val = self.read_u8()?; - Ok(FeatureSupport { - standby_supported: val.contains(0x80), - suspend_supported: val.contains(0x40), - active_off_supported: val.contains(0x20), - features: if digital { - FeatureSupport2::Digital { - rgb444_supported: true, - ycrcb422_supported: val.contains(0x10), - ycrcb444_supported: val.contains(0x08), - } - } else { - FeatureSupport2::Analog { - display_color_type: match (val >> 3) & 0b11 { - 0b00 => DisplayColorType::Monochrome, - 0b01 => DisplayColorType::Rgb, - 0b10 => DisplayColorType::NonRgb, - _ => DisplayColorType::Undefined, - }, - } - }, - srgb_is_default_color_space: val.contains(0x04), - preferred_mode_is_native: val.contains(0x02), - display_is_continuous_frequency: val.contains(0x01), - }) - } - - fn parse_chromaticity_coordinates(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::ChromaticityCoordinates); - let b = self.read_n::<10>()?; - let rx = ((b[0] as u16 >> 6) & 0b11) + ((b[2] as u16) << 2); - let ry = ((b[0] as u16 >> 4) & 0b11) + ((b[3] as u16) << 2); - let gx = ((b[0] as u16 >> 2) & 0b11) + ((b[4] as u16) << 2); - let gy = ((b[0] as u16 >> 0) & 0b11) + ((b[5] as u16) << 2); - let bx = ((b[1] as u16 >> 6) & 0b11) + ((b[6] as u16) << 2); - let by = ((b[1] as u16 >> 4) & 0b11) + ((b[7] as u16) << 2); - let wx = ((b[1] as u16 >> 2) & 0b11) + ((b[8] as u16) << 2); - let wy = ((b[1] as u16 >> 0) & 0b11) + ((b[9] as u16) << 2); - Ok(ChromaticityCoordinates { - red_x: rx, - red_y: ry, - green_x: gx, - green_y: gy, - blue_x: bx, - blue_y: by, - white_x: wx, - white_y: wy, - }) - } - - fn parse_established_timings(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::EstablishedTimings); - let b = self.read_n::<3>()?; - Ok(EstablishedTimings { - s_720x400_70: b[0].contains(0x80), - s_720x400_88: b[0].contains(0x40), - s_640x480_60: b[0].contains(0x20), - s_640x480_67: b[0].contains(0x10), - s_640x480_72: b[0].contains(0x08), - s_640x480_75: b[0].contains(0x04), - s_800x600_56: b[0].contains(0x02), - s_800x600_60: b[0].contains(0x01), - s_800x600_72: b[0].contains(0x80), - s_800x600_75: b[0].contains(0x40), - s_832x624_75: b[0].contains(0x20), - s_1024x768_87: b[0].contains(0x10), - s_1024x768_60: b[0].contains(0x08), - s_1024x768_70: b[0].contains(0x04), - s_1024x768_75: b[0].contains(0x02), - s_1280x1024_75: b[0].contains(0x01), - s_1152x870_75: b[0].contains(0x80), - }) - } - - fn parse_standard_timing(&mut self, revision: u8, a: u8, b: u8) -> Option { - if a == 0 { - return None; - } - Some(StandardTiming { - x_resolution: (a as u16 + 31) * 8, - aspect_ratio: match b >> 6 { - 0b00 if revision < 3 => AspectRatio::A1_1, - 0b00 => AspectRatio::A16_10, - 0b01 => AspectRatio::A4_3, - 0b10 => AspectRatio::A5_4, - _ => AspectRatio::A16_9, - }, - vertical_frequency: 60 + (b & 0b111111), - }) - } - - fn parse_standard_timings2( - &mut self, - revision: u8, - b: &[u8; 18], - ) -> [Option; 6] { - let mut res = [None; 6]; - for i in 0..6 { - let x = b[5 + 2 * i]; - let y = b[5 + 2 * i + 1]; - res[i] = self.parse_standard_timing(revision, x, y); - } - res - } - - fn parse_color_point(&mut self, b: &[u8; 18]) -> (ColorPoint, Option) { - let mut res = [Default::default(); 2]; - for n in 0..2 { - let b = &b[5 * (n + 1)..]; - res[n] = ColorPoint { - white_point_index: b[0], - white_point_x: ((b[2] as u16) << 2) | ((b[1] as u16) >> 2), - white_point_y: ((b[3] as u16) << 2) | ((b[1] as u16) & 0b11), - gamma: if b[4] == 0xff { - None - } else { - Some((b[5] as f64 + 100.0) / 100.0) - }, - }; - } - let second = if res[1].white_point_index != 0 { - Some(res[1]) - } else { - None - }; - (res[0], second) - } - - fn parse_standard_timings( - &mut self, - revision: u8, - ) -> Result<[Option; 8], EdidError> { - let _ctx = self.push_ctx(EdidParseContext::StandardTimings); - let bytes = self.read_n::<16>()?; - let mut res = [None; 8]; - for i in 0..8 { - let a = bytes[2 * i]; - let b = bytes[2 * i + 1]; - if (a, b) != (1, 1) { - res[i] = self.parse_standard_timing(revision, a, b); - } - } - Ok(res) - } - - fn parse_detailed_timing_descriptor(&self, b: &[u8; 18]) -> DetailedTimingDescriptor { - let l = b[17]; - DetailedTimingDescriptor { - pixel_clock_khz: u16::from_le_bytes([b[0], b[1]]) as u32 * 10_000, - horizontal_addressable_pixels: u16::from_le_bytes([b[2], b[4] >> 4]), - horizontal_blanking_pixels: u16::from_le_bytes([b[3], b[4] & 0b1111]), - vertical_addressable_lines: u16::from_le_bytes([b[5], b[7] >> 4]), - vertical_blanking_lines: u16::from_le_bytes([b[6], b[7] & 0b1111]), - horizontal_front_porch_pixels: u16::from_le_bytes([b[8], b[11] >> 6]), - horizontal_sync_pulse_pixels: u16::from_le_bytes([b[9], (b[11] >> 4) & 0b11]), - vertical_front_porch_lines: (b[10] >> 4) | ((b[11] & 0b1100) << 2), - vertical_sync_pulse_lines: (b[10] & 0b1111) | ((b[11] & 0b11) << 4), - horizontal_addressable_mm: u16::from_le_bytes([b[12], b[14] >> 4]), - vertical_addressable_mm: u16::from_le_bytes([b[13], b[14] & 0b1111]), - horizontal_left_border_pixels: b[15], - vertical_top_border_pixels: b[16], - interlaced: l.contains(0x80), - stereo_viewing_support: match ((l >> 4) & 0b110) | (l & 0b1) { - 0b010 => StereoViewingSupport::FieldSequentialRightDuringStereoSync, - 0b100 => StereoViewingSupport::FieldSequentialLeftDuringStereoSync, - 0b011 => StereoViewingSupport::TwoWayInterleavedRightImageOnEvenLines, - 0b101 => StereoViewingSupport::TwoWayInterleavedLeftImageOnEvenLines, - 0b110 => StereoViewingSupport::FourWayInterleaved, - 0b111 => StereoViewingSupport::SideBySideInterleaved, - _ => StereoViewingSupport::None, - }, - sync: if l.contains(0b10000) { - if l.contains(0b01000) { - SyncSignal::DigitalSeparate { - vertical_sync_is_positive: l.contains(0b100), - horizontal_sync_is_positive: l.contains(0b10), - } - } else { - SyncSignal::DigitalComposite { - with_serration: l.contains(0b100), - horizontal_sync_is_positive: l.contains(0b10), - } - } - } else { - SyncSignal::Analog { - ty: if l.contains(0b1000) { - AnalogSyncType::BipolarAnalogComposite - } else { - AnalogSyncType::AnalogComposite - }, - with_serrations: l.contains(0b100), - sync_on_all_signals: l.contains(0b10), - } - }, - } - } - - fn parse_display_range_limits_and_additional_timing( - &self, - b: &[u8; 18], - ) -> DisplayRangeLimitsAndAdditionalTiming { - let min_vert_off = b[4].contains(0b0001); - let max_vert_off = min_vert_off || b[4].contains(0b0010); - let min_horz_off = b[4].contains(0b0100); - let max_horz_off = min_horz_off || b[4].contains(0b1000); - DisplayRangeLimitsAndAdditionalTiming { - vertical_field_rate_min: b[5] as u16 + if min_vert_off { 255 } else { 0 }, - vertical_field_rate_max: b[6] as u16 + if max_vert_off { 255 } else { 0 }, - horizontal_field_rate_min: b[7] as u16 + if min_horz_off { 255 } else { 0 }, - horizontal_field_rate_max: b[8] as u16 + if max_horz_off { 255 } else { 0 }, - maximum_pixel_clock_mhz: b[9] as u16 * 10, - extended_timing_information: match b[10] { - 0x0 => ExtendedTimingInformation::DefaultGtf, - 0x1 => ExtendedTimingInformation::NoTimingInformation, - 0x2 => ExtendedTimingInformation::SecondaryGtf { - start_frequency: b[12] as u16, - c_value: b[13] as u16, - m_value: u16::from_le_bytes([b[14], b[15]]), - k_value: b[16], - j_value: b[17] as u16, - }, - 0x4 => ExtendedTimingInformation::Cvt { - cvt_major_version: b[11] >> 4, - cvt_minor_version: b[11] & 0b1111, - additional_clock_precision: b[12] >> 2, - maximum_active_pixels_per_line: if b[13] == 0 { - None - } else { - Some((((b[12] as u16 & 0b11) << 8) | b[13] as u16) * 8) - }, - ar_4_3: b[14].contains(0x80), - ar_16_9: b[14].contains(0x40), - ar_16_10: b[14].contains(0x20), - ar_5_4: b[14].contains(0x10), - ar_15_9: b[14].contains(0x08), - ar_preference: match b[15] >> 5 { - 0b000 => AspectRatioPreference::A4_3, - 0b001 => AspectRatioPreference::A16_9, - 0b010 => AspectRatioPreference::A16_10, - 0b011 => AspectRatioPreference::A5_4, - 0b100 => AspectRatioPreference::A15_9, - n => AspectRatioPreference::Unknown(n), - }, - cvt_rb_reduced_blanking_preferred: b[15].contains(0b10000), - cvt_standard_blanking: b[15].contains(0b1000), - scaling_support_horizontal_shrink: b[16].contains(0x80), - scaling_support_horizontal_stretch: b[16].contains(0x40), - scaling_support_vertical_shrink: b[16].contains(0x20), - scaling_support_vertical_stretch: b[16].contains(0x10), - preferred_vertical_refresh_rate_hz: b[17], - }, - n => ExtendedTimingInformation::Unknown(n), - }, - } - } - - fn parse_established_timings3(&self, b: &[u8; 18]) -> EstablishedTimings3 { - EstablishedTimings3 { - s640x350_85: b[6].contains(0x80), - s640x400_85: b[6].contains(0x40), - s720x400_85: b[6].contains(0x20), - s640x480_85: b[6].contains(0x10), - s848x480_60: b[6].contains(0x08), - s800x600_85: b[6].contains(0x04), - s1024x768_85: b[6].contains(0x02), - s1152x864_75: b[6].contains(0x01), - s1280x768_60_rb: b[7].contains(0x80), - s1280x768_60: b[7].contains(0x40), - s1280x768_75: b[7].contains(0x20), - s1280x768_85: b[7].contains(0x10), - s1280x960_60: b[7].contains(0x08), - s1280x960_85: b[7].contains(0x04), - s1280x1024_60: b[7].contains(0x02), - s1280x1024_85: b[7].contains(0x01), - s1360x768_60: b[8].contains(0x80), - s1440x900_60_rb: b[8].contains(0x40), - s1440x900_60: b[8].contains(0x20), - s1440x900_75: b[8].contains(0x10), - s1440x900_85: b[8].contains(0x08), - s1400x1050_60_rb: b[8].contains(0x04), - s1400x1050_60: b[8].contains(0x02), - s1400x1050_75: b[8].contains(0x01), - s1400x1050_85: b[9].contains(0x80), - s1680x1050_60_rb: b[9].contains(0x40), - s1680x1050_60: b[9].contains(0x20), - s1680x1050_75: b[9].contains(0x10), - s1680x1050_85: b[9].contains(0x08), - s1600x1200_60: b[9].contains(0x04), - s1600x1200_65: b[9].contains(0x02), - s1600x1200_70: b[9].contains(0x01), - s1600x1200_75: b[10].contains(0x80), - s1600x1200_85: b[10].contains(0x40), - s1792x1344_60: b[10].contains(0x20), - s1792x1344_75: b[10].contains(0x10), - s1856x1392_60: b[10].contains(0x08), - s1856x1392_75: b[10].contains(0x04), - s1920x1200_60_rb: b[10].contains(0x02), - s1920x1200_60: b[10].contains(0x01), - s1920x1200_75: b[11].contains(0x80), - s1920x1200_85: b[11].contains(0x40), - s1920x1440_60: b[11].contains(0x20), - s1920x1440_75: b[11].contains(0x10), - } - } - - fn parse_color_management_data(&self, b: &[u8; 18]) -> ColorManagementData { - ColorManagementData { - red_a3: u16::from_le_bytes([b[6], b[7]]), - red_a2: u16::from_le_bytes([b[8], b[9]]), - green_a3: u16::from_le_bytes([b[10], b[11]]), - green_a2: u16::from_le_bytes([b[12], b[13]]), - blue_a3: u16::from_le_bytes([b[14], b[15]]), - blue_a2: u16::from_le_bytes([b[16], b[17]]), - } - } - - fn parse_cvt3_byte_codes(&self, b: &[u8; 18]) -> [Cvt3ByteCode; 4] { - let parse = |n: usize| { - let b = &b[6 + 3 * n..]; - Cvt3ByteCode { - addressable_lines_per_field: u16::from_le_bytes([b[0], b[1] >> 4]), - aspect_ration: match (b[1] >> 2) & 0b11 { - 0 => CvtAspectRatio::A4_3, - 1 => CvtAspectRatio::A16_9, - 2 => CvtAspectRatio::A16_10, - _ => CvtAspectRatio::A15_9, - }, - preferred_vertical_rate: match (b[2] >> 5) & 0b11 { - 0 => CvtPreferredVerticalRate::R50, - 1 => CvtPreferredVerticalRate::R60, - 2 => CvtPreferredVerticalRate::R75, - _ => CvtPreferredVerticalRate::R85, - }, - r50: b[2].contains(0b10000), - r60: b[2].contains(0b01000), - r75: b[2].contains(0b00100), - r85: b[2].contains(0b00010), - r60_reduced_blanking: b[2].contains(0b00001), - } - }; - [parse(0), parse(1), parse(2), parse(3)] - } - - fn parse_descriptor(&mut self, revision: u8) -> Result, EdidError> { - let _ctx = self.push_ctx(EdidParseContext::Descriptor); - let b = self.read_n::<18>()?; - let str = || { - let mut s = &b[5..]; - if let Some(n) = s.find_byte(b'\n') { - s = &s[..n]; - }; - let mut res = String::new(); - for &b in s { - res.push_str(CP437[b as usize]); - } - res - }; - let res = if (b[0], b[1]) == (0, 0) { - match b[3] { - 0xff => Descriptor::DisplayProductSerialNumber(str()), - 0xfe => Descriptor::AlphanumericDataString(str()), - 0xfd => Descriptor::DisplayRangeLimitsAndAdditionalTiming( - self.parse_display_range_limits_and_additional_timing(b), - ), - 0xfc => Descriptor::DisplayProductName(str()), - 0xfb => { - let (first, second) = self.parse_color_point(b); - Descriptor::ColorPoint(first, second) - } - 0xfa => { - Descriptor::StandardTimingIdentifier(self.parse_standard_timings2(revision, b)) - } - 0xf9 => Descriptor::ColorManagementData(self.parse_color_management_data(b)), - 0xf8 => Descriptor::Cvt3ByteCode(self.parse_cvt3_byte_codes(b)), - 0xf7 => Descriptor::EstablishedTimings3(self.parse_established_timings3(b)), - 0x10 => return Ok(None), - n => Descriptor::Unknown(n), - } - } else { - Descriptor::DetailedTimingDescriptor(self.parse_detailed_timing_descriptor(b)) - }; - Ok(Some(res)) - } - - fn parse_descriptors(&mut self, revision: u8) -> Result<[Option; 4], EdidError> { - let _ctx = self.push_ctx(EdidParseContext::Descriptors); - let mut res = [None, None, None, None]; - for res in &mut res { - *res = self.parse_descriptor(revision)?; - } - Ok(res) - } - - fn parse_base_block(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::BaseBlock); - self.parse_magic()?; - let id_manufacturer_name = self.parse_id_manufacturer_name()?; - let id_product_code = self.read_u16()?; - let id_serial_number = self.read_u32()?; - let mut week_of_manufacture = None; - let mut model_year = None; - let mut year_of_manufacture = None; - { - let &[a, b] = self.read_n()?; - if matches!(a, 1..=0x36) { - week_of_manufacture = Some(a); - } - let year = b as u16 + 1990; - if a == 0xff { - model_year = Some(year); - } else { - year_of_manufacture = Some(year); - } - } - let &[edid_version, edid_revision] = self.read_n()?; - let video_input_definition = self.parse_video_input_definition()?; - let is_digital = matches!(video_input_definition, VideoInputDefinition::Digital { .. }); - let screen_dimensions = self.parse_screen_dimensions()?; - let gamma = self.parse_gamma()?; - let feature_support = self.parse_feature_support(is_digital)?; - let chromaticity_coordinates = self.parse_chromaticity_coordinates()?; - let established_timings = self.parse_established_timings()?; - let standard_timings = self.parse_standard_timings(edid_revision)?; - let descriptors = self.parse_descriptors(edid_revision)?; - let num_extensions = self.read_u8()?; - let _checksum = self.read_u8()?; - Ok(EdidBaseBlock { - id_manufacturer_name, - id_product_code, - id_serial_number, - week_of_manufacture, - model_year, - year_of_manufacture, - edid_version, - edid_revision, - video_input_definition, - screen_dimensions, - gamma, - feature_support, - chromaticity_coordinates, - established_timings, - standard_timings, - descriptors, - num_extensions, - }) - } - - fn parse_cta_amd_vendor_data_block(&mut self) -> Result { - let _ = self.read_n::<2>()?; - Ok(CtaDataBlock::VendorAmd(CtaAmdVendorDataBlock { - minimum_refresh_hz: self.read_u8()?, - maximum_refresh_hz: self.read_u8()?, - })) - } - - fn parse_cta_vendor_data_block(&mut self) -> Result { - match self.read_n::<3>()? { - [0x1A, 0x00, 0x00] => self.parse_cta_amd_vendor_data_block(), - _ => Ok(CtaDataBlock::Unknown), - } - } - - fn parse_cta_colorimetry_data_block(&mut self) -> Result { - let [lo, hi] = *self.read_n::<2>()?; - Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock { - bt2020_rgb: lo.contains(0x80), - bt2020_ycc: lo.contains(0x40), - bt2020_cycc: lo.contains(0x20), - op_rgb: lo.contains(0x10), - op_ycc_601601: lo.contains(0x08), - s_ycc_601: lo.contains(0x04), - xv_ycc_709: lo.contains(0x02), - xv_ycc_601: lo.contains(0x01), - dci_p3: hi.contains(0x80), - })) - } - - fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result { - let et = self.read_u8()?; - let _ = self.read_u8()?; - let mut read_luminance = |min: bool| { - let v = self.read_u8().unwrap_or_default(); - if v == 0 { - None - } else if min { - Some((v as f64 / 255.0).powi(2) / 100.0) - } else { - Some(50.0 * 2.0f64.powf(v as f64 / 32.0)) - } - }; - Ok(CtaDataBlock::StaticHdrMetadata( - CtaStaticHdrMetadataDataBlock { - traditional_gamma_sdr_luminance: et.contains(0x01), - traditional_gamma_hdr_luminance: et.contains(0x02), - smpte_st_2084: et.contains(0x04), - hlg: et.contains(0x08), - max_luminance: read_luminance(false), - max_frame_average_luminance: read_luminance(false), - min_luminance: read_luminance(true), - }, - )) - } - - fn parse_cta_extended_data_block(&mut self) -> Result { - match self.read_u8()? { - 0x5 => self.parse_cta_colorimetry_data_block(), - 0x6 => self.parse_cta_hdr_static_metadata_data_block(), - _ => Ok(CtaDataBlock::Unknown), - } - } - - fn parse_cta_data_block(&mut self, tag: u8) -> Result { - match tag { - 0x3 => self.parse_cta_vendor_data_block(), - 0x7 => self.parse_cta_extended_data_block(), - _ => Ok(CtaDataBlock::Unknown), - } - } - - fn parse_cta_extension_v3(&mut self) -> Result { - let detailed_timing_descriptors_offset = self.read_u8()? as usize; - let _ = self.read_u8()?; - let mut data_blocks = vec![]; - while self.pos < detailed_timing_descriptors_offset { - let b1 = self.read_u8()?; - let data = self.read_var_n(b1 as usize & 0x1f)?; - let mut parser = self.nest(data); - match parser.parse_cta_data_block(b1 >> 5) { - Ok(d) => data_blocks.push(d), - Err(e) => { - self.saved_ctx = parser.saved_ctx; - self.store_error(e); - } - } - } - Ok(EdidExtension::CtaV3(CtaExtensionV3 { data_blocks })) - } - - fn parse_cta_extension(&mut self) -> Result { - // https://web.archive.org/web/20171201033424/https://standards.cta.tech/kwspub/published_docs/CTA-861-G_FINAL_revised_2017.pdf - match self.read_u8()? { - 0x3 => self.parse_cta_extension_v3(), - _ => Ok(EdidExtension::Unknown), - } - } - - fn parse_extension_impl(&mut self) -> Result { - match self.read_u8()? { - 0x2 => self.parse_cta_extension(), - _ => Ok(EdidExtension::Unknown), - } - } - - fn parse_extension(&mut self) -> Result { - let _ctx = self.push_ctx(EdidParseContext::Extension); - let data = self.read_n::<128>()?; - let mut parser = self.nest(data); - let res = parser.parse_extension_impl(); - if res.is_err() { - self.saved_ctx = parser.saved_ctx; - } - res - } - - fn parse(&mut self) -> Result { - let bb = self.parse_base_block()?; - let mut exts = vec![]; - while !self.is_empty() { - match self.parse_extension() { - Ok(e) => exts.push(e), - Err(e) => self.store_error(e), - } - } - Ok(EdidFile { - base_block: bb, - extension_blocks: exts, - }) - } -} - -#[derive(Debug)] -pub enum DisplayColorType { - Monochrome, - Rgb, - NonRgb, - Undefined, -} - -#[derive(Debug)] -#[expect(dead_code)] -pub enum FeatureSupport2 { - Analog { - display_color_type: DisplayColorType, - }, - Digital { - rgb444_supported: bool, - ycrcb444_supported: bool, - ycrcb422_supported: bool, - }, -} - -#[derive(Debug)] -#[expect(dead_code)] -pub struct FeatureSupport { - pub standby_supported: bool, - pub suspend_supported: bool, - pub active_off_supported: bool, - pub features: FeatureSupport2, - pub srgb_is_default_color_space: bool, - pub preferred_mode_is_native: bool, - pub display_is_continuous_frequency: bool, -} - -#[derive(Debug)] -#[expect(dead_code)] -pub struct EdidBaseBlock { - pub id_manufacturer_name: BString, - pub id_product_code: u16, - pub id_serial_number: u32, - pub week_of_manufacture: Option, - pub model_year: Option, - pub year_of_manufacture: Option, - pub edid_version: u8, - pub edid_revision: u8, - pub video_input_definition: VideoInputDefinition, - pub screen_dimensions: ScreenDimensions, - pub gamma: Option, - pub feature_support: FeatureSupport, - pub chromaticity_coordinates: ChromaticityCoordinates, - pub established_timings: EstablishedTimings, - pub standard_timings: [Option; 8], - pub descriptors: [Option; 4], - pub num_extensions: u8, -} - -#[derive(Debug)] -pub enum EdidExtension { - Unknown, - CtaV3(CtaExtensionV3), -} - -#[derive(Debug)] -pub struct CtaExtensionV3 { - pub data_blocks: Vec, -} - -#[derive(Debug)] -pub enum CtaDataBlock { - Unknown, - VendorAmd(CtaAmdVendorDataBlock), - Colorimetry(CtaColorimetryDataBlock), - StaticHdrMetadata(CtaStaticHdrMetadataDataBlock), -} - -#[derive(Debug)] -pub struct CtaAmdVendorDataBlock { - pub minimum_refresh_hz: u8, - #[expect(dead_code)] - pub maximum_refresh_hz: u8, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct CtaColorimetryDataBlock { - pub bt2020_rgb: bool, - pub bt2020_ycc: bool, - pub bt2020_cycc: bool, - pub op_rgb: bool, - pub op_ycc_601601: bool, - pub s_ycc_601: bool, - pub xv_ycc_709: bool, - pub xv_ycc_601: bool, - pub dci_p3: bool, -} - -#[derive(Copy, Clone, Debug)] -#[expect(dead_code)] -pub struct CtaStaticHdrMetadataDataBlock { - pub traditional_gamma_sdr_luminance: bool, - pub traditional_gamma_hdr_luminance: bool, - pub smpte_st_2084: bool, - pub hlg: bool, - pub max_luminance: Option, - pub max_frame_average_luminance: Option, - pub min_luminance: Option, -} - -#[derive(Debug)] -pub struct EdidFile { - pub base_block: EdidBaseBlock, - pub extension_blocks: Vec, -} - -#[derive(Debug, Error)] -pub enum EdidError { - #[error("Unexpected end-of-file")] - UnexpectedEof, - #[error("Invalid magic header")] - InvalidMagic(BString), -} - -pub fn parse(data: &[u8]) -> Result { - let mut parser = EdidParser { - data, - pos: 0, - context: Rc::new(Default::default()), - saved_ctx: vec![], - errors: vec![], - }; - parser.parse() -} - -const CP437: &[&str] = &[ - "\u{0}", "☺", "☻", "♥", "♦", "♣", "♠", "•", "◘", "○", "◙", "♂", "♀", "♪", "♫", "☼", "►", "◄", - "↕", "‼", "¶", "§", "▬", "↨", "↑", "↓", "→", "←", "∟", "↔", "▲", "▼", " ", "!", "\"", "#", "$", - "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", - "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", - "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", - "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "⌂", "Ç", "ü", "é", "â", - "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ", "ô", "ö", "ò", "û", - "ù", "ÿ", "Ö", "Ü", "¢", "£", "¥", "₧", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "⌐", - "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "╡", "╢", "╖", "╕", "╣", "║", "╗", "╝", - "╜", "╛", "┐", "└", "┴", "┬", "├", "─", "┼", "╞", "╟", "╚", "╔", "╩", "╦", "╠", "═", "╬", "╧", - "╨", "╤", "╥", "╙", "╘", "╒", "╓", "╫", "╪", "┘", "┌", "█", "▄", "▌", "▐", "▀", "α", "ß", "Γ", - "π", "Σ", "σ", "µ", "τ", "Φ", "Θ", "Ω", "δ", "∞", "φ", "ε", "∩", "≡", "±", "≥", "≤", "⌠", "⌡", - "÷", "≈", "°", "∙", "·", "√", "ⁿ", "²", "■", "\u{a0}", -]; +pub use jay_edid::*; diff --git a/src/fixed.rs b/src/fixed.rs index 5ff30c05..dc201edd 100644 --- a/src/fixed.rs +++ b/src/fixed.rs @@ -1,137 +1 @@ -use std::{ - cmp::Ordering, - fmt::{Debug, Display, Formatter}, - ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}, -}; - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] -#[repr(transparent)] -pub struct Fixed(pub i32); - -impl Fixed { - pub const EPSILON: Self = Fixed(1); - - pub fn is_integer(self) -> bool { - self.0 & 255 == 0 - } - - pub fn from_f64(f: f64) -> Self { - Self((f * 256.0) as i32) - } - - pub fn from_f32(f: f32) -> Self { - Self::from_f64(f as f64) - } - - pub fn to_f64(self) -> f64 { - self.0 as f64 / 256.0 - } - - pub fn to_f32(self) -> f32 { - self.0 as f32 / 256.0 - } - - pub fn from_1616(i: i32) -> Self { - Self(i >> 8) - } - - pub fn to_int(self) -> i32 { - self.0 >> 8 - } - - pub fn from_int(i: i32) -> Self { - Self(i << 8) - } - - pub fn round_down(self) -> i32 { - self.0 >> 8 - } - - pub fn apply_fract(self, i: i32) -> Self { - Self((i << 8) | (self.0 & 255)) - } -} - -impl PartialEq for Fixed { - fn eq(&self, other: &i32) -> bool { - self.0 == *other << 8 - } -} - -impl PartialOrd for Fixed { - fn partial_cmp(&self, other: &i32) -> Option { - self.0.partial_cmp(&(*other << 8)) - } -} - -impl Debug for Fixed { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.to_f64(), f) - } -} - -impl Display for Fixed { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.to_f64(), f) - } -} - -impl Sub for Fixed { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl Add for Fixed { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Sub for Fixed { - type Output = Self; - - fn sub(self, rhs: i32) -> Self::Output { - Self(self.0 - (rhs << 8)) - } -} - -impl Add for Fixed { - type Output = Self; - - fn add(self, rhs: i32) -> Self::Output { - Self(self.0 + (rhs << 8)) - } -} - -impl Mul for Fixed { - type Output = Self; - - fn mul(self, rhs: i32) -> Self::Output { - Self(self.0 * rhs) - } -} - -impl Div for Fixed { - type Output = Self; - - fn div(self, rhs: i32) -> Self::Output { - Self(self.0 / rhs) - } -} - -impl AddAssign for Fixed { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; - } -} - -impl SubAssign for Fixed { - fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0; - } -} +pub use jay_units::fixed::*; diff --git a/src/format.rs b/src/format.rs index f5751a45..a4cad104 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,583 +1 @@ -use { - crate::{ - gfx_apis::gl::sys::{GL_BGRA_EXT, GL_RGBA, GL_RGBA8, GL_UNSIGNED_BYTE, GLenum, GLint}, - pipewire::pw_pod::{ - SPA_VIDEO_FORMAT_ABGR_210LE, SPA_VIDEO_FORMAT_ARGB_210LE, SPA_VIDEO_FORMAT_BGR, - SPA_VIDEO_FORMAT_BGR15, SPA_VIDEO_FORMAT_BGR16, SPA_VIDEO_FORMAT_BGRA, - SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_GRAY8, SPA_VIDEO_FORMAT_RGB, - SPA_VIDEO_FORMAT_RGB16, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_RGBx, - SPA_VIDEO_FORMAT_UNKNOWN, SPA_VIDEO_FORMAT_xBGR_210LE, SPA_VIDEO_FORMAT_xRGB_210LE, - SpaVideoFormat, - }, - }, - ahash::AHashMap, - ash::vk, - jay_config::video::Format as ConfigFormat, - std::{ - fmt::{self, Debug, Write}, - sync::LazyLock, - }, -}; - -#[derive(Copy, Clone, Debug)] -pub struct FormatShmInfo { - pub gl_format: GLint, - pub gl_internal_format: GLenum, - pub gl_type: GLint, -} - -#[derive(Copy, Clone, Debug)] -pub struct Format { - pub name: &'static str, - pub vk_format: vk::Format, - pub drm: u32, - pub wl_id: Option, - pub external_only_guess: bool, - pub has_alpha: bool, - pub pipewire: SpaVideoFormat, - pub opaque: Option<&'static Format>, - pub shm_info: Option, - pub config: ConfigFormat, - pub bpp: u32, -} - -const fn default(config: ConfigFormat) -> Format { - Format { - name: "", - vk_format: vk::Format::UNDEFINED, - drm: 0, - wl_id: None, - external_only_guess: false, - has_alpha: false, - pipewire: SPA_VIDEO_FORMAT_UNKNOWN, - opaque: None, - shm_info: None, - config, - bpp: 4, - } -} - -impl PartialEq for Format { - fn eq(&self, other: &Self) -> bool { - self.drm == other.drm - } -} - -impl Eq for Format {} - -static FORMATS_MAP: LazyLock> = LazyLock::new(|| { - let mut map = AHashMap::new(); - for format in FORMATS { - assert!(map.insert(format.drm, format).is_none()); - } - map -}); - -static PW_FORMATS_MAP: LazyLock> = LazyLock::new(|| { - let mut map = AHashMap::new(); - for format in FORMATS { - if format.pipewire != SPA_VIDEO_FORMAT_UNKNOWN { - assert!(map.insert(format.pipewire, format).is_none()); - } - } - map -}); - -static FORMATS_REFS: LazyLock> = LazyLock::new(|| FORMATS.iter().collect()); - -static FORMATS_NAMES: LazyLock> = LazyLock::new(|| { - let mut map = AHashMap::new(); - for format in FORMATS { - assert!(map.insert(format.name, format).is_none()); - } - map -}); - -static FORMATS_CONFIG: LazyLock> = LazyLock::new(|| { - let mut map = AHashMap::new(); - for format in FORMATS { - assert!(map.insert(format.config, format).is_none()); - } - map -}); - -#[test] -fn formats_dont_panic() { - formats(); - pw_formats(); - named_formats(); - config_formats(); -} - -pub fn formats() -> &'static AHashMap { - &FORMATS_MAP -} - -pub fn pw_formats() -> &'static AHashMap { - &PW_FORMATS_MAP -} - -pub fn ref_formats() -> &'static [&'static Format] { - &FORMATS_REFS -} - -pub fn named_formats() -> &'static AHashMap<&'static str, &'static Format> { - &FORMATS_NAMES -} - -pub fn config_formats() -> &'static AHashMap { - &FORMATS_CONFIG -} - -const fn fourcc_code(a: char, b: char, c: char, d: char) -> u32 { - (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) -} - -#[expect(dead_code)] -pub fn debug(fourcc: u32) -> impl Debug { - fmt::from_fn(move |fmt| { - fmt.write_char(fourcc as u8 as char)?; - fmt.write_char((fourcc >> 8) as u8 as char)?; - fmt.write_char((fourcc >> 16) as u8 as char)?; - fmt.write_char((fourcc >> 24) as u8 as char)?; - Ok(()) - }) -} - -const ARGB8888_ID: u32 = 0; -const ARGB8888_DRM: u32 = fourcc_code('A', 'R', '2', '4'); - -const XRGB8888_ID: u32 = 1; -const XRGB8888_DRM: u32 = fourcc_code('X', 'R', '2', '4'); - -pub fn map_wayland_format_id(id: u32) -> u32 { - match id { - ARGB8888_ID => ARGB8888_DRM, - XRGB8888_ID => XRGB8888_DRM, - _ => id, - } -} - -pub static ARGB8888: &Format = &Format { - name: "argb8888", - shm_info: Some(FormatShmInfo { - gl_format: GL_BGRA_EXT, - gl_internal_format: GL_RGBA8, - gl_type: GL_UNSIGNED_BYTE, - }), - vk_format: vk::Format::B8G8R8A8_UNORM, - bpp: 4, - drm: ARGB8888_DRM, - wl_id: Some(ARGB8888_ID), - external_only_guess: false, - has_alpha: true, - pipewire: SPA_VIDEO_FORMAT_BGRA, - opaque: Some(XRGB8888), - config: ConfigFormat::ARGB8888, -}; - -pub static XRGB8888: &Format = &Format { - name: "xrgb8888", - shm_info: Some(FormatShmInfo { - gl_format: GL_BGRA_EXT, - gl_internal_format: GL_RGBA8, - gl_type: GL_UNSIGNED_BYTE, - }), - vk_format: vk::Format::B8G8R8A8_UNORM, - bpp: 4, - drm: XRGB8888_DRM, - wl_id: Some(XRGB8888_ID), - external_only_guess: false, - has_alpha: false, - pipewire: SPA_VIDEO_FORMAT_BGRx, - opaque: None, - config: ConfigFormat::XRGB8888, -}; - -static ABGR8888: &Format = &Format { - name: "abgr8888", - shm_info: Some(FormatShmInfo { - gl_format: GL_RGBA, - gl_internal_format: GL_RGBA8, - gl_type: GL_UNSIGNED_BYTE, - }), - vk_format: vk::Format::R8G8B8A8_UNORM, - bpp: 4, - drm: fourcc_code('A', 'B', '2', '4'), - wl_id: None, - external_only_guess: false, - has_alpha: true, - pipewire: SPA_VIDEO_FORMAT_RGBA, - opaque: Some(XBGR8888), - config: ConfigFormat::ABGR8888, -}; - -static XBGR8888: &Format = &Format { - name: "xbgr8888", - shm_info: Some(FormatShmInfo { - gl_format: GL_RGBA, - gl_internal_format: GL_RGBA8, - gl_type: GL_UNSIGNED_BYTE, - }), - vk_format: vk::Format::R8G8B8A8_UNORM, - bpp: 4, - drm: fourcc_code('X', 'B', '2', '4'), - wl_id: None, - external_only_guess: false, - has_alpha: false, - pipewire: SPA_VIDEO_FORMAT_RGBx, - opaque: None, - config: ConfigFormat::XBGR8888, -}; - -static R8: &Format = &Format { - name: "r8", - vk_format: vk::Format::R8_UNORM, - bpp: 1, - drm: fourcc_code('R', '8', ' ', ' '), - pipewire: SPA_VIDEO_FORMAT_GRAY8, - ..default(ConfigFormat::R8) -}; - -static GR88: &Format = &Format { - name: "gr88", - vk_format: vk::Format::R8G8_UNORM, - bpp: 2, - drm: fourcc_code('G', 'R', '8', '8'), - ..default(ConfigFormat::GR88) -}; - -static RGB888: &Format = &Format { - name: "rgb888", - vk_format: vk::Format::B8G8R8_UNORM, - bpp: 3, - drm: fourcc_code('R', 'G', '2', '4'), - pipewire: SPA_VIDEO_FORMAT_BGR, - ..default(ConfigFormat::RGB888) -}; - -static BGR888: &Format = &Format { - name: "bgr888", - vk_format: vk::Format::R8G8B8_UNORM, - bpp: 3, - drm: fourcc_code('B', 'G', '2', '4'), - pipewire: SPA_VIDEO_FORMAT_RGB, - ..default(ConfigFormat::BGR888) -}; - -static RGBA4444: &Format = &Format { - name: "rgba4444", - vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('R', 'A', '1', '2'), - has_alpha: true, - opaque: Some(RGBX4444), - ..default(ConfigFormat::RGBA4444) -}; - -static RGBX4444: &Format = &Format { - name: "rgbx4444", - vk_format: vk::Format::R4G4B4A4_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('R', 'X', '1', '2'), - ..default(ConfigFormat::RGBX4444) -}; - -static BGRA4444: &Format = &Format { - name: "bgra4444", - vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('B', 'A', '1', '2'), - has_alpha: true, - opaque: Some(BGRX4444), - ..default(ConfigFormat::BGRA4444) -}; - -static BGRX4444: &Format = &Format { - name: "bgrx4444", - vk_format: vk::Format::B4G4R4A4_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('B', 'X', '1', '2'), - ..default(ConfigFormat::BGRX4444) -}; - -static RGB565: &Format = &Format { - name: "rgb565", - vk_format: vk::Format::R5G6B5_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('R', 'G', '1', '6'), - pipewire: SPA_VIDEO_FORMAT_BGR16, - ..default(ConfigFormat::RGB565) -}; - -static BGR565: &Format = &Format { - name: "bgr565", - vk_format: vk::Format::B5G6R5_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('B', 'G', '1', '6'), - pipewire: SPA_VIDEO_FORMAT_RGB16, - ..default(ConfigFormat::BGR565) -}; - -static RGBA5551: &Format = &Format { - name: "rgba5551", - vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('R', 'A', '1', '5'), - has_alpha: true, - opaque: Some(RGBX5551), - ..default(ConfigFormat::RGBA5551) -}; - -static RGBX5551: &Format = &Format { - name: "rgbx5551", - vk_format: vk::Format::R5G5B5A1_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('R', 'X', '1', '5'), - ..default(ConfigFormat::RGBX5551) -}; - -static BGRA5551: &Format = &Format { - name: "bgra5551", - vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('B', 'A', '1', '5'), - has_alpha: true, - opaque: Some(BGRX5551), - ..default(ConfigFormat::BGRA5551) -}; - -static BGRX5551: &Format = &Format { - name: "bgrx5551", - vk_format: vk::Format::B5G5R5A1_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('B', 'X', '1', '5'), - ..default(ConfigFormat::BGRX5551) -}; - -static ARGB1555: &Format = &Format { - name: "argb1555", - vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('A', 'R', '1', '5'), - has_alpha: true, - opaque: Some(XRGB1555), - ..default(ConfigFormat::ARGB1555) -}; - -static XRGB1555: &Format = &Format { - name: "xrgb1555", - vk_format: vk::Format::A1R5G5B5_UNORM_PACK16, - bpp: 2, - drm: fourcc_code('X', 'R', '1', '5'), - pipewire: SPA_VIDEO_FORMAT_BGR15, - ..default(ConfigFormat::XRGB1555) -}; - -static ARGB2101010: &Format = &Format { - name: "argb2101010", - vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, - bpp: 4, - drm: fourcc_code('A', 'R', '3', '0'), - has_alpha: true, - opaque: Some(XRGB2101010), - pipewire: SPA_VIDEO_FORMAT_ARGB_210LE, - ..default(ConfigFormat::ARGB2101010) -}; - -static XRGB2101010: &Format = &Format { - name: "xrgb2101010", - vk_format: vk::Format::A2R10G10B10_UNORM_PACK32, - bpp: 4, - drm: fourcc_code('X', 'R', '3', '0'), - pipewire: SPA_VIDEO_FORMAT_xRGB_210LE, - ..default(ConfigFormat::XRGB2101010) -}; - -static ABGR2101010: &Format = &Format { - name: "abgr2101010", - vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, - bpp: 4, - drm: fourcc_code('A', 'B', '3', '0'), - has_alpha: true, - opaque: Some(XBGR2101010), - pipewire: SPA_VIDEO_FORMAT_ABGR_210LE, - ..default(ConfigFormat::ABGR2101010) -}; - -static XBGR2101010: &Format = &Format { - name: "xbgr2101010", - vk_format: vk::Format::A2B10G10R10_UNORM_PACK32, - bpp: 4, - drm: fourcc_code('X', 'B', '3', '0'), - pipewire: SPA_VIDEO_FORMAT_xBGR_210LE, - ..default(ConfigFormat::XBGR2101010) -}; - -static ABGR16161616: &Format = &Format { - name: "abgr16161616", - vk_format: vk::Format::R16G16B16A16_UNORM, - bpp: 8, - drm: fourcc_code('A', 'B', '4', '8'), - has_alpha: true, - opaque: Some(XBGR16161616), - ..default(ConfigFormat::ABGR16161616) -}; - -static XBGR16161616: &Format = &Format { - name: "xbgr16161616", - vk_format: vk::Format::R16G16B16A16_UNORM, - bpp: 8, - drm: fourcc_code('X', 'B', '4', '8'), - ..default(ConfigFormat::XBGR16161616) -}; - -pub static ABGR16161616F: &Format = &Format { - name: "abgr16161616f", - vk_format: vk::Format::R16G16B16A16_SFLOAT, - bpp: 8, - drm: fourcc_code('A', 'B', '4', 'H'), - has_alpha: true, - opaque: Some(XBGR16161616F), - ..default(ConfigFormat::ABGR16161616F) -}; - -static XBGR16161616F: &Format = &Format { - name: "xbgr16161616f", - vk_format: vk::Format::R16G16B16A16_SFLOAT, - bpp: 8, - drm: fourcc_code('X', 'B', '4', 'H'), - ..default(ConfigFormat::XBGR16161616F) -}; - -static BGR161616: &Format = &Format { - name: "bgr161616", - vk_format: vk::Format::R16G16B16_UNORM, - bpp: 6, - drm: fourcc_code('B', 'G', '4', '8'), - ..default(ConfigFormat::BGR161616) -}; - -static R16F: &Format = &Format { - name: "r16f", - vk_format: vk::Format::R16_SFLOAT, - bpp: 2, - drm: fourcc_code('R', ' ', ' ', 'H'), - ..default(ConfigFormat::R16F) -}; - -static GR1616F: &Format = &Format { - name: "gr1616f", - vk_format: vk::Format::R16G16_SFLOAT, - bpp: 4, - drm: fourcc_code('G', 'R', ' ', 'H'), - ..default(ConfigFormat::GR1616F) -}; - -static BGR161616F: &Format = &Format { - name: "bgr161616f", - vk_format: vk::Format::R16G16B16_SFLOAT, - bpp: 6, - drm: fourcc_code('B', 'G', 'R', 'H'), - ..default(ConfigFormat::BGR161616F) -}; - -static R32F: &Format = &Format { - name: "r32f", - vk_format: vk::Format::R32_SFLOAT, - bpp: 4, - drm: fourcc_code('R', ' ', ' ', 'F'), - ..default(ConfigFormat::R32F) -}; - -static GR3232F: &Format = &Format { - name: "gr3232f", - vk_format: vk::Format::R32G32_SFLOAT, - bpp: 8, - drm: fourcc_code('G', 'R', ' ', 'F'), - ..default(ConfigFormat::GR3232F) -}; - -static BGR323232F: &Format = &Format { - name: "bgr323232f", - vk_format: vk::Format::R32G32B32_SFLOAT, - bpp: 12, - drm: fourcc_code('B', 'G', 'R', 'F'), - ..default(ConfigFormat::BGR323232F) -}; - -static ABGR32323232F: &Format = &Format { - name: "abgr32323232f", - vk_format: vk::Format::R32G32B32A32_SFLOAT, - bpp: 16, - drm: fourcc_code('A', 'B', '8', 'F'), - has_alpha: true, - ..default(ConfigFormat::ABGR32323232F) -}; - -pub static FORMATS: &[Format] = &[ - *ARGB8888, - *XRGB8888, - *ABGR8888, - *XBGR8888, - *R8, - *GR88, - *RGB888, - *BGR888, - #[cfg(target_endian = "little")] - *RGBA4444, - #[cfg(target_endian = "little")] - *RGBX4444, - #[cfg(target_endian = "little")] - *BGRA4444, - #[cfg(target_endian = "little")] - *BGRX4444, - #[cfg(target_endian = "little")] - *RGB565, - #[cfg(target_endian = "little")] - *BGR565, - #[cfg(target_endian = "little")] - *RGBA5551, - #[cfg(target_endian = "little")] - *RGBX5551, - #[cfg(target_endian = "little")] - *BGRA5551, - #[cfg(target_endian = "little")] - *BGRX5551, - #[cfg(target_endian = "little")] - *ARGB1555, - #[cfg(target_endian = "little")] - *XRGB1555, - #[cfg(target_endian = "little")] - *ARGB2101010, - #[cfg(target_endian = "little")] - *XRGB2101010, - #[cfg(target_endian = "little")] - *ABGR2101010, - #[cfg(target_endian = "little")] - *XBGR2101010, - #[cfg(target_endian = "little")] - *ABGR16161616, - #[cfg(target_endian = "little")] - *XBGR16161616, - #[cfg(target_endian = "little")] - *ABGR16161616F, - #[cfg(target_endian = "little")] - *XBGR16161616F, - #[cfg(target_endian = "little")] - *BGR161616, - #[cfg(target_endian = "little")] - *R16F, - #[cfg(target_endian = "little")] - *GR1616F, - #[cfg(target_endian = "little")] - *BGR161616F, - #[cfg(target_endian = "little")] - *R32F, - #[cfg(target_endian = "little")] - *GR3232F, - #[cfg(target_endian = "little")] - *BGR323232F, - #[cfg(target_endian = "little")] - *ABGR32323232F, -]; +pub use jay_formats::*; diff --git a/src/gfx_apis/gl/gl/sys.rs b/src/gfx_apis/gl/gl/sys.rs index 5d79cba8..aae348df 100644 --- a/src/gfx_apis/gl/gl/sys.rs +++ b/src/gfx_apis/gl/gl/sys.rs @@ -13,9 +13,6 @@ pub type GLuint = c::c_uint; egl_transparent!(GLeglImageOES); -pub const GL_RGBA: GLint = 0x1908; -pub const GL_RGBA8: GLenum = 0x8058; -pub const GL_BGRA_EXT: GLint = 0x80E1; pub const GL_CLAMP_TO_EDGE: GLint = 0x812F; pub const GL_COLOR_ATTACHMENT0: GLenum = 0x8CE0; pub const GL_COLOR_BUFFER_BIT: GLbitfield = 0x00004000; @@ -40,7 +37,6 @@ pub const GL_TEXTURE_WRAP_T: GLenum = 0x2803; pub const GL_TRIANGLE_STRIP: GLenum = 0x0005; pub const GL_TRIANGLES: GLenum = 0x0004; pub const GL_UNPACK_ROW_LENGTH_EXT: GLenum = 0x0CF2; -pub const GL_UNSIGNED_BYTE: GLint = 0x1401; pub const GL_VERTEX_SHADER: GLenum = 0x8B31; pub const GL_BLEND: GLenum = 0x0BE2; pub const GL_ONE: GLenum = 1; diff --git a/src/gfx_apis/vulkan/renderer.rs b/src/gfx_apis/vulkan/renderer.rs index d0a48d26..4983a178 100644 --- a/src/gfx_apis/vulkan/renderer.rs +++ b/src/gfx_apis/vulkan/renderer.rs @@ -2871,7 +2871,7 @@ impl ColorTransforms { mut color: Color, ) -> Color { if let Some(ct) = self.get_or_create(src, dst, intent) { - color = ct.matrix * color; + color = apply_color_matrix(ct.matrix, color); }; color } @@ -2896,6 +2896,25 @@ impl ColorTransforms { } } +fn apply_color_matrix(matrix: ColorMatrix, color: Color) -> Color { + let mut rgba = color.to_array(Eotf::Linear); + let a = rgba[3]; + if a < 1.0 && a > 0.0 { + for c in &mut rgba[..3] { + *c /= a; + } + } + let [r, g, b] = matrix * [rgba[0] as f64, rgba[1] as f64, rgba[2] as f64]; + Color::new( + Eotf::Linear, + AlphaMode::Straight, + r as f32, + g as f32, + b as f32, + a, + ) +} + #[derive(Default)] struct EotfArgsCache { map: AHashMap<(EotfCacheKey, bool), EotfArg>, diff --git a/src/globals.rs b/src/globals.rs index dbc5a650..4b828a10 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -156,7 +156,8 @@ pub trait Global: GlobalBase { true } fn permitted(&self, caps: ClientCaps, xwayland: bool) -> bool { - caps.contains(self.required_caps()) && (xwayland || !self.xwayland_only()) + let _ = caps; + xwayland || !self.xwayland_only() } fn not_permitted(&self, caps: ClientCaps, xwayland: bool) -> bool { !self.permitted(caps, xwayland) @@ -345,7 +346,7 @@ impl Globals { } pub fn notify_all(&self, registry: &Rc) { - let caps = registry.client.effective_caps.get(); + let caps = ClientCaps::all(); let xwayland = registry.client.is_xwayland; let globals = self.registry.lock(); macro_rules! emit { @@ -429,7 +430,7 @@ impl Globals { } for client in state.clients.clients.borrow().values() { let client = &client.data; - let caps = client.effective_caps.get(); + let caps = ClientCaps::all(); let xwayland = client.is_xwayland; for global in &singletons { if global.permitted(caps, xwayland) { diff --git a/src/ifs.rs b/src/ifs.rs index f29b0d67..36d5f8a1 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -13,7 +13,6 @@ pub mod head_management; pub mod hyprland_focus_grab_manager_v1; pub mod hyprland_focus_grab_v1; pub mod ipc; -pub mod jay_acceptor_request; pub mod jay_client_query; pub mod jay_color_management; pub mod jay_compositor; diff --git a/src/ifs/jay_acceptor_request.rs b/src/ifs/jay_acceptor_request.rs deleted file mode 100644 index b9a73184..00000000 --- a/src/ifs/jay_acceptor_request.rs +++ /dev/null @@ -1,60 +0,0 @@ -use { - crate::{ - client::{Client, ClientError}, - leaks::Tracker, - object::{Object, Version}, - utils::errorfmt::ErrorFmt, - wire::{JayAcceptorRequestId, jay_acceptor_request::*}, - }, - std::{error::Error, rc::Rc}, - thiserror::Error, -}; - -pub struct JayAcceptorRequest { - pub id: JayAcceptorRequestId, - pub client: Rc, - pub tracker: Tracker, - pub version: Version, -} - -impl JayAcceptorRequest { - pub fn send_done(&self, name: &str) { - self.client.event(Done { - self_id: self.id, - name, - }); - } - - pub fn send_failed(&self, err: impl Error) { - let msg = &ErrorFmt(err).to_string(); - self.client.event(Failed { - self_id: self.id, - msg, - }); - } -} - -impl JayAcceptorRequestRequestHandler for JayAcceptorRequest { - type Error = JayAcceptorRequestError; - - fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { - self.client.remove_obj(self)?; - Ok(()) - } -} - -object_base! { - self = JayAcceptorRequest; - version = self.version; -} - -impl Object for JayAcceptorRequest {} - -simple_add_obj!(JayAcceptorRequest); - -#[derive(Debug, Error)] -pub enum JayAcceptorRequestError { - #[error(transparent)] - ClientError(Box), -} -efrom!(JayAcceptorRequestError, ClientError); diff --git a/src/ifs/jay_client_query.rs b/src/ifs/jay_client_query.rs index bba06f2b..2a20e109 100644 --- a/src/ifs/jay_client_query.rs +++ b/src/ifs/jay_client_query.rs @@ -9,7 +9,7 @@ use { jay_client_query::{ AddAll, AddId, Comm, Destroy, Done, End, Exe, Execute, IsXwayland, JayClientQueryRequestHandler, Pid, SandboxAppId, SandboxEngine, SandboxInstanceId, - Sandboxed, Start, Tag, Uid, + Sandboxed, Start, Uid, }, }, }, @@ -26,8 +26,6 @@ pub struct JayClientQuery { all: Cell, } -const TAG_SINCE: Version = Version(25); - impl JayClientQuery { pub fn new(client: &Rc, id: JayClientQueryId, version: Version) -> Self { Self { @@ -97,14 +95,6 @@ impl JayClientQueryRequestHandler for JayClientQuery { instance_id, }); } - if self.version >= TAG_SINCE - && let Some(tag) = &client.acceptor.tag - { - self.client.event(Tag { - self_id: self.id, - tag, - }); - } self.client.event(End { self_id: self.id }); }; if self.all.get() { diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4ccc45db..c4be67f8 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -5,7 +5,6 @@ use { compositor::LogLevel, globals::{Global, GlobalName}, ifs::{ - jay_acceptor_request::JayAcceptorRequest, jay_client_query::JayClientQuery, jay_color_management::JayColorManagement, jay_ei_session_builder::JayEiSessionBuilder, @@ -503,27 +502,6 @@ impl JayCompositorRequestHandler for JayCompositor { Ok(()) } - fn get_tagged_acceptor( - &self, - req: GetTaggedAcceptor<'_>, - _slf: &Rc, - ) -> Result<(), Self::Error> { - let obj = Rc::new(JayAcceptorRequest { - id: req.id, - client: self.client.clone(), - tracker: Default::default(), - version: self.version, - }); - track!(self.client, obj); - self.client.add_client_obj(&obj)?; - let state = &self.client.state; - match state.tagged_acceptors.get(state, req.tag) { - Ok(d) => obj.send_done(&d), - Err(e) => obj.send_failed(e), - } - Ok(()) - } - fn get_sync_file_surface( &self, req: GetSyncFileSurface, diff --git a/src/ifs/wl_registry.rs b/src/ifs/wl_registry.rs index f0dfda67..3b5e62bc 100644 --- a/src/ifs/wl_registry.rs +++ b/src/ifs/wl_registry.rs @@ -1,6 +1,6 @@ use { crate::{ - client::Client, + client::{Client, ClientCaps}, globals::{Global, GlobalName, GlobalsError, Singleton}, leaks::Tracker, object::{Interface, Object, Version}, @@ -61,11 +61,7 @@ impl WlRegistryRequestHandler for WlRegistry { fn bind(&self, bind: Bind, _slf: &Rc) -> Result<(), Self::Error> { let name = GlobalName::from_raw(bind.name); let globals = &self.client.state.globals; - let global = globals.get( - name, - self.client.effective_caps.get(), - self.client.is_xwayland, - )?; + let global = globals.get(name, ClientCaps::all(), self.client.is_xwayland)?; if global.interface().name() != bind.interface { return Err(WlRegistryError::InvalidInterface(InterfaceError { name: global.name(), diff --git a/src/ifs/wl_surface/wp_color_management_surface_v1.rs b/src/ifs/wl_surface/wp_color_management_surface_v1.rs index 9783aa87..36f4de9b 100644 --- a/src/ifs/wl_surface/wp_color_management_surface_v1.rs +++ b/src/ifs/wl_surface/wp_color_management_surface_v1.rs @@ -1,7 +1,7 @@ use { crate::{ client::{Client, ClientError}, - cmm::cmm_render_intent::RenderIntent, + cmm::cmm_render_intent, ifs::wl_surface::WlSurface, leaks::Tracker, object::{Object, Version}, @@ -52,7 +52,7 @@ impl WpColorManagementSurfaceV1RequestHandler for WpColorManagementSurfaceV1 { req: SetImageDescription, _slf: &Rc, ) -> Result<(), Self::Error> { - let Some(intent) = RenderIntent::from_wayland(req.render_intent, self.version) else { + let Some(intent) = cmm_render_intent::from_wayland(req.render_intent, self.version) else { return Err(WpColorManagementSurfaceV1Error::UnsupportedRenderIntent( req.render_intent, )); diff --git a/src/ifs/wp_security_context_v1.rs b/src/ifs/wp_security_context_v1.rs index 26e6525f..1eb1ee99 100644 --- a/src/ifs/wp_security_context_v1.rs +++ b/src/ifs/wp_security_context_v1.rs @@ -87,7 +87,6 @@ impl WpSecurityContextV1RequestHandler for WpSecurityContextV1 { self.instance_id.take(), &self.listen_fd, &self.close_fd, - self.client.bounding_caps_for_children.get(), ); Ok(()) } diff --git a/src/it.rs b/src/it.rs index 1ce659ee..7e08fc5c 100644 --- a/src/it.rs +++ b/src/it.rs @@ -126,7 +126,7 @@ fn run_test(it_run: &ItRun, test: &'static dyn TestCase, cfg: Rc) { let mut addr: c::sockaddr_un = uapi::pod_zeroed(); addr.sun_family = c::AF_UNIX as _; let acceptor = state.acceptor.get().unwrap(); - let path = acceptor.secure_path(); + let path = acceptor.socket_path(); let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); sun_path[..path.len()].copy_from_slice(path.as_bytes()); sun_path[path.len()] = 0; diff --git a/src/macros.rs b/src/macros.rs index df16289b..e9f6f4eb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -370,6 +370,7 @@ macro_rules! dedicated_add_global { }; } +#[expect(unused_macros)] macro_rules! assert_size_eq { ($t:ty, $u:ty) => {{ struct AssertEqSize(std::marker::PhantomData, std::marker::PhantomData); @@ -401,6 +402,7 @@ macro_rules! assert_size_le { }}; } +#[expect(unused_macros)] macro_rules! assert_align_eq { ($t:ty, $u:ty) => {{ struct AssertEqAlign(std::marker::PhantomData, std::marker::PhantomData); @@ -632,86 +634,6 @@ macro_rules! bitflags { }; } -macro_rules! pw_opcodes { - ($name:ident; $($var:ident = $val:expr,)*) => { - #[derive(Copy, Clone, Debug)] - pub enum $name { - $( - $var, - )* - } - - #[allow(clippy::allow_attributes, dead_code)] - impl $name { - pub fn from_id(id: u8) -> Option { - let v = match id { - $($val => Self::$var,)* - _ => return None, - }; - Some(v) - } - - pub fn name(self) -> &'static str { - match self { - $(Self::$var => stringify!($var),)* - } - } - } - - impl crate::pipewire::pw_object::PwOpcode for $name { - fn id(&self) -> u8 { - match self { - $(Self::$var => $val,)* - } - } - } - } -} - -macro_rules! pw_object_base { - ($name:ident, $if:expr, $events:ident; $($event:ident => $method:ident,)*) => { - impl crate::pipewire::pw_object::PwObjectBase for $name { - fn data(&self) -> &crate::pipewire::pw_object::PwObjectData { - &self.data - } - - fn interface(&self) -> &str { - $if - } - - fn handle_msg(self: std::rc::Rc, opcode: u8, parser: crate::pipewire::pw_parser::PwParser<'_>) -> Result<(), crate::pipewire::pw_object::PwObjectError> { - match $events::from_id(opcode) { - None => Err(crate::pipewire::pw_object::PwObjectError { - interface: $if, - source: crate::pipewire::pw_object::PwObjectErrorType::UnknownEvent(opcode), - }), - Some(m) => { - let (res, method) = match m { - $( - $events::$event => (self.$method(parser), stringify!($event)), - )* - }; - match res { - Ok(_) => Ok(()), - Err(source) => Err(crate::pipewire::pw_object::PwObjectError { - interface: $if, - source: crate::pipewire::pw_object::PwObjectErrorType::EventError { - method, - source: Box::new(source), - }, - }) - } - }, - } - } - - fn event_name(&self, opcode: u8) -> Option<&'static str> { - $events::from_id(opcode).map(|o| o.name()) - } - } - } -} - macro_rules! ei_id { ($name:ident) => { #[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] @@ -829,12 +751,6 @@ macro_rules! not_matches { }; } -macro_rules! jay_allow_realtime_config_so { - () => { - "JAY_ALLOW_REALTIME_CONFIG_SO" - }; -} - #[allow(clippy::allow_attributes, unused_macros)] macro_rules! dbg { ($val:expr) => { diff --git a/src/main.rs b/src/main.rs index 161d3d99..9ca91200 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,8 +89,6 @@ mod logind; mod object; mod output_schedule; mod pango; -mod pipewire; -mod portal; mod pr_caps; mod rect; mod renderer; @@ -99,7 +97,6 @@ mod screenshoter; mod security_context_acceptor; mod sighand; mod state; -mod tagged_acceptor; mod tasks; mod text; mod theme; diff --git a/src/pipewire.rs b/src/pipewire.rs deleted file mode 100644 index af77bc66..00000000 --- a/src/pipewire.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod pw_con; -pub mod pw_formatter; -pub mod pw_ifs; -pub mod pw_mem; -pub mod pw_object; -pub mod pw_parser; -pub mod pw_pod; diff --git a/src/pipewire/pw_con.rs b/src/pipewire/pw_con.rs deleted file mode 100644 index b994acd3..00000000 --- a/src/pipewire/pw_con.rs +++ /dev/null @@ -1,453 +0,0 @@ -use { - crate::{ - async_engine::{AsyncEngine, SpawnedFuture}, - io_uring::{IoUring, IoUringError}, - pipewire::{ - pw_formatter::{PwFormatter, format}, - pw_ifs::{ - pw_client::{PwClient, PwClientMethods}, - pw_client_node::{ - PW_CLIENT_NODE_FACTORY, PW_CLIENT_NODE_INTERFACE, PW_CLIENT_NODE_VERSION, - PwClientNode, - }, - pw_core::{PW_CORE_VERSION, PwCore, PwCoreMethods}, - pw_registry::{PW_REGISTRY_VERSION, PwRegistry}, - }, - pw_mem::PwMemPool, - pw_object::{PwObject, PwObjectData, PwObjectError, PwOpcode}, - pw_parser::{PwParser, PwParserError}, - }, - utils::{ - bitfield::Bitfield, - bufio::{BufIo, BufIoError, BufIoIncoming, BufIoMessage}, - clonecell::CloneCell, - copyhashmap::CopyHashMap, - errorfmt::ErrorFmt, - hash_map_ext::HashMapExt, - numcell::NumCell, - oserror::{OsError, OsErrorExt2}, - xrd::xrd, - }, - }, - std::{ - cell::{Cell, RefCell}, - fmt::Display, - io::Write, - rc::{Rc, Weak}, - }, - thiserror::Error, - uapi::{OwnedFd, c}, -}; - -#[derive(Debug, Error)] -pub enum PwConError { - #[error("Could not create a unix socket")] - CreateSocket(#[source] OsError), - #[error("Could not connect to the pipewire daemon")] - ConnectSocket(#[source] IoUringError), - #[error(transparent)] - BufIoError(#[from] BufIoError), - #[error("Server did not sent a required fd")] - MissingFd, - #[error("XDG_RUNTIME_DIR is not set")] - XrdNotSet, - #[error(transparent)] - PwObjectError(#[from] PwObjectError), - #[error(transparent)] - PwParserError(#[from] PwParserError), -} - -pub struct PwConHolder { - pub con: Rc, - outgoing: Cell>>, - incoming: Cell>>, -} - -pub struct PwCon { - send_seq: NumCell, - pub io: Rc, - holder: CloneCell>, - dead: Cell, - pub objects: CopyHashMap>, - pub ids: RefCell, - pub mem: PwMemPool, - pub ring: Rc, - pub eng: Rc, - pub owner: CloneCell>>, - - registry_generation: Cell, - ack_registry_generation: Cell, -} - -pub trait PwConOwner { - fn killed(&self) {} -} - -impl PwCon { - pub fn create_client_node(self: &Rc, props: &[(String, String)]) -> Rc { - let node = Rc::new(PwClientNode { - data: self.proxy_data(), - con: self.clone(), - ios: Default::default(), - owner: CloneCell::new(None), - ports: Default::default(), - port_out_free: RefCell::new(Default::default()), - port_in_free: RefCell::new(Default::default()), - activation: Default::default(), - transport_in: Cell::new(None), - transport_out: Default::default(), - activations: Default::default(), - }); - if !self.dead.get() { - self.objects.set(node.data.id, node.clone()); - } - self.create_object( - PW_CLIENT_NODE_FACTORY, - PW_CLIENT_NODE_INTERFACE, - PW_CLIENT_NODE_VERSION, - props, - node.data.id, - ); - node.send_update(); - node - } - - pub fn destroy_obj(&self, obj: &impl PwObject) { - obj.break_loops(); - self.send2(0, "core", PwCoreMethods::Destroy, |f| { - f.write_struct(|f| { - f.write_uint(obj.data().id); - }); - }); - self.objects.remove(&obj.data().id); - } - - pub fn kill(&self) { - for obj in self.objects.lock().drain_values() { - obj.break_loops(); - } - self.io.shutdown(); - self.dead.set(true); - if let Some(con) = self.holder.get().upgrade() { - con.outgoing.take(); - con.incoming.take(); - } - if let Some(owner) = self.owner.take() { - owner.killed(); - } - } - - pub fn id(&self) -> u32 { - self.ids.borrow_mut().acquire() - } - - pub fn proxy_data(&self) -> PwObjectData { - PwObjectData { - id: self.id(), - bound_id: Cell::new(None), - sync_id: Default::default(), - } - } - - pub fn send(&self, proxy: &P, opcode: O, f: F) - where - P: PwObject, - O: PwOpcode, - F: FnOnce(&mut PwFormatter), - { - self.send2(proxy.data().id, proxy.interface(), opcode, f); - } - - pub fn send2(&self, id: u32, interface: &str, opcode: O, f: F) - where - O: PwOpcode, - F: FnOnce(&mut PwFormatter), - { - if self.dead.get() { - return; - } - let mut buf = self.io.buf(); - let mut fds = vec![]; - format( - &mut buf, - &mut fds, - id, - opcode.id(), - self.send_seq.fetch_add(1), - |fmt| { - f(fmt); - if self.ack_registry_generation.get() != self.registry_generation.get() { - let generation = self.registry_generation.get(); - fmt.write_struct(|f| { - f.write_id(FOOTER_REGISTRY_GENERATION); - f.write_struct(|f| { - f.write_ulong(generation); - }); - }); - self.ack_registry_generation.set(generation); - } - }, - ); - if log::log_enabled!(log::Level::Trace) { - log::trace!("CALL {}@{}: `{:?}`:", interface, id, opcode); - let mut parser = PwParser::new(&buf[16..buf.len()], &fds); - while parser.len() > 0 { - log::trace!("{:#?}", parser.read_pod().unwrap()); - } - } - self.io.send(BufIoMessage { - fds, - buf: buf.unwrap(), - }); - } - - #[expect(dead_code)] - pub fn sync(&self, p: &P) { - let seq = p.data().sync_id.fetch_add(1) + 1; - self.send2(0, "core", PwCoreMethods::Sync, |f| { - f.write_struct(|f| { - f.write_uint(p.data().id); - f.write_uint(seq); - }); - }); - } - - pub fn send_hello(&self) { - self.send2(0, "core", PwCoreMethods::Hello, |f| { - f.write_struct(|f| f.write_int(PW_CORE_VERSION)); - }); - } - - #[expect(dead_code)] - pub fn get_registry(self: &Rc) -> Rc { - let registry = Rc::new(PwRegistry { - data: self.proxy_data(), - _con: self.clone(), - }); - if !self.dead.get() { - self.objects.set(registry.data.id, registry.clone()); - } - self.send2(0, "core", PwCoreMethods::GetRegistry, |f| { - f.write_struct(|f| { - f.write_int(PW_REGISTRY_VERSION); - f.write_uint(registry.data.id); - }); - }); - registry - } - - pub fn create_object( - &self, - factory: &str, - ty: &str, - version: i32, - props: &[(String, String)], - new_id: u32, - ) { - self.send2(0, "core", PwCoreMethods::CreateObject, |f| { - f.write_struct(|f| { - f.write_string(factory); - f.write_string(ty); - f.write_int(version); - f.write_struct(|f| { - f.write_int(props.len() as _); - for (key, val) in props { - f.write_string(key); - f.write_string(val); - } - }); - f.write_uint(new_id); - }); - }); - } - - pub fn send_properties(&self) { - self.send2(1, "client", PwClientMethods::UpdateProperties, |f| { - f.write_struct(|f| { - f.write_struct(|f| { - f.write_int(1); - f.write_string("application.name"); - f.write_string("jay-portal"); - }); - }); - }); - } - - async fn handle_outgoing(self: Rc) { - if let Err(e) = self.io.clone().outgoing().await { - log::error!("{}", ErrorFmt(e)); - } - self.kill(); - } - - async fn handle_incoming(self: Rc) { - let incoming = Incoming { - incoming: self.io.clone().incoming(), - con: self.clone(), - buf: vec![], - fds: vec![], - }; - incoming.run().await; - } -} - -impl Drop for PwConHolder { - fn drop(&mut self) { - self.con.owner.take(); - self.con.kill(); - } -} - -impl PwConHolder { - pub async fn new(eng: &Rc, ring: &Rc) -> Result, PwConError> { - let fd = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) - .map(Rc::new) - .map_os_err(PwConError::CreateSocket)?; - let mut addr = c::sockaddr_un { - sun_family: c::AF_UNIX as _, - ..uapi::pod_zeroed() - }; - let xrd = match xrd() { - Some(xrd) => xrd, - _ => return Err(PwConError::XrdNotSet), - }; - { - let mut path = uapi::as_bytes_mut(&mut addr.sun_path[..]); - let _ = write!(path, "{}/pipewire-0", xrd); - } - if let Err(e) = ring.connect(&fd, &addr).await { - return Err(PwConError::ConnectSocket(e)); - } - let io = Rc::new(BufIo::new(&fd, ring)); - let data = Rc::new(PwCon { - send_seq: Default::default(), - io, - holder: Default::default(), - dead: Cell::new(false), - objects: Default::default(), - ids: Default::default(), - mem: Default::default(), - ring: ring.clone(), - eng: eng.clone(), - owner: Default::default(), - registry_generation: Cell::new(0), - ack_registry_generation: Cell::new(0), - }); - let core = Rc::new(PwCore { - data: data.proxy_data(), - con: data.clone(), - }); - let client = Rc::new(PwClient { - data: data.proxy_data(), - _con: data.clone(), - }); - data.objects.set(0, core.clone()); - data.objects.set(1, client.clone()); - data.send_hello(); - data.send_properties(); - let con = Rc::new(PwConHolder { - outgoing: Cell::new(Some( - eng.spawn("pw outgoing", data.clone().handle_outgoing()), - )), - incoming: Cell::new(Some( - eng.spawn("pw incoming", data.clone().handle_incoming()), - )), - con: data, - }); - con.con.holder.set(Rc::downgrade(&con)); - Ok(con) - } -} - -struct Incoming { - con: Rc, - incoming: BufIoIncoming, - buf: Vec, - fds: Vec>, -} - -impl Incoming { - async fn run(mut self) { - loop { - if let Err(e) = self.handle_msg().await { - log::error!("Could not handle incoming message: {}", ErrorFmt(e)); - self.con.kill(); - return; - } - } - } - - async fn handle_msg(&mut self) -> Result<(), PwConError> { - self.buf.clear(); - self.incoming.fill_msg_buf(16, &mut self.buf).await?; - let id: u32 = uapi::pod_read(&self.buf[0..4]).unwrap(); - let p2: u32 = uapi::pod_read(&self.buf[4..8]).unwrap(); - let opcode = (p2 >> 24) as u8; - let size = (p2 & 0xff_ffff) as usize; - let _seq: u32 = uapi::pod_read(&self.buf[8..12]).unwrap(); - let n_fds: u32 = uapi::pod_read(&self.buf[12..16]).unwrap(); - for _ in 0..n_fds { - match self.incoming.fds.pop_front() { - Some(fd) => self.fds.push(fd), - _ => return Err(PwConError::MissingFd), - } - } - self.buf.clear(); - self.incoming.fill_msg_buf(size, &mut self.buf).await?; - if let Err(e) = self.handle_msg_data(id, opcode) { - log::warn!("Could not handle incoming message: {}", ErrorFmt(e)); - } - self.fds.clear(); - Ok(()) - } - - fn handle_msg_data(&self, id: u32, opcode: u8) -> Result<(), PwConError> { - let parser = PwParser::new(&self.buf, &self.fds); - { - let mut parser = parser; - parser.skip()?; - if parser.len() > 0 { - let s1 = parser.read_struct()?; - let mut p2 = s1.fields; - while p2.len() > 0 { - let opcode = p2.read_id()?; - let s2 = p2.read_struct()?; - if opcode == FOOTER_REGISTRY_GENERATION { - let mut p3 = s2.fields; - let generation = p3.read_ulong()?; - self.con.registry_generation.set(generation); - log::debug!("registry generation = {}", generation); - } else { - log::warn!("Unknown message footer: {}", opcode); - } - } - } - } - if let Some(obj) = self.con.objects.get(&id) { - 'log: { - if log::log_enabled!(log::Level::Trace) { - let s; - let op: &dyn Display = match obj.event_name(opcode) { - Some(e) => { - s = e; - if e == "Done" && obj.interface() == "core" { - break 'log; - } - &s - } - _ => &opcode, - }; - log::trace!("EVENT {}@{}: `{}`:", obj.interface(), obj.data().id, op); - let mut parser = parser; - while parser.len() > 0 { - log::trace!("{:#?}", parser.read_pod().unwrap()); - } - } - } - obj.handle_msg(opcode, parser)?; - } - Ok(()) - } -} - -const FOOTER_REGISTRY_GENERATION: u32 = 0; diff --git a/src/pipewire/pw_formatter.rs b/src/pipewire/pw_formatter.rs deleted file mode 100644 index c10eb02a..00000000 --- a/src/pipewire/pw_formatter.rs +++ /dev/null @@ -1,312 +0,0 @@ -use { - crate::{ - pipewire::pw_pod::{ - PW_TYPE_Array, PW_TYPE_Bitmap, PW_TYPE_Bool, PW_TYPE_Bytes, PW_TYPE_Choice, - PW_TYPE_Double, PW_TYPE_Fd, PW_TYPE_Float, PW_TYPE_Fraction, PW_TYPE_Id, PW_TYPE_Int, - PW_TYPE_Long, PW_TYPE_None, PW_TYPE_Object, PW_TYPE_Rectangle, PW_TYPE_String, - PW_TYPE_Struct, PwChoiceType, PwPodObjectType, PwPodType, PwPropFlag, - }, - utils::buf::DynamicBuf, - }, - std::rc::Rc, - uapi::OwnedFd, -}; - -pub struct PwFormatter<'a> { - data: &'a mut DynamicBuf, - fds: &'a mut Vec>, - array: bool, - first: bool, -} - -impl<'a> PwFormatter<'a> { - pub fn write_bool(&mut self, b: bool) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&4u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Bool.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&(b as u32))); - if !self.array { - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - } - self.first = false; - } - - pub fn write_id(&mut self, id: u32) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&4u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Id.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&id)); - if !self.array { - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - } - self.first = false; - } - - pub fn write_object(&mut self, ty: PwPodObjectType, id: u32, f: F) - where - F: FnOnce(&mut PwObjectFormatter), - { - let start = self.data.len(); - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Object.0)); - self.data.extend_from_slice(uapi::as_bytes(&ty.0)); - self.data.extend_from_slice(uapi::as_bytes(&id)); - let mut fmt = PwObjectFormatter { - data: self.data, - fds: self.fds, - }; - f(&mut fmt); - let len = (self.data.len() - start - 8) as u32; - self.data[start..start + 4].copy_from_slice(uapi::as_bytes(&len)); - } - - pub fn write_uint(&mut self, int: u32) { - self.write_int(int as _) - } - - pub fn write_int(&mut self, int: i32) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&4u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Int.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&int)); - if !self.array { - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - } - self.first = false; - } - - pub fn write_ulong(&mut self, long: u64) { - self.write_long(long as _) - } - - pub fn write_long(&mut self, long: i64) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&8u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Long.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&long)); - self.first = false; - } - - #[expect(dead_code)] - pub fn write_float(&mut self, float: f32) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&4u32)); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Float.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&float)); - if !self.array { - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - } - self.first = false; - } - - #[expect(dead_code)] - pub fn write_double(&mut self, double: f64) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&8u32)); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Double.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&double)); - } - - pub fn write_string + ?Sized>(&mut self, s: &S) { - let s = s.as_ref(); - self.data - .extend_from_slice(uapi::as_bytes(&(s.len() as u32 + 1))); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_String.0)); - self.data.extend_from_slice(s); - self.data.push(0); - self.pad(); - } - - #[expect(dead_code)] - pub fn write_bytes(&mut self, s: &[u8]) { - self.data - .extend_from_slice(uapi::as_bytes(&(s.len() as u32))); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Bytes.0)); - self.data.extend_from_slice(s); - self.pad(); - } - - pub fn write_rectangle(&mut self, width: u32, height: u32) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&8u32)); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Rectangle.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&width)); - self.data.extend_from_slice(uapi::as_bytes(&height)); - self.first = false; - } - - #[expect(dead_code)] - pub fn write_fraction(&mut self, num: i32, denom: i32) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&8u32)); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Fraction.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&num)); - self.data.extend_from_slice(uapi::as_bytes(&denom)); - self.first = false; - } - - pub fn write_none(&mut self) { - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_None.0)); - } - self.first = false; - } - - #[expect(dead_code)] - pub fn write_bitmap(&mut self, s: &[u8]) { - self.data - .extend_from_slice(uapi::as_bytes(&(s.len() as u32))); - self.data - .extend_from_slice(uapi::as_bytes(&PW_TYPE_Bitmap.0)); - self.data.extend_from_slice(s); - self.pad(); - } - - pub fn write_fd(&mut self, fd: &Rc) { - let pos = self.fds.len() as u64; - self.fds.push(fd.clone()); - if !self.array || self.first { - self.data.extend_from_slice(uapi::as_bytes(&8u32)); - self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Fd.0)); - } - self.data.extend_from_slice(uapi::as_bytes(&pos)); - self.first = false; - } - - pub fn write_struct(&mut self, f: F) - where - F: FnOnce(&mut PwFormatter), - { - self.write_compound(PW_TYPE_Struct, |fmt| { - let mut fmt = PwFormatter { - data: fmt.data, - fds: fmt.fds, - array: false, - first: false, - }; - f(&mut fmt); - }); - } - - #[expect(dead_code)] - pub fn write_array(&mut self, f: F) - where - F: FnOnce(&mut PwFormatter), - { - self.write_compound(PW_TYPE_Array, |fmt| { - fmt.write_array_body(f); - }); - self.pad(); - } - - fn write_array_body(&mut self, f: F) - where - F: FnOnce(&mut PwFormatter), - { - let mut fmt = PwFormatter { - data: self.data, - fds: self.fds, - array: true, - first: true, - }; - f(&mut fmt); - if fmt.first { - fmt.write_none(); - } - } - - pub fn write_choice(&mut self, ty: PwChoiceType, flags: u32, f: F) - where - F: FnOnce(&mut PwFormatter), - { - self.write_compound(PW_TYPE_Choice, |fmt| { - fmt.data.extend_from_slice(uapi::as_bytes(&ty.0)); - fmt.data.extend_from_slice(uapi::as_bytes(&flags)); - fmt.write_array_body(f); - }); - self.pad(); - } - - fn write_compound(&mut self, ty: PwPodType, f: F) - where - F: FnOnce(&mut PwFormatter), - { - let start = self.data.len(); - self.data.extend_from_slice(uapi::as_bytes(&0u32)); - self.data.extend_from_slice(uapi::as_bytes(&ty.0)); - f(self); - let len = (self.data.len() - start - 8) as u32; - self.data[start..start + 4].copy_from_slice(uapi::as_bytes(&len)); - } - - fn pad(&mut self) { - let todo = self.data.len().wrapping_neg() & 7; - self.data.extend_from_slice(&uapi::as_bytes(&0u64)[..todo]); - } -} - -pub struct PwObjectFormatter<'a> { - data: &'a mut DynamicBuf, - fds: &'a mut Vec>, -} - -impl<'a> PwObjectFormatter<'a> { - pub fn write_property(&mut self, key: u32, flags: PwPropFlag, f: F) - where - F: FnOnce(&mut PwFormatter), - { - self.data.extend_from_slice(uapi::as_bytes(&key)); - self.data.extend_from_slice(uapi::as_bytes(&flags.0)); - let mut fmt = PwFormatter { - data: self.data, - fds: self.fds, - array: false, - first: false, - }; - f(&mut fmt); - } -} - -pub fn format( - buf: &mut DynamicBuf, - fds: &mut Vec>, - id: u32, - opcode: u8, - seq: u32, - f: F, -) where - F: FnOnce(&mut PwFormatter), -{ - buf.clear(); - buf.extend_from_slice(uapi::as_bytes(&id)); - buf.extend_from_slice(uapi::as_bytes(&0u32)); - buf.extend_from_slice(uapi::as_bytes(&seq)); - buf.extend_from_slice(uapi::as_bytes(&0u32)); - let mut fmt = PwFormatter { - data: buf, - fds, - array: false, - first: false, - }; - f(&mut fmt); - let p2 = (buf.len() - 16) as u32 | ((opcode as u32) << 24); - buf[4..8].copy_from_slice(uapi::as_bytes(&p2)); - let nfds = fds.len() as u32; - buf[12..16].copy_from_slice(uapi::as_bytes(&nfds)); -} diff --git a/src/pipewire/pw_ifs.rs b/src/pipewire/pw_ifs.rs deleted file mode 100644 index 91651f17..00000000 --- a/src/pipewire/pw_ifs.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod pw_client; -pub mod pw_client_node; -pub mod pw_core; -pub mod pw_registry; diff --git a/src/pipewire/pw_ifs/pw_client.rs b/src/pipewire/pw_ifs/pw_client.rs deleted file mode 100644 index 361592a4..00000000 --- a/src/pipewire/pw_ifs/pw_client.rs +++ /dev/null @@ -1,61 +0,0 @@ -use { - crate::pipewire::{ - pw_con::PwCon, - pw_object::{PwObject, PwObjectData}, - pw_parser::{PwParser, PwParserError}, - }, - std::rc::Rc, - thiserror::Error, -}; - -pw_opcodes! { - PwClientMethods; - - Error = 1, - UpdateProperties = 2, - GetPermissions = 3, - UpdatePermissions = 4, -} - -pw_opcodes! { - PwClientEvents; - - Info = 0, - Permissions = 1, -} - -pub struct PwClient { - pub data: PwObjectData, - pub _con: Rc, -} - -impl PwClient { - fn handle_info(&self, mut p: PwParser<'_>) -> Result<(), PwClientError> { - let s1 = p.read_struct()?; - let mut p2 = s1.fields; - let _id = p2.read_int()?; - let _change_mask = p2.read_long()?; - let props = p2.read_dict_struct()?; - log::debug!("Pipewire properties: {:#?}", props); - Ok(()) - } - - fn handle_permissions(&self, _p: PwParser<'_>) -> Result<(), PwClientError> { - Ok(()) - } -} - -pw_object_base! { - PwClient, "client", PwClientEvents; - - Info => handle_info, - Permissions => handle_permissions, -} - -impl PwObject for PwClient {} - -#[derive(Debug, Error)] -pub enum PwClientError { - #[error(transparent)] - PwParserError(#[from] PwParserError), -} diff --git a/src/pipewire/pw_ifs/pw_client_node.rs b/src/pipewire/pw_ifs/pw_client_node.rs deleted file mode 100644 index b445aac5..00000000 --- a/src/pipewire/pw_ifs/pw_client_node.rs +++ /dev/null @@ -1,892 +0,0 @@ -#![allow(non_upper_case_globals)] - -use { - crate::{ - async_engine::SpawnedFuture, - format::{Format, pw_formats}, - pipewire::{ - pw_con::PwCon, - pw_mem::{PwMemError, PwMemMap, PwMemSlice, PwMemTyped}, - pw_object::{PwObject, PwObjectData}, - pw_parser::{PwParser, PwParserError}, - pw_pod::{ - PW_CHOICE_Enum, PW_CHOICE_Flags, PW_NODE_ACTIVATION_FINISHED, - PW_NODE_ACTIVATION_NOT_TRIGGERED, PW_NODE_ACTIVATION_TRIGGERED, PW_OBJECT_Format, - PW_OBJECT_ParamBuffers, PW_OBJECT_ParamMeta, PW_PROP_DONT_FIXATE, PW_TYPE_Long, - PwIoType, PwPod, PwPodFraction, PwPodObject, PwPodRectangle, PwPropFlag, - SPA_DATA_DmaBuf, SPA_DATA_FLAG_READABLE, SPA_DATA_MemFd, SPA_DATA_MemPtr, - SPA_DIRECTION_INPUT, SPA_DIRECTION_OUTPUT, SPA_FORMAT_VIDEO_format, - SPA_FORMAT_VIDEO_framerate, SPA_FORMAT_VIDEO_modifier, SPA_FORMAT_VIDEO_size, - SPA_FORMAT_mediaSubtype, SPA_FORMAT_mediaType, SPA_IO_Buffers, SPA_META_Bitmap, - SPA_META_Busy, SPA_META_Control, SPA_META_Cursor, SPA_META_Header, - SPA_META_VideoCrop, SPA_META_VideoDamage, SPA_NODE_BUFFERS_FLAG_ALLOC, - SPA_NODE_COMMAND_Pause, SPA_NODE_COMMAND_Start, SPA_NODE_COMMAND_Suspend, - SPA_PARAM_BUFFERS_blocks, SPA_PARAM_BUFFERS_buffers, SPA_PARAM_BUFFERS_dataType, - SPA_PARAM_Buffers, SPA_PARAM_EnumFormat, SPA_PARAM_Format, SPA_PARAM_INFO, - SPA_PARAM_INFO_READ, SPA_PARAM_INFO_SERIAL, SPA_PARAM_META_size, - SPA_PARAM_META_type, SPA_PARAM_Meta, SPA_PORT_FLAG, - SPA_PORT_FLAG_CAN_ALLOC_BUFFERS, SpaDataFlags, SpaDataType, SpaDirection, - SpaIoType, SpaMediaSubtype, SpaMediaType, SpaMetaType, SpaNodeBuffersFlags, - SpaNodeCommand, SpaParamType, SpaVideoFormat, pw_node_activation, spa_chunk, - spa_io_buffers, spa_meta_bitmap, spa_meta_busy, spa_meta_cursor, spa_meta_header, - spa_meta_region, - }, - }, - utils::{ - bitfield::Bitfield, buf::TypedBuf, clonecell::CloneCell, copyhashmap::CopyHashMap, - errorfmt::ErrorFmt, option_ext::OptionExt, - }, - video::{Modifier, dmabuf::DmaBuf}, - }, - std::{ - cell::{Cell, RefCell}, - rc::Rc, - sync::atomic::Ordering::{Relaxed, Release}, - }, - thiserror::Error, - uapi::OwnedFd, -}; - -pw_opcodes! { - PwClientNodeMethods; - - GetNode = 1, - Update = 2, - PortUpdate = 3, - SetActive = 4, - Event = 5, - PortBuffers = 6, -} - -pw_opcodes! { - PwClientNodeEvents; - - Transport = 0, - SetParam = 1, - SetIo = 2, - Event = 3, - Command = 4, - AddPort = 5, - RemovePort = 6, - PortSetParam = 7, - PortUseBuffers = 8, - PortSetIo = 9, - SetActivation = 10, - PortSetMixInfo = 11, -} - -pub trait PwClientNodeOwner { - fn port_format_changed(&self, port: &Rc) { - let _ = port; - } - fn use_buffers(self: Rc, port: &Rc) { - let _ = port; - } - fn start(self: Rc) {} - fn pause(self: Rc) {} - fn suspend(self: Rc) {} - fn bound_id(&self, id: u32) { - let _ = id; - } -} - -bitflags! { - PwClientNodePortChanges: u32; - - CHANGED_SUPPORTED_PARAMS = 1 << 0, -} - -bitflags! { - PwClientNodePortSupportedMetas: u32; - - SUPPORTED_META_HEADER = 1 << 0, - SUPPORTED_META_BUSY = 1 << 1, - SUPPORTED_META_VIDEO_CROP = 1 << 2, -} - -pub struct PwClientNodePort { - pub node: Rc, - - pub direction: SpaDirection, - pub id: u32, - - pub _destroyed: Cell, - - pub negotiated_format: RefCell, - pub supported_formats: RefCell, - pub supported_metas: Cell, - pub can_alloc_buffers: Cell, - - pub buffers: RefCell>>, - - pub buffer_config: RefCell, - - pub io_buffers: CloneCell>>>, - - pub serial: Cell, -} - -#[derive(Copy, Clone, Debug, Default)] -pub struct PwClientNodeBufferConfig { - pub num_buffers: Option, - pub planes: Option, - pub data_type: SpaDataType, -} - -pub struct PwClientNodeBuffer { - pub _meta_header: Option>>, - pub _meta_busy: Option>>, - pub meta_video_crop: Option>>, - pub chunks: Vec>>, - pub _slices: Vec>, -} - -#[derive(Clone, Debug)] -pub struct PwClientNodePortSupportedFormat { - pub format: &'static Format, - pub modifiers: Vec, -} - -#[derive(Clone, Debug, Default)] -pub struct PwClientNodePortSupportedFormats { - pub media_type: Option, - pub media_sub_type: Option, - pub video_size: Option, - pub formats: Vec, -} - -#[derive(Clone, Debug, Default)] -pub struct PwClientNodePortFormat { - pub media_type: Option, - pub media_sub_type: Option, - pub video_size: Option, - pub format: Option<&'static Format>, - pub modifiers: Option>, - pub framerate: Option, -} - -pub struct PwClientNode { - pub data: PwObjectData, - pub con: Rc, - pub ios: CopyHashMap>, - - pub owner: CloneCell>>, - - pub ports: CopyHashMap<(SpaDirection, u32), Rc>, - - pub port_out_free: RefCell, - pub port_in_free: RefCell, - - pub activation: CloneCell>>>, - pub transport_in: Cell>>, - pub transport_out: CloneCell>>, - - pub activations: CopyHashMap>, -} - -pub struct PwNodeActivation { - pub activation: Rc>, - pub fd: Rc, -} - -// pub struct PwNodeBuffer { -// pub width: i32, -// pub height: i32, -// pub stride: i32, -// pub offset: i32, -// pub fd: Rc, -// } - -pub const PW_CLIENT_NODE_FACTORY: &str = "client-node"; -pub const PW_CLIENT_NODE_INTERFACE: &str = "PipeWire:Interface:ClientNode"; -pub const PW_CLIENT_NODE_VERSION: i32 = 4; - -#[expect(dead_code)] -const PW_CLIENT_NODE_UPDATE_PARAMS: u32 = 1 << 0; -const PW_CLIENT_NODE_UPDATE_INFO: u32 = 1 << 1; - -const SPA_NODE_CHANGE_MASK_FLAGS: u64 = 1 << 0; -#[expect(dead_code)] -const SPA_NODE_CHANGE_MASK_PROPS: u64 = 1 << 1; -const SPA_NODE_CHANGE_MASK_PARAMS: u64 = 1 << 2; - -const PW_CLIENT_NODE_PORT_UPDATE_PARAMS: u32 = 1 << 0; -const PW_CLIENT_NODE_PORT_UPDATE_INFO: u32 = 1 << 1; - -const SPA_PORT_CHANGE_MASK_FLAGS: u64 = 1 << 0; -const SPA_PORT_CHANGE_MASK_RATE: u64 = 1 << 1; -#[expect(dead_code)] -const SPA_PORT_CHANGE_MASK_PROPS: u64 = 1 << 2; -const SPA_PORT_CHANGE_MASK_PARAMS: u64 = 1 << 3; - -impl PwClientNode { - pub fn send_update(&self) { - self.con.send(self, PwClientNodeMethods::Update, |f| { - f.write_struct(|f| { - f.write_uint(PW_CLIENT_NODE_UPDATE_INFO); - f.write_uint(0); - f.write_struct(|f| { - f.write_uint(0); - f.write_uint(1); - f.write_ulong(SPA_NODE_CHANGE_MASK_PARAMS | SPA_NODE_CHANGE_MASK_FLAGS); - f.write_ulong(0); - f.write_uint(0); - f.write_uint(0); - }); - }); - }); - } - - pub fn send_active(&self, active: bool) { - self.con.send(self, PwClientNodeMethods::SetActive, |f| { - f.write_struct(|f| { - f.write_bool(active); - }); - }); - } - - pub fn create_port( - self: &Rc, - output: bool, - supported_formats: PwClientNodePortSupportedFormats, - num_buffers: Option, - ) -> Rc { - let (ids, direction) = match output { - true => (&self.port_out_free, SPA_DIRECTION_OUTPUT), - false => (&self.port_in_free, SPA_DIRECTION_INPUT), - }; - let port = Rc::new(PwClientNodePort { - node: self.clone(), - direction, - id: ids.borrow_mut().acquire(), - _destroyed: Cell::new(false), - negotiated_format: Default::default(), - supported_formats: RefCell::new(supported_formats), - supported_metas: Cell::new(PwClientNodePortSupportedMetas::none()), - can_alloc_buffers: Cell::new(false), - buffers: RefCell::new(vec![]), - buffer_config: RefCell::new(PwClientNodeBufferConfig { - num_buffers, - planes: None, - data_type: SPA_DATA_DmaBuf, - }), - io_buffers: Default::default(), - serial: Cell::new(false), - }); - self.ports.set((direction, port.id), port.clone()); - port - } - - pub fn send_port_output_buffers(&self, port: &PwClientNodePort, buffers: &[DmaBuf]) { - self.con.send(self, PwClientNodeMethods::PortBuffers, |f| { - f.write_struct(|f| { - // direction - f.write_uint(port.direction.0); - // id - f.write_uint(port.id); - // mix_id - f.write_int(-1); - // n_buffers - f.write_uint(buffers.len() as _); - for buffer in buffers { - // n_datas - f.write_uint(buffer.planes.len() as _); - for plane in &buffer.planes { - // type - f.write_id(SPA_DATA_DmaBuf.0); - // fd - f.write_fd(&plane.fd); - // flags - f.write_uint(SPA_DATA_FLAG_READABLE.0); - // offset - f.write_uint(plane.offset); - // size - f.write_uint(plane.stride * buffer.height as u32); - } - } - }); - }); - } - - pub fn send_port_update(&self, port: &PwClientNodePort, fixate: bool) { - port.serial.set(!port.serial.get()); - let serial = match port.serial.get() { - true => SPA_PARAM_INFO_SERIAL, - false => SPA_PARAM_INFO::none(), - }; - self.con.send(self, PwClientNodeMethods::PortUpdate, |f| { - f.write_struct(|f| { - // direction - f.write_uint(port.direction.0); - // id - f.write_uint(port.id); - // change flags - f.write_uint(PW_CLIENT_NODE_PORT_UPDATE_PARAMS | PW_CLIENT_NODE_PORT_UPDATE_INFO); - let sm = port.supported_metas.get(); - let mut metas = vec![]; - if sm.contains(SUPPORTED_META_HEADER) { - metas.push((SPA_META_Header, size_of::())); - } - if sm.contains(SUPPORTED_META_BUSY) { - metas.push((SPA_META_Busy, size_of::())); - } - if sm.contains(SUPPORTED_META_VIDEO_CROP) { - metas.push((SPA_META_VideoCrop, size_of::())); - } - let sf = &*port.supported_formats.borrow(); - let num_formats = sf.formats.len() as u32; - let bc = &*port.buffer_config.borrow(); - let num_params = metas.len() as u32 + num_formats + 1; - - // num params - f.write_uint(num_params); - for format in &sf.formats { - f.write_object(PW_OBJECT_Format, SPA_PARAM_EnumFormat.0, |f| { - if let Some(mt) = sf.media_type { - f.write_property(SPA_FORMAT_mediaType.0, PwPropFlag::none(), |f| { - f.write_id(mt.0); - }); - } - if let Some(mst) = sf.media_sub_type { - f.write_property(SPA_FORMAT_mediaSubtype.0, PwPropFlag::none(), |f| { - f.write_id(mst.0); - }); - } - f.write_property(SPA_FORMAT_VIDEO_format.0, PwPropFlag::none(), |f| { - f.write_choice(PW_CHOICE_Enum, 0, |f| { - f.write_id(format.format.pipewire.0); - f.write_id(format.format.pipewire.0); - }); - }); - f.write_property( - SPA_FORMAT_VIDEO_modifier.0, - if fixate { - PwPropFlag::none() - } else { - PW_PROP_DONT_FIXATE - }, - |f| { - f.write_choice(PW_CHOICE_Enum, 0, |f| { - f.write_ulong(format.modifiers[0]); - for modifier in &format.modifiers { - f.write_ulong(*modifier); - } - }); - }, - ); - if let Some(vs) = sf.video_size { - f.write_property(SPA_FORMAT_VIDEO_size.0, PwPropFlag::none(), |f| { - f.write_choice(PW_CHOICE_Enum, 0, |f| { - f.write_rectangle(vs.width, vs.height); - f.write_rectangle(vs.width, vs.height); - }); - }); - } - }); - } - f.write_object(PW_OBJECT_ParamBuffers, SPA_PARAM_Buffers.0, |f| { - if let Some(num_buffers) = bc.num_buffers { - f.write_property(SPA_PARAM_BUFFERS_buffers.0, PwPropFlag::none(), |f| { - f.write_uint(num_buffers as _); - }); - } - if let Some(planes) = bc.planes { - f.write_property(SPA_PARAM_BUFFERS_blocks.0, PwPropFlag::none(), |f| { - f.write_uint(planes as _); - }); - } - f.write_property(SPA_PARAM_BUFFERS_dataType.0, PwPropFlag::none(), |f| { - f.write_choice(PW_CHOICE_Flags, 0, |f| { - f.write_uint(1 << bc.data_type.0); - }); - }); - }); - for (key, size) in metas { - f.write_object(PW_OBJECT_ParamMeta, SPA_PARAM_Meta.0, |f| { - f.write_property(SPA_PARAM_META_type.0, PwPropFlag::none(), |f| { - f.write_id(key.0); - }); - f.write_property(SPA_PARAM_META_size.0, PwPropFlag::none(), |f| { - f.write_uint(size as u32); - }); - }); - } - f.write_struct(|f| { - // change mask - f.write_ulong( - SPA_PORT_CHANGE_MASK_FLAGS - // | SPA_PORT_CHANGE_MASK_PROPS - | SPA_PORT_CHANGE_MASK_PARAMS - | SPA_PORT_CHANGE_MASK_RATE, - ); - let mut flags = SPA_PORT_FLAG::none(); - if port.can_alloc_buffers.get() { - flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; - } - // flags - f.write_ulong(flags.0); - // rate num - f.write_int(0); - // rate denom - f.write_int(1); - // num props - f.write_int(0); - let num_params = 3; - // num params - f.write_uint(num_params); - f.write_id(SPA_PARAM_EnumFormat.0); - f.write_uint((SPA_PARAM_INFO_READ | serial).0); - f.write_id(SPA_PARAM_Buffers.0); - f.write_uint((SPA_PARAM_INFO_READ | serial).0); - f.write_id(SPA_PARAM_Meta.0); - f.write_uint(SPA_PARAM_INFO_READ.0); - }); - }); - }); - } - - fn handle_set_param(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { - Ok(()) - } - - fn handle_set_io(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s = p.read_struct()?; - let mut p2 = s.fields; - let id = PwIoType(p2.read_id()?); - let memid = p2.read_uint()?; - let offset = p2.read_uint()?; - let size = p2.read_uint()?; - log::debug!("set io {:?}", id); - if memid == !0 { - self.ios.remove(&id); - } else { - let map = match self.con.mem.map(memid, offset, size) { - Ok(m) => m, - Err(e) => { - log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); - return Ok(()); - } - }; - self.ios.set(id, map); - } - Ok(()) - } - - fn handle_event(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { - Ok(()) - } - - fn handle_command(self: &Rc, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s1 = p.read_struct()?; - let mut p1 = s1.fields; - let obj = p1.read_object()?; - match SpaNodeCommand(obj.id) { - SPA_NODE_COMMAND_Start => { - if let Some(owner) = self.owner.get() { - owner.start(); - } - } - SPA_NODE_COMMAND_Pause => { - if let Some(owner) = self.owner.get() { - owner.pause(); - } - } - SPA_NODE_COMMAND_Suspend => { - if let Some(owner) = self.owner.get() { - owner.suspend(); - } - } - v => { - log::warn!("Unhandled node command {:?}", v); - } - } - Ok(()) - } - - fn handle_add_port(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { - Ok(()) - } - - fn handle_remove_port(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { - Ok(()) - } - - fn port_set_format( - &self, - port: &Rc, - obj: Option>, - ) -> Result<(), PwClientNodeError> { - let mut obj = match obj { - Some(obj) => obj, - _ => { - port.negotiated_format.take(); - return Ok(()); - } - }; - let mut format = PwClientNodePortFormat::default(); - if let Some(mt) = obj.get_param(SPA_FORMAT_mediaType.0)? { - format.media_type = Some(SpaMediaType(mt.pod.get_id()?)); - } - if let Some(mt) = obj.get_param(SPA_FORMAT_mediaSubtype.0)? { - format.media_sub_type = Some(SpaMediaSubtype(mt.pod.get_id()?)); - } - if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_size.0)? { - format.video_size = Some(mt.pod.get_rectangle()?); - } - if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_format.0)? - && let Some(fmt) = pw_formats().get(&SpaVideoFormat(mt.pod.get_id()?)) - { - format.format = Some(*fmt); - } - if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_modifier.0)? - && let PwPod::Choice(mods) = mt.pod - { - let mut p1 = mods.elements.elements; - p1.read_pod_body_packed(PW_TYPE_Long, 8)?; - while p1.len() > 0 { - let modifier = p1.read_pod_body_packed(PW_TYPE_Long, 8)?; - if let PwPod::Long(modifier) = modifier { - format - .modifiers - .get_or_insert_default_ext() - .push(modifier as u64); - } - } - } - if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_framerate.0)? { - format.framerate = Some(mt.pod.get_fraction()?); - } - *port.negotiated_format.borrow_mut() = format; - Ok(()) - } - - fn handle_port_set_param(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s1 = p.read_struct()?; - let mut p1 = s1.fields; - let direction = SpaDirection(p1.read_uint()?); - let port_id = p1.read_uint()?; - let id = SpaParamType(p1.read_id()?); - let _flags = p1.read_int()?; - let obj = p1.read_object_opt()?; - let port = self.get_port(direction, port_id)?; - match id { - SPA_PARAM_Format => { - self.port_set_format(&port, obj)?; - if let Some(owner) = self.owner.get() { - owner.port_format_changed(&port); - } - } - _ => { - log::warn!( - "port_set_param: Ignoring unexpected port parameter {:?}", - id - ); - } - } - Ok(()) - } - - fn handle_port_use_buffers(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s1 = p.read_struct()?; - let mut p1 = s1.fields; - let direction = SpaDirection(p1.read_uint()?); - let port_id = p1.read_uint()?; - let _mix_id = p1.read_int()?; - let buffer_flags = SpaNodeBuffersFlags(p1.read_uint()?); - let n_buffers = p1.read_uint()?; - let port = self.get_port(direction, port_id)?; - - let mut res = vec![]; - - for _ in 0..n_buffers { - let mem_id = p1.read_uint()?; - let offset = p1.read_uint()?; - let size = p1.read_uint()?; - let n_metas = p1.read_uint()?; - - let mut meta_header = Default::default(); - let mut meta_video_crop = Default::default(); - let mut meta_busy = Default::default(); - let mut chunks = vec![]; - let mut slices = vec![]; - - let mem = self.con.mem.map(mem_id, offset, size)?; - - log::debug!(" mem_id={}, offset={}, size={}", mem_id, offset, size); - log::debug!(" n_metas={}", n_metas); - - let mut offset = 0; - - for _ in 0..n_metas { - let ty = SpaMetaType(p1.read_id()?); - let size = p1.read_uint()? as usize; - - match ty { - SPA_META_Header => { - let header = mem.typed_at::(offset); - meta_header = Some(header); - } - SPA_META_VideoCrop => { - let crop = mem.typed_at::(offset); - meta_video_crop = Some(crop); - } - SPA_META_VideoDamage => { - let _video_damage = mem.typed_at::(offset); - } - SPA_META_Bitmap => { - let _bitmap = mem.typed_at::(offset); - } - SPA_META_Cursor => { - let _cursor = mem.typed_at::(offset); - } - SPA_META_Control => {} - SPA_META_Busy => { - let busy = mem.typed_at::(offset); - meta_busy = Some(busy); - } - _ => {} - } - - offset += (size + 7) & !7; - } - - let n_datas = p1.read_uint()?; - - log::debug!(" offset = {}, n_datas={}", offset, n_datas); - - for _ in 0..n_datas { - let ty = SpaDataType(p1.read_id()?); - let data_id = p1.read_uint()?; - let _flags = SpaDataFlags(p1.read_uint()?); - let mapoffset = p1.read_uint()?; - let maxsize = p1.read_uint()?; - - chunks.push(mem.typed_at(offset)); - offset += size_of::(); - - if !buffer_flags.contains(SPA_NODE_BUFFERS_FLAG_ALLOC) { - if ty == SPA_DATA_MemPtr { - let offset = data_id as usize; - slices.push(mem.slice(offset..offset + maxsize as usize)); - } else if ty == SPA_DATA_MemFd { - let mem = self.con.mem.map(data_id, mapoffset, maxsize)?; - slices.push(mem.slice(0..maxsize as usize)); - } - } - } - - res.push(Rc::new(PwClientNodeBuffer { - _meta_header: meta_header, - _meta_busy: meta_busy, - meta_video_crop, - chunks, - _slices: slices, - })); - } - - *port.buffers.borrow_mut() = res; - - if let Some(owner) = self.owner.get() { - owner.use_buffers(&port); - } - - Ok(()) - } - - fn handle_port_set_io(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s = p.read_struct()?; - let mut p2 = s.fields; - let direction = SpaDirection(p2.read_uint()?); - let port_id = p2.read_uint()?; - let mix_id = p2.read_uint()?; - let id = SpaIoType(p2.read_id()?); - let mem_id = p2.read_uint()?; - let offset = p2.read_uint()?; - let size = p2.read_uint()?; - let port = self.get_port(direction, port_id)?; - match id { - SPA_IO_Buffers if mix_id == 0 => { - if mem_id == !0 { - port.io_buffers.take(); - } else { - let io_buffers = self - .con - .mem - .map(mem_id, offset, size)? - .typed::(); - unsafe { - io_buffers.read().buffer_id.store(!0, Relaxed); - io_buffers.read().status.store(0, Relaxed); - } - port.io_buffers.set(Some(io_buffers)); - } - } - _ => {} - } - Ok(()) - } - - fn handle_transport(self: &Rc, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s = p.read_struct()?; - let mut p2 = s.fields; - let readfd = p2.read_fd()?; - let writefd = p2.read_fd()?; - let memid = p2.read_uint()?; - let offset = p2.read_uint()?; - let size = p2.read_uint()?; - let map = match self.con.mem.map(memid, offset, size) { - Ok(m) => m, - Err(e) => { - log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); - return Ok(()); - } - }; - let typed = map.typed::(); - self.activation.set(Some(typed.clone())); - self.transport_in.set(Some( - self.con - .eng - .spawn("pw transport in", self.clone().transport_in(typed, readfd)), - )); - self.transport_out.set(Some(writefd)); - Ok(()) - } - - fn handle_set_activation( - self: &Rc, - mut p: PwParser<'_>, - ) -> Result<(), PwClientNodeError> { - let s = p.read_struct()?; - let mut p2 = s.fields; - let node = p2.read_uint()?; - let signalfd = p2.read_fd_opt()?; - if let Some(signalfd) = signalfd { - let memid = p2.read_uint()?; - let offset = p2.read_uint()?; - let size = p2.read_uint()?; - let map = match self.con.mem.map(memid, offset, size) { - Ok(m) => m, - Err(e) => { - log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); - return Ok(()); - } - }; - let typed = map.typed::(); - self.activations.set( - node, - Rc::new(PwNodeActivation { - activation: typed, - fd: signalfd, - }), - ); - } else { - self.activations.remove(&node); - } - Ok(()) - } - - fn get_port( - &self, - direction: SpaDirection, - port_id: u32, - ) -> Result, PwClientNodeError> { - match self.ports.get(&(direction, port_id)) { - Some(p) => Ok(p), - _ => Err(PwClientNodeError::UnknownPort(direction, port_id)), - } - } - - fn handle_port_set_mix_info(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { - let s1 = p.read_struct()?; - let mut p1 = s1.fields; - let direction = SpaDirection(p1.read_uint()?); - let port_id = p1.read_uint()?; - let mix_id = p1.read_int()?; - let peer_id = p1.read_int()?; - let dict = p1.read_dict_struct()?; - let _port = self.get_port(direction, port_id)?; - log::debug!( - "mix info: mix_id={}, peer_id={}, dict={:#?}", - mix_id, - peer_id, - dict - ); - Ok(()) - } - - async fn transport_in( - self: Rc, - _activation: Rc>, - fd: Rc, - ) { - let mut buf = TypedBuf::::new(); - loop { - if let Err(e) = self.con.ring.read(&fd, buf.buf()).await { - log::error!("Could not read from eventfd: {}", ErrorFmt(e)); - return; - } - if let Some(activation) = self.activation.get() { - let activation = unsafe { activation.read() }; - activation - .status - .store(PW_NODE_ACTIVATION_FINISHED.0, Relaxed); - } - } - } - - pub fn drive(&self) { - for activation in self.activations.lock().values() { - let a = unsafe { activation.activation.read() }; - let required = a.state[0].required.load(Relaxed); - a.state[0].pending.store(required - 1, Relaxed); - if required == 1 { - a.status.store(PW_NODE_ACTIVATION_TRIGGERED.0, Release); - let _ = uapi::eventfd_write(activation.fd.raw(), 1); - } else { - a.status.store(PW_NODE_ACTIVATION_NOT_TRIGGERED.0, Release); - } - } - } -} - -pw_object_base! { - PwClientNode, "client-node", PwClientNodeEvents; - - Transport => handle_transport, - SetParam => handle_set_param, - SetIo => handle_set_io, - Event => handle_event, - Command => handle_command, - AddPort => handle_add_port, - RemovePort => handle_remove_port, - PortSetParam => handle_port_set_param, - PortUseBuffers => handle_port_use_buffers, - PortSetIo => handle_port_set_io, - SetActivation => handle_set_activation, - PortSetMixInfo => handle_port_set_mix_info, -} - -impl PwObject for PwClientNode { - fn bound_id(&self, id: u32) { - if let Some(owner) = self.owner.get() { - owner.bound_id(id); - } - } - - fn break_loops(&self) { - self.owner.take(); - self.ports.clear(); - self.transport_in.take(); - self.transport_out.take(); - } -} - -#[derive(Debug, Error)] -pub enum PwClientNodeError { - #[error(transparent)] - PwParserError(#[from] PwParserError), - #[error(transparent)] - PwMemError(#[from] PwMemError), - #[error("Unknown port {0:?}@{1}")] - UnknownPort(SpaDirection, u32), -} diff --git a/src/pipewire/pw_ifs/pw_core.rs b/src/pipewire/pw_ifs/pw_core.rs deleted file mode 100644 index b2685f26..00000000 --- a/src/pipewire/pw_ifs/pw_core.rs +++ /dev/null @@ -1,186 +0,0 @@ -#![allow(non_upper_case_globals)] - -use { - crate::{ - pipewire::{ - pw_con::PwCon, - pw_mem::{PwMem, PwMemType}, - pw_object::{PwObject, PwObjectData}, - pw_parser::{PwParser, PwParserError}, - pw_pod::{SPA_DATA_DmaBuf, SPA_DATA_MemFd, SpaDataType}, - }, - utils::bitflags::BitflagsExt, - }, - std::rc::Rc, - thiserror::Error, -}; - -pub struct PwCore { - pub data: PwObjectData, - pub con: Rc, -} - -pw_opcodes! { - PwCoreMethods; - - Hello = 1, - Sync = 2, - Pong = 3, - Error = 4, - GetRegistry = 5, - CreateObject = 6, - Destroy = 7, -} - -pw_opcodes! { - PwCoreEvents; - - Info = 0, - Done = 1, - Ping = 2, - Error = 3, - RemoveId = 4, - BoundId = 5, - AddMem = 6, - RemoveMem = 7, -} - -pub const PW_CORE_VERSION: i32 = 3; - -impl PwCore { - pub fn handle_info(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_int()?; - let cookie = p2.read_int()?; - let user_name = p2.read_string()?; - let host_name = p2.read_string()?; - let version_name = p2.read_string()?; - let name = p2.read_string()?; - let change_mask = p2.read_long()?; - let dict = p2.read_dict_struct()?; - log::info!( - "info: id={id}, cookie={cookie}, user_name={user_name}, host_name={host_name}, version_name={version_name}, name={name}, change_mask={change_mask}" - ); - log::info!("dict: {:#?}", dict); - Ok(()) - } - - pub fn handle_done(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_uint()?; - let seq = p2.read_uint()?; - if let Some(obj) = self.con.objects.get(&id) - && obj.data().sync_id.get() <= seq - { - obj.data().sync_id.set(seq); - obj.done(); - } - Ok(()) - } - - pub fn handle_ping(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_int()?; - let seq = p2.read_int()?; - self.con.send(self, PwCoreMethods::Pong, |f| { - f.write_struct(|f| { - f.write_int(id); - f.write_int(seq); - }); - }); - Ok(()) - } - - pub fn handle_error(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_int()?; - let seq = p2.read_int()?; - let res = p2.read_int()?; - let error = p2.read_string()?; - log::info!("error: id={id}, seq={seq}, res={res}, error={error}"); - Ok(()) - } - - pub fn handle_remove_id(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_uint()?; - self.con.objects.remove(&id); - self.con.ids.borrow_mut().release(id); - Ok(()) - } - - pub fn handle_bound_id(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_uint()?; - let bound_id = p2.read_uint()?; - if let Some(obj) = self.con.objects.get(&id) { - obj.data().bound_id.set(Some(bound_id)); - obj.bound_id(bound_id); - } - Ok(()) - } - - pub fn handle_add_mem(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_uint()?; - let ty = SpaDataType(p2.read_id()?); - let fd = p2.read_fd()?; - let flags = p2.read_int()?; - let read = flags.contains(1); - let write = flags.contains(2); - let ty = match ty { - SPA_DATA_MemFd => PwMemType::MemFd, - SPA_DATA_DmaBuf => PwMemType::DmaBuf, - _ => { - log::error!("Ignoring unknown mem type {:?}", ty); - return Ok(()); - } - }; - self.con.mem.mems.set( - id, - Rc::new(PwMem { - _ty: ty, - read, - write, - fd, - }), - ); - Ok(()) - } - - pub fn handle_remove_mem(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { - let s1 = p1.read_struct()?; - let mut p2 = s1.fields; - let id = p2.read_uint()?; - self.con.mem.mems.remove(&id); - Ok(()) - } -} - -pw_object_base! { - PwCore, "core", PwCoreEvents; - - Info => handle_info, - Done => handle_done, - Ping => handle_ping, - Error => handle_error, - RemoveId => handle_remove_id, - BoundId => handle_bound_id, - AddMem => handle_add_mem, - RemoveMem => handle_remove_mem, -} - -impl PwObject for PwCore {} - -#[derive(Debug, Error)] -pub enum PwCoreError { - #[error(transparent)] - PwParserError(#[from] PwParserError), -} diff --git a/src/pipewire/pw_ifs/pw_registry.rs b/src/pipewire/pw_ifs/pw_registry.rs deleted file mode 100644 index f1c54b89..00000000 --- a/src/pipewire/pw_ifs/pw_registry.rs +++ /dev/null @@ -1,48 +0,0 @@ -use { - crate::pipewire::{ - pw_con::PwCon, - pw_object::{PwObject, PwObjectData}, - pw_parser::{PwParser, PwParserError}, - }, - std::rc::Rc, - thiserror::Error, -}; - -pub const PW_REGISTRY_VERSION: i32 = 3; - -pw_opcodes! { - PwRegistryEvents; - - Global = 0, - GlobalRemove = 1, -} - -pub struct PwRegistry { - pub data: PwObjectData, - pub _con: Rc, -} - -impl PwRegistry { - fn handle_global(&self, _p: PwParser<'_>) -> Result<(), PwRegistryError> { - Ok(()) - } - - fn handle_global_remove(&self, _p: PwParser<'_>) -> Result<(), PwRegistryError> { - Ok(()) - } -} - -pw_object_base! { - PwRegistry, "registry", PwRegistryEvents; - - Global => handle_global, - GlobalRemove => handle_global_remove, -} - -impl PwObject for PwRegistry {} - -#[derive(Debug, Error)] -pub enum PwRegistryError { - #[error(transparent)] - PwParserError(#[from] PwParserError), -} diff --git a/src/pipewire/pw_mem.rs b/src/pipewire/pw_mem.rs deleted file mode 100644 index 3d96b0c1..00000000 --- a/src/pipewire/pw_mem.rs +++ /dev/null @@ -1,155 +0,0 @@ -use { - crate::utils::{ - copyhashmap::CopyHashMap, - mmap::{Mmapped, mmap}, - oserror::OsError, - page_size::page_size, - ptr_ext::{MutPtrExt, PtrExt}, - }, - std::{marker::PhantomData, ops::Range, rc::Rc}, - thiserror::Error, - uapi::{OwnedFd, Pod, c}, -}; - -#[derive(Default)] -pub struct PwMemPool { - pub mems: CopyHashMap>, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum PwMemType { - MemFd, - DmaBuf, -} - -pub struct PwMem { - pub _ty: PwMemType, - pub read: bool, - pub write: bool, - pub fd: Rc, -} - -pub struct PwMemMap { - pub _mem: Rc, - pub range: Range, - pub map: Mmapped, -} - -pub struct PwMemTyped { - mem: Rc, - offset: usize, - _phantom: PhantomData, -} - -#[expect(dead_code)] -pub struct PwMemSlice { - mem: Rc, - range: Range, -} - -impl PwMemPool { - pub fn map(&self, memid: u32, offset: u32, size: u32) -> Result, PwMemError> { - match self.mems.get(&memid) { - Some(m) => m.map(offset, size), - _ => Err(PwMemError::MemidDoesNotExist(memid)), - } - } -} - -impl PwMem { - pub fn map(self: &Rc, offset: u32, size: u32) -> Result, PwMemError> { - let mask = page_size() - 1; - let offset = offset as usize; - let size = size as usize; - let start = offset & !mask; - let dist = offset - start; - let len = (size + dist + mask) & !mask; - let range = dist..dist + size; - let mut prot = 0; - if self.read { - prot |= c::PROT_READ; - } - if self.write { - prot |= c::PROT_WRITE; - } - let map = match mmap(len as _, prot, c::MAP_SHARED, self.fd.raw(), start as _) { - Ok(m) => m, - Err(e) => return Err(PwMemError::MmapFailed(e)), - }; - Ok(Rc::new(PwMemMap { - _mem: self.clone(), - range, - map, - })) - } -} - -impl PwMemMap { - #[expect(dead_code)] - pub unsafe fn read(&self) -> &T { - self.check::(0); - unsafe { (self.map.ptr.cast::().add(self.range.start) as *const T).deref() } - } - - #[expect(dead_code)] - pub unsafe fn write(&self) -> &mut T { - self.check::(0); - unsafe { (self.map.ptr.cast::().add(self.range.start) as *mut T).deref_mut() } - } - - #[expect(dead_code)] - pub unsafe fn bytes_mut(&self) -> &mut [u8] { - unsafe { - std::slice::from_raw_parts_mut( - self.map.ptr.cast::().add(self.range.start) as _, - self.range.len(), - ) - } - } - - fn check(&self, offset: usize) { - assert!(offset <= self.range.len()); - assert!(size_of::() <= self.range.len() - offset); - assert_eq!((align_of::() - 1) & (self.range.start + offset), 0); - } - - pub fn typed(self: &Rc) -> Rc> { - self.typed_at(0) - } - - pub fn typed_at(self: &Rc, offset: usize) -> Rc> { - self.check::(offset); - Rc::new(PwMemTyped { - mem: self.clone(), - offset: self.range.start + offset, - _phantom: Default::default(), - }) - } - - pub fn slice(self: &Rc, range: Range) -> Rc { - assert!(range.start <= self.range.len()); - assert!(range.len() <= self.range.len() - range.start); - Rc::new(PwMemSlice { - mem: self.clone(), - range: self.range.start + range.start..self.range.start + range.end, - }) - } -} - -impl PwMemTyped { - pub unsafe fn read(&self) -> &T { - unsafe { (self.mem.map.ptr.cast::().add(self.offset) as *const T).deref() } - } - - pub unsafe fn write(&self) -> &mut T { - unsafe { (self.mem.map.ptr.cast::().add(self.offset) as *mut T).deref_mut() } - } -} - -#[derive(Debug, Error)] -pub enum PwMemError { - #[error("mmap failed")] - MmapFailed(#[source] OsError), - #[error("memid {0} does not exist")] - MemidDoesNotExist(u32), -} diff --git a/src/pipewire/pw_object.rs b/src/pipewire/pw_object.rs deleted file mode 100644 index 9b487aeb..00000000 --- a/src/pipewire/pw_object.rs +++ /dev/null @@ -1,52 +0,0 @@ -use { - crate::{pipewire::pw_parser::PwParser, utils::numcell::NumCell}, - std::{cell::Cell, fmt::Debug, rc::Rc}, - thiserror::Error, -}; - -pub trait PwObjectBase { - fn data(&self) -> &PwObjectData; - fn interface(&self) -> &str; - fn handle_msg(self: Rc, opcode: u8, parser: PwParser<'_>) -> Result<(), PwObjectError>; - fn event_name(&self, opcode: u8) -> Option<&'static str>; -} - -pub trait PwObject: PwObjectBase { - fn bound_id(&self, id: u32) { - let _ = id; - } - - fn done(&self) {} - - fn break_loops(&self) {} -} - -pub struct PwObjectData { - pub id: u32, - pub bound_id: Cell>, - pub sync_id: NumCell, -} - -#[derive(Debug, Error)] -#[error("An error occurred in a `{interface}`")] -pub struct PwObjectError { - pub interface: &'static str, - #[source] - pub source: PwObjectErrorType, -} - -#[derive(Debug, Error)] -pub enum PwObjectErrorType { - #[error("Unknown event {0}")] - UnknownEvent(u8), - #[error("An error occurred in event `{method}`")] - EventError { - method: &'static str, - #[source] - source: Box, - }, -} - -pub trait PwOpcode: Debug { - fn id(&self) -> u8; -} diff --git a/src/pipewire/pw_parser.rs b/src/pipewire/pw_parser.rs deleted file mode 100644 index 08140be5..00000000 --- a/src/pipewire/pw_parser.rs +++ /dev/null @@ -1,312 +0,0 @@ -#![allow(non_upper_case_globals)] - -use { - crate::pipewire::pw_pod::{ - PW_CHOICE_None, PW_TYPE_Array, PW_TYPE_Bitmap, PW_TYPE_Bool, PW_TYPE_Bytes, PW_TYPE_Choice, - PW_TYPE_Double, PW_TYPE_Fd, PW_TYPE_Float, PW_TYPE_Fraction, PW_TYPE_Id, PW_TYPE_Int, - PW_TYPE_Long, PW_TYPE_None, PW_TYPE_Object, PW_TYPE_Pod, PW_TYPE_Pointer, - PW_TYPE_Rectangle, PW_TYPE_Sequence, PW_TYPE_String, PW_TYPE_Struct, PwChoiceType, - PwControlType, PwPod, PwPodArray, PwPodChoice, PwPodControl, PwPodFraction, PwPodObject, - PwPodObjectType, PwPodPointer, PwPodRectangle, PwPodSequence, PwPodStruct, PwPodType, - PwPointerType, PwProp, PwPropFlag, - }, - ahash::AHashMap, - bstr::{BStr, BString, ByteSlice}, - std::{fmt::Debug, mem::MaybeUninit, rc::Rc}, - thiserror::Error, - uapi::{OwnedFd, Pod}, -}; - -#[derive(Debug, Error)] -pub enum PwParserError { - #[error("Unexpected EOF")] - UnexpectedEof, - #[error("Message references an FD that is out of bounds")] - MissingFd, - #[error("Array element type has size of 0")] - ZeroSizedArrayElementType, - #[error("Unknown POD type: {0:?}")] - UnknownType(PwPodType), - #[error("Unexpected POD type: Expected {0:?}, got {1:?}")] - UnexpectedPodType(PwPodType, PwPodType), -} - -#[derive(Copy, Clone)] -pub struct PwParser<'a> { - data: &'a [u8], - fds: &'a [Rc], - pos: usize, -} - -impl<'a> PwParser<'a> { - pub fn new(data: &'a [u8], fds: &'a [Rc]) -> Self { - Self { data, fds, pos: 0 } - } - - pub fn reset(&mut self) { - self.pos = 0; - } - - fn read_raw(&mut self, offset: usize) -> Result { - if self.pos + offset + size_of::() <= self.data.len() { - unsafe { - let mut res = MaybeUninit::uninit(); - let src = self.data[self.pos + offset..].as_ptr(); - std::ptr::copy_nonoverlapping(src, res.as_mut_ptr() as _, size_of::()); - Ok(res.assume_init()) - } - } else { - Err(PwParserError::UnexpectedEof) - } - } - - pub fn len(&self) -> usize { - self.data.len() - self.pos - } - - pub fn pos(&self) -> usize { - self.pos - } - - fn read_array(&mut self, offset: usize, len: usize) -> Result, PwParserError> { - let child_len = self.read_raw::(offset)? as usize; - if child_len == 0 { - return Err(PwParserError::ZeroSizedArrayElementType); - } - let ty = PwPodType(self.read_raw(offset + 4)?); - Ok(PwPodArray { - ty, - child_len, - n_elements: (len - 8) / child_len, - elements: PwParser::new( - &self.data[self.pos + offset + 8..self.pos + offset + len], - self.fds, - ), - }) - } - - pub fn read_dict_struct(&mut self) -> Result, PwParserError> { - let s2 = self.read_struct()?; - let mut p3 = s2.fields; - let num_dict_entries = p3.read_int()?; - let mut de = AHashMap::new(); - for _ in 0..num_dict_entries { - de.insert(p3.read_string()?.to_owned(), p3.read_string()?.to_owned()); - } - Ok(de) - } - - pub fn read_struct(&mut self) -> Result, PwParserError> { - match self.read_pod()? { - PwPod::Struct(s) => Ok(s), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Struct, v.ty())), - } - } - - pub fn read_uint(&mut self) -> Result { - self.read_int().map(|v| v as u32) - } - - pub fn read_int(&mut self) -> Result { - match self.read_value()? { - PwPod::Int(s) => Ok(s), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Int, v.ty())), - } - } - - pub fn read_object_opt(&mut self) -> Result>, PwParserError> { - match self.read_pod()? { - PwPod::Object(p) => Ok(Some(p)), - PwPod::None => Ok(None), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Object, v.ty())), - } - } - - pub fn read_object(&mut self) -> Result, PwParserError> { - match self.read_object_opt()? { - Some(p) => Ok(p), - _ => Err(PwParserError::UnexpectedPodType( - PW_TYPE_Object, - PW_TYPE_None, - )), - } - } - - pub fn read_id(&mut self) -> Result { - match self.read_value()? { - PwPod::Id(s) => Ok(s), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, v.ty())), - } - } - - pub fn read_fd_opt(&mut self) -> Result>, PwParserError> { - match self.read_pod()? { - PwPod::Fd(idx) if idx == !0 => Ok(None), - PwPod::Fd(idx) => match self.fds.get(idx as usize) { - Some(fd) => Ok(Some(fd.clone())), - _ => Err(PwParserError::MissingFd), - }, - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, v.ty())), - } - } - - pub fn read_fd(&mut self) -> Result, PwParserError> { - match self.read_fd_opt()? { - Some(fd) => Ok(fd), - _ => Err(PwParserError::MissingFd), - } - } - - pub fn read_ulong(&mut self) -> Result { - self.read_long().map(|l| l as _) - } - - pub fn read_long(&mut self) -> Result { - match self.read_value()? { - PwPod::Long(s) => Ok(s), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Long, v.ty())), - } - } - - pub fn read_string(&mut self) -> Result<&'a BStr, PwParserError> { - match self.read_value()? { - PwPod::String(s) => Ok(s), - v => Err(PwParserError::UnexpectedPodType(PW_TYPE_String, v.ty())), - } - } - - pub fn read_value(&mut self) -> Result, PwParserError> { - let mut v = self.read_pod(); - if let Ok(PwPod::Choice(v)) = &mut v - && v.ty == PW_CHOICE_None - && v.elements.n_elements > 0 - { - return v - .elements - .elements - .read_pod_body_packed(v.elements.ty, v.elements.child_len); - } - v - } - - pub fn read_pod(&mut self) -> Result, PwParserError> { - let len = self.read_raw::(0)? as usize; - let ty = PwPodType(self.read_raw(4)?); - self.pos += 8; - self.read_pod_body(ty, len) - } - - pub fn read_pod_body_packed( - &mut self, - ty: PwPodType, - len: usize, - ) -> Result, PwParserError> { - self.read_pod_body2(ty, len, true) - } - - pub fn read_pod_body(&mut self, ty: PwPodType, len: usize) -> Result, PwParserError> { - self.read_pod_body2(ty, len, false) - } - - fn read_pod_body2( - &mut self, - ty: PwPodType, - len: usize, - packed: bool, - ) -> Result, PwParserError> { - let size = if packed { len } else { (len + 7) & !7 }; - if self.len() < size { - return Err(PwParserError::UnexpectedEof); - } - let val = match ty { - PW_TYPE_None => PwPod::None, - PW_TYPE_Bool => PwPod::Bool(self.read_raw::(0)? != 0), - PW_TYPE_Id => PwPod::Id(self.read_raw(0)?), - PW_TYPE_Int => PwPod::Int(self.read_raw(0)?), - PW_TYPE_Long => PwPod::Long(self.read_raw(0)?), - PW_TYPE_Float => PwPod::Float(self.read_raw(0)?), - PW_TYPE_Double => PwPod::Double(self.read_raw(0)?), - PW_TYPE_String => { - let s = if len == 0 { - &[][..] - } else { - &self.data[self.pos..self.pos + len - 1] - }; - PwPod::String(s.as_bstr()) - } - PW_TYPE_Bytes => PwPod::Bytes(&self.data[self.pos..self.pos + len]), - PW_TYPE_Rectangle => PwPod::Rectangle(PwPodRectangle { - width: self.read_raw(0)?, - height: self.read_raw(4)?, - }), - PW_TYPE_Fraction => PwPod::Fraction(PwPodFraction { - num: self.read_raw(0)?, - denom: self.read_raw(4)?, - }), - PW_TYPE_Bitmap => PwPod::Bitmap(&self.data[self.pos..self.pos + len]), - PW_TYPE_Array => PwPod::Array(self.read_array(0, len)?), - PW_TYPE_Struct => PwPod::Struct(PwPodStruct { - fields: PwParser::new(&self.data[self.pos..self.pos + len], self.fds), - }), - PW_TYPE_Object => PwPod::Object(PwPodObject { - ty: PwPodObjectType(self.read_raw(0)?), - id: self.read_raw(4)?, - probs: PwParser::new(&self.data[self.pos + 8..self.pos + len], self.fds), - }), - PW_TYPE_Sequence => PwPod::Sequence(PwPodSequence { - unit: self.read_raw(0)?, - controls: PwParser::new(&self.data[self.pos + 8..self.pos + len], self.fds), - }), - PW_TYPE_Pointer => PwPod::Pointer(PwPodPointer { - _ty: PwPointerType(self.read_raw(0)?), - _value: self.read_raw(8)?, - }), - PW_TYPE_Fd => PwPod::Fd(self.read_raw(0)?), - PW_TYPE_Choice => PwPod::Choice(PwPodChoice { - ty: PwChoiceType(self.read_raw(0)?), - flags: self.read_raw(4)?, - elements: self.read_array(8, len - 8)?, - }), - PW_TYPE_Pod => { - let pos = self.pos; - let pod = self.read_pod()?; - self.pos = pos; - pod - } - _ => return Err(PwParserError::UnknownType(ty)), - }; - self.pos += size; - Ok(val) - } - - pub fn read_prop(&mut self) -> Result, PwParserError> { - let key = self.read_raw(0)?; - let flags = PwPropFlag(self.read_raw(4)?); - self.pos += 8; - Ok(PwProp { - key, - flags, - pod: self.read_pod()?, - }) - } - - pub fn read_control(&mut self) -> Result, PwParserError> { - let offset = self.read_raw(0)?; - let ty = PwControlType(self.read_raw(4)?); - self.pos += 8; - Ok(PwPodControl { - _offset: offset, - _ty: ty, - _value: self.read_pod()?, - }) - } - - pub fn skip(&mut self) -> Result<(), PwParserError> { - let size = self.read_raw::(0)? as usize; - if self.len() < size + 8 { - return Err(PwParserError::UnexpectedEof); - } - self.pos += size + 8; - Ok(()) - } -} diff --git a/src/pipewire/pw_pod.rs b/src/pipewire/pw_pod.rs deleted file mode 100644 index ffb37194..00000000 --- a/src/pipewire/pw_pod.rs +++ /dev/null @@ -1,1451 +0,0 @@ -#![allow(non_upper_case_globals, non_camel_case_types)] - -mod pw_debug; - -use { - crate::pipewire::pw_parser::{PwParser, PwParserError}, - bstr::BStr, - std::{ - fmt::{Debug, Formatter}, - sync::atomic::{AtomicI32, AtomicU32}, - }, - uapi::{Pod, c}, -}; - -macro_rules! ty { - ($name:ident; $($id:ident = $val:expr,)*) => { - #[derive(Copy, Clone, Eq, PartialEq, Hash)] - #[repr(transparent)] - pub struct $name(pub u32); - - $( - pub const $id: $name = $name($val); - )* - - impl $name { - pub fn name(self) -> Option<&'static str> { - let res = match self { - $( - $id => stringify!($id), - )* - _ => return None, - }; - Some(res) - } - } - - impl Debug for $name { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self.name() { - Some(n) => write!(f, "{}", n), - _ => write!(f, "{}({})", stringify!($name), self.0), - } - } - } - } -} - -ty! { - PwPodType; - - PW_TYPE_None = 0x01, - PW_TYPE_Bool = 0x02, - PW_TYPE_Id = 0x03, - PW_TYPE_Int = 0x04, - PW_TYPE_Long = 0x05, - PW_TYPE_Float = 0x06, - PW_TYPE_Double = 0x07, - PW_TYPE_String = 0x08, - PW_TYPE_Bytes = 0x09, - PW_TYPE_Rectangle = 0x0A, - PW_TYPE_Fraction = 0x0B, - PW_TYPE_Bitmap = 0x0C, - PW_TYPE_Array = 0x0D, - PW_TYPE_Struct = 0x0E, - PW_TYPE_Object = 0x0F, - PW_TYPE_Sequence = 0x10, - PW_TYPE_Pointer = 0x11, - PW_TYPE_Fd = 0x12, - PW_TYPE_Choice = 0x13, - PW_TYPE_Pod = 0x14, -} - -ty! { - PwPodObjectType; - - PW_COMMAND_Device = 0x30001, - PW_COMMAND_Node = 0x30002, - - PW_OBJECT_PropInfo = 0x40001, - PW_OBJECT_Props = 0x40002, - PW_OBJECT_Format = 0x40003, - PW_OBJECT_ParamBuffers = 0x40004, - PW_OBJECT_ParamMeta = 0x40005, - PW_OBJECT_ParamIO = 0x40006, - PW_OBJECT_ParamProfile = 0x40007, - PW_OBJECT_ParamPortConfig = 0x40008, - PW_OBJECT_ParamRoute = 0x40009, - PW_OBJECT_Profiler = 0x4000A, - PW_OBJECT_ParamLatency = 0x4000B, - PW_OBJECT_ParamProcessLatency = 0x4000C, -} - -ty! { - SpaParamType; - - SPA_PARAM_Invalid = 0, - SPA_PARAM_PropInfo = 1, - SPA_PARAM_Props = 2, - SPA_PARAM_EnumFormat = 3, - SPA_PARAM_Format = 4, - SPA_PARAM_Buffers = 5, - SPA_PARAM_Meta = 6, - SPA_PARAM_IO = 7, - SPA_PARAM_EnumProfile = 8, - SPA_PARAM_Profile = 9, - SPA_PARAM_EnumPortConfig = 10, - SPA_PARAM_PortConfig = 11, - SPA_PARAM_EnumRoute = 12, - SPA_PARAM_Route = 13, - SPA_PARAM_Control = 14, - SPA_PARAM_Latency = 15, - SPA_PARAM_ProcessLatency = 16, -} - -ty! { - SpaFormat; - - SPA_FORMAT_START = 0x00000, - - SPA_FORMAT_mediaType = 0x00001, - SPA_FORMAT_mediaSubtype = 0x00002, - - SPA_FORMAT_START_Audio = 0x10000, - SPA_FORMAT_AUDIO_format = 0x10001, - SPA_FORMAT_AUDIO_flags = 0x10002, - SPA_FORMAT_AUDIO_rate = 0x10003, - SPA_FORMAT_AUDIO_channels = 0x10004, - SPA_FORMAT_AUDIO_position = 0x10005, - SPA_FORMAT_AUDIO_iec958Codec = 0x10006, - SPA_FORMAT_AUDIO_bitorder = 0x10007, - SPA_FORMAT_AUDIO_interleave = 0x10008, - - SPA_FORMAT_START_Video = 0x20000, - SPA_FORMAT_VIDEO_format = 0x20001, - SPA_FORMAT_VIDEO_modifier = 0x20002, - SPA_FORMAT_VIDEO_size = 0x20003, - SPA_FORMAT_VIDEO_framerate = 0x20004, - SPA_FORMAT_VIDEO_maxFramerate = 0x20005, - SPA_FORMAT_VIDEO_views = 0x20006, - SPA_FORMAT_VIDEO_interlaceMode = 0x20007, - SPA_FORMAT_VIDEO_pixelAspectRatio = 0x20008, - SPA_FORMAT_VIDEO_multiviewMode = 0x20009, - SPA_FORMAT_VIDEO_multiviewFlags = 0x2000A, - SPA_FORMAT_VIDEO_chromaSite = 0x2000B, - SPA_FORMAT_VIDEO_colorRange = 0x2000C, - SPA_FORMAT_VIDEO_colorMatrix = 0x2000D, - SPA_FORMAT_VIDEO_transferFunction = 0x2000E, - SPA_FORMAT_VIDEO_colorPrimaries = 0x2000F, - SPA_FORMAT_VIDEO_profile = 0x20010, - SPA_FORMAT_VIDEO_level = 0x20011, - SPA_FORMAT_VIDEO_H264_streamFormat = 0x20012, - SPA_FORMAT_VIDEO_H264_alignment = 0x20013, - - SPA_FORMAT_START_Image = 0x30000, - SPA_FORMAT_START_Binary = 0x40000, - SPA_FORMAT_START_Stream = 0x50000, - SPA_FORMAT_START_Application = 0x60000, -} - -bitflags! { - SPA_PARAM_INFO: u32; - - SPA_PARAM_INFO_SERIAL = 1<<0, - SPA_PARAM_INFO_READ = 1<<1, - SPA_PARAM_INFO_WRITE = 1<<2, -} - -ty! { - PwControlType; - - PW_CONTROL_PropInfo = 1, - PW_CONTROL_Props = 2, - PW_CONTROL_Format = 3, -} - -ty! { - PwPointerType; - - PW_POINTER_Buffer = 0x10001, - PW_POINTER_Meta = 0x10002, - PW_POINTER_Dict = 0x10003, -} - -ty! { - PwChoiceType; - - PW_CHOICE_None = 0, - PW_CHOICE_Range = 1, - PW_CHOICE_Step = 2, - PW_CHOICE_Enum = 3, - PW_CHOICE_Flags = 4, -} - -ty! { - PwIoType; - - PW_IO_Buffers = 1, - PW_IO_Range = 2, - PW_IO_Clock = 3, - PW_IO_Latency = 4, - PW_IO_Control = 5, - PW_IO_Notify = 6, - PW_IO_Position = 7, - PW_IO_RateMatch = 8, - PW_IO_Memory = 9, -} - -bitflags! { - PwPropFlag: u32; - - PW_PROP_READONLY = 1 << 0, - PW_PROP_HARDWARE = 1 << 1, - PW_PROP_HINT_DICT = 1 << 2, - PW_PROP_MANDATORY = 1 << 3, - PW_PROP_DONT_FIXATE = 1 << 4, -} - -ty! { - SpaMediaType; - - SPA_MEDIA_TYPE_unknown = 0, - SPA_MEDIA_TYPE_audio = 1, - SPA_MEDIA_TYPE_video = 2, - SPA_MEDIA_TYPE_image = 3, - SPA_MEDIA_TYPE_binary = 4, - SPA_MEDIA_TYPE_stream = 5, - SPA_MEDIA_TYPE_application = 6, -} - -ty! { - SpaMediaSubtype; - - SPA_MEDIA_SUBTYPE_unknown = 0x00000, - SPA_MEDIA_SUBTYPE_raw = 0x00001, - SPA_MEDIA_SUBTYPE_dsp = 0x00002, - SPA_MEDIA_SUBTYPE_iec958 = 0x00003, - SPA_MEDIA_SUBTYPE_dsd = 0x00004, - - SPA_MEDIA_SUBTYPE_START_Audio = 0x10000, - SPA_MEDIA_SUBTYPE_mp3 = 0x10001, - SPA_MEDIA_SUBTYPE_aac = 0x10002, - SPA_MEDIA_SUBTYPE_vorbis = 0x10003, - SPA_MEDIA_SUBTYPE_wma = 0x10004, - SPA_MEDIA_SUBTYPE_ra = 0x10005, - SPA_MEDIA_SUBTYPE_sbc = 0x10006, - SPA_MEDIA_SUBTYPE_adpcm = 0x10007, - SPA_MEDIA_SUBTYPE_g723 = 0x10008, - SPA_MEDIA_SUBTYPE_g726 = 0x10009, - SPA_MEDIA_SUBTYPE_g729 = 0x1000A, - SPA_MEDIA_SUBTYPE_amr = 0x1000B, - SPA_MEDIA_SUBTYPE_gsm = 0x1000C, - - SPA_MEDIA_SUBTYPE_START_Video = 0x20000, - SPA_MEDIA_SUBTYPE_h264 = 0x20001, - SPA_MEDIA_SUBTYPE_mjpg = 0x20002, - SPA_MEDIA_SUBTYPE_dv = 0x20003, - SPA_MEDIA_SUBTYPE_mpegts = 0x20004, - SPA_MEDIA_SUBTYPE_h263 = 0x20005, - SPA_MEDIA_SUBTYPE_mpeg1 = 0x20006, - SPA_MEDIA_SUBTYPE_mpeg2 = 0x20007, - SPA_MEDIA_SUBTYPE_mpeg4 = 0x20008, - SPA_MEDIA_SUBTYPE_xvid = 0x20009, - SPA_MEDIA_SUBTYPE_vc1 = 0x2000A, - SPA_MEDIA_SUBTYPE_vp8 = 0x2000B, - SPA_MEDIA_SUBTYPE_vp9 = 0x2000C, - SPA_MEDIA_SUBTYPE_bayer = 0x2000D, - - SPA_MEDIA_SUBTYPE_START_Image = 0x30000, - SPA_MEDIA_SUBTYPE_jpeg = 0x30001, - - SPA_MEDIA_SUBTYPE_START_Binary = 0x40000, - - SPA_MEDIA_SUBTYPE_START_Stream = 0x50000, - SPA_MEDIA_SUBTYPE_midi = 0x50001, - - SPA_MEDIA_SUBTYPE_START_Application = 0x60000, - SPA_MEDIA_SUBTYPE_control = 0x60001, -} - -ty! { - SpaAudioFormat; - - SPA_AUDIO_FORMAT_UNKNOWN = 0x000, - SPA_AUDIO_FORMAT_ENCODED = 0x001, - - SPA_AUDIO_FORMAT_START_Interleaved = 0x100, - SPA_AUDIO_FORMAT_S8 = 0x101, - SPA_AUDIO_FORMAT_U8 = 0x102, - SPA_AUDIO_FORMAT_S16_LE = 0x103, - SPA_AUDIO_FORMAT_S16_BE = 0x104, - SPA_AUDIO_FORMAT_U16_LE = 0x105, - SPA_AUDIO_FORMAT_U16_BE = 0x106, - SPA_AUDIO_FORMAT_S24_32_LE = 0x107, - SPA_AUDIO_FORMAT_S24_32_BE = 0x108, - SPA_AUDIO_FORMAT_U24_32_LE = 0x109, - SPA_AUDIO_FORMAT_U24_32_BE = 0x10A, - SPA_AUDIO_FORMAT_S32_LE = 0x10B, - SPA_AUDIO_FORMAT_S32_BE = 0x10C, - SPA_AUDIO_FORMAT_U32_LE = 0x10D, - SPA_AUDIO_FORMAT_U32_BE = 0x10E, - SPA_AUDIO_FORMAT_S24_LE = 0x10F, - SPA_AUDIO_FORMAT_S24_BE = 0x110, - SPA_AUDIO_FORMAT_U24_LE = 0x111, - SPA_AUDIO_FORMAT_U24_BE = 0x112, - SPA_AUDIO_FORMAT_S20_LE = 0x113, - SPA_AUDIO_FORMAT_S20_BE = 0x114, - SPA_AUDIO_FORMAT_U20_LE = 0x115, - SPA_AUDIO_FORMAT_U20_BE = 0x116, - SPA_AUDIO_FORMAT_S18_LE = 0x117, - SPA_AUDIO_FORMAT_S18_BE = 0x118, - SPA_AUDIO_FORMAT_U18_LE = 0x119, - SPA_AUDIO_FORMAT_U18_BE = 0x11A, - SPA_AUDIO_FORMAT_F32_LE = 0x11B, - SPA_AUDIO_FORMAT_F32_BE = 0x11C, - SPA_AUDIO_FORMAT_F64_LE = 0x11D, - SPA_AUDIO_FORMAT_F64_BE = 0x11E, - - SPA_AUDIO_FORMAT_ULAW = 0x11F, - SPA_AUDIO_FORMAT_ALAW = 0x120, - - SPA_AUDIO_FORMAT_START_Planar = 0x200, - SPA_AUDIO_FORMAT_U8P = 0x201, - SPA_AUDIO_FORMAT_S16P = 0x202, - SPA_AUDIO_FORMAT_S24_32P = 0x203, - SPA_AUDIO_FORMAT_S32P = 0x204, - SPA_AUDIO_FORMAT_S24P = 0x205, - SPA_AUDIO_FORMAT_F32P = 0x206, - SPA_AUDIO_FORMAT_F64P = 0x207, - SPA_AUDIO_FORMAT_S8P = 0x208, - - SPA_AUDIO_FORMAT_START_Other = 0x400, -} - -ty! { - SpaVideoFormat; - - SPA_VIDEO_FORMAT_UNKNOWN = 000, - SPA_VIDEO_FORMAT_ENCODED = 001, - SPA_VIDEO_FORMAT_I420 = 002, - SPA_VIDEO_FORMAT_YV12 = 003, - SPA_VIDEO_FORMAT_YUY2 = 004, - SPA_VIDEO_FORMAT_UYVY = 005, - SPA_VIDEO_FORMAT_AYUV = 006, - SPA_VIDEO_FORMAT_RGBx = 007, - SPA_VIDEO_FORMAT_BGRx = 008, - SPA_VIDEO_FORMAT_xRGB = 009, - SPA_VIDEO_FORMAT_xBGR = 010, - SPA_VIDEO_FORMAT_RGBA = 011, - SPA_VIDEO_FORMAT_BGRA = 012, - SPA_VIDEO_FORMAT_ARGB = 013, - SPA_VIDEO_FORMAT_ABGR = 014, - SPA_VIDEO_FORMAT_RGB = 015, - SPA_VIDEO_FORMAT_BGR = 016, - SPA_VIDEO_FORMAT_Y41B = 017, - SPA_VIDEO_FORMAT_Y42B = 018, - SPA_VIDEO_FORMAT_YVYU = 019, - SPA_VIDEO_FORMAT_Y444 = 020, - SPA_VIDEO_FORMAT_v210 = 021, - SPA_VIDEO_FORMAT_v216 = 022, - SPA_VIDEO_FORMAT_NV12 = 023, - SPA_VIDEO_FORMAT_NV21 = 024, - SPA_VIDEO_FORMAT_GRAY8 = 025, - SPA_VIDEO_FORMAT_GRAY16_BE = 026, - SPA_VIDEO_FORMAT_GRAY16_LE = 027, - SPA_VIDEO_FORMAT_v308 = 028, - SPA_VIDEO_FORMAT_RGB16 = 029, - SPA_VIDEO_FORMAT_BGR16 = 030, - SPA_VIDEO_FORMAT_RGB15 = 031, - SPA_VIDEO_FORMAT_BGR15 = 032, - SPA_VIDEO_FORMAT_UYVP = 033, - SPA_VIDEO_FORMAT_A420 = 034, - SPA_VIDEO_FORMAT_RGB8P = 035, - SPA_VIDEO_FORMAT_YUV9 = 036, - SPA_VIDEO_FORMAT_YVU9 = 037, - SPA_VIDEO_FORMAT_IYU1 = 038, - SPA_VIDEO_FORMAT_ARGB64 = 039, - SPA_VIDEO_FORMAT_AYUV64 = 040, - SPA_VIDEO_FORMAT_r210 = 041, - SPA_VIDEO_FORMAT_I420_10BE = 042, - SPA_VIDEO_FORMAT_I420_10LE = 043, - SPA_VIDEO_FORMAT_I422_10BE = 044, - SPA_VIDEO_FORMAT_I422_10LE = 045, - SPA_VIDEO_FORMAT_Y444_10BE = 046, - SPA_VIDEO_FORMAT_Y444_10LE = 047, - SPA_VIDEO_FORMAT_GBR = 048, - SPA_VIDEO_FORMAT_GBR_10BE = 049, - SPA_VIDEO_FORMAT_GBR_10LE = 050, - SPA_VIDEO_FORMAT_NV16 = 051, - SPA_VIDEO_FORMAT_NV24 = 052, - SPA_VIDEO_FORMAT_NV12_64Z32 = 053, - SPA_VIDEO_FORMAT_A420_10BE = 054, - SPA_VIDEO_FORMAT_A420_10LE = 055, - SPA_VIDEO_FORMAT_A422_10BE = 056, - SPA_VIDEO_FORMAT_A422_10LE = 057, - SPA_VIDEO_FORMAT_A444_10BE = 058, - SPA_VIDEO_FORMAT_A444_10LE = 059, - SPA_VIDEO_FORMAT_NV61 = 060, - SPA_VIDEO_FORMAT_P010_10BE = 061, - SPA_VIDEO_FORMAT_P010_10LE = 062, - SPA_VIDEO_FORMAT_IYU2 = 063, - SPA_VIDEO_FORMAT_VYUY = 064, - SPA_VIDEO_FORMAT_GBRA = 065, - SPA_VIDEO_FORMAT_GBRA_10BE = 066, - SPA_VIDEO_FORMAT_GBRA_10LE = 067, - SPA_VIDEO_FORMAT_GBR_12BE = 068, - SPA_VIDEO_FORMAT_GBR_12LE = 069, - SPA_VIDEO_FORMAT_GBRA_12BE = 070, - SPA_VIDEO_FORMAT_GBRA_12LE = 071, - SPA_VIDEO_FORMAT_I420_12BE = 072, - SPA_VIDEO_FORMAT_I420_12LE = 073, - SPA_VIDEO_FORMAT_I422_12BE = 074, - SPA_VIDEO_FORMAT_I422_12LE = 075, - SPA_VIDEO_FORMAT_Y444_12BE = 076, - SPA_VIDEO_FORMAT_Y444_12LE = 077, - SPA_VIDEO_FORMAT_RGBA_F16 = 078, - SPA_VIDEO_FORMAT_RGBA_F32 = 079, - SPA_VIDEO_FORMAT_xRGB_210LE = 080, - SPA_VIDEO_FORMAT_xBGR_210LE = 081, - SPA_VIDEO_FORMAT_RGBx_102LE = 082, - SPA_VIDEO_FORMAT_BGRx_102LE = 083, - SPA_VIDEO_FORMAT_ARGB_210LE = 084, - SPA_VIDEO_FORMAT_ABGR_210LE = 085, - SPA_VIDEO_FORMAT_RGBA_102LE = 086, - SPA_VIDEO_FORMAT_BGRA_102LE = 087, -} - -ty! { - SpaVideoInterlaceMode; - - SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE = 0, - SPA_VIDEO_INTERLACE_MODE_INTERLEAVED = 1, - SPA_VIDEO_INTERLACE_MODE_MIXED = 2, - SPA_VIDEO_INTERLACE_MODE_FIELDS = 3, -} - -ty! { - SpaVideoMultiviewMode; - - SPA_VIDEO_MULTIVIEW_MODE_NONE = !0, - SPA_VIDEO_MULTIVIEW_MODE_MONO = 0, - - SPA_VIDEO_MULTIVIEW_MODE_LEFT = 1, - SPA_VIDEO_MULTIVIEW_MODE_RIGHT = 2, - - SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE = 3, - SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE_QUINCUNX = 4, - SPA_VIDEO_MULTIVIEW_MODE_COLUMN_INTERLEAVED = 5, - SPA_VIDEO_MULTIVIEW_MODE_ROW_INTERLEAVED = 6, - SPA_VIDEO_MULTIVIEW_MODE_TOP_BOTTOM = 7, - SPA_VIDEO_MULTIVIEW_MODE_CHECKERBOARD = 8, - - SPA_VIDEO_MULTIVIEW_MODE_FRAME_BY_FRAME = 32, - SPA_VIDEO_MULTIVIEW_MODE_MULTIVIEW_FRAME_BY_FRAME = 33, - SPA_VIDEO_MULTIVIEW_MODE_SEPARATED = 34, -} - -bitflags! { - SpaVideoMultiviewFlags: u32; - - SPA_VIDEO_MULTIVIEW_FLAGS_NONE = 0, - SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_VIEW_FIRST = 1 << 0, - SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLIPPED = 1 << 1, - SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLOPPED = 1 << 2, - SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLIPPED = 1 << 3, - SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLOPPED = 1 << 4, - SPA_VIDEO_MULTIVIEW_FLAGS_HALF_ASPECT = 1 << 14, - SPA_VIDEO_MULTIVIEW_FLAGS_MIXED_MONO = 1 << 15, -} - -bitflags! { - SpaVideoChromaSite: u32; - - SPA_VIDEO_CHROMA_SITE_UNKNOWN = 0, - SPA_VIDEO_CHROMA_SITE_NONE = 1 << 0, - SPA_VIDEO_CHROMA_SITE_H_COSITED = 1 << 1, - SPA_VIDEO_CHROMA_SITE_V_COSITED = 1 << 2, - SPA_VIDEO_CHROMA_SITE_ALT_LINE = 1 << 3, -} - -ty! { - SpaVideoColorRange; - - SPA_VIDEO_COLOR_RANGE_UNKNOWN = 0, - SPA_VIDEO_COLOR_RANGE_0_255 = 1, - SPA_VIDEO_COLOR_RANGE_16_235 = 2, -} - -ty! { - SpaVideoColorMatrix; - - SPA_VIDEO_COLOR_MATRIX_UNKNOWN = 0, - SPA_VIDEO_COLOR_MATRIX_RGB = 1, - SPA_VIDEO_COLOR_MATRIX_FCC = 2, - SPA_VIDEO_COLOR_MATRIX_BT709 = 3, - SPA_VIDEO_COLOR_MATRIX_BT601 = 4, - SPA_VIDEO_COLOR_MATRIX_SMPTE240M = 5, - SPA_VIDEO_COLOR_MATRIX_BT2020 = 6, -} - -ty! { - SpaVideoTransferFunction; - - SPA_VIDEO_TRANSFER_UNKNOWN = 0, - SPA_VIDEO_TRANSFER_GAMMA10 = 1, - SPA_VIDEO_TRANSFER_GAMMA18 = 2, - SPA_VIDEO_TRANSFER_GAMMA20 = 3, - SPA_VIDEO_TRANSFER_GAMMA22 = 4, - SPA_VIDEO_TRANSFER_BT709 = 5, - SPA_VIDEO_TRANSFER_SMPTE240M = 6, - SPA_VIDEO_TRANSFER_SRGB = 7, - SPA_VIDEO_TRANSFER_GAMMA28 = 8, - SPA_VIDEO_TRANSFER_LOG100 = 9, - SPA_VIDEO_TRANSFER_LOG316 = 10, - SPA_VIDEO_TRANSFER_BT2020_12 = 11, - SPA_VIDEO_TRANSFER_ADOBERGB = 12, -} - -ty! { - SpaVideoColorPrimaries; - - SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN = 0, - SPA_VIDEO_COLOR_PRIMARIES_BT709 = 1, - SPA_VIDEO_COLOR_PRIMARIES_BT470M = 2, - SPA_VIDEO_COLOR_PRIMARIES_BT470BG = 3, - SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M = 4, - SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M = 5, - SPA_VIDEO_COLOR_PRIMARIES_FILM = 6, - SPA_VIDEO_COLOR_PRIMARIES_BT2020 = 7, - SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB = 8, -} - -ty! { - SpaH264StreamFormat; - - SPA_H264_STREAM_FORMAT_UNKNOWN = 0, - SPA_H264_STREAM_FORMAT_AVC = 1, - SPA_H264_STREAM_FORMAT_AVC3 = 2, - SPA_H264_STREAM_FORMAT_BYTESTREAM = 3, -} - -ty! { - SpaH264Alignment; - - SPA_H264_ALIGNMENT_UNKNOWN = 0, - SPA_H264_ALIGNMENT_AU = 1, - SPA_H264_ALIGNMENT_NAL = 2, -} - -ty! { - SpaParamBuffers; - - SPA_PARAM_BUFFERS_START = 0, - SPA_PARAM_BUFFERS_buffers = 1, - SPA_PARAM_BUFFERS_blocks = 2, - SPA_PARAM_BUFFERS_size = 3, - SPA_PARAM_BUFFERS_stride = 4, - SPA_PARAM_BUFFERS_align = 5, - SPA_PARAM_BUFFERS_dataType = 6, -} - -ty! { - SpaDataType; - - SPA_DATA_Invalid = 0, - SPA_DATA_MemPtr = 1, - SPA_DATA_MemFd = 2, - SPA_DATA_DmaBuf = 3, - SPA_DATA_MemId = 4, -} - -impl Default for SpaDataType { - fn default() -> Self { - SPA_DATA_Invalid - } -} - -bitflags! { - SpaNodeBuffersFlags: u32; - - SPA_NODE_BUFFERS_FLAG_ALLOC = 1 << 0, -} - -bitflags! { - SpaDataFlags: u32; - - SPA_DATA_FLAG_READABLE = 1 << 0, - SPA_DATA_FLAG_WRITABLE = 1 << 1, - SPA_DATA_FLAG_DYNAMIC = 1 << 2, -} - -bitflags! { - SpaDataTypes: u32; - - SPA_DATA_MASK_Invalid = 1, - SPA_DATA_MASK_MemPtr = 2, - SPA_DATA_MASK_MemFd = 4, - SPA_DATA_MASK_DmaBuf = 8, - SPA_DATA_MASK_MemId = 16, -} - -ty! { - SpaParamMeta; - - SPA_PARAM_META_START = 0, - SPA_PARAM_META_type = 1, - SPA_PARAM_META_size = 2, -} - -ty! { - SpaParamIo; - - SPA_PARAM_IO_START = 0, - SPA_PARAM_IO_id = 1, - SPA_PARAM_IO_size = 2, -} - -ty! { - SpaIoType; - - SPA_IO_Invalid = 0, - SPA_IO_Buffers = 1, - SPA_IO_Range = 2, - SPA_IO_Clock = 3, - SPA_IO_Latency = 4, - SPA_IO_Control = 5, - SPA_IO_Notify = 6, - SPA_IO_Position = 7, - SPA_IO_RateMatch = 8, - SPA_IO_Memory = 9, -} - -ty! { - SpaParamProfile; - - SPA_PARAM_PROFILE_START = 0, - SPA_PARAM_PROFILE_index = 1, - SPA_PARAM_PROFILE_name = 2, - SPA_PARAM_PROFILE_description = 3, - SPA_PARAM_PROFILE_priority = 4, - SPA_PARAM_PROFILE_available = 5, - SPA_PARAM_PROFILE_info = 6, - SPA_PARAM_PROFILE_classes = 7, - SPA_PARAM_PROFILE_save = 8, -} - -ty! { - SpaParamAvailability; - - SPA_PARAM_AVAILABILITY_unknown = 0, - SPA_PARAM_AVAILABILITY_no = 1, - SPA_PARAM_AVAILABILITY_yes = 2, -} - -ty! { - SpaParamPortConfig; - - SPA_PARAM_PORT_CONFIG_START = 0, - SPA_PARAM_PORT_CONFIG_direction = 1, - SPA_PARAM_PORT_CONFIG_mode = 2, - SPA_PARAM_PORT_CONFIG_monitor = 3, - SPA_PARAM_PORT_CONFIG_control = 4, - SPA_PARAM_PORT_CONFIG_format = 5, -} - -ty! { - SpaDirection; - - SPA_DIRECTION_INPUT = 0, - SPA_DIRECTION_OUTPUT = 1, -} - -ty! { - SpaParamRoute; - - SPA_PARAM_ROUTE_START = 0, - SPA_PARAM_ROUTE_index = 1, - SPA_PARAM_ROUTE_direction = 2, - SPA_PARAM_ROUTE_device = 3, - SPA_PARAM_ROUTE_name = 4, - SPA_PARAM_ROUTE_description = 5, - SPA_PARAM_ROUTE_priority = 6, - SPA_PARAM_ROUTE_available = 7, - SPA_PARAM_ROUTE_info = 8, - SPA_PARAM_ROUTE_profiles = 9, - SPA_PARAM_ROUTE_props = 10, - SPA_PARAM_ROUTE_devices = 11, - SPA_PARAM_ROUTE_profile = 12, - SPA_PARAM_ROUTE_save = 13, -} - -ty! { - SpaProfiler; - - SPA_PROFILER_START = 0x0000000, - - SPA_PROFILER_START_Driver = 0x0010000, - SPA_PROFILER_info = 0x0010001, - SPA_PROFILER_clock = 0x0010002, - SPA_PROFILER_driverBlock = 0x0010003, - SPA_PROFILER_START_Follower = 0x0020000, - SPA_PROFILER_followerBlock = 0x0020001, - SPA_PROFILER_START_CUSTOM = 0x1000000, -} - -ty! { - SpaParamLatency; - - SPA_PARAM_LATENCY_START = 0, - SPA_PARAM_LATENCY_direction = 1, - SPA_PARAM_LATENCY_minQuantum = 2, - SPA_PARAM_LATENCY_maxQuantum = 3, - SPA_PARAM_LATENCY_minRate = 4, - SPA_PARAM_LATENCY_maxRate = 5, - SPA_PARAM_LATENCY_minNs = 6, - SPA_PARAM_LATENCY_maxNs = 7, -} - -ty! { - SpaParamProcessLatency; - - SPA_PARAM_PROCESS_LATENCY_START = 0, - SPA_PARAM_PROCESS_LATENCY_quantum = 1, - SPA_PARAM_PROCESS_LATENCY_rate = 2, - SPA_PARAM_PROCESS_LATENCY_ns = 3, -} - -ty! { - SpaParamPortConfigMode; - - SPA_PARAM_PORT_CONFIG_MODE_none = 0, - SPA_PARAM_PORT_CONFIG_MODE_passthrough = 1, - SPA_PARAM_PORT_CONFIG_MODE_convert = 2, - SPA_PARAM_PORT_CONFIG_MODE_dsp = 3, -} - -bitflags! { - SpaMetaHeaderFlags: u32; - - SPA_META_HEADER_FLAG_DISCONT = 1 << 0, - SPA_META_HEADER_FLAG_CORRUPTED = 1 << 1, - SPA_META_HEADER_FLAG_MARKER = 1 << 2, - SPA_META_HEADER_FLAG_HEADER = 1 << 3, - SPA_META_HEADER_FLAG_GAP = 1 << 4, - SPA_META_HEADER_FLAG_DELTA_UNIT = 1 << 5, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_meta_header { - pub flags: SpaMetaHeaderFlags, - pub offset: u32, - pub pts: i64, - pub dts_offset: i64, - pub seq: u64, -} - -unsafe impl Pod for spa_meta_header {} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_point { - pub x: i32, - pub y: i32, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_region { - pub position: spa_point, - pub size: spa_rectangle, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_meta_region { - pub region: spa_region, -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_meta_bitmap { - pub format: SpaVideoFormat, - pub size: spa_rectangle, - pub stride: i32, - pub offset: u32, -} - -unsafe impl Pod for spa_meta_bitmap {} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_meta_cursor { - pub id: u32, - pub flags: u32, - pub position: spa_point, - pub hotspot: spa_point, - pub bitmap_offset: u32, -} - -unsafe impl Pod for spa_meta_cursor {} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_meta_busy { - pub flags: u32, - pub count: u32, -} - -unsafe impl Pod for spa_meta_busy {} - -unsafe impl Pod for spa_meta_region {} - -ty! { - SpaMetaType; - - SPA_META_Invalid = 0, - SPA_META_Header = 1, - SPA_META_VideoCrop = 2, - SPA_META_VideoDamage = 3, - SPA_META_Bitmap = 4, - SPA_META_Cursor = 5, - SPA_META_Control = 6, - SPA_META_Busy = 7, -} - -ty! { - SpaPropInfo; - - SPA_PROP_INFO_START = 0, - SPA_PROP_INFO_id = 1, - SPA_PROP_INFO_name = 2, - SPA_PROP_INFO_type = 3, - SPA_PROP_INFO_labels = 4, - SPA_PROP_INFO_container = 5, - SPA_PROP_INFO_params = 6, - SPA_PROP_INFO_description = 7, -} - -ty! { - SpaProp; - - SPA_PROP_START = 0x0000000, - SPA_PROP_unknown = 0x0000001, - SPA_PROP_START_Device = 0x0000100, - SPA_PROP_device = 0x0000101, - SPA_PROP_deviceName = 0x0000102, - SPA_PROP_deviceFd = 0x0000103, - SPA_PROP_card = 0x0000104, - SPA_PROP_cardName = 0x0000105, - SPA_PROP_minLatency = 0x0000106, - SPA_PROP_maxLatency = 0x0000107, - SPA_PROP_periods = 0x0000108, - SPA_PROP_periodSize = 0x0000109, - SPA_PROP_periodEvent = 0x000010A, - SPA_PROP_live = 0x000010B, - SPA_PROP_rate = 0x000010C, - SPA_PROP_quality = 0x000010D, - SPA_PROP_bluetoothAudioCodec = 0x000010E, - SPA_PROP_START_Audio = 0x0010000, - SPA_PROP_waveType = 0x0010001, - SPA_PROP_frequency = 0x0010002, - SPA_PROP_volume = 0x0010003, - SPA_PROP_mute = 0x0010004, - SPA_PROP_patternType = 0x0010005, - SPA_PROP_ditherType = 0x0010006, - SPA_PROP_truncate = 0x0010007, - SPA_PROP_channelVolumes = 0x0010008, - SPA_PROP_volumeBase = 0x0010009, - SPA_PROP_volumeStep = 0x001000A, - SPA_PROP_channelMap = 0x001000B, - SPA_PROP_monitorMute = 0x001000C, - SPA_PROP_monitorVolumes = 0x001000D, - SPA_PROP_latencyOffsetNsec = 0x001000E, - SPA_PROP_softMute = 0x001000F, - SPA_PROP_softVolumes = 0x0010010, - SPA_PROP_iec958Codecs = 0x0010011, - SPA_PROP_START_Video = 0x0020000, - SPA_PROP_brightness = 0x0020001, - SPA_PROP_contrast = 0x0020002, - SPA_PROP_saturation = 0x0020003, - SPA_PROP_hue = 0x0020004, - SPA_PROP_gamma = 0x0020005, - SPA_PROP_exposure = 0x0020006, - SPA_PROP_gain = 0x0020007, - SPA_PROP_sharpness = 0x0020008, - SPA_PROP_START_Other = 0x0080000, - SPA_PROP_params = 0x0080001, - SPA_PROP_START_CUSTOM = 0x1000000, -} - -ty! { - SpaAudioChannel; - - SPA_AUDIO_CHANNEL_UNKNOWN = 0x00000, - SPA_AUDIO_CHANNEL_NA = 0x00001, - - SPA_AUDIO_CHANNEL_MONO = 0x00002, - - SPA_AUDIO_CHANNEL_FL = 0x00003, - SPA_AUDIO_CHANNEL_FR = 0x00004, - SPA_AUDIO_CHANNEL_FC = 0x00005, - SPA_AUDIO_CHANNEL_LFE = 0x00006, - SPA_AUDIO_CHANNEL_SL = 0x00007, - SPA_AUDIO_CHANNEL_SR = 0x00008, - SPA_AUDIO_CHANNEL_FLC = 0x00009, - SPA_AUDIO_CHANNEL_FRC = 0x0000A, - SPA_AUDIO_CHANNEL_RC = 0x0000B, - SPA_AUDIO_CHANNEL_RL = 0x0000C, - SPA_AUDIO_CHANNEL_RR = 0x0000D, - SPA_AUDIO_CHANNEL_TC = 0x0000E, - SPA_AUDIO_CHANNEL_TFL = 0x0000F, - SPA_AUDIO_CHANNEL_TFC = 0x00010, - SPA_AUDIO_CHANNEL_TFR = 0x00011, - SPA_AUDIO_CHANNEL_TRL = 0x00012, - SPA_AUDIO_CHANNEL_TRC = 0x00013, - SPA_AUDIO_CHANNEL_TRR = 0x00014, - SPA_AUDIO_CHANNEL_RLC = 0x00015, - SPA_AUDIO_CHANNEL_RRC = 0x00016, - SPA_AUDIO_CHANNEL_FLW = 0x00017, - SPA_AUDIO_CHANNEL_FRW = 0x00018, - SPA_AUDIO_CHANNEL_LFE2 = 0x00019, - SPA_AUDIO_CHANNEL_FLH = 0x0001A, - SPA_AUDIO_CHANNEL_FCH = 0x0001B, - SPA_AUDIO_CHANNEL_FRH = 0x0001C, - SPA_AUDIO_CHANNEL_TFLC = 0x0001D, - SPA_AUDIO_CHANNEL_TFRC = 0x0001E, - SPA_AUDIO_CHANNEL_TSL = 0x0001F, - SPA_AUDIO_CHANNEL_TSR = 0x00020, - SPA_AUDIO_CHANNEL_LLFE = 0x00021, - SPA_AUDIO_CHANNEL_RLFE = 0x00022, - SPA_AUDIO_CHANNEL_BC = 0x00023, - SPA_AUDIO_CHANNEL_BLC = 0x00024, - SPA_AUDIO_CHANNEL_BRC = 0x00025, - - SPA_AUDIO_CHANNEL_AUX0 = 0x01000, - SPA_AUDIO_CHANNEL_AUX1 = 0x01001, - SPA_AUDIO_CHANNEL_AUX2 = 0x01002, - SPA_AUDIO_CHANNEL_AUX3 = 0x01003, - SPA_AUDIO_CHANNEL_AUX4 = 0x01004, - SPA_AUDIO_CHANNEL_AUX5 = 0x01005, - SPA_AUDIO_CHANNEL_AUX6 = 0x01006, - SPA_AUDIO_CHANNEL_AUX7 = 0x01007, - SPA_AUDIO_CHANNEL_AUX8 = 0x01008, - SPA_AUDIO_CHANNEL_AUX9 = 0x01009, - SPA_AUDIO_CHANNEL_AUX10 = 0x0100A, - SPA_AUDIO_CHANNEL_AUX11 = 0x0100B, - SPA_AUDIO_CHANNEL_AUX12 = 0x0100C, - SPA_AUDIO_CHANNEL_AUX13 = 0x0100D, - SPA_AUDIO_CHANNEL_AUX14 = 0x0100E, - SPA_AUDIO_CHANNEL_AUX15 = 0x0100F, - SPA_AUDIO_CHANNEL_AUX16 = 0x01010, - SPA_AUDIO_CHANNEL_AUX17 = 0x01011, - SPA_AUDIO_CHANNEL_AUX18 = 0x01012, - SPA_AUDIO_CHANNEL_AUX19 = 0x01013, - SPA_AUDIO_CHANNEL_AUX20 = 0x01014, - SPA_AUDIO_CHANNEL_AUX21 = 0x01015, - SPA_AUDIO_CHANNEL_AUX22 = 0x01016, - SPA_AUDIO_CHANNEL_AUX23 = 0x01017, - SPA_AUDIO_CHANNEL_AUX24 = 0x01018, - SPA_AUDIO_CHANNEL_AUX25 = 0x01019, - SPA_AUDIO_CHANNEL_AUX26 = 0x0101A, - SPA_AUDIO_CHANNEL_AUX27 = 0x0101B, - SPA_AUDIO_CHANNEL_AUX28 = 0x0101C, - SPA_AUDIO_CHANNEL_AUX29 = 0x0101D, - SPA_AUDIO_CHANNEL_AUX30 = 0x0101E, - SPA_AUDIO_CHANNEL_AUX31 = 0x0101F, - SPA_AUDIO_CHANNEL_AUX32 = 0x01020, - SPA_AUDIO_CHANNEL_AUX33 = 0x01021, - SPA_AUDIO_CHANNEL_AUX34 = 0x01022, - SPA_AUDIO_CHANNEL_AUX35 = 0x01023, - SPA_AUDIO_CHANNEL_AUX36 = 0x01024, - SPA_AUDIO_CHANNEL_AUX37 = 0x01025, - SPA_AUDIO_CHANNEL_AUX38 = 0x01026, - SPA_AUDIO_CHANNEL_AUX39 = 0x01027, - SPA_AUDIO_CHANNEL_AUX40 = 0x01028, - SPA_AUDIO_CHANNEL_AUX41 = 0x01029, - SPA_AUDIO_CHANNEL_AUX42 = 0x0102A, - SPA_AUDIO_CHANNEL_AUX43 = 0x0102B, - SPA_AUDIO_CHANNEL_AUX44 = 0x0102C, - SPA_AUDIO_CHANNEL_AUX45 = 0x0102D, - SPA_AUDIO_CHANNEL_AUX46 = 0x0102E, - SPA_AUDIO_CHANNEL_AUX47 = 0x0102F, - SPA_AUDIO_CHANNEL_AUX48 = 0x01030, - SPA_AUDIO_CHANNEL_AUX49 = 0x01031, - SPA_AUDIO_CHANNEL_AUX50 = 0x01032, - SPA_AUDIO_CHANNEL_AUX51 = 0x01033, - SPA_AUDIO_CHANNEL_AUX52 = 0x01034, - SPA_AUDIO_CHANNEL_AUX53 = 0x01035, - SPA_AUDIO_CHANNEL_AUX54 = 0x01036, - SPA_AUDIO_CHANNEL_AUX55 = 0x01037, - SPA_AUDIO_CHANNEL_AUX56 = 0x01038, - SPA_AUDIO_CHANNEL_AUX57 = 0x01039, - SPA_AUDIO_CHANNEL_AUX58 = 0x0103A, - SPA_AUDIO_CHANNEL_AUX59 = 0x0103B, - SPA_AUDIO_CHANNEL_AUX60 = 0x0103C, - SPA_AUDIO_CHANNEL_AUX61 = 0x0103D, - SPA_AUDIO_CHANNEL_AUX62 = 0x0103E, - SPA_AUDIO_CHANNEL_AUX63 = 0x0103F, - - SPA_AUDIO_CHANNEL_LAST_Aux = 0x01fff, - - SPA_AUDIO_CHANNEL_START_Custom = 0x10000, -} - -ty! { - SpaAudioIec958Codec; - - SPA_AUDIO_IEC958_CODEC_UNKNOWN = 0, - - SPA_AUDIO_IEC958_CODEC_PCM = 1, - SPA_AUDIO_IEC958_CODEC_DTS = 2, - SPA_AUDIO_IEC958_CODEC_AC3 = 3, - SPA_AUDIO_IEC958_CODEC_MPEG = 4, - SPA_AUDIO_IEC958_CODEC_MPEG2_AAC = 5, - - SPA_AUDIO_IEC958_CODEC_EAC3 = 6, - - SPA_AUDIO_IEC958_CODEC_TRUEHD = 7, - SPA_AUDIO_IEC958_CODEC_DTSHD = 8, -} - -ty! { - SpaParamBitorder; - - SPA_PARAM_BITORDER_unknown = 0, - SPA_PARAM_BITORDER_msb = 1, - SPA_PARAM_BITORDER_lsb = 2, -} -ty! { - SpaNodeCommand; - - SPA_NODE_COMMAND_Suspend = 0, - SPA_NODE_COMMAND_Pause = 1, - SPA_NODE_COMMAND_Start = 2, - SPA_NODE_COMMAND_Enable = 3, - SPA_NODE_COMMAND_Disable = 4, - SPA_NODE_COMMAND_Flush = 5, - SPA_NODE_COMMAND_Drain = 6, - SPA_NODE_COMMAND_Marker = 7, - SPA_NODE_COMMAND_ParamBegin = 8, - SPA_NODE_COMMAND_ParamEnd = 9, - SPA_NODE_COMMAND_RequestProcess = 10, -} - -#[derive(Copy, Clone)] -pub enum PwPod<'a> { - None, - Bool(bool), - Id(u32), - Int(i32), - Long(i64), - Float(f32), - Double(f64), - String(&'a BStr), - Bytes(&'a [u8]), - Rectangle(PwPodRectangle), - Fraction(PwPodFraction), - Bitmap(&'a [u8]), - Array(PwPodArray<'a>), - Struct(PwPodStruct<'a>), - Object(PwPodObject<'a>), - Sequence(PwPodSequence<'a>), - Pointer(PwPodPointer), - Fd(u64), - Choice(PwPodChoice<'a>), -} - -#[derive(Copy, Clone, Debug)] -pub struct PwPodRectangle { - pub width: u32, - pub height: u32, -} - -#[derive(Copy, Clone, Debug)] -pub struct PwPodFraction { - pub num: u32, - pub denom: u32, -} - -#[derive(Copy, Clone)] -pub struct PwPodArray<'a> { - pub ty: PwPodType, - pub child_len: usize, - pub n_elements: usize, - pub elements: PwParser<'a>, -} - -#[derive(Copy, Clone)] -pub struct PwPodStruct<'a> { - pub fields: PwParser<'a>, -} - -#[derive(Copy, Clone)] -pub struct PwPodObject<'a> { - pub ty: PwPodObjectType, - pub id: u32, - pub probs: PwParser<'a>, -} - -impl<'a> PwPodObject<'a> { - pub fn get_param(&mut self, key: u32) -> Result>, PwParserError> { - let start = self.probs.pos(); - loop { - if self.probs.len() == 0 { - self.probs.reset(); - } else { - let prob = self.probs.read_prop()?; - if prob.key == key { - return Ok(Some(prob)); - } - } - if self.probs.pos() == start { - return Ok(None); - } - } - } -} - -#[derive(Copy, Clone)] -pub struct PwPodSequence<'a> { - pub unit: u32, - pub controls: PwParser<'a>, -} - -#[derive(Copy, Clone, Debug)] -pub struct PwPodControl<'a> { - pub _offset: u32, - pub _ty: PwControlType, - pub _value: PwPod<'a>, -} - -#[derive(Copy, Clone, Debug)] -pub struct PwPodPointer { - pub _ty: PwPointerType, - pub _value: usize, -} - -#[derive(Copy, Clone, Debug)] -pub struct PwPodChoice<'a> { - pub ty: PwChoiceType, - pub flags: u32, - pub elements: PwPodArray<'a>, -} - -#[derive(Copy, Clone, Debug)] -pub struct PwProp<'a> { - pub key: u32, - pub flags: PwPropFlag, - pub pod: PwPod<'a>, -} - -impl<'a> PwPod<'a> { - pub fn ty(&self) -> PwPodType { - match self { - PwPod::None => PW_TYPE_None, - PwPod::Bool(_) => PW_TYPE_Bool, - PwPod::Id(_) => PW_TYPE_Id, - PwPod::Int(_) => PW_TYPE_Int, - PwPod::Long(_) => PW_TYPE_Long, - PwPod::Float(_) => PW_TYPE_Float, - PwPod::Double(_) => PW_TYPE_Double, - PwPod::String(_) => PW_TYPE_String, - PwPod::Bytes(_) => PW_TYPE_Bytes, - PwPod::Rectangle(_) => PW_TYPE_Rectangle, - PwPod::Fraction(_) => PW_TYPE_Fraction, - PwPod::Bitmap(_) => PW_TYPE_Bitmap, - PwPod::Array(_) => PW_TYPE_Array, - PwPod::Struct(_) => PW_TYPE_Struct, - PwPod::Object(_) => PW_TYPE_Object, - PwPod::Sequence(_) => PW_TYPE_Sequence, - PwPod::Pointer(_) => PW_TYPE_Pointer, - PwPod::Fd(_) => PW_TYPE_Fd, - PwPod::Choice(_) => PW_TYPE_Choice, - } - } - - pub fn get_fraction(&self) -> Result { - match self.get_value()? { - PwPod::Fraction(i) => Ok(i), - _ => Err(PwParserError::UnexpectedPodType( - PW_TYPE_Fraction, - self.ty(), - )), - } - } - - pub fn get_rectangle(&self) -> Result { - match self.get_value()? { - PwPod::Rectangle(i) => Ok(i), - _ => Err(PwParserError::UnexpectedPodType( - PW_TYPE_Rectangle, - self.ty(), - )), - } - } - - pub fn get_id(&self) -> Result { - match self.get_value()? { - PwPod::Id(i) => Ok(i), - _ => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, self.ty())), - } - } - - pub fn get_value(mut self) -> Result, PwParserError> { - if let PwPod::Choice(v) = &mut self - && v.ty == PW_CHOICE_None - && v.elements.n_elements > 0 - { - return v - .elements - .elements - .read_pod_body_packed(v.elements.ty, v.elements.child_len); - } - Ok(self) - } -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_fraction { - pub num: u32, - pub denom: u32, -} - -bitflags! { - SPA_IO_SEGMENT_VIDEO_FLAG: u32; - - SPA_IO_SEGMENT_VIDEO_FLAG_VALID = 1<<0, - SPA_IO_SEGMENT_VIDEO_FLAG_DROP_FRAME = 1<<1, - SPA_IO_SEGMENT_VIDEO_FLAG_PULL_DOWN = 1<<2, - SPA_IO_SEGMENT_VIDEO_FLAG_INTERLACED = 1<<3, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_segment_video { - pub flags: SPA_IO_SEGMENT_VIDEO_FLAG, - pub offset: u32, - pub framerate: spa_fraction, - pub hours: u32, - pub minutes: u32, - pub seconds: u32, - pub frames: u32, - pub field_count: u32, - pub padding: [u32; 11], -} - -bitflags! { - SPA_IO_SEGMENT_BAR_FLAG: u32; - - SPA_IO_SEGMENT_BAR_FLAG_VALID = 1<<0, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_segment_bar { - pub flags: SPA_IO_SEGMENT_BAR_FLAG, - pub offset: u32, - pub signature_num: f32, - pub signature_denom: f32, - pub bpm: f64, - pub beat: f64, - pub padding: [u32; 8], -} - -bitflags! { - SPA_IO_SEGMENT_FLAG: u32; - - SPA_IO_SEGMENT_FLAG_LOOPING = 1<<0, - SPA_IO_SEGMENT_FLAG_NO_POSITION = 1<<1, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_segment { - pub version: u32, - pub flags: SPA_IO_SEGMENT_FLAG, - pub start: u64, - pub duration: u64, - pub rate: f64, - pub position: u64, - pub bar: spa_io_segment_bar, - pub video: spa_io_segment_video, -} - -bitflags! { - SPA_IO_CLOCK_FLAG: u32; - - SPA_IO_CLOCK_FLAG_FREEWHEEL = 1<<0, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_clock { - pub flags: SPA_IO_CLOCK_FLAG, - pub id: u32, - pub name: [u8; 64], - pub nsec: u64, - pub rate: spa_fraction, - pub position: u64, - pub duration: u64, - pub delay: i64, - pub rate_diff: f64, - pub next_nsec: u64, - pub padding: [u32; 8], -} - -#[repr(C)] -#[derive(Copy, Clone, Debug)] -pub struct spa_rectangle { - pub width: u32, - pub height: u32, -} - -bitflags! { - SPA_IO_VIDEO_SIZE: u32; - - SPA_IO_VIDEO_SIZE_VALID = 1<<0, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_video_size { - pub flags: SPA_IO_VIDEO_SIZE, - pub stride: u32, - pub size: spa_rectangle, - pub framerate: spa_fraction, - pub padding: [u32; 4], -} - -pub const SPA_IO_POSITION_MAX_SEGMENTS: usize = 8; - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_position { - pub clock: spa_io_clock, - pub video: spa_io_video_size, - pub offset: i64, - pub state: u32, - pub n_segments: u32, - pub segments: [spa_io_segment; SPA_IO_POSITION_MAX_SEGMENTS], -} - -#[repr(C)] -#[derive(Debug)] -pub struct pw_node_activation_state { - pub status: c::c_int, - pub required: AtomicI32, - pub pending: AtomicI32, -} - -ty! { - PW_NODE_ACTIVATION; - - PW_NODE_ACTIVATION_NOT_TRIGGERED = 0, - PW_NODE_ACTIVATION_TRIGGERED = 1, - PW_NODE_ACTIVATION_AWAKE = 2, - PW_NODE_ACTIVATION_FINISHED = 3, -} - -ty! { - PW_NODE_ACTIVATION_COMMAND; - - PW_NODE_ACTIVATION_COMMAND_NONE = 0, - PW_NODE_ACTIVATION_COMMAND_START = 1, - PW_NODE_ACTIVATION_COMMAND_STOP = 2, -} - -#[repr(C)] -#[derive(Debug)] -pub struct pw_node_activation { - pub status: AtomicU32, - - pub flags: c::c_uint, - - pub state: [pw_node_activation_state; 2], - - pub signal_time: u64, - pub awake_time: u64, - pub finish_time: u64, - pub prev_signal_time: u64, - - pub reposition: spa_io_segment, - pub segment: spa_io_segment, - - pub segment_owner: [u32; 32], - pub position: spa_io_position, - - pub sync_timeout: u64, - pub sync_left: u64, - - pub cpu_load: [f32; 3], - pub xrun_count: u32, - pub xrun_time: u64, - pub xrun_delay: u64, - pub max_delay: u64, - - pub command: PW_NODE_ACTIVATION_COMMAND, - pub reposition_owner: u32, -} - -unsafe impl Pod for pw_node_activation {} - -bitflags! { - SPA_PORT_FLAG: u64; - - SPA_PORT_FLAG_REMOVABLE = 1<<0, - SPA_PORT_FLAG_OPTIONAL = 1<<1, - SPA_PORT_FLAG_CAN_ALLOC_BUFFERS = 1<<2, - SPA_PORT_FLAG_IN_PLACE = 1<<3, - SPA_PORT_FLAG_NO_REF = 1<<4, - SPA_PORT_FLAG_LIVE = 1<<5, - SPA_PORT_FLAG_PHYSICAL = 1<<6, - SPA_PORT_FLAG_TERMINAL = 1<<7, - SPA_PORT_FLAG_DYNAMIC_DATA = 1<<8, -} - -bitflags! { - SpaStatus: u32; - - SPA_STATUS_NEED_DATA = 1 << 0, - SPA_STATUS_HAVE_DATA = 1 << 1, - SPA_STATUS_STOPPED = 1 << 2, - SPA_STATUS_DRAINED = 1 << 3, -} - -#[repr(C)] -#[derive(Debug)] -pub struct spa_io_buffers { - pub status: AtomicU32, - pub buffer_id: AtomicU32, -} - -unsafe impl Pod for spa_io_buffers {} - -bitflags! { - SpaChunkFlags: u32; - - SPA_CHUNK_FLAG_CORRUPTED = 1, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct spa_chunk { - pub offset: u32, - pub size: u32, - pub stride: u32, - pub flags: SpaChunkFlags, -} - -unsafe impl Pod for spa_chunk {} diff --git a/src/pipewire/pw_pod/pw_debug.rs b/src/pipewire/pw_pod/pw_debug.rs deleted file mode 100644 index feb98420..00000000 --- a/src/pipewire/pw_pod/pw_debug.rs +++ /dev/null @@ -1,464 +0,0 @@ -use { - crate::{ - pipewire::{ - pw_parser::PwParser, - pw_pod::{ - PW_COMMAND_Node, PW_OBJECT_Format, PW_OBJECT_ParamBuffers, PW_OBJECT_ParamIO, - PW_OBJECT_ParamLatency, PW_OBJECT_ParamMeta, PW_OBJECT_ParamPortConfig, - PW_OBJECT_ParamProcessLatency, PW_OBJECT_ParamProfile, PW_OBJECT_ParamRoute, - PW_OBJECT_Profiler, PW_OBJECT_PropInfo, PW_OBJECT_Props, PW_TYPE_Id, PwPod, - PwPodArray, PwPodObject, PwPodObjectType, PwPodSequence, PwPodStruct, PwPodType, - PwProp, SPA_FORMAT_AUDIO_bitorder, SPA_FORMAT_AUDIO_format, - SPA_FORMAT_AUDIO_iec958Codec, SPA_FORMAT_AUDIO_position, - SPA_FORMAT_VIDEO_H264_alignment, SPA_FORMAT_VIDEO_H264_streamFormat, - SPA_FORMAT_VIDEO_chromaSite, SPA_FORMAT_VIDEO_colorMatrix, - SPA_FORMAT_VIDEO_colorPrimaries, SPA_FORMAT_VIDEO_colorRange, - SPA_FORMAT_VIDEO_format, SPA_FORMAT_VIDEO_interlaceMode, - SPA_FORMAT_VIDEO_multiviewFlags, SPA_FORMAT_VIDEO_multiviewMode, - SPA_FORMAT_VIDEO_transferFunction, SPA_FORMAT_mediaSubtype, SPA_FORMAT_mediaType, - SPA_PARAM_BUFFERS_dataType, SPA_PARAM_IO_id, SPA_PARAM_META_type, - SPA_PARAM_PORT_CONFIG_direction, SPA_PARAM_PORT_CONFIG_mode, - SPA_PARAM_PROFILE_available, SPA_PARAM_ROUTE_available, SPA_PARAM_ROUTE_direction, - SPA_PROP_channelMap, SPA_PROP_iec958Codecs, SpaAudioChannel, SpaAudioFormat, - SpaAudioIec958Codec, SpaDataTypes, SpaDirection, SpaFormat, SpaH264Alignment, - SpaH264StreamFormat, SpaIoType, SpaMediaSubtype, SpaMediaType, SpaMetaType, - SpaNodeCommand, SpaParamAvailability, SpaParamBitorder, SpaParamBuffers, - SpaParamIo, SpaParamLatency, SpaParamMeta, SpaParamPortConfig, - SpaParamPortConfigMode, SpaParamProcessLatency, SpaParamProfile, SpaParamRoute, - SpaParamType, SpaProfiler, SpaProp, SpaPropInfo, SpaVideoChromaSite, - SpaVideoColorMatrix, SpaVideoColorPrimaries, SpaVideoColorRange, SpaVideoFormat, - SpaVideoInterlaceMode, SpaVideoMultiviewFlags, SpaVideoMultiviewMode, - SpaVideoTransferFunction, - }, - }, - utils::errorfmt::ErrorFmt, - }, - std::{ - fmt, - fmt::{Debug, DebugList, Formatter, Write}, - }, -}; - -trait PwPodObjectDebugger: Sync { - fn debug_property(&self, fmt: &mut Formatter<'_>, value: PwProp<'_>) -> std::fmt::Result; - fn id_name(&self, id: u32) -> Option<&'static str>; -} - -struct PwPodObjectDebuggerSimple { - key_name: F, - debug_pod: G, - id_name: H, -} - -impl PwPodObjectDebugger for PwPodObjectDebuggerSimple -where - F: Fn(u32) -> Option<&'static str> + Sync, - G: Fn(u32, &mut Formatter<'_>, PwPod<'_>) -> std::fmt::Result + Sync, - H: Fn(u32) -> Option<&'static str> + Sync, -{ - fn debug_property(&self, fmt: &mut Formatter<'_>, value: PwProp<'_>) -> std::fmt::Result { - let mut s = fmt.debug_struct("PwProp"); - match (self.key_name)(value.key) { - Some(n) => s.field("key", &n), - _ => s.field("key", &value.key), - }; - s.field("flags", &value.flags) - .field( - "pod", - &fmt::from_fn(|f| (self.debug_pod)(value.key, f, value.pod)), - ) - .finish() - } - - fn id_name(&self, id: u32) -> Option<&'static str> { - (self.id_name)(id) - } -} - -fn choice_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, ty: PwPodType, f: F) -> std::fmt::Result -where - F: Fn(&mut Formatter<'_>, PwPod<'_>) -> std::fmt::Result, -{ - match p { - PwPod::Choice(c) if c.elements.ty == ty => fmt - .debug_struct("choice") - .field("ty", &c.ty) - .field("flags", &c.flags) - .field( - "elements", - &fmt::from_fn(|fmt| { - array_body_debug(fmt, c.elements, |l, p| { - match p.read_pod_body_packed(ty, c.elements.child_len) { - Ok(p) => { - l.entry(&fmt::from_fn(|fmt| f(fmt, p))); - true - } - Err(e) => { - let e = ErrorFmt(e); - l.entry(&fmt::from_fn(|fmt| { - write!(fmt, "Could not read choice element: {}", e) - })); - false - } - } - }) - }), - ) - .finish(), - _ if p.ty() == ty => f(fmt, p), - _ => p.fmt(fmt), - } -} - -fn id_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, f: F) -> std::fmt::Result -where - F: Fn(&mut Formatter<'_>, u32) -> std::fmt::Result, -{ - choice_debug(fmt, p, PW_TYPE_Id, |fmt, p| match p { - PwPod::Id(id) => f(fmt, id), - _ => p.fmt(fmt), - }) -} - -fn array_body_debug(fmt: &mut Formatter<'_>, mut a: PwPodArray<'_>, f: F) -> std::fmt::Result -where - F: Fn(&mut DebugList, &mut PwParser<'_>) -> bool, -{ - let mut l = fmt.debug_list(); - for _ in 0..a.n_elements { - if !f(&mut l, &mut a.elements) { - break; - } - } - l.finish() -} - -fn array_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, ty: PwPodType, f: F) -> std::fmt::Result -where - F: Fn(&mut DebugList, &mut PwParser<'_>) -> bool, -{ - match p { - PwPod::Array(a) if a.ty == ty => array_body_debug(fmt, a, f), - _ => p.fmt(fmt), - } -} - -fn array_id_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, f: F) -> std::fmt::Result -where - F: Fn(&mut DebugList, u32) -> T, -{ - array_debug(fmt, p, PW_TYPE_Id, |l, p| match p.read_id() { - Ok(a) => { - f(l, a); - true - } - Err(e) => { - let e = ErrorFmt(e); - l.entry(&fmt::from_fn(|f| write!(f, "Could not read id: {}", e))); - false - } - }) -} - -fn object_id_name(id: u32) -> Option<&'static str> { - SpaParamType(id).name() -} - -fn command_id_name(id: u32) -> Option<&'static str> { - SpaNodeCommand(id).name() -} - -static PROP_INFO_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaPropInfo(key).name(), - debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), - id_name: object_id_name, -}; - -static PROPS_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaProp(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaProp(key) { - SPA_PROP_channelMap => array_id_debug(f, p, |l, a| { - l.entry(&SpaAudioChannel(a)); - }), - SPA_PROP_iec958Codecs => array_id_debug(f, p, |l, a| { - l.entry(&SpaAudioIec958Codec(a)); - }), - _ => p.fmt(f), - }, -}; - -static FORMAT_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaFormat(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaFormat(key) { - SPA_FORMAT_mediaType => id_debug(f, p, |f, a| SpaMediaType(a).fmt(f)), - SPA_FORMAT_mediaSubtype => id_debug(f, p, |f, a| SpaMediaSubtype(a).fmt(f)), - SPA_FORMAT_AUDIO_format => id_debug(f, p, |f, a| SpaAudioFormat(a).fmt(f)), - SPA_FORMAT_AUDIO_position => array_id_debug(f, p, |l, a| { - l.entry(&SpaAudioChannel(a)); - }), - SPA_FORMAT_AUDIO_iec958Codec => id_debug(f, p, |f, a| SpaAudioIec958Codec(a).fmt(f)), - SPA_FORMAT_AUDIO_bitorder => id_debug(f, p, |f, a| SpaParamBitorder(a).fmt(f)), - SPA_FORMAT_VIDEO_format => id_debug(f, p, |f, a| SpaVideoFormat(a).fmt(f)), - SPA_FORMAT_VIDEO_interlaceMode => id_debug(f, p, |f, a| SpaVideoInterlaceMode(a).fmt(f)), - SPA_FORMAT_VIDEO_multiviewMode => id_debug(f, p, |f, a| SpaVideoMultiviewMode(a).fmt(f)), - SPA_FORMAT_VIDEO_multiviewFlags => id_debug(f, p, |f, a| SpaVideoMultiviewFlags(a).fmt(f)), - SPA_FORMAT_VIDEO_chromaSite => id_debug(f, p, |f, a| SpaVideoChromaSite(a).fmt(f)), - SPA_FORMAT_VIDEO_colorRange => id_debug(f, p, |f, a| SpaVideoColorRange(a).fmt(f)), - SPA_FORMAT_VIDEO_colorMatrix => id_debug(f, p, |f, a| SpaVideoColorMatrix(a).fmt(f)), - SPA_FORMAT_VIDEO_transferFunction => { - id_debug(f, p, |f, a| SpaVideoTransferFunction(a).fmt(f)) - } - SPA_FORMAT_VIDEO_colorPrimaries => id_debug(f, p, |f, a| SpaVideoColorPrimaries(a).fmt(f)), - SPA_FORMAT_VIDEO_H264_streamFormat => id_debug(f, p, |f, a| SpaH264StreamFormat(a).fmt(f)), - SPA_FORMAT_VIDEO_H264_alignment => id_debug(f, p, |f, a| SpaH264Alignment(a).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PARAM_BUFFERS_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamBuffers(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamBuffers(key) { - SPA_PARAM_BUFFERS_dataType => match p { - PwPod::Int(v) => SpaDataTypes(v as _).fmt(f), - _ => p.fmt(f), - }, - _ => p.fmt(f), - }, -}; - -static PARAM_META_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamMeta(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamMeta(key) { - SPA_PARAM_META_type => id_debug(f, p, |f, b| SpaMetaType(b).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PARAM_IO_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamIo(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamIo(key) { - SPA_PARAM_IO_id => id_debug(f, p, |f, b| SpaIoType(b).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PARAM_PROFILE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamProfile(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamProfile(key) { - SPA_PARAM_PROFILE_available => id_debug(f, p, |f, b| SpaParamAvailability(b).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PARAM_PORT_CONFIG_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamPortConfig(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamPortConfig(key) { - SPA_PARAM_PORT_CONFIG_direction => id_debug(f, p, |f, b| SpaDirection(b).fmt(f)), - SPA_PARAM_PORT_CONFIG_mode => id_debug(f, p, |f, b| SpaParamPortConfigMode(b).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PARAM_ROUTE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamRoute(key).name(), - id_name: object_id_name, - debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamRoute(key) { - SPA_PARAM_ROUTE_direction => id_debug(f, p, |f, b| SpaDirection(b).fmt(f)), - SPA_PARAM_ROUTE_available => id_debug(f, p, |f, b| SpaParamAvailability(b).fmt(f)), - _ => p.fmt(f), - }, -}; - -static PROFILER_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaProfiler(key).name(), - id_name: object_id_name, - debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), -}; - -static PARAM_LATENCY_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamLatency(key).name(), - id_name: object_id_name, - debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), -}; - -static PARAM_PROCESS_LATENCY_DEBUGGER: &'static dyn PwPodObjectDebugger = - &PwPodObjectDebuggerSimple { - key_name: |key| SpaParamProcessLatency(key).name(), - id_name: object_id_name, - debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), - }; - -static COMMAND_NODE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { - key_name: |key| SpaNodeCommand(key).name(), - id_name: command_id_name, - debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), -}; - -fn object_debugger(obj: PwPodObjectType) -> Option<&'static dyn PwPodObjectDebugger> { - let res: &dyn PwPodObjectDebugger = match obj { - PW_OBJECT_PropInfo => PROP_INFO_DEBUGGER, - PW_OBJECT_Props => PROPS_DEBUGGER, - PW_OBJECT_Format => FORMAT_DEBUGGER, - PW_OBJECT_ParamBuffers => PARAM_BUFFERS_DEBUGGER, - PW_OBJECT_ParamMeta => PARAM_META_DEBUGGER, - PW_OBJECT_ParamIO => PARAM_IO_DEBUGGER, - PW_OBJECT_ParamProfile => PARAM_PROFILE_DEBUGGER, - PW_OBJECT_ParamPortConfig => PARAM_PORT_CONFIG_DEBUGGER, - PW_OBJECT_ParamRoute => PARAM_ROUTE_DEBUGGER, - PW_OBJECT_Profiler => PROFILER_DEBUGGER, - PW_OBJECT_ParamLatency => PARAM_LATENCY_DEBUGGER, - PW_OBJECT_ParamProcessLatency => PARAM_PROCESS_LATENCY_DEBUGGER, - PW_COMMAND_Node => COMMAND_NODE_DEBUGGER, - _ => return None, - }; - Some(res) -} - -impl<'a> Debug for PwPodObject<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let debugger = object_debugger(self.ty); - let mut s = f.debug_struct("object"); - s.field("type", &self.ty); - let name; - let mut id: &dyn Debug = &self.id; - if let Some(d) = debugger - && let Some(n) = d.id_name(self.id) - { - name = n; - id = &name; - } - s.field("id", id); - s.field( - "props", - &fmt::from_fn(|f| { - let mut l = f.debug_list(); - let mut parser = self.probs; - while parser.len() > 0 { - match parser.read_prop() { - Ok(p) => match debugger { - Some(d) => l.entry(&fmt::from_fn(|fmt| d.debug_property(fmt, p))), - _ => l.entry(&p), - }, - Err(e) => { - let e = ErrorFmt(e); - l.entry(&fmt::from_fn(|f| { - write!(f, "Could not read object property: {}", &e) - })); - break; - } - }; - } - l.finish() - }), - ); - s.finish() - } -} - -impl<'a> Debug for PwPodSequence<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut s = f.debug_struct("sequence"); - s.field("unit", &self.unit); - s.field( - "controls", - &fmt::from_fn(|f| { - let mut l = f.debug_list(); - let mut parser = self.controls; - while parser.len() > 0 { - match parser.read_control() { - Ok(c) => l.entry(&c), - Err(e) => { - let e = ErrorFmt(e); - l.entry(&fmt::from_fn(|f| { - write!(f, "Could not read control element: {}", &e) - })); - break; - } - }; - } - l.finish() - }), - ); - s.finish() - } -} - -impl<'a> Debug for PwPodStruct<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut parser = self.fields; - let mut s = f.debug_struct("struct"); - let mut field = String::new(); - for i in 0.. { - if parser.len() == 0 { - break; - } - field.clear(); - let _ = write!(&mut field, "\"{}\"", i); - match parser.read_pod() { - Ok(p) => s.field(&field, &p), - Err(e) => { - let e = ErrorFmt(e); - s.field( - &field, - &fmt::from_fn(|f| write!(f, "Could not parse struct field: {}", &e)), - ); - break; - } - }; - } - s.finish() - } -} - -impl<'a> Debug for PwPodArray<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut list = f.debug_list(); - let mut parser = self.elements; - for _ in 0..self.n_elements { - match parser.read_pod_body_packed(self.ty, self.child_len) { - Ok(e) => list.entry(&e), - Err(e) => { - let e = ErrorFmt(e); - list.entry(&fmt::from_fn(|f| { - write!(f, "Could not parse array element: {}", &e) - })); - break; - } - }; - } - list.finish() - } -} - -impl<'a> Debug for PwPod<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - PwPod::None => write!(f, "None"), - PwPod::Bool(b) => write!(f, "{}", b), - PwPod::Id(id) => write!(f, "id({})", id), - PwPod::Int(i) => write!(f, "int({})", i), - PwPod::Long(l) => write!(f, "long({})", l), - PwPod::Float(v) => write!(f, "float({})", v), - PwPod::Double(d) => write!(f, "double({})", d), - PwPod::String(s) => write!(f, "string({:?})", s), - PwPod::Bytes(b) => write!(f, "bytes(len = {})", b.len()), - PwPod::Rectangle(r) => write!(f, "rectangle({}x{})", r.width, r.height), - PwPod::Fraction(v) => write!(f, "fraction({}/{})", v.num, v.denom), - PwPod::Bitmap(b) => write!(f, "bitmap(len = {})", b.len()), - PwPod::Array(a) => a.fmt(f), - PwPod::Struct(s) => s.fmt(f), - PwPod::Object(o) => o.fmt(f), - PwPod::Sequence(s) => s.fmt(f), - PwPod::Pointer(p) => p.fmt(f), - PwPod::Fd(v) => write!(f, "fd({})", v), - PwPod::Choice(c) => c.fmt(f), - } - } -} diff --git a/src/portal.rs b/src/portal.rs deleted file mode 100644 index f8847498..00000000 --- a/src/portal.rs +++ /dev/null @@ -1,353 +0,0 @@ -mod ptl_display; -mod ptl_remote_desktop; -mod ptl_render_ctx; -mod ptl_screencast; -mod ptl_session; -mod ptl_text; -mod ptr_gui; - -use { - crate::{ - async_engine::AsyncEngine, - cli::GlobalArgs, - cmm::cmm_manager::ColorManager, - compositor::LogLevel, - dbus::{ - BUS_DEST, BUS_PATH, DBUS_NAME_FLAG_DO_NOT_QUEUE, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER, - Dbus, DbusSocket, - }, - eventfd_cache::EventfdCache, - forker::ForkerError, - io_uring::IoUring, - logger::Logger, - pipewire::pw_con::{PwCon, PwConHolder, PwConOwner}, - portal::{ - ptl_display::{PortalDisplay, PortalDisplayId, watch_displays}, - ptl_remote_desktop::add_remote_desktop_dbus_members, - ptl_render_ctx::PortalRenderCtx, - ptl_screencast::add_screencast_dbus_members, - ptl_session::PortalSession, - }, - utils::{ - clone3::{Forked, fork_with_pidfd}, - copyhashmap::CopyHashMap, - errorfmt::ErrorFmt, - line_logger::log_lines, - numcell::NumCell, - oserror::{OsError, OsErrorExt}, - pipe::{Pipe, pipe}, - process_name::set_process_name, - run_toplevel::RunToplevel, - xrd::xrd, - }, - version::VERSION, - video::dmabuf::DmaBufIds, - wheel::Wheel, - wire_dbus::org, - }, - std::{ - ffi::OsStr, - io::{BufReader, BufWriter}, - os::unix::{ffi::OsStrExt, process::CommandExt}, - process::{Command, exit}, - rc::{Rc, Weak}, - sync::Arc, - }, - thiserror::Error, - uapi::{OwnedFd, WEXITSTATUS, c, getpid}, -}; - -const PORTAL_SUCCESS: u32 = 0; -#[expect(dead_code)] -const PORTAL_CANCELLED: u32 = 1; -#[expect(dead_code)] -const PORTAL_ENDED: u32 = 2; - -pub fn run_freestanding(global: GlobalArgs) { - let logger = Logger::install_stderr(global.log_level); - run(logger, true); -} - -#[derive(Debug, Error)] -pub enum PortalError { - #[error("Could not create pipe")] - CreatePipe(#[source] OsError), - #[error("Could not fork")] - Fork(#[source] ForkerError), -} - -pub struct PortalStartup { - logs: Rc, - pid: c::pid_t, - pidfd: Rc, -} - -impl PortalStartup { - pub async fn spawn(self, eng: Rc, ring: Rc, logger: Arc) { - let f1 = eng.spawn("check portal exit code", { - let ring = ring.clone(); - async move { - if let Err(e) = ring.readable(&self.pidfd).await { - log::error!( - "Could not wait for portal pidfd to become readable: {}", - ErrorFmt(e) - ); - return; - } - let (_, status) = match uapi::waitpid(self.pid, 0).to_os_error() { - Ok(r) => r, - Err(e) => { - log::error!( - "Could not retrieve exit status of portal ({}): {}", - self.pid, - ErrorFmt(e), - ); - return; - } - }; - let status = WEXITSTATUS(status); - if status != 0 { - log::error!("Portal exited with non-0 exit code: {status}"); - } - } - }); - let f2 = eng.spawn("portal logger", { - let ring = ring.clone(); - let logger = logger.clone(); - async move { - let res = log_lines(&ring, &self.logs, |left, right| { - logger.write_raw(left); - logger.write_raw(right); - logger.write_raw(b" (portal)\n"); - }) - .await; - if let Err(e) = res { - log::error!("Could not read portal logs: {}", ErrorFmt(e)); - } - } - }); - f1.await; - f2.await; - } -} - -pub fn run_from_compositor(level: LogLevel) -> Result { - let Pipe { read, write } = match pipe() { - Ok(p) => p, - Err(e) => return Err(PortalError::CreatePipe(e)), - }; - let fork = match fork_with_pidfd(false) { - Ok(f) => f, - Err(e) => return Err(PortalError::Fork(e)), - }; - match fork { - Forked::Parent { pidfd, pid } => Ok(PortalStartup { - logs: Rc::new(read), - pid, - pidfd: Rc::new(pidfd), - }), - Forked::Child { .. } => { - drop(read); - let logger = Logger::install_pipe(write, level); - run(logger, false); - } - } -} - -fn run(logger: Arc, freestanding: bool) -> ! { - let Pipe { read, write } = match pipe() { - Ok(p) => p, - Err(e) => { - fatal!("Could not create a pipe: {}", ErrorFmt(e)); - } - }; - let fork = match fork_with_pidfd(false) { - Ok(f) => f, - Err(e) => { - fatal!("Could not fork: {}", ErrorFmt(e)); - } - }; - let Forked::Parent { pid, .. } = fork else { - drop(read); - run2(logger, write); - exit(0); - }; - drop(write); - let read = BufReader::new(read); - let Ok(log_file) = bincode::deserialize_from::<_, Vec>(read) else { - let (_, status) = match uapi::waitpid(pid, 0).to_os_error() { - Ok(r) => r, - Err(e) => { - fatal!( - "Could not retrieve exit status of portal ({pid}): {}", - ErrorFmt(e), - ); - } - }; - exit(WEXITSTATUS(status)); - }; - if freestanding { - let e = Command::new("tail") - .arg("-f") - .arg("-n") - .arg("+1") - .arg(OsStr::from_bytes(&log_file)) - .exec(); - fatal!("Could not exec `tail`: {}", ErrorFmt(e)); - } - exit(0); -} - -fn run2(logger: Arc, path_sink: OwnedFd) { - let eng = AsyncEngine::new(); - let ring = match IoUring::new(&eng, 32) { - Ok(r) => r, - Err(e) => { - fatal!("Could not create an IO-uring: {}", ErrorFmt(e)); - } - }; - let _f = eng.spawn( - "portal", - run_async(eng.clone(), ring.clone(), logger, path_sink), - ); - if let Err(e) = ring.run() { - fatal!("The IO-uring returned an error: {}", ErrorFmt(e)); - } -} - -async fn run_async( - eng: Rc, - ring: Rc, - logger: Arc, - path_sink: OwnedFd, -) { - let (_rtl_future, rtl) = RunToplevel::install(&eng); - let dbus = Dbus::new(&eng, &ring, &rtl); - let dbus = init_dbus_session(&dbus, logger, path_sink).await; - let xrd = match xrd() { - Some(xrd) => xrd, - _ => { - fatal!("XDG_RUNTIME_DIR is not set"); - } - }; - let wheel = match Wheel::new(&eng, &ring) { - Ok(w) => w, - Err(e) => { - fatal!("Could not create a timer wheel: {}", ErrorFmt(e)); - } - }; - let pw_con = match PwConHolder::new(&eng, &ring).await { - Ok(p) => Some(p), - Err(e) => { - log::error!("Could not connect to pipewire: {}", ErrorFmt(e)); - None - } - }; - let eventfd_cache = EventfdCache::new(&ring, &eng); - let state = Rc::new(PortalState { - xrd, - ring, - eventfd_cache, - eng, - wheel, - displays: Default::default(), - dbus, - sessions: Default::default(), - next_id: NumCell::new(1), - render_ctxs: Default::default(), - dma_buf_ids: Default::default(), - pw_con: pw_con.as_ref().map(|c| c.con.clone()), - color_manager: ColorManager::new(), - }); - if let Some(pw_con) = &pw_con { - pw_con.con.owner.set(Some(state.clone())); - } - let _root = { - let obj = state - .dbus - .add_object("/org/freedesktop/portal/desktop") - .unwrap(); - if let Some(pw_con) = &pw_con { - add_screencast_dbus_members(&state, &pw_con.con, &obj); - } - add_remote_desktop_dbus_members(&state, &obj); - obj - }; - watch_displays(state.clone()).await; -} - -const UNIQUE_NAME: &str = "org.freedesktop.impl.portal.desktop.jay"; - -async fn init_dbus_session(dbus: &Dbus, logger: Arc, path_sink: OwnedFd) -> Rc { - let session = match dbus.session().await { - Ok(s) => s, - Err(e) => { - fatal!("Could not connect to dbus session daemon: {}", ErrorFmt(e)); - } - }; - let rv = session - .call_async( - BUS_DEST, - BUS_PATH, - org::freedesktop::dbus::RequestName { - name: UNIQUE_NAME.into(), - flags: DBUS_NAME_FLAG_DO_NOT_QUEUE, - }, - ) - .await; - match rv { - Ok(r) if r.get().rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { - log::info!("Acquired unique name {}", UNIQUE_NAME); - let log_file = logger.redirect("portal"); - log::info!("version = {VERSION}"); - let sink = BufWriter::new(path_sink); - if let Err(e) = bincode::serialize_into(sink, log_file.as_bytes()) { - log::error!("Could not send log file to parent: {}", ErrorFmt(e)); - } - if let Err(e) = uapi::setsid().to_os_error() { - log::error!("setsid failed: {}", ErrorFmt(e)); - } - log::info!("pid = {}", getpid()); - set_process_name("jay portal"); - session - } - Ok(_) => { - log::info!("Portal is already running"); - exit(0); - } - Err(e) => { - fatal!( - "Could not communicate with the session bus: {}", - ErrorFmt(e) - ); - } - } -} - -struct PortalState { - xrd: String, - ring: Rc, - eventfd_cache: Rc, - eng: Rc, - wheel: Rc, - displays: CopyHashMap>, - dbus: Rc, - sessions: CopyHashMap>, - next_id: NumCell, - render_ctxs: CopyHashMap>, - dma_buf_ids: Rc, - pw_con: Option>, - color_manager: Rc, -} - -impl PortalState { - pub fn id>(&self) -> T { - T::from(self.next_id.fetch_add(1)) - } -} - -impl PwConOwner for PortalState { - fn killed(&self) { - fatal!("The pipewire connection has been closed"); - } -} diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs deleted file mode 100644 index f3c30db4..00000000 --- a/src/portal/ptl_display.rs +++ /dev/null @@ -1,562 +0,0 @@ -use { - crate::{ - gfx_api::{GfxApi, GfxFormat, cross_intersect_formats}, - gfx_apis::create_gfx_context, - globals::GlobalName, - ifs::wl_seat::POINTER, - object::Version, - portal::{ - PortalState, - ptl_render_ctx::{PortalRenderCtx, PortalServerRenderCtx}, - ptl_session::PortalSession, - ptr_gui::WindowData, - }, - utils::{ - bitflags::BitflagsExt, - clonecell::CloneCell, - copyhashmap::CopyHashMap, - errorfmt::ErrorFmt, - hash_map_ext::HashMapExt, - opaque::{Opaque, opaque}, - oserror::OsErrorExt, - }, - video::drm::Drm, - wire::{ - JayCompositor, WlCompositor, WlOutput, WlSeat, WlSurfaceId, WpFractionalScaleManagerV1, - WpViewporter, ZwlrLayerShellV1, ZwpLinuxDmabufV1, wl_pointer, - }, - wl_usr::{ - UsrCon, UsrConOwner, - usr_ifs::{ - usr_jay_compositor::UsrJayCompositor, - usr_jay_output::{UsrJayOutput, UsrJayOutputOwner}, - usr_jay_pointer::UsrJayPointer, - usr_jay_render_ctx::UsrJayRenderCtxOwner, - usr_jay_workspace::{UsrJayWorkspace, UsrJayWorkspaceOwner}, - usr_jay_workspace_watcher::{UsrJayWorkspaceWatcher, UsrJayWorkspaceWatcherOwner}, - usr_linux_dmabuf::UsrLinuxDmabuf, - usr_wl_compositor::UsrWlCompositor, - usr_wl_output::{UsrWlOutput, UsrWlOutputOwner}, - usr_wl_pointer::{UsrWlPointer, UsrWlPointerOwner}, - usr_wl_registry::{UsrWlRegistry, UsrWlRegistryOwner}, - usr_wl_seat::{UsrWlSeat, UsrWlSeatOwner}, - usr_wlr_layer_shell::UsrWlrLayerShell, - usr_wp_fractional_scale_manager::UsrWpFractionalScaleManager, - usr_wp_viewporter::UsrWpViewporter, - }, - }, - }, - ahash::AHashMap, - std::{ - cell::{Cell, RefCell}, - ops::Deref, - os::unix::ffi::OsStrExt, - rc::Rc, - str::FromStr, - }, - uapi::{AsUstr, OwnedFd, c}, -}; - -struct PortalDisplayPrelude { - con: Rc, - state: Rc, - registry: Rc, - globals: RefCell>>, -} - -shared_ids!(PortalDisplayId); -pub struct PortalDisplay { - pub id: PortalDisplayId, - pub unique_id: Opaque, - pub con: Rc, - pub(super) state: Rc, - registry: Rc, - _workspace_watcher: Rc, - pub dmabuf: CloneCell>>, - - pub jc: Rc, - pub ls: Rc, - pub comp: Rc, - pub fsm: Rc, - pub vp: Rc, - pub render_ctx: CloneCell>>, - - pub outputs: CopyHashMap>, - pub seats: CopyHashMap>, - pub workspaces: CopyHashMap>, - - pub windows: CopyHashMap>, - pub sessions: CopyHashMap>, -} - -pub struct PortalOutput { - pub global_id: GlobalName, - pub dpy: Rc, - pub wl: Rc, - pub jay: Rc, -} - -pub struct PortalSeat { - pub global_id: GlobalName, - pub dpy: Rc, - pub wl: Rc, - pub jay_pointer: Rc, - pub pointer: CloneCell>>, - pub name: RefCell, - pub capabilities: Cell, - pub pointer_focus: CloneCell>>, -} - -impl UsrWlSeatOwner for PortalSeat { - fn name(&self, name: &str) { - *self.name.borrow_mut() = name.to_string(); - } - - fn capabilities(self: Rc, value: u32) { - let old = self.capabilities.replace(value); - if old.contains(POINTER) != value.contains(POINTER) { - if old.contains(POINTER) { - if let Some(pointer) = self.pointer.take() { - pointer.con.remove_obj(pointer.deref()); - } - } else { - let pointer = self.wl.get_pointer(); - pointer.owner.set(Some(self.clone())); - self.pointer.set(Some(pointer)); - } - } - } -} - -impl UsrWlPointerOwner for PortalSeat { - fn enter(self: Rc, ev: &wl_pointer::Enter) { - if let Some(window) = self.dpy.windows.get(&ev.surface) { - self.pointer_focus.set(Some(window.clone())); - window.motion(&self, ev.surface_x, ev.surface_y, true); - } - } - - fn leave(self: Rc, _ev: &wl_pointer::Leave) { - self.pointer_focus.take(); - } - - fn motion(self: Rc, ev: &wl_pointer::Motion) { - if let Some(window) = self.pointer_focus.get() { - window.motion(&self, ev.surface_x, ev.surface_y, false); - } - } - - fn button(self: Rc, ev: &wl_pointer::Button) { - if let Some(window) = self.pointer_focus.get() { - window.button(&self, ev.button, ev.state); - } - } -} - -impl UsrWlRegistryOwner for PortalDisplayPrelude { - fn global(self: Rc, name: GlobalName, interface: &str, version: u32) { - self.globals - .borrow_mut() - .entry(interface.to_string()) - .or_default() - .push((name, version)); - } -} - -impl UsrJayRenderCtxOwner for PortalDisplay { - fn no_device(&self) { - self.render_ctx.take(); - } - - fn device(&self, fd: Rc, server_formats: Option>) { - self.render_ctx.take(); - let drm = match Drm::open_existing(fd) { - Ok(d) => d, - Err(e) => { - log::error!("Could not open the drm device: {}", ErrorFmt(e)); - return; - } - }; - let dev_id = drm.dev(); - let mut render_ctx = None; - if let Some(ctx) = self.state.render_ctxs.get(&dev_id) - && let Some(ctx) = ctx.upgrade() - { - render_ctx = Some(ctx); - } - if render_ctx.is_none() { - let ctx = match create_gfx_context( - &self.state.eng, - &self.state.ring, - &self.state.eventfd_cache, - &drm, - GfxApi::OpenGl, - None, - ) { - Ok(c) => c, - Err(e) => { - log::error!( - "Could not create render context from drm device: {}", - ErrorFmt(e) - ); - return; - } - }; - let ctx = Rc::new(PortalRenderCtx { - _dev_id: dev_id, - ctx, - }); - self.state.render_ctxs.set(dev_id, Rc::downgrade(&ctx)); - render_ctx = Some(ctx); - } - if let Some(ctx) = render_ctx { - let client_formats = ctx.ctx.formats(); - let usable_formats = match &server_formats { - None => client_formats.clone(), - Some(server_formats) => { - Rc::new(cross_intersect_formats(client_formats, server_formats)) - } - }; - self.render_ctx.set(Some(Rc::new(PortalServerRenderCtx { - ctx, - usable_formats, - server_formats, - }))); - } - } -} - -impl UsrConOwner for PortalDisplay { - fn killed(&self) { - log::info!("Removing display {}", self.id); - for sc in self.sessions.lock().drain_values() { - sc.kill(); - } - self.windows.clear(); - self.state.displays.remove(&self.id); - } -} - -impl UsrWlRegistryOwner for PortalDisplay { - fn global(self: Rc, name: GlobalName, interface: &str, version: u32) { - if interface == WlOutput.name() { - add_output(&self, name, version); - } else if interface == WlSeat.name() { - add_seat(&self, name, version); - } else if interface == ZwpLinuxDmabufV1.name() { - let ls = Rc::new(UsrLinuxDmabuf { - id: self.con.id(), - con: self.con.clone(), - owner: Default::default(), - version: Version(version.min(5)), - }); - self.con.add_object(ls.clone()); - self.registry.bind(name, ls.deref()); - self.dmabuf.set(Some(ls)); - } - } -} - -impl UsrJayWorkspaceWatcherOwner for PortalDisplay { - fn new(self: Rc, ev: Rc, linear_id: u32) { - ev.owner.set(Some(self.clone())); - self.workspaces.set(linear_id, ev); - } -} - -impl UsrJayWorkspaceOwner for PortalDisplay { - fn destroyed(&self, ws: &UsrJayWorkspace) { - self.workspaces.remove(&ws.linear_id.get()); - self.con.remove_obj(ws); - } -} - -impl UsrJayOutputOwner for PortalOutput { - fn destroyed(&self) { - log::info!( - "Display {}: Output {} removed", - self.dpy.con.server_id, - self.global_id, - ); - self.dpy.outputs.remove(&self.global_id); - self.dpy.con.remove_obj(self.wl.deref()); - self.dpy.con.remove_obj(self.jay.deref()); - } -} - -impl UsrWlOutputOwner for PortalOutput {} - -async fn maybe_add_display(state: &Rc, name: &str) { - let tail = match name.strip_prefix("wayland-") { - Some(t) => t, - _ => return, - }; - let head = match tail.strip_suffix(".jay") { - Some(h) => h, - _ => return, - }; - let num = match u32::from_str(head) { - Ok(n) => n, - _ => return, - }; - let path = format!("{}/{}", state.xrd, name); - let con = match UsrCon::new( - &state.ring, - &state.wheel, - &state.eng, - &state.dma_buf_ids, - &path, - num, - ) - .await - { - Ok(c) => c, - Err(e) => { - log::error!( - "Could not connect to wayland display {}: {}", - name, - ErrorFmt(e) - ); - return; - } - }; - let registry = con.get_registry(); - let dpy = Rc::new(PortalDisplayPrelude { - con: con.clone(), - state: state.clone(), - registry, - globals: Default::default(), - }); - dpy.registry.owner.set(Some(dpy.clone())); - con.sync(move || { - finish_display_connect(dpy); - }); - log::info!("Connected to wayland display {num}: {name}"); -} - -fn finish_display_connect(dpy: Rc) { - let mut jc_opt = None; - let mut ls_opt = None; - let mut fsm_opt = None; - let mut comp_opt = None; - let mut vp_opt = None; - let mut dmabuf_opt = None; - let mut outputs = vec![]; - let mut seats = vec![]; - for (interface, instances) in dpy.globals.borrow_mut().deref() { - for &(name, version) in instances { - if interface == JayCompositor.name() { - let jc = Rc::new(UsrJayCompositor { - id: dpy.con.id(), - con: dpy.con.clone(), - owner: Default::default(), - caps: Default::default(), - version: Version(version.min(12)), - }); - dpy.con.add_object(jc.clone()); - dpy.registry.bind(name, jc.deref()); - jc_opt = Some(jc); - } else if interface == WpFractionalScaleManagerV1.name() { - let ls = Rc::new(UsrWpFractionalScaleManager { - id: dpy.con.id(), - con: dpy.con.clone(), - version: Version(version.min(1)), - }); - dpy.con.add_object(ls.clone()); - dpy.registry.bind(name, ls.deref()); - fsm_opt = Some(ls); - } else if interface == ZwlrLayerShellV1.name() { - let ls = Rc::new(UsrWlrLayerShell { - id: dpy.con.id(), - con: dpy.con.clone(), - version: Version(version.min(5)), - }); - dpy.con.add_object(ls.clone()); - dpy.registry.bind(name, ls.deref()); - ls_opt = Some(ls); - } else if interface == WpViewporter.name() { - let ls = Rc::new(UsrWpViewporter { - id: dpy.con.id(), - con: dpy.con.clone(), - version: Version(version.min(1)), - }); - dpy.con.add_object(ls.clone()); - dpy.registry.bind(name, ls.deref()); - vp_opt = Some(ls); - } else if interface == WlCompositor.name() { - let ls = Rc::new(UsrWlCompositor { - id: dpy.con.id(), - con: dpy.con.clone(), - version: Version(version.min(6)), - }); - dpy.con.add_object(ls.clone()); - dpy.registry.bind(name, ls.deref()); - comp_opt = Some(ls); - } else if interface == ZwpLinuxDmabufV1.name() { - let ls = Rc::new(UsrLinuxDmabuf { - id: dpy.con.id(), - con: dpy.con.clone(), - owner: Default::default(), - version: Version(version.min(5)), - }); - dpy.con.add_object(ls.clone()); - dpy.registry.bind(name, ls.deref()); - dmabuf_opt = Some(ls); - } else if interface == WlOutput.name() { - outputs.push((name, version)); - } else if interface == WlSeat.name() { - seats.push((name, version)); - } - } - } - macro_rules! get { - ($opt:expr, $ty:expr) => { - match $opt { - Some(c) => c, - _ => { - log::error!("Compositor did not advertise a {}", $ty.name()); - dpy.con.kill(); - return; - } - } - }; - } - let jc = get!(jc_opt, JayCompositor); - let ls = get!(ls_opt, ZwlrLayerShellV1); - let comp = get!(comp_opt, WlCompositor); - let fsm = get!(fsm_opt, WpFractionalScaleManagerV1); - let vp = get!(vp_opt, WpViewporter); - let ww = jc.watch_workspaces(); - - let dpy = Rc::new(PortalDisplay { - id: dpy.state.id(), - unique_id: opaque(), - con: dpy.con.clone(), - state: dpy.state.clone(), - registry: dpy.registry.clone(), - _workspace_watcher: ww.clone(), - dmabuf: CloneCell::new(dmabuf_opt), - jc, - outputs: Default::default(), - render_ctx: Default::default(), - seats: Default::default(), - ls, - comp, - fsm, - vp, - windows: Default::default(), - sessions: Default::default(), - workspaces: Default::default(), - }); - - dpy.state.displays.set(dpy.id, dpy.clone()); - dpy.con.owner.set(Some(dpy.clone())); - dpy.registry.owner.set(Some(dpy.clone())); - ww.owner.set(Some(dpy.clone())); - - let jrc = dpy.jc.get_render_context(); - jrc.owner.set(Some(dpy.clone())); - - for (name, version) in outputs { - add_output(&dpy, name, version); - } - for (name, version) in seats { - add_seat(&dpy, name, version); - } - log::info!("Display {} initialized", dpy.id); -} - -fn add_seat(dpy: &Rc, name: GlobalName, version: u32) { - let wl = Rc::new(UsrWlSeat { - id: dpy.con.id(), - con: dpy.con.clone(), - owner: Default::default(), - version: Version(version.min(9)), - }); - dpy.con.add_object(wl.clone()); - dpy.registry.bind(name, wl.deref()); - let jay_pointer = dpy.jc.get_pointer(&wl); - let js = Rc::new(PortalSeat { - global_id: name, - dpy: dpy.clone(), - wl, - jay_pointer, - pointer: Default::default(), - name: RefCell::new("".to_string()), - capabilities: Cell::new(0), - pointer_focus: Default::default(), - }); - js.wl.owner.set(Some(js.clone())); - dpy.seats.set(name, js); -} - -fn add_output(dpy: &Rc, name: GlobalName, version: u32) { - let wl = Rc::new(UsrWlOutput { - id: dpy.con.id(), - con: dpy.con.clone(), - owner: Default::default(), - version: Version(version.min(4)), - name: Default::default(), - }); - dpy.con.add_object(wl.clone()); - dpy.registry.bind(name, wl.deref()); - let jo = dpy.jc.get_output(&wl); - let po = Rc::new(PortalOutput { - global_id: name, - dpy: dpy.clone(), - wl: wl.clone(), - jay: jo.clone(), - }); - po.wl.owner.set(Some(po.clone())); - po.jay.owner.set(Some(po.clone())); - dpy.outputs.set(name, po); -} - -pub(super) async fn watch_displays(state: Rc) { - let inotify = Rc::new(uapi::inotify_init1(c::IN_CLOEXEC).unwrap()); - if let Err(e) = - uapi::inotify_add_watch(inotify.raw(), state.xrd.as_str(), c::IN_CREATE).to_os_error() - { - log::error!("Cannot watch directory `{}`: {}", state.xrd, ErrorFmt(e)); - return; - } - let rd = match std::fs::read_dir(&state.xrd) { - Ok(rd) => rd, - Err(e) => { - log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); - return; - } - }; - for entry in rd { - let entry = match entry { - Ok(e) => e, - Err(e) => { - log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); - return; - } - }; - if let Ok(s) = std::str::from_utf8(entry.file_name().as_bytes()) { - maybe_add_display(&state, s).await; - } - } - let mut buf = vec![0u8; 4096]; - loop { - if let Err(e) = state.ring.readable(&inotify).await { - log::error!("Cannot wait for `{}` to change: {}", state.xrd, ErrorFmt(e)); - } - let events = match uapi::inotify_read(inotify.raw(), &mut buf[..]) { - Ok(s) => s, - Err(e) => { - log::error!("Could not read from inotify fd: {}", ErrorFmt(e)); - return; - } - }; - for event in events { - if event.mask.contains(c::IN_CREATE) - && let Ok(s) = std::str::from_utf8(event.name().as_ustr().as_bytes()) - { - maybe_add_display(&state, s).await; - } - } - } -} diff --git a/src/portal/ptl_remote_desktop.rs b/src/portal/ptl_remote_desktop.rs deleted file mode 100644 index 0cb465d9..00000000 --- a/src/portal/ptl_remote_desktop.rs +++ /dev/null @@ -1,328 +0,0 @@ -mod remote_desktop_gui; - -use { - crate::{ - dbus::{DbusObject, PendingReply, prelude::Variant}, - ifs::jay_compositor::CREATE_EI_SESSION_SINCE, - portal::{ - PORTAL_SUCCESS, PortalState, - ptl_display::{PortalDisplay, PortalDisplayId}, - ptl_remote_desktop::remote_desktop_gui::SelectionGui, - ptl_screencast::ScreencastPhase, - ptl_session::{PortalSession, PortalSessionReply}, - }, - utils::{ - clonecell::{CloneCell, UnsafeCellCloneSafe}, - copyhashmap::CopyHashMap, - }, - wire_dbus::{ - org, - org::freedesktop::impl_::portal::{ - remote_desktop::{ - ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply, - SelectDevices, SelectDevicesReply, Start, StartReply, - }, - session::CloseReply as SessionCloseReply, - }, - }, - wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner}, - }, - std::{cell::Cell, ops::Deref, rc::Rc}, - uapi::OwnedFd, -}; - -#[derive(Clone)] -pub enum RemoteDesktopPhase { - Init, - DevicesSelected, - Selecting(Rc), - Starting(Rc), - Started(Rc), - Terminated, -} - -unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {} - -pub struct SelectingDisplay { - pub session: Rc, - pub request_obj: Rc, - pub guis: CopyHashMap>, -} - -pub struct StartingRemoteDesktop { - pub session: Rc, - pub request_obj: Rc, - pub dpy: Rc, - pub ei_session: Rc, -} - -pub struct StartedRemoteDesktop { - pub session: Rc, - pub dpy: Rc, - pub ei_session: Rc, - pub ei_fd: Cell>>, -} - -bitflags! { - DeviceTypes: u32; - - KEYBOARD = 1, - POINTER = 2, - TOUCHSCREEN = 4, -} - -impl UsrJayEiSessionOwner for StartingRemoteDesktop { - fn created(&self, fd: &Rc) { - let started = Rc::new(StartedRemoteDesktop { - session: self.session.clone(), - dpy: self.dpy.clone(), - ei_session: self.ei_session.clone(), - ei_fd: Cell::new(Some(fd.clone())), - }); - self.session - .rd_phase - .set(RemoteDesktopPhase::Started(started.clone())); - started.ei_session.owner.set(Some(started.clone())); - if let ScreencastPhase::SourcesSelected(s) = self.session.sc_phase.get() { - self.session.screencast_restore( - &self.request_obj, - s.restore_data.take(), - Some(self.dpy.clone()), - ); - } else { - self.session.send_start_reply(None, None, None); - } - } - - fn failed(&self, reason: &str) { - log::error!("Could not create session: {}", reason); - self.session.reply_err(reason); - self.session.kill(); - } -} - -impl SelectingDisplay { - pub fn starting(&self, dpy: &Rc) { - let builder = dpy.jc.create_ei_session(); - builder.set_app_id(&self.session.app); - let ei_session = builder.commit(); - let starting = Rc::new(StartingRemoteDesktop { - session: self.session.clone(), - request_obj: self.request_obj.clone(), - dpy: dpy.clone(), - ei_session, - }); - self.session - .rd_phase - .set(RemoteDesktopPhase::Starting(starting.clone())); - starting.ei_session.owner.set(Some(starting.clone())); - dpy.sessions.set( - self.session.session_obj.path().to_owned(), - self.session.clone(), - ); - } -} - -impl PortalSession { - fn dbus_select_devices( - self: &Rc, - _req: SelectDevices, - reply: PendingReply>, - ) { - match self.rd_phase.get() { - RemoteDesktopPhase::Init => {} - _ => { - self.kill(); - reply.err("Devices have already been selected"); - return; - } - } - self.rd_phase.set(RemoteDesktopPhase::DevicesSelected); - reply.ok(&SelectDevicesReply { - response: PORTAL_SUCCESS, - results: Default::default(), - }); - } - - fn dbus_start_remote_desktop( - self: &Rc, - req: Start<'_>, - reply: PendingReply>, - ) { - match self.rd_phase.get() { - RemoteDesktopPhase::DevicesSelected => {} - _ => { - self.kill(); - reply.err("Session is not in the correct phase for starting"); - return; - } - } - let request_obj = match self.state.dbus.add_object(req.handle.to_string()) { - Ok(r) => r, - Err(_) => { - self.kill(); - reply.err("Request handle is not unique"); - return; - } - }; - { - use org::freedesktop::impl_::portal::request::*; - request_obj.add_method::({ - let slf = self.clone(); - move |_, pr| { - slf.kill(); - pr.ok(&CloseReply); - } - }); - } - let guis = CopyHashMap::new(); - for dpy in self.state.displays.lock().values() { - if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE { - guis.set(dpy.id, SelectionGui::new(self, dpy)); - } - } - if guis.is_empty() { - self.kill(); - reply.err("There are no running displays"); - return; - } - self.start_reply - .set(Some(PortalSessionReply::RemoteDesktop(reply))); - self.rd_phase - .set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay { - session: self.clone(), - request_obj: Rc::new(request_obj), - guis, - }))); - } - - fn dbus_connect_to_eis( - self: &Rc, - _req: ConnectToEIS, - reply: PendingReply, - ) { - let RemoteDesktopPhase::Started(started) = self.rd_phase.get() else { - self.kill(); - reply.err("Sources have already been selected"); - return; - }; - let Some(fd) = started.ei_fd.take() else { - self.kill(); - reply.err("EI file descriptor has already been consumed"); - return; - }; - reply.ok(&ConnectToEISReply { fd }); - } -} - -impl UsrJayEiSessionOwner for StartedRemoteDesktop { - fn destroyed(&self) { - self.session.kill(); - } -} - -pub(super) fn add_remote_desktop_dbus_members(state_: &Rc, object: &DbusObject) { - use org::freedesktop::impl_::portal::remote_desktop::*; - let state = state_.clone(); - object.add_method::(move |req, pr| { - dbus_create_session(&state, req, pr); - }); - let state = state_.clone(); - object.add_method::(move |req, pr| { - dbus_select_devices(&state, req, pr); - }); - let state = state_.clone(); - object.add_method::(move |req, pr| { - dbus_start(&state, req, pr); - }); - let state = state_.clone(); - object.add_method::(move |req, pr| { - dbus_connect_to_eis(&state, req, pr); - }); - object.set_property::(Variant::U32(DeviceTypes::all().0)); - object.set_property::(Variant::U32(2)); -} - -fn dbus_create_session( - state: &Rc, - req: CreateSession, - reply: PendingReply>, -) { - log::info!("Create remote desktop session {:#?}", req); - if state.sessions.contains(req.session_handle.0.deref()) { - reply.err("Session already exists"); - return; - } - let obj = match state.dbus.add_object(req.session_handle.0.to_string()) { - Ok(obj) => obj, - Err(_) => { - reply.err("Session path is not unique"); - return; - } - }; - let session = Rc::new(PortalSession { - _id: state.id(), - state: state.clone(), - pw_con: state.pw_con.clone(), - app: req.app_id.to_string(), - session_obj: obj, - sc_phase: CloneCell::new(ScreencastPhase::Init), - rd_phase: CloneCell::new(RemoteDesktopPhase::Init), - start_reply: Default::default(), - }); - { - use org::freedesktop::impl_::portal::session::*; - let ses = session.clone(); - session.session_obj.add_method::(move |_, pr| { - ses.kill(); - pr.ok(&SessionCloseReply); - }); - session.session_obj.set_property::(Variant::U32(2)); - } - state - .sessions - .set(req.session_handle.0.to_string(), session); - reply.ok(&CreateSessionReply { - response: PORTAL_SUCCESS, - results: Default::default(), - }); -} - -fn dbus_select_devices( - state: &Rc, - req: SelectDevices, - reply: PendingReply>, -) { - if let Some(s) = get_session(state, &reply, &req.session_handle.0) { - s.dbus_select_devices(req, reply); - } -} - -fn dbus_start(state: &Rc, req: Start, reply: PendingReply>) { - if let Some(s) = get_session(state, &reply, &req.session_handle.0) { - s.dbus_start_remote_desktop(req, reply); - } -} - -fn dbus_connect_to_eis( - state: &Rc, - req: ConnectToEIS, - reply: PendingReply, -) { - if let Some(s) = get_session(state, &reply, &req.session_handle.0) { - s.dbus_connect_to_eis(req, reply); - } -} - -fn get_session( - state: &Rc, - reply: &PendingReply, - handle: &str, -) -> Option> { - let res = state.sessions.get(handle); - if res.is_none() { - let msg = format!("Remote desktop session `{}` does not exist", handle); - reply.err(&msg); - } - res -} diff --git a/src/portal/ptl_remote_desktop/remote_desktop_gui.rs b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs deleted file mode 100644 index f9b52189..00000000 --- a/src/portal/ptl_remote_desktop/remote_desktop_gui.rs +++ /dev/null @@ -1,160 +0,0 @@ -use { - crate::{ - globals::GlobalName, - ifs::wl_seat::{BTN_LEFT, wl_pointer::PRESSED}, - portal::{ - ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, - ptl_remote_desktop::{PortalSession, RemoteDesktopPhase}, - ptr_gui::{ - Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, - OverlayWindowOwner, - }, - }, - theme::Color, - utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt}, - }, - std::rc::Rc, -}; - -const H_MARGIN: f32 = 30.0; -const V_MARGIN: f32 = 20.0; - -pub struct SelectionGui { - remote_desktop_session: Rc, - dpy: Rc, - surfaces: CopyHashMap>, -} - -pub struct SelectionGuiSurface { - gui: Rc, - output: Rc, - overlay: Rc, -} - -struct StaticButton { - surface: Rc, - role: ButtonRole, -} - -#[derive(Copy, Clone, Eq, PartialEq)] -enum ButtonRole { - Accept, - Reject, -} - -impl SelectionGui { - pub fn kill(&self, upwards: bool) { - for surface in self.surfaces.lock().drain_values() { - surface.overlay.data.kill(false); - } - if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.rd_phase.get() { - s.guis.remove(&self.dpy.id); - if upwards && s.guis.is_empty() { - self.remote_desktop_session.kill(); - } - } - } -} - -fn create_accept_gui(surface: &Rc) -> Rc { - let app = &surface.gui.remote_desktop_session.app; - let text = if app.is_empty() { - format!("An application wants to generate/monitor input") - } else { - format!("`{}` wants to generate/monitor input", app) - }; - let label = Rc::new(Label::default()); - *label.text.borrow_mut() = text; - let accept_button = static_button(surface, ButtonRole::Accept, "Allow"); - let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); - for button in [&accept_button, &reject_button] { - button.border_color.set(Color::from_gray_srgb(100)); - button.border.set(2.0); - button.padding.set(5.0); - } - accept_button.bg_color.set(Color::from_srgb(170, 200, 170)); - accept_button - .bg_hover_color - .set(Color::from_srgb(170, 255, 170)); - reject_button.bg_color.set(Color::from_srgb(200, 170, 170)); - reject_button - .bg_hover_color - .set(Color::from_srgb(255, 170, 170)); - let flow = Rc::new(Flow::default()); - flow.orientation.set(Orientation::Vertical); - flow.cross_align.set(Align::Center); - flow.in_margin.set(V_MARGIN); - flow.cross_margin.set(H_MARGIN); - *flow.elements.borrow_mut() = vec![label, accept_button, reject_button]; - flow -} - -impl OverlayWindowOwner for SelectionGuiSurface { - fn kill(&self, upwards: bool) { - self.gui.dpy.windows.remove(&self.overlay.data.surface.id); - self.gui.surfaces.remove(&self.output.global_id); - if upwards && self.gui.surfaces.is_empty() { - self.gui.kill(true); - } - } -} - -impl SelectionGui { - pub fn new(ss: &Rc, dpy: &Rc) -> Rc { - let gui = Rc::new(SelectionGui { - remote_desktop_session: ss.clone(), - dpy: dpy.clone(), - surfaces: Default::default(), - }); - for output in dpy.outputs.lock().values() { - let sgs = Rc::new(SelectionGuiSurface { - gui: gui.clone(), - output: output.clone(), - overlay: OverlayWindow::new(output), - }); - let element = create_accept_gui(&sgs); - sgs.overlay.data.content.set(Some(element)); - gui.dpy - .windows - .set(sgs.overlay.data.surface.id, sgs.overlay.data.clone()); - gui.surfaces.set(output.global_id, sgs); - } - gui - } -} - -impl ButtonOwner for StaticButton { - fn button(&self, _seat: &PortalSeat, button: u32, state: u32) { - if button != BTN_LEFT || state != PRESSED { - return; - } - match self.role { - ButtonRole::Accept => { - log::info!("User has accepted the request"); - let selecting = match self.surface.gui.remote_desktop_session.rd_phase.get() { - RemoteDesktopPhase::Selecting(selecting) => selecting, - _ => return, - }; - for gui in selecting.guis.lock().drain_values() { - gui.kill(false); - } - selecting.starting(&self.surface.output.dpy); - } - ButtonRole::Reject => { - log::info!("User has rejected the remote desktop request"); - self.surface.gui.remote_desktop_session.kill(); - } - } - } -} - -fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc