1
0
Fork 0
forked from wry/wry

Compare commits

..

6 commits
dpms ... master

77 changed files with 8378 additions and 433 deletions

View file

@ -77,6 +77,20 @@ You can also right-click any title in a container to toggle mono mode.
In mono mode, scroll over the title bar to cycle between windows in the In mono mode, scroll over the title bar to cycle between windows in the
container. container.
## Autotiling
Autotiling makes newly tiled windows alternate split direction from the focused
tiled window. The first split uses the containing group direction, then later
windows wrap the focused tile in the opposite direction, producing a horizontal,
vertical, horizontal pattern as the layout grows.
```toml
[shortcuts]
alt-a = "toggle-autotile"
```
Manual grouping and split commands still use the direction you request.
## Fullscreen ## Fullscreen
Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire Press `alt-u` (`toggle-fullscreen`) to make the focused window fill the entire

View file

@ -640,6 +640,22 @@ impl ConfigClient {
self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); self.send(&ClientMessage::SetWindowWorkspace { window, workspace });
} }
pub fn seat_send_to_scratchpad(&self, seat: Seat, name: &str) {
self.send(&ClientMessage::SeatSendToScratchpad { seat, name });
}
pub fn seat_toggle_scratchpad(&self, seat: Seat, name: &str) {
self.send(&ClientMessage::SeatToggleScratchpad { seat, name });
}
pub fn seat_cycle_scratchpad(&self, seat: Seat, name: &str) {
self.send(&ClientMessage::SeatCycleScratchpad { seat, name });
}
pub fn window_send_to_scratchpad(&self, window: Window, name: &str) {
self.send(&ClientMessage::WindowSendToScratchpad { window, name });
}
pub fn seat_split(&self, seat: Seat) -> Axis { pub fn seat_split(&self, seat: Seat) -> Axis {
let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat });
get_response!(res, Axis::Horizontal, GetSplit { axis }); get_response!(res, Axis::Horizontal, GetSplit { axis });
@ -1023,6 +1039,26 @@ impl ConfigClient {
self.send(&ClientMessage::SetUiDragThreshold { threshold }); 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) { pub fn set_color_management_enabled(&self, enabled: bool) {
self.send(&ClientMessage::SetColorManagementEnabled { enabled }); self.send(&ClientMessage::SetColorManagementEnabled { enabled });
} }
@ -1327,14 +1363,6 @@ impl ConfigClient {
self.send(&ClientMessage::SetIdle { timeout }) 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) { pub fn set_idle_grace_period(&self, period: Duration) {
self.send(&ClientMessage::SetIdleGracePeriod { period }) self.send(&ClientMessage::SetIdleGracePeriod { period })
} }
@ -2067,6 +2095,12 @@ impl ConfigClient {
self.send(&ClientMessage::SetAutotile { enabled }); self.send(&ClientMessage::SetAutotile { enabled });
} }
pub fn get_autotile(&self) -> bool {
let res = self.send_with_response(&ClientMessage::GetAutotile);
get_response!(res, false, GetAutotile { enabled });
enabled
}
pub fn set_tab_title_align(&self, align: u32) { pub fn set_tab_title_align(&self, align: u32) {
self.send(&ClientMessage::SetTabTitleAlign { align }); self.send(&ClientMessage::SetTabTitleAlign { align });
} }

View file

@ -286,6 +286,18 @@ pub enum ClientMessage<'a> {
seat: Seat, seat: Seat,
workspace: Workspace, workspace: Workspace,
}, },
SeatSendToScratchpad {
seat: Seat,
name: &'a str,
},
SeatToggleScratchpad {
seat: Seat,
name: &'a str,
},
SeatCycleScratchpad {
seat: Seat,
name: &'a str,
},
GetTimer { GetTimer {
name: &'a str, name: &'a str,
}, },
@ -475,12 +487,6 @@ pub enum ClientMessage<'a> {
SetIdle { SetIdle {
timeout: Duration, timeout: Duration,
}, },
SetKeyPressEnablesDpms {
enabled: bool,
},
SetMouseMoveEnablesDpms {
enabled: bool,
},
MoveToOutput { MoveToOutput {
workspace: WorkspaceSource, workspace: WorkspaceSource,
connector: Connector, connector: Connector,
@ -551,6 +557,24 @@ pub enum ClientMessage<'a> {
SetUiDragThreshold { SetUiDragThreshold {
threshold: i32, 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 { SetXScalingMode {
mode: XScalingMode, mode: XScalingMode,
}, },
@ -675,6 +699,10 @@ pub enum ClientMessage<'a> {
window: Window, window: Window,
workspace: Workspace, workspace: Workspace,
}, },
WindowSendToScratchpad {
window: Window,
name: &'a str,
},
SetWindowFullscreen { SetWindowFullscreen {
window: Window, window: Window,
fullscreen: bool, fullscreen: bool,
@ -911,6 +939,7 @@ pub enum ClientMessage<'a> {
SetAutotile { SetAutotile {
enabled: bool, enabled: bool,
}, },
GetAutotile,
SetTabTitleAlign { SetTabTitleAlign {
align: u32, align: u32,
}, },
@ -1177,6 +1206,9 @@ pub enum Response {
GetCornerRadius { GetCornerRadius {
radius: f32, radius: f32,
}, },
GetAutotile {
enabled: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -466,6 +466,33 @@ impl Seat {
get!().set_seat_workspace(self, workspace) get!().set_seat_workspace(self, workspace)
} }
/// Sends the currently focused window to a scratchpad.
///
/// Use an empty string for the default scratchpad.
pub fn send_to_scratchpad(self, name: &str) {
get!().seat_send_to_scratchpad(self, name)
}
/// Toggles a scratchpad.
///
/// If the scratchpad has a visible window, that window is hidden. Otherwise, the
/// most recently hidden window in the scratchpad is shown on the current workspace.
/// Scratchpad windows are always shown floating.
/// Use an empty string for the default scratchpad.
pub fn toggle_scratchpad(self, name: &str) {
get!().seat_toggle_scratchpad(self, name)
}
/// Cycles through the windows of a scratchpad, one at a time.
///
/// With nothing shown, the first window is brought up; each further invocation
/// hides the current window and shows the next; after the last window the
/// scratchpad is hidden again. Scratchpad windows are always shown floating.
/// Use an empty string for the default scratchpad.
pub fn cycle_scratchpad(self, name: &str) {
get!().seat_cycle_scratchpad(self, name)
}
/// Toggles whether the currently focused window is fullscreen. /// Toggles whether the currently focused window is fullscreen.
pub fn toggle_fullscreen(self) { pub fn toggle_fullscreen(self) {
let c = get!(); let c = get!();

View file

@ -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. /// Exits the compositor.
pub fn quit() { pub fn quit() {
get!().quit() get!().quit()
@ -252,20 +273,6 @@ pub fn set_idle(timeout: Option<Duration>) {
get!().set_idle(timeout.unwrap_or_default()) 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. /// Configures the idle grace period.
/// ///
/// The grace period starts after the idle timeout expires. During the grace period, the /// The grace period starts after the idle timeout expires. During the grace period, the
@ -301,6 +308,42 @@ pub fn set_ui_drag_threshold(threshold: i32) {
get!().set_ui_drag_threshold(threshold); 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. /// Enables or disables the color-management protocol.
/// ///
/// The default is `false`. /// The default is `false`.
@ -410,14 +453,21 @@ pub fn get_corner_radius() -> f32 {
/// Enables or disables autotiling. /// Enables or disables autotiling.
/// ///
/// When enabled, new windows are automatically placed in a perpendicular /// When enabled, newly tiled windows alternate split orientation from the
/// sub-container if the predicted body would be narrower than tall (or vice versa). /// focused tiled window: the first split uses the containing group's direction,
/// then subsequent splits wrap the focused window in the perpendicular
/// direction.
/// ///
/// The default is `false`. /// The default is `false`.
pub fn set_autotile(enabled: bool) { pub fn set_autotile(enabled: bool) {
get!().set_autotile(enabled) get!().set_autotile(enabled)
} }
/// Returns whether autotiling is enabled.
pub fn get_autotile() -> bool {
get!(false).get_autotile()
}
/// Sets the horizontal alignment of title text within tab buttons. /// Sets the horizontal alignment of title text within tab buttons.
/// ///
/// - `"start"` — left-aligned (default) /// - `"start"` — left-aligned (default)

View file

@ -205,6 +205,13 @@ impl Window {
get!().set_window_workspace(self, workspace) get!().set_window_workspace(self, workspace)
} }
/// Sends the window to a scratchpad.
///
/// Use an empty string for the default scratchpad.
pub fn send_to_scratchpad(self, name: &str) {
get!().window_send_to_scratchpad(self, name)
}
/// Toggles whether the currently focused window is fullscreen. /// Toggles whether the currently focused window is fullscreen.
pub fn toggle_fullscreen(self) { pub fn toggle_fullscreen(self) {
self.set_fullscreen(!self.fullscreen()) self.set_fullscreen(!self.fullscreen())

1233
src/animation.rs Normal file

File diff suppressed because it is too large Load diff

3405
src/animation/multiphase.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@ mod color;
mod color_management; mod color_management;
mod config; mod config;
mod damage_tracking; mod damage_tracking;
mod dpms;
mod duration; mod duration;
mod generate; mod generate;
mod idle; mod idle;
@ -86,8 +85,6 @@ pub enum Cmd {
Screenshot(ScreenshotArgs), Screenshot(ScreenshotArgs),
/// Inspect/modify the idle (screensaver) settings. /// Inspect/modify the idle (screensaver) settings.
Idle(IdleArgs), Idle(IdleArgs),
/// Turn monitors on or off.
Dpms(DpmsArgs),
/// Run a privileged program. /// Run a privileged program.
RunPrivileged(RunPrivilegedArgs), RunPrivileged(RunPrivilegedArgs),
/// Run a program with a connection tag. /// Run a program with a connection tag.
@ -134,19 +131,6 @@ pub struct IdleArgs {
pub command: Option<IdleCmd>, pub command: Option<IdleCmd>,
} }
#[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)] #[derive(Args, Debug)]
pub struct RunPrivilegedArgs { pub struct RunPrivilegedArgs {
/// The program to run /// The program to run
@ -266,7 +250,6 @@ pub fn main() {
Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a), Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a),
Cmd::Screenshot(a) => screenshot::main(cli.global, a), Cmd::Screenshot(a) => screenshot::main(cli.global, a),
Cmd::Idle(a) => idle::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::Unlock => unlock::main(cli.global),
Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a),
Cmd::RunTagged(a) => run_tagged::main(cli.global, a), Cmd::RunTagged(a) => run_tagged::main(cli.global, a),

View file

@ -1,23 +0,0 @@
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<ToolClient>, 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;
}

View file

@ -279,14 +279,11 @@ fn start_compositor2(
change: Default::default(), change: Default::default(),
timeout: Cell::new(Duration::from_secs(10 * 60)), timeout: Cell::new(Duration::from_secs(10 * 60)),
grace_period: Cell::new(Duration::from_secs(5)), 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(), timeout_changed: Default::default(),
inhibitors: Default::default(), inhibitors: Default::default(),
inhibitors_changed: Default::default(), inhibitors_changed: Default::default(),
inhibited_idle_notifications: Default::default(), inhibited_idle_notifications: Default::default(),
backend_idle: Cell::new(true), backend_idle: Cell::new(true),
dpms_off_by_command: Cell::new(false),
in_grace_period: Cell::new(false), in_grace_period: Cell::new(false),
}, },
run_args, run_args,
@ -363,6 +360,13 @@ fn start_compositor2(
cpu_worker, cpu_worker,
ui_drag_enabled: Cell::new(true), ui_drag_enabled: Cell::new(true),
ui_drag_threshold_squared: Cell::new(10), 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(), toplevels: Default::default(),
const_40hz_latch: Default::default(), const_40hz_latch: Default::default(),
tray_item_ids: Default::default(), tray_item_ids: Default::default(),
@ -399,6 +403,7 @@ fn start_compositor2(
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
virtual_outputs: Default::default(), virtual_outputs: Default::default(),
clean_logs_older_than: Default::default(), clean_logs_older_than: Default::default(),
scratchpads: Default::default(),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);

View file

@ -658,17 +658,23 @@ impl ConfigProxyHandler {
} }
fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.move_focused(direction.into()); let seat = self.get_seat(seat)?;
Ok(()) seat.move_focused(direction.into());
Ok(())
})
} }
fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> {
let window = self.get_window(window)?; self.state.with_layout_animations(|| {
if let Some(c) = toplevel_parent_container(&*window) { let window = self.get_window(window)?;
c.move_child(window, direction.into()); if let Some(float) = window.tl_data().float.get() {
} float.move_by_direction(direction.into());
Ok(()) } 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> { 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)); 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( fn handle_set_direct_scanout_enabled(
&self, &self,
device: Option<DrmDevice>, device: Option<DrmDevice>,
@ -1069,6 +1100,32 @@ impl ConfigProxyHandler {
Ok(()) Ok(())
} }
fn handle_seat_send_to_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() {
self.state.send_to_scratchpad(name, toplevel);
}
Ok(())
})
}
fn handle_seat_toggle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
self.state.toggle_scratchpad(&seat, name);
Ok(())
})
}
fn handle_seat_cycle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
self.state.cycle_scratchpad(&seat, name);
Ok(())
})
}
fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> {
let window = self.get_window(window)?; let window = self.get_window(window)?;
let name = self.get_workspace(ws)?; let name = self.get_workspace(ws)?;
@ -1083,6 +1140,14 @@ impl ConfigProxyHandler {
Ok(()) Ok(())
} }
fn handle_window_send_to_scratchpad(&self, window: Window, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let window = self.get_window(window)?;
self.state.send_to_scratchpad(name, window);
Ok(())
})
}
fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?; let dev = self.get_device_handler_data(device)?;
let name = dev.device.name(); let name = dev.device.name();
@ -1134,14 +1199,6 @@ impl ConfigProxyHandler {
self.state.idle.set_timeout(&self.state, timeout); 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) { fn handle_set_idle_grace_period(&self, period: Duration) {
self.state.idle.set_grace_period(&self.state, period); self.state.idle.set_grace_period(&self.state, period);
} }
@ -1732,9 +1789,11 @@ impl ConfigProxyHandler {
} }
fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> { fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.set_mono(mono); let seat = self.get_seat(seat)?;
Ok(()) seat.set_mono(mono);
Ok(())
})
} }
fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> { fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> {
@ -1748,11 +1807,13 @@ impl ConfigProxyHandler {
} }
fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> { fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> {
let window = self.get_window(window)?; self.state.with_layout_animations(|| {
if let Some(c) = toplevel_parent_container(&*window) { let window = self.get_window(window)?;
c.set_mono(mono.then_some(window.as_ref())); if let Some(c) = toplevel_parent_container(&*window) {
} c.set_mono(mono.then_some(window.as_ref()));
Ok(()) }
Ok(())
})
} }
fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> {
@ -1767,15 +1828,19 @@ impl ConfigProxyHandler {
} }
fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.set_split(axis.into()); let seat = self.get_seat(seat)?;
Ok(()) seat.set_split(axis.into());
Ok(())
})
} }
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> { fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.toggle_tab(); let seat = self.get_seat(seat)?;
Ok(()) seat.toggle_tab();
Ok(())
})
} }
fn handle_seat_make_group( fn handle_seat_make_group(
@ -1784,27 +1849,35 @@ impl ConfigProxyHandler {
axis: Axis, axis: Axis,
ephemeral: bool, ephemeral: bool,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.make_group(axis.into(), ephemeral); let seat = self.get_seat(seat)?;
Ok(()) seat.make_group(axis.into(), ephemeral);
Ok(())
})
} }
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> { fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.change_group_opposite(); let seat = self.get_seat(seat)?;
Ok(()) seat.change_group_opposite();
Ok(())
})
} }
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> { fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.equalize(recursive); let seat = self.get_seat(seat)?;
Ok(()) seat.equalize(recursive);
Ok(())
})
} }
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> { fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_layout_animations(|| {
seat.move_tab(right); let seat = self.get_seat(seat)?;
Ok(()) seat.move_tab(right);
Ok(())
})
} }
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
@ -1819,11 +1892,13 @@ impl ConfigProxyHandler {
} }
fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> {
let window = self.get_window(window)?; self.state.with_layout_animations(|| {
if let Some(c) = toplevel_parent_container(&*window) { let window = self.get_window(window)?;
c.set_split(axis.into()); if let Some(c) = toplevel_parent_container(&*window) {
} c.set_split(axis.into());
Ok(()) }
Ok(())
})
} }
fn handle_add_shortcut( fn handle_add_shortcut(
@ -1963,9 +2038,11 @@ impl ConfigProxyHandler {
} }
fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?; self.state.with_linear_layout_animations(|| {
seat.set_floating(floating); let seat = self.get_seat(seat)?;
Ok(()) seat.set_floating(floating);
Ok(())
})
} }
fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> {
@ -1977,9 +2054,11 @@ impl ConfigProxyHandler {
} }
fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> {
let window = self.get_window(window)?; self.state.with_linear_layout_animations(|| {
toplevel_set_floating(&self.state, window, floating); let window = self.get_window(window)?;
Ok(()) toplevel_set_floating(&self.state, window, floating);
Ok(())
})
} }
fn handle_add_pollable(self: &Rc<Self>, fd: i32) -> Result<(), CphError> { fn handle_add_pollable(self: &Rc<Self>, fd: i32) -> Result<(), CphError> {
@ -2729,8 +2808,10 @@ impl ConfigProxyHandler {
dx2: i32, dx2: i32,
dy2: i32, dy2: i32,
) -> Result<(), CphError> { ) -> Result<(), CphError> {
self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2); self.state.with_layout_animations(|| {
Ok(()) self.get_window(window)?.tl_resize(dx1, dy1, dx2, dy2);
Ok(())
})
} }
fn handle_window_exists(&self, window: Window) { fn handle_window_exists(&self, window: Window) {
@ -2942,6 +3023,15 @@ impl ConfigProxyHandler {
ClientMessage::SetSeatWorkspace { seat, workspace } => self ClientMessage::SetSeatWorkspace { seat, workspace } => self
.handle_set_seat_workspace(seat, workspace) .handle_set_seat_workspace(seat, workspace)
.wrn("set_seat_workspace")?, .wrn("set_seat_workspace")?,
ClientMessage::SeatSendToScratchpad { seat, name } => self
.handle_seat_send_to_scratchpad(seat, name)
.wrn("seat_send_to_scratchpad")?,
ClientMessage::SeatToggleScratchpad { seat, name } => self
.handle_seat_toggle_scratchpad(seat, name)
.wrn("seat_toggle_scratchpad")?,
ClientMessage::SeatCycleScratchpad { seat, name } => self
.handle_seat_cycle_scratchpad(seat, name)
.wrn("seat_cycle_scratchpad")?,
ClientMessage::GetConnector { ty, idx } => { ClientMessage::GetConnector { ty, idx } => {
self.handle_get_connector(ty, idx).wrn("get_connector")? self.handle_get_connector(ty, idx).wrn("get_connector")?
} }
@ -3137,12 +3227,6 @@ impl ConfigProxyHandler {
.handle_get_input_device_devnode(device) .handle_get_input_device_devnode(device)
.wrn("get_input_device_devnode")?, .wrn("get_input_device_devnode")?,
ClientMessage::SetIdle { timeout } => self.handle_set_idle(timeout), 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 { ClientMessage::MoveToOutput {
workspace, workspace,
connector, connector,
@ -3207,6 +3291,17 @@ impl ConfigProxyHandler {
ClientMessage::SetUiDragThreshold { threshold } => { ClientMessage::SetUiDragThreshold { threshold } => {
self.handle_set_ui_drag_threshold(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 ClientMessage::SetXScalingMode { mode } => self
.handle_set_x_scaling_mode(mode) .handle_set_x_scaling_mode(mode)
.wrn("set_x_scaling_mode")?, .wrn("set_x_scaling_mode")?,
@ -3321,6 +3416,9 @@ impl ConfigProxyHandler {
ClientMessage::SetWindowWorkspace { window, workspace } => self ClientMessage::SetWindowWorkspace { window, workspace } => self
.handle_set_window_workspace(window, workspace) .handle_set_window_workspace(window, workspace)
.wrn("set_window_workspace")?, .wrn("set_window_workspace")?,
ClientMessage::WindowSendToScratchpad { window, name } => self
.handle_window_send_to_scratchpad(window, name)
.wrn("window_send_to_scratchpad")?,
ClientMessage::SetWindowFullscreen { window, fullscreen } => self ClientMessage::SetWindowFullscreen { window, fullscreen } => self
.handle_set_window_fullscreen(window, fullscreen) .handle_set_window_fullscreen(window, fullscreen)
.wrn("set_window_fullscreen")?, .wrn("set_window_fullscreen")?,
@ -3535,6 +3633,11 @@ impl ConfigProxyHandler {
ClientMessage::SetAutotile { enabled } => { ClientMessage::SetAutotile { enabled } => {
self.state.theme.autotile_enabled.set(enabled); self.state.theme.autotile_enabled.set(enabled);
} }
ClientMessage::GetAutotile => {
self.respond(Response::GetAutotile {
enabled: self.state.theme.autotile_enabled.get(),
});
}
ClientMessage::SeatToggleExpand { .. } => { ClientMessage::SeatToggleExpand { .. } => {
// Removed feature; kept for binary protocol compatibility. // Removed feature; kept for binary protocol compatibility.
} }

View file

@ -1,6 +1,5 @@
use { use {
crate::{ crate::{
backend::transaction::BackendConnectorTransactionError,
client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId}, client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId},
compositor::LogLevel, compositor::LogLevel,
globals::{Global, GlobalName}, globals::{Global, GlobalName},
@ -79,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal { impl Global for JayCompositorGlobal {
fn version(&self) -> u32 { fn version(&self) -> u32 {
31 30
} }
fn required_caps(&self) -> ClientCaps { fn required_caps(&self) -> ClientCaps {
@ -543,14 +542,6 @@ impl JayCompositorRequestHandler for JayCompositor {
}); });
Ok(()) Ok(())
} }
fn set_dpms(&self, req: SetDpms, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.client
.state
.set_dpms_active(req.active != 0)
.map_err(JayCompositorError::SetDpms)?;
Ok(())
}
} }
object_base! { object_base! {
@ -568,7 +559,5 @@ pub enum JayCompositorError {
ClientError(Box<ClientError>), ClientError(Box<ClientError>),
#[error("Unknown log level {0}")] #[error("Unknown log level {0}")]
UnknownLogLevel(u32), UnknownLogLevel(u32),
#[error("Could not set DPMS state")]
SetDpms(#[source] BackendConnectorTransactionError),
} }
efrom!(JayCompositorError, ClientError); efrom!(JayCompositorError, ClientError);

View file

@ -936,6 +936,9 @@ impl WlSeatGlobal {
{ {
c.move_child(tl, direction); c.move_child(tl, direction);
self.maybe_schedule_warp_mouse_to_focus(); 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();
} }
} }

View file

@ -1520,25 +1520,25 @@ impl WlSurface {
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds()); let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
let pos = self.buffer_abs_pos.get(); let pos = self.buffer_abs_pos.get();
let apply_damage = |pos: Rect| { let apply_damage = |pos: Rect| {
if pending.damage_full { let clip_damage = |mut damage: Rect| {
let mut damage = pos; damage = damage.intersect(pos);
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
damage = damage.intersect(bounds); damage = damage.intersect(bounds);
} }
self.client.state.damage(damage); damage
};
if pending.damage_full {
self.client.state.damage(clip_damage(pos));
} else { } else {
let matrix = self.damage_matrix.get(); let matrix = self.damage_matrix.get();
if let Some(buffer) = self.buffer.get() { if let Some(buffer) = self.buffer.get() {
for damage in &pending.buffer_damage { for damage in &pending.buffer_damage {
let mut damage = matrix.apply( let damage = matrix.apply(
pos.x1(), pos.x1(),
pos.y1(), pos.y1(),
damage.intersect(buffer.buffer.buf.rect), damage.intersect(buffer.buffer.buf.rect),
); );
if let Some(bounds) = bounds { self.client.state.damage(clip_damage(damage));
damage = damage.intersect(bounds);
}
self.client.state.damage(damage);
} }
} }
for damage in &pending.surface_damage { for damage in &pending.surface_damage {
@ -1550,8 +1550,7 @@ impl WlSurface {
let y2 = (damage.y2() + scale - 1) / scale; let y2 = (damage.y2() + scale - 1) / scale;
damage = Rect::new_saturating(x1, y1, x2, y2); damage = Rect::new_saturating(x1, y1, x2, y2);
} }
damage = damage.intersect(bounds.unwrap_or(pos)); self.client.state.damage(clip_damage(damage));
self.client.state.damage(damage);
} }
} }
}; };

View file

@ -628,6 +628,11 @@ fn schedule_async_upload(
{ {
back_tex_opt = None; 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 = || { let damage_full = || {
back.damage.clear(); back.damage.clear();
back.damage.damage(slice::from_ref(&buf.rect)); back.damage.damage(slice::from_ref(&buf.rect));

View file

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
ifs::wl_surface::{ ifs::wl_surface::{
SurfaceExt, WlSurface, WlSurfaceError, PendingState, SurfaceExt, WlSurface, WlSurfaceError,
x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow}, x_surface::{xwayland_surface_v1::XwaylandSurfaceV1, xwindow::Xwindow},
}, },
leaks::Tracker, leaks::Tracker,
@ -30,6 +30,22 @@ impl SurfaceExt for XSurface {
win.node_layer() win.node_layer()
} }
fn before_apply_commit(
self: Rc<Self>,
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<Self>) { fn after_apply_commit(self: Rc<Self>) {
if let Some(xwindow) = self.xwindow.get() { if let Some(xwindow) = self.xwindow.get() {
xwindow.map_status_changed(); xwindow.map_status_changed();
@ -45,6 +61,7 @@ impl SurfaceExt for XSurface {
} }
self.surface.unset_ext(); self.surface.unset_ext();
if let Some(xwindow) = self.xwindow.take() { if let Some(xwindow) = self.xwindow.take() {
xwindow.queue_spawn_out();
xwindow.tl_destroy(); xwindow.tl_destroy();
xwindow.data.window.set(None); xwindow.data.window.set(None);
xwindow.data.surface_id.set(None); xwindow.data.surface_id.set(None);

View file

@ -1,5 +1,6 @@
use { use {
crate::{ crate::{
animation::RetainedToplevel,
client::Client, client::Client,
cursor::KnownCursor, cursor::KnownCursor,
fixed::Fixed, fixed::Fixed,
@ -252,6 +253,11 @@ impl Xwindow {
self.x.surface.buffer.is_some() && self.data.info.mapped.get() 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 { fn map_change(&self) -> Change {
match (self.may_be_mapped(), self.is_mapped()) { match (self.may_be_mapped(), self.is_mapped()) {
(true, false) => Change::Map, (true, false) => Change::Map,
@ -274,6 +280,7 @@ impl Xwindow {
match map_change { match map_change {
Change::None => return, Change::None => return,
Change::Unmap => { Change::Unmap => {
self.queue_spawn_out();
self.data self.data
.info .info
.pending_extents .pending_extents
@ -514,6 +521,10 @@ impl ToplevelNodeBase for Xwindow {
Some(self.x.surface.clone()) Some(self.x.surface.clone())
} }
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
RetainedToplevel::capture_surface(&self.x.surface, (0, 0))
}
fn tl_admits_children(&self) -> bool { fn tl_admits_children(&self) -> bool {
false false
} }

View file

@ -226,6 +226,10 @@ pub trait XdgSurfaceExt: Debug {
// nothing // nothing
} }
fn prepare_unmap(&self) {
// nothing
}
fn extents_changed(&self) { fn extents_changed(&self) {
// nothing // nothing
} }
@ -664,6 +668,15 @@ impl SurfaceExt for XdgSurface {
if let Some(serial) = pending.serial.take() { if let Some(serial) = pending.serial.take() {
self.applied_serial.set(serial); 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(()) Ok(())
} }

View file

@ -2,6 +2,7 @@ pub mod xdg_dialog_v1;
use { use {
crate::{ crate::{
animation::RetainedToplevel,
bugs, bugs,
bugs::Bugs, bugs::Bugs,
client::{Client, ClientError}, client::{Client, ClientError},
@ -259,6 +260,7 @@ impl XdgToplevelRequestHandler for XdgToplevel {
type Error = XdgToplevelError; type Error = XdgToplevelError;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> { fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.queue_spawn_out();
self.tl_destroy(); self.tl_destroy();
self.xdg.unset_ext(); self.xdg.unset_ext();
{ {
@ -398,6 +400,11 @@ impl XdgToplevelRequestHandler for XdgToplevel {
} }
impl XdgToplevel { impl XdgToplevel {
fn queue_spawn_out(&self) {
self.toplevel_data
.queue_spawn_out(self, self.tl_animation_snapshot());
}
fn map( fn map(
self: &Rc<Self>, self: &Rc<Self>,
parent: Option<&XdgToplevel>, parent: Option<&XdgToplevel>,
@ -779,6 +786,11 @@ impl ToplevelNodeBase for XdgToplevel {
Some(self.xdg.surface.clone()) Some(self.xdg.surface.clone())
} }
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
let geo = self.xdg.geometry();
RetainedToplevel::capture_surface(&self.xdg.surface, (-geo.x1(), -geo.y1()))
}
fn tl_restack_popups(&self) { fn tl_restack_popups(&self) {
self.xdg.restack_popups(); self.xdg.restack_popups();
} }
@ -818,6 +830,10 @@ impl XdgSurfaceExt for XdgToplevel {
self.after_commit(None); self.after_commit(None);
} }
fn prepare_unmap(&self) {
self.queue_spawn_out();
}
fn extents_changed(&self) { fn extents_changed(&self) {
self.toplevel_data.pos.set(self.xdg.extents.get()); self.toplevel_data.pos.set(self.xdg.extents.get());
self.tl_extents_changed(); self.tl_extents_changed();

View file

@ -284,6 +284,27 @@ impl TestConfig {
}) })
} }
pub fn send_to_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
self.send(ClientMessage::SeatSendToScratchpad {
seat: Seat(seat.raw() as _),
name,
})
}
pub fn toggle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
self.send(ClientMessage::SeatToggleScratchpad {
seat: Seat(seat.raw() as _),
name,
})
}
pub fn cycle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
self.send(ClientMessage::SeatCycleScratchpad {
seat: Seat(seat.raw() as _),
name,
})
}
fn clear(&self) { fn clear(&self) {
unsafe { unsafe {
if let Some(srv) = self.srv.take() { if let Some(srv) = self.srv.take() {
@ -331,6 +352,10 @@ impl TestConfig {
pub fn set_show_titles(&self, show: bool) -> TestResult { pub fn set_show_titles(&self, show: bool) -> TestResult {
self.send(ClientMessage::SetShowTitles { show }) self.send(ClientMessage::SetShowTitles { show })
} }
pub fn set_autotile(&self, enabled: bool) -> TestResult {
self.send(ClientMessage::SetAutotile { enabled })
}
} }
impl Drop for TestConfig { impl Drop for TestConfig {

View file

@ -29,6 +29,17 @@ impl TestViewport {
Ok(()) Ok(())
} }
pub fn unset_source(&self) -> Result<(), TestError> {
self.tran.send(SetSource {
self_id: self.id,
x: Fixed::from_int(-1),
y: Fixed::from_int(-1),
width: Fixed::from_int(-1),
height: Fixed::from_int(-1),
})?;
Ok(())
}
pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> { pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> {
self.tran.send(SetDestination { self.tran.send(SetDestination {
self_id: self.id, self_id: self.id,
@ -37,6 +48,15 @@ impl TestViewport {
})?; })?;
Ok(()) Ok(())
} }
pub fn unset_destination(&self) -> Result<(), TestError> {
self.tran.send(SetDestination {
self_id: self.id,
width: -1,
height: -1,
})?;
Ok(())
}
} }
impl Drop for TestViewport { impl Drop for TestViewport {

View file

@ -85,6 +85,8 @@ mod t0051_pointer_warp;
mod t0052_bar; mod t0052_bar;
mod t0053_theme; mod t0053_theme;
mod t0054_subsurface_already_attached; mod t0054_subsurface_already_attached;
mod t0055_autotiling;
mod t0055_scratchpad;
pub trait TestCase: Sync { pub trait TestCase: Sync {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
@ -158,5 +160,7 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
t0052_bar, t0052_bar,
t0053_theme, t0053_theme,
t0054_subsurface_already_attached, t0054_subsurface_already_attached,
t0055_autotiling,
t0055_scratchpad,
} }
} }

View file

@ -1,7 +1,6 @@
use { use {
crate::{ crate::{
it::{test_error::TestError, testrun::TestRun}, it::{test_error::TestError, testrun::TestRun},
rect::Rect,
tree::Node, tree::Node,
}, },
std::rc::Rc, std::rc::Rc,
@ -11,29 +10,19 @@ testcase!();
/// Create and map a single surface /// Create and map a single surface
async fn test(run: Rc<TestRun>) -> Result<(), TestError> { async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
run.backend.install_default()?; let ds = run.create_default_setup().await?;
let client = run.create_client().await?; let client = run.create_client().await?;
let window = client.create_window().await?; let window = client.create_window().await?;
window.map().await?; window.map().await?;
tassert_eq!(window.tl.core.width.get(), 800); let workspace_rect = ds.output.workspace_rect.get();
tassert_eq!(
window.tl.core.height.get(),
600 - 2 * run.state.theme.title_plus_underline_height()
);
tassert_eq!( tassert_eq!(window.tl.core.width.get(), workspace_rect.width());
window.tl.server.node_absolute_position(), tassert_eq!(window.tl.core.height.get(), workspace_rect.height());
Rect::new_sized(
0, tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect);
2 * run.state.theme.title_plus_underline_height(),
window.tl.core.width.get(),
window.tl.core.height.get(),
)
.unwrap()
);
Ok(()) Ok(())
} }

View file

@ -11,7 +11,7 @@ testcase!();
/// Create and map two surfaces /// Create and map two surfaces
async fn test(run: Rc<TestRun>) -> Result<(), TestError> { async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
run.backend.install_default()?; let ds = run.create_default_setup().await?;
let client = run.create_client().await?; let client = run.create_client().await?;
@ -21,17 +21,30 @@ async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
let window2 = client.create_window().await?; let window2 = client.create_window().await?;
window2.map().await?; window2.map().await?;
let otop = 2 * run.state.theme.title_plus_underline_height(); let workspace_rect = ds.output.workspace_rect.get();
let bw = run.state.theme.sizes.border_width.get(); let bw = run.state.theme.sizes.border_width.get();
let child_width = (workspace_rect.width() - bw) / 2;
tassert_eq!( tassert_eq!(
window.tl.server.node_absolute_position(), window.tl.server.node_absolute_position(),
Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap() Rect::new_sized(
workspace_rect.x1(),
workspace_rect.y1(),
child_width,
workspace_rect.height(),
)
.unwrap()
); );
tassert_eq!( tassert_eq!(
window2.tl.server.node_absolute_position(), window2.tl.server.node_absolute_position(),
Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap() Rect::new_sized(
workspace_rect.x1() + child_width + bw,
workspace_rect.y1(),
child_width,
workspace_rect.height(),
)
.unwrap()
); );
Ok(()) Ok(())

View file

@ -48,13 +48,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let mono_container = w_mono2.tl.container_parent()?; let mono_container = w_mono2.tl.container_parent()?;
let container_pos = mono_container.tl_data().pos.get(); let container_pos = mono_container.tl_data().pos.get();
let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0] let (tab_x, tab_y) = {
.move_(container_pos.x1(), container_pos.y1()); let tab_bar = mono_container.tab_bar.borrow();
ds.mouse.abs( let Some(tab_bar) = tab_bar.as_ref() else {
&ds.connector, bail!("no tab bar");
w_mono1_title.x1() as _, };
w_mono1_title.y1() as _, let w_mono1_title = &tab_bar.entries[0];
); (
container_pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
container_pos.y1() + tab_bar.height / 2,
)
};
ds.mouse.abs(&ds.connector, tab_x as _, tab_y as _);
client.sync().await; client.sync().await;
tassert!(enters.next().is_err()); tassert!(enters.next().is_err());

View file

@ -26,12 +26,18 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let container = w_mono2.tl.container_parent()?; let container = w_mono2.tl.container_parent()?;
let pos = container.tl_data().pos.get(); let pos = container.tl_data().pos.get();
let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1()); let (tab_x, tab_y) = {
ds.mouse.abs( let tab_bar = container.tab_bar.borrow();
&ds.connector, let Some(tab_bar) = tab_bar.as_ref() else {
w_mono1_title.x1() as f64, bail!("no tab bar");
w_mono1_title.y1() as f64, };
); let w_mono1_title = &tab_bar.entries[0];
(
pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
pos.y1() + tab_bar.height / 2,
)
};
ds.mouse.abs(&ds.connector, tab_x as f64, tab_y as f64);
client.sync().await; client.sync().await;
let enters = dss.kb.enter.expect()?; let enters = dss.kb.enter.expect()?;

View file

@ -2,7 +2,7 @@ use {
crate::{ crate::{
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED, ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
it::{ it::{
test_error::TestResult, test_error::{TestErrorExt, TestResult},
test_utils::{ test_utils::{
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt, test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
}, },
@ -10,7 +10,7 @@ use {
}, },
}, },
isnt::std_1::collections::IsntHashSetExt, isnt::std_1::collections::IsntHashSetExt,
std::rc::Rc, std::{rc::Rc, time::Duration},
}; };
testcase!(); testcase!();
@ -19,6 +19,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let ds = run.create_default_setup().await?; let ds = run.create_default_setup().await?;
let client = run.create_client().await?; let client = run.create_client().await?;
let default_seat = client.get_default_seat().await?;
let win1 = client.create_window().await?; let win1 = client.create_window().await?;
win1.set_color(255, 0, 0, 255); win1.set_color(255, 0, 0, 255);
@ -44,5 +45,23 @@ async fn test(run: Rc<TestRun>) -> TestResult {
client.sync().await; client.sync().await;
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED)); tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
let leaves = default_seat.kb.leave.expect()?;
let enters = default_seat.kb.enter.expect()?;
run.cfg.set_idle(Duration::from_micros(100))?;
run.cfg.set_idle_grace_period(Duration::from_secs(0))?;
run.state.wheel.timeout(3).await?;
client.sync().await;
tassert!(win2.tl.core.states.borrow().contains(&STATE_SUSPENDED));
let leave = leaves.next().with_context(|| "no leave on suspend")?;
tassert_eq!(leave.surface, win2.surface.id);
ds.mouse.rel(1.0, 1.0);
client.sync().await;
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
let enter = enters.next().with_context(|| "no enter on restore")?;
tassert_eq!(enter.surface, win2.surface.id);
Ok(()) Ok(())
} }

View file

@ -308,9 +308,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let output_damage = connector_data.damage.borrow(); let output_damage = connector_data.damage.borrow();
tassert!(!output_damage.is_empty()); tassert!(!output_damage.is_empty());
// Buffer damage is transformed by the damage matrix which includes the surface position // The test window maps its 1x1 buffer through a viewport to the full window size.
// The buffer damage (0,0,1,1) should be transformed to surface coordinates let expected_buffer_damage = surface_pos;
let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1());
// Find the exact output damage that matches our expected buffer damage // Find the exact output damage that matches our expected buffer damage
let mut found_exact_buffer_damage = false; let mut found_exact_buffer_damage = false;
@ -331,10 +330,12 @@ async fn test(run: Rc<TestRun>) -> TestResult {
// Test 7: Check output damage from existing window's viewport (which already has scaling) // Test 7: Check output damage from existing window's viewport (which already has scaling)
connector_data.damage.borrow_mut().clear(); connector_data.damage.borrow_mut().clear();
// The existing window was created with create_surface_ext() which automatically creates a viewport // The existing window was created with create_surface_ext() which automatically creates a viewport.
// Let's verify that the viewport's existing scaling affects buffer damage correctly // Commit the viewport size change separately; that commit intentionally damages the old/new extents.
// First, let's modify the viewport scaling that already exists on the window window.surface.viewport.set_destination(150, 100)?;
window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100 window.surface.commit()?;
client.sync().await;
connector_data.damage.borrow_mut().clear();
// Add buffer damage to test viewport scaling coordinate transformation // Add buffer damage to test viewport scaling coordinate transformation
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
@ -346,8 +347,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let output_damage = connector_data.damage.borrow(); let output_damage = connector_data.damage.borrow();
tassert!(!output_damage.is_empty()); tassert!(!output_damage.is_empty());
// With viewporter scaling, the 1x1 buffer damage should scale to 150x100 // With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination.
// and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136) let surface_pos = window.surface.server.buffer_abs_pos.get();
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap(); let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
let expected_output_damage = let expected_output_damage =
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1()); expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
@ -402,8 +403,9 @@ async fn test(run: Rc<TestRun>) -> TestResult {
rotation_window.map().await?; rotation_window.map().await?;
client.sync().await; client.sync().await;
// Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions // Disable viewporter to rely purely on buffer dimensions.
rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter rotation_window.surface.viewport.unset_source()?;
rotation_window.surface.viewport.unset_destination()?;
// Use a rectangular buffer (4x2) so rotation has a visible geometric effect // Use a rectangular buffer (4x2) so rotation has a visible geometric effect
// Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer // Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer

View file

@ -0,0 +1,58 @@
use {
crate::{
it::{test_error::TestResult, testrun::TestRun},
tree::{ContainerSplit, Node, ToplevelNodeBase},
},
std::rc::Rc,
};
testcase!();
async fn test(run: Rc<TestRun>) -> TestResult {
run.backend.install_default()?;
run.cfg.set_autotile(true)?;
let client = run.create_client().await?;
let win1 = client.create_window().await?;
win1.map().await?;
let root = win1.tl.container_parent()?;
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
let win2 = client.create_window().await?;
win2.map().await?;
client.sync().await;
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
tassert_eq!(win1.tl.container_parent()?.node_id(), root.node_id());
tassert_eq!(win2.tl.container_parent()?.node_id(), root.node_id());
let win3 = client.create_window().await?;
win3.map().await?;
client.sync().await;
let v_group = win3.tl.container_parent()?;
tassert_eq!(root.split.get(), ContainerSplit::Horizontal);
tassert_eq!(v_group.split.get(), ContainerSplit::Vertical);
tassert_eq!(win2.tl.container_parent()?.node_id(), v_group.node_id());
let win4 = client.create_window().await?;
win4.map().await?;
client.sync().await;
let h_group = win4.tl.container_parent()?;
tassert_eq!(h_group.split.get(), ContainerSplit::Horizontal);
tassert_eq!(win3.tl.container_parent()?.node_id(), h_group.node_id());
let h_parent = match h_group
.tl_data()
.parent
.get()
.and_then(|p| p.node_into_container())
{
Some(parent) => parent,
None => bail!("autotile group does not have a container parent"),
};
tassert_eq!(h_parent.node_id(), v_group.node_id());
Ok(())
}

View file

@ -0,0 +1,107 @@
use {
crate::{
it::{test_error::TestResult, testrun::TestRun},
tree::{Node, ToplevelNodeBase},
},
std::rc::Rc,
};
testcase!();
async fn test(run: Rc<TestRun>) -> TestResult {
let ds = run.create_default_setup().await?;
let client = run.create_client().await?;
let win1 = client.create_window().await?;
win1.map2().await?;
let win2 = client.create_window().await?;
win2.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(win1.tl.server.node_visible());
tassert!(!win2.tl.server.node_visible());
run.cfg.show_workspace(ds.seat.id(), "2")?;
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(win2.tl.server.node_visible());
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2");
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(!win2.tl.server.node_visible());
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(win2.tl.server.node_visible());
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2");
run.cfg.show_workspace(ds.seat.id(), "3")?;
client.sync().await;
tassert!(!win2.tl.server.node_visible());
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(win2.tl.server.node_visible());
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3");
// Scratchpad windows are always shown floating.
tassert!(win2.tl.server.tl_data().parent_is_float.get());
// Park win2 again, then build a multi-window scratchpad and cycle it.
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(!win2.tl.server.node_visible());
// Build a three-window scratchpad. Each window is focused right after it is
// mapped, so sending the focused window parks them in a known order.
let cyc1 = client.create_window().await?;
cyc1.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
let cyc2 = client.create_window().await?;
cyc2.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
let cyc3 = client.create_window().await?;
cyc3.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// Nothing shown: cycle brings up the first window (insertion order: cyc1).
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// Scratchpad windows are always shown floating.
tassert!(cyc1.tl.server.tl_data().parent_is_float.get());
// Cycle advances one at a time.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(cyc3.tl.server.node_visible());
// On the final window, the next cycle hides everything.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// And it wraps back to the first window.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(cyc1.tl.server.node_visible());
Ok(())
}

View file

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

View file

@ -1,7 +1,11 @@
use { use {
crate::{ crate::{
animation::{
RetainedContent, RetainedExitFrame, RetainedExitLayer, RetainedSurface,
RetainedToplevel,
},
cmm::cmm_render_intent::RenderIntent, cmm::cmm_render_intent::RenderIntent,
gfx_api::{AcquireSync, AlphaMode, GfxApiOpt, ReleaseSync, SampleRect}, gfx_api::{AcquireSync, AlphaMode, BufferResv, GfxApiOpt, ReleaseSync, SampleRect},
ifs::wl_surface::{ ifs::wl_surface::{
SurfaceBuffer, WlSurface, SurfaceBuffer, WlSurface,
x_surface::xwindow::Xwindow, x_surface::xwindow::Xwindow,
@ -14,8 +18,8 @@ use {
state::State, state::State,
theme::{Color, CornerRadius}, theme::{Color, CornerRadius},
tree::{ tree::{
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData,
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar, ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
}, },
}, },
std::{ops::Deref, rc::Rc, slice}, std::{ops::Deref, rc::Rc, slice},
@ -200,14 +204,22 @@ impl Renderer<'_> {
self.render_workspace(&ws, x, y); 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 { macro_rules! render_stacked {
($stack:expr) => { ($stack:expr) => {
for stacked in $stack.iter() { for stacked in $stack.iter() {
if stacked.node_visible() { if stacked.node_visible() {
self.base.sync(); self.base.sync();
let pos = stacked.node_absolute_position(); let pos = stacked.node_absolute_position();
if pos.intersects(&opos) { let visual = self.state.animations.visual_rect(
let (x, y) = opos.translate(pos.x1(), pos.y1()); 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); stacked.node_render(self, x, y, None);
} }
} }
@ -215,6 +227,7 @@ impl Renderer<'_> {
}; };
} }
render_stacked!(self.state.root.stacked); 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 // Flush RoundedFillRect ops from container/float borders so they don't
// sort after (and render on top of) layer-shell CopyTexture ops. // sort after (and render on top of) layer-shell CopyTexture ops.
self.base.sync(); self.base.sync();
@ -453,6 +466,265 @@ impl Renderer<'_> {
.fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y); .fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y);
} }
fn presentation_child_body(
&self,
container: &ContainerNode,
child: &Rc<dyn ToplevelNode>,
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<dyn ToplevelNode>,
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<SampleRect>,
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<dyn BufferResv>),
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<dyn BufferResv>),
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) { pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) {
self.render_container_decorations(container, x, y); self.render_container_decorations(container, x, y);
@ -465,6 +737,7 @@ impl Renderer<'_> {
} }
} }
let mb = container.mono_body.get(); 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 { if self.state.theme.sizes.gap.get() != 0 {
let bw = self.state.theme.sizes.border_width.get(); let bw = self.state.theme.sizes.border_width.get();
let border_color = self.state.theme.colors.border.get(); let border_color = self.state.theme.colors.border.get();
@ -476,10 +749,10 @@ impl Renderer<'_> {
}; };
if !child.node.node_is_container() { if !child.node.node_is_container() {
let frame = Rect::new_sized_saturating( let frame = Rect::new_sized_saturating(
mb.x1() - bw, visual_mb.x1() - bw,
mb.y1() - bw, visual_mb.y1() - bw,
mb.width() + 2 * bw, visual_mb.width() + 2 * bw,
mb.height() + 2 * bw, visual_mb.height() + 2 * bw,
); );
self.render_rounded_frame( self.render_rounded_frame(
frame, frame,
@ -491,14 +764,17 @@ impl Renderer<'_> {
); );
} }
} }
let body = mb.move_(x, y); let body = visual_mb.move_(x, y);
let body = self.base.scale_rect(body); let content = container
let content = container.mono_content.get(); .mono_content
self.stretch = if content.width() != mb.width() || content.height() != mb.height() { .get()
Some(self.base.scale_point(mb.width(), mb.height())) .at_point(visual_mb.x1(), visual_mb.y1());
} else { self.stretch =
None 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() { if self.state.theme.sizes.gap.get() != 0 && !child.node.node_is_container() {
let cr = self.state.theme.corner_radius.get(); let cr = self.state.theme.corner_radius.get();
if !cr.is_zero() { if !cr.is_zero() {
@ -507,9 +783,16 @@ impl Renderer<'_> {
self.corner_radius = Some(inner_cr); self.corner_radius = Some(inner_cr);
} }
} }
child if !child.node.node_is_container() {
.node self.render_window_body_background(body);
.node_render(self, x + content.x1(), y + content.y1(), Some(&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.stretch = None;
self.corner_radius = None; self.corner_radius = None;
} else { } else {
@ -524,10 +807,13 @@ impl Renderer<'_> {
}; };
let cr = self.state.theme.corner_radius.get(); let cr = self.state.theme.corner_radius.get();
for child in container.children.iter() { for child in container.children.iter() {
let body = child.body.get(); let layout_body = child.body.get();
if body.x1() >= container.width.get() || body.y1() >= container.height.get() { if layout_body.x1() >= container.width.get()
|| layout_body.y1() >= container.height.get()
{
break; break;
} }
let body = self.presentation_child_body(container, &child.node, layout_body);
if gap != 0 { if gap != 0 {
let c = if child.border_color_is_focused.get() { let c = if child.border_color_is_focused.get() {
&focused_border_color &focused_border_color
@ -544,7 +830,7 @@ impl Renderer<'_> {
self.render_rounded_frame(frame, c, cr, bw, x, y); 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 = self.stretch =
if content.width() != body.width() || content.height() != body.height() { if content.width() != body.width() || content.height() != body.height() {
Some(self.base.scale_point(body.width(), body.height())) Some(self.base.scale_point(body.width(), body.height()))
@ -556,10 +842,16 @@ impl Renderer<'_> {
self.corner_radius = Some(inner_cr); self.corner_radius = Some(inner_cr);
} }
let body = body.move_(x, y); 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); let body = self.base.scale_rect(body);
child self.render_child_or_snapshot(
.node &child.node,
.node_render(self, x + content.x1(), y + content.y1(), Some(&body)); x + content.x1(),
y + content.y1(),
Some(&body),
);
self.stretch = None; self.stretch = None;
self.corner_radius = None; self.corner_radius = None;
} }
@ -793,6 +1085,10 @@ impl Renderer<'_> {
_ => return, _ => return,
}; };
let pos = floating.position.get(); 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 theme = &self.state.theme;
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let bc = if floating.active.get() { let bc = if floating.active.get() {
@ -801,16 +1097,27 @@ impl Renderer<'_> {
theme.colors.border.get() theme.colors.border.get()
}; };
let cr = theme.corner_radius.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); self.render_rounded_frame(outer, &bc, cr, bw, x, y);
let body = let body = Rect::new_sized_saturating(
Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw); x + bw,
y + bw,
visual.width() - 2 * bw,
visual.height() - 2 * bw,
);
let scissor_body = self.base.scale_rect(body); 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() { if !cr.is_zero() {
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32))); let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
self.corner_radius = Some(inner_cr); 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; self.corner_radius = None;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use { use {
crate::{ crate::{
backend::transaction::BackendConnectorTransactionError, backend::transaction::{BackendConnectorTransactionError, ConnectorTransaction},
state::State, state::State,
utils::{ utils::{
errorfmt::ErrorFmt, errorfmt::ErrorFmt,
@ -136,7 +136,15 @@ impl Idle {
} }
fn try_set_idle(&self, idle: bool) -> Result<(), BackendConnectorTransactionError> { fn try_set_idle(&self, idle: bool) -> Result<(), BackendConnectorTransactionError> {
self.state.set_connectors_active(!idle) 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(())
} }
} }

View file

@ -1,6 +1,6 @@
use { use {
crate::{ crate::{
backend::{InputDevice, InputDeviceCapability, InputEvent, KeyState}, backend::{InputDevice, InputDeviceCapability},
ifs::wl_seat::PX_PER_SCROLL, ifs::wl_seat::PX_PER_SCROLL,
state::{DeviceHandlerData, InputDeviceData, State}, state::{DeviceHandlerData, InputDeviceData, State},
tasks::udev_utils::{UdevProps, udev_props}, tasks::udev_utils::{UdevProps, udev_props},
@ -80,21 +80,13 @@ impl DeviceHandler {
} }
if let Some(seat) = self.data.seat.get() { if let Some(seat) = self.data.seat.get() {
let mut any_events = false; let mut any_events = false;
let mut key_press = false;
let mut mouse_move = false;
while let Some(event) = self.dev.event() { 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); seat.event(&self.data, event);
any_events = true; any_events = true;
} }
if any_events { if any_events {
seat.mark_last_active(); seat.mark_last_active();
self.state.input_occurred(key_press, mouse_move); self.state.input_occurred();
} }
} else { } else {
while self.dev.event().is_some() { while self.dev.event().is_some() {
@ -113,16 +105,3 @@ impl DeviceHandler {
self.data.set_seat(&self.state, None); 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),
}
}

View file

@ -330,7 +330,7 @@ impl ToolClient {
self_id: s.registry, self_id: s.registry,
name: s.jay_compositor.0, name: s.jay_compositor.0,
interface: JayCompositor.name(), interface: JayCompositor.name(),
version: s.jay_compositor.1.min(31), version: s.jay_compositor.1.min(30),
id: id.into(), id: id.into(),
}); });
self.jay_compositor.set(Some(id)); self.jay_compositor.set(Some(id));

View file

@ -32,6 +32,7 @@ use {
numcell::NumCell, numcell::NumCell,
on_drop_event::OnDropEvent, on_drop_event::OnDropEvent,
rc_eq::rc_eq, rc_eq::rc_eq,
scroller::Scroller,
threshold_counter::ThresholdCounter, threshold_counter::ThresholdCounter,
}, },
}, },
@ -131,6 +132,8 @@ pub struct ContainerNode {
pub content_height: Cell<i32>, pub content_height: Cell<i32>,
pub sum_factors: Cell<f64>, pub sum_factors: Cell<f64>,
pub layout_scheduled: Cell<bool>, pub layout_scheduled: Cell<bool>,
animate_next_layout: Cell<bool>,
pub mono_transition_animation_pending: Cell<bool>,
compute_render_positions_scheduled: Cell<bool>, compute_render_positions_scheduled: Cell<bool>,
num_children: NumCell<usize>, num_children: NumCell<usize>,
pub children: LinkedList<ContainerChild>, pub children: LinkedList<ContainerChild>,
@ -148,6 +151,7 @@ pub struct ContainerNode {
pub child_removed: Rc<LazyEventSource>, pub child_removed: Rc<LazyEventSource>,
pub all_children_resized: Rc<LazyEventSource>, pub all_children_resized: Rc<LazyEventSource>,
pub tab_bar: RefCell<Option<TabBar>>, pub tab_bar: RefCell<Option<TabBar>>,
scroll: Scroller,
pub update_tab_textures_scheduled: Cell<bool>, pub update_tab_textures_scheduled: Cell<bool>,
pub ephemeral: Cell<Ephemeral>, pub ephemeral: Cell<Ephemeral>,
} }
@ -238,6 +242,8 @@ impl ContainerNode {
content_height: Cell::new(0), content_height: Cell::new(0),
sum_factors: Cell::new(1.0), sum_factors: Cell::new(1.0),
layout_scheduled: Cell::new(false), 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), compute_render_positions_scheduled: Cell::new(false),
num_children: NumCell::new(1), num_children: NumCell::new(1),
children, children,
@ -262,6 +268,7 @@ impl ContainerNode {
child_removed: state.lazy_event_sources.create_source(), child_removed: state.lazy_event_sources.create_source(),
all_children_resized: state.post_layout_event_sources.create_source(), all_children_resized: state.post_layout_event_sources.create_source(),
tab_bar: RefCell::new(None), tab_bar: RefCell::new(None),
scroll: Default::default(),
update_tab_textures_scheduled: Cell::new(false), update_tab_textures_scheduled: Cell::new(false),
ephemeral: Cell::new(Ephemeral::Off), ephemeral: Cell::new(Ephemeral::Off),
}); });
@ -286,6 +293,47 @@ impl ContainerNode {
self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new)); self.add_child_x(prev, new, |prev, new| self.add_child_after_(prev, new));
} }
pub fn add_tiled_child_after(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
if !self.state.theme.autotile_enabled.get()
|| self.mono_child.is_some()
|| self.num_children.get() <= 1
{
self.add_child_after(prev, new);
return;
}
let focused = self
.child_nodes
.borrow()
.get(&prev.node_id())
.map(|n| n.to_ref());
let Some(focused) = focused else {
log::error!(
"Tried to autotile a child into a container but the preceding node is not in the container"
);
return;
};
let focused_node = focused.node.clone();
let focused_active = focused_node.tl_data().active();
let sub = ContainerNode::new(
&self.state,
&self.workspace.get(),
focused_node.clone(),
self.split.get().other(),
);
// Autotile-created groups are structural and collapse once only one
// child remains. Explicit make-group commands control their own
// grouping through the regular manual paths.
sub.ephemeral.set(Ephemeral::On);
sub.append_child(new);
let sub_id = sub.node_id();
self.clone().cnode_replace_child(&*focused_node, sub);
if focused_active
&& let Some(group) = self.child_nodes.borrow().get(&sub_id).map(|n| n.to_ref())
{
self.update_child_active(&group, true, 1);
}
}
pub fn add_child_before(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) { pub fn add_child_before(self: &Rc<Self>, prev: &dyn Node, new: Rc<dyn ToplevelNode>) {
self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new)); self.add_child_x(prev, new, |prev, new| self.add_child_before_(prev, new));
} }
@ -436,6 +484,10 @@ impl ContainerNode {
} }
fn schedule_layout(self: &Rc<Self>) { fn schedule_layout(self: &Rc<Self>) {
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) { if !self.layout_scheduled.replace(true) {
self.state.pending_container_layout.push(self.clone()); self.state.pending_container_layout.push(self.clone());
} }
@ -467,6 +519,7 @@ impl ContainerNode {
fn perform_layout(self: &Rc<Self>) { fn perform_layout(self: &Rc<Self>) {
self.layout_scheduled.set(false); self.layout_scheduled.set(false);
if self.num_children.get() == 0 { if self.num_children.get() == 0 {
self.mono_transition_animation_pending.set(false);
return; return;
} }
if let Some(child) = self.mono_child.get() { if let Some(child) = self.mono_child.get() {
@ -484,6 +537,7 @@ impl ContainerNode {
self.damage(); self.damage();
} }
} }
self.mono_transition_animation_pending.set(false);
} }
fn perform_mono_layout(self: &Rc<Self>, child: &ContainerChild) { fn perform_mono_layout(self: &Rc<Self>, child: &ContainerChild) {
@ -656,6 +710,7 @@ impl ContainerNode {
op.child.factor.set(child_factor); op.child.factor.set(child_factor);
self.sum_factors.set(sum_factors); self.sum_factors.set(sum_factors);
// log::info!("pointer_move"); // log::info!("pointer_move");
self.state.suppress_animations_for_next_layout.set(true);
self.schedule_layout_immediate(); self.schedule_layout_immediate();
} }
} }
@ -741,6 +796,18 @@ impl ContainerNode {
self.activate_child2(child, false); self.activate_child2(child, false);
} }
fn activate_child_from_input(
self: &Rc<Self>,
child: &NodeRef<ContainerChild>,
seat: &Rc<WlSeatGlobal>,
) {
self.activate_child(child);
child
.node
.clone()
.node_do_focus(seat, Direction::Unspecified);
}
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) { fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
if let Some(mc) = self.mono_child.get() { if let Some(mc) = self.mono_child.get() {
if mc.node.node_id() == child.node.node_id() { if mc.node.node_id() == child.node.node_id() {
@ -816,6 +883,7 @@ impl ContainerNode {
} }
} }
self.mono_child.set(child.clone()); self.mono_child.set(child.clone());
self.mono_transition_animation_pending.set(true);
if child.is_some() { if child.is_some() {
self.rebuild_tab_bar(); self.rebuild_tab_bar();
} else { } else {
@ -1357,42 +1425,6 @@ impl ContainerNode {
} }
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) { pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
// Autotile: if the container would become too narrow/tall, wrap the
// focused child and new node in a perpendicular sub-container.
if self.state.theme.autotile_enabled.get() && self.mono_child.is_none() {
let (pw, ph) = self.predict_child_body_size();
let opposite = match self.split.get() {
ContainerSplit::Horizontal if pw > 0 && ph > 0 && pw < ph => {
Some(ContainerSplit::Vertical)
}
ContainerSplit::Vertical if pw > 0 && ph > 0 && ph < pw => {
Some(ContainerSplit::Horizontal)
}
_ => None,
};
if let Some(opp_split) = opposite {
if let Some(focused) = self.focus_history.last() {
if self.num_children.get() <= 1 {
// Single child, autotile not applicable.
} else {
let focused_node = focused.node.clone();
let was_ephemeral = self.ephemeral.replace(Ephemeral::Off);
self.clone().cnode_remove_child2(&*focused_node, true);
self.ephemeral.set(was_ephemeral);
let sub = ContainerNode::new(
&self.state,
&self.workspace.get(),
focused_node,
opp_split,
);
sub.ephemeral.set(Ephemeral::On);
sub.append_child(node);
self.append_child(sub);
return;
}
}
}
}
let (split, right) = direction_to_split(direction); let (split, right) = direction_to_split(direction);
if split != self.split.get() || right { if split != self.split.get() || right {
self.append_child(node); self.append_child(node);
@ -1502,7 +1534,7 @@ impl ContainerNode {
fn button( fn button(
self: Rc<Self>, self: Rc<Self>,
id: CursorType, id: CursorType,
_seat: &Rc<WlSeatGlobal>, seat: &Rc<WlSeatGlobal>,
_time_usec: u64, _time_usec: u64,
pressed: bool, pressed: bool,
button: u32, button: u32,
@ -1532,7 +1564,7 @@ impl ContainerNode {
if let Some(child) = children.get(&child_id) { if let Some(child) = children.get(&child_id) {
let child_ref = child.to_ref(); let child_ref = child.to_ref();
drop(children); drop(children);
self.activate_child(&child_ref); self.activate_child_from_input(&child_ref, seat);
} }
return; return;
} }
@ -1759,10 +1791,42 @@ enum SeatOpKind {
pub async fn container_layout(state: Rc<State>) { pub async fn container_layout(state: Rc<State>) {
loop { loop {
let container = state.pending_container_layout.pop().await; let first = state.pending_container_layout.pop().await;
if container.layout_scheduled.get() { let mut containers = vec![first];
container.perform_layout(); 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);
} }
} }
@ -2017,31 +2081,33 @@ impl Node for ContainerNode {
self.button(id, seat, time_usec, state == ButtonState::Pressed, button); self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
} }
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) { fn node_on_axis_event(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
if self.mono_child.is_none() { if self.mono_child.is_none() {
return; return;
} }
// Use vertical scroll (index 1) to switch tabs. let steps = match self.scroll.handle(event) {
let v = match event.v120[1].get() { Some(steps) => steps,
Some(v) if v != 0 => v,
_ => return, _ => return,
}; };
let mono = match self.mono_child.get() { let mut target = match self.mono_child.get() {
Some(m) => m, Some(m) => m,
None => return, None => return,
}; };
let next = if v > 0 { let current_id = target.node.node_id();
// Scroll down → next tab. for _ in 0..steps.abs() {
mono.next().or_else(|| self.children.first()) let next = if steps > 0 {
} else { target.next().or_else(|| self.children.first())
// Scroll up → previous tab. } else {
mono.prev().or_else(|| self.children.last()) target.prev().or_else(|| self.children.last())
}; };
if let Some(next) = next { match next {
if next.node.node_id() != mono.node.node_id() { Some(next) => target = next,
self.activate_child(&next); None => break,
} }
} }
if target.node.node_id() != current_id {
self.activate_child_from_input(&target, seat);
}
} }
fn node_on_leave(&self, seat: &WlSeatGlobal) { fn node_on_leave(&self, seat: &WlSeatGlobal) {
@ -2259,6 +2325,11 @@ impl ContainingNode for ContainerNode {
} }
// log::info!("cnode_remove_child2"); // log::info!("cnode_remove_child2");
self.rebuild_tab_bar(); 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.schedule_layout();
self.cancel_seat_ops(); self.cancel_seat_ops();
self.child_removed.trigger(); self.child_removed.trigger();

View file

@ -8,18 +8,25 @@ use {
renderer::Renderer, renderer::Renderer,
state::State, state::State,
tree::{ tree::{
FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
WorkspaceNodeId, walker::NodeVisitor, WorkspaceNodeId, walker::NodeVisitor,
}, },
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
}, },
std::{cell::Cell, ops::Deref, rc::Rc}, std::{
cell::{Cell, RefCell},
mem,
ops::Deref,
rc::{Rc, Weak},
},
}; };
pub struct DisplayNode { pub struct DisplayNode {
pub id: NodeId, pub id: NodeId,
pub extents: Cell<Rect>, pub extents: Cell<Rect>,
visible: Cell<bool>,
suspend_restore_kb_foci: RefCell<Vec<(Rc<WlSeatGlobal>, Weak<dyn Node>)>>,
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>, pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>, pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>, pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
@ -31,6 +38,8 @@ impl DisplayNode {
let slf = Self { let slf = Self {
id, id,
extents: Default::default(), extents: Default::default(),
visible: Default::default(),
suspend_restore_kb_foci: Default::default(),
outputs: Default::default(), outputs: Default::default(),
stacked: Default::default(), stacked: Default::default(),
stacked_above_layers: Default::default(), stacked_above_layers: Default::default(),
@ -71,6 +80,17 @@ impl DisplayNode {
pub fn update_visible(&self, state: &State) { pub fn update_visible(&self, state: &State) {
let visible = state.root_visible(); let visible = state.root_visible();
let was_visible = self.visible.replace(visible);
if !visible && was_visible {
let mut foci = self.suspend_restore_kb_foci.borrow_mut();
foci.clear();
for seat in state.globals.seats.lock().values() {
let node = seat.get_keyboard_node();
if node.node_id() != self.id {
foci.push((seat.clone(), Rc::downgrade(&node)));
}
}
}
for output in self.outputs.lock().values() { for output in self.outputs.lock().values() {
output.update_visible(); output.update_visible();
} }
@ -82,6 +102,20 @@ impl DisplayNode {
for seat in state.globals.seats.lock().values() { for seat in state.globals.seats.lock().values() {
seat.set_visible(visible); seat.set_visible(visible);
} }
if visible && !was_visible {
for (seat, node) in mem::take(&mut *self.suspend_restore_kb_foci.borrow_mut()) {
if seat.get_keyboard_node().node_id() == self.id {
if let Some(node) = node.upgrade()
&& node.node_visible()
{
seat.focus_node(node);
} else {
seat.get_fallback_output()
.take_keyboard_navigation_focus(&seat, Direction::Unspecified);
}
}
}
}
if visible { if visible {
state.damage(self.extents.get()); state.damage(self.extents.get());
} }

View file

@ -31,6 +31,9 @@ use {
}; };
tree_id!(FloatNodeId); tree_id!(FloatNodeId);
const COMMAND_MOVE_DELTA: i32 = 100;
pub struct FloatNode { pub struct FloatNode {
pub id: FloatNodeId, pub id: FloatNodeId,
pub state: Rc<State>, pub state: Rc<State>,
@ -153,6 +156,13 @@ impl FloatNode {
_ => return, _ => return,
}; };
let pos = self.position.get(); 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 theme = &self.state.theme;
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let cpos = Rect::new_sized_saturating( let cpos = Rect::new_sized_saturating(
@ -363,6 +373,50 @@ impl FloatNode {
y2 += y1 - pos.y1(); y2 += y1 - pos.y1();
} }
let new_pos = Rect::new_saturating(x1, y1, x2, y2); let new_pos = Rect::new_saturating(x1, y1, x2, y2);
self.set_position(new_pos);
}
pub fn move_by_direction(self: &Rc<Self>, 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<Self>, 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); self.position.set(new_pos);
if self.visible.get() { if self.visible.get() {
self.state.damage(pos); self.state.damage(pos);
@ -791,13 +845,7 @@ impl ContainingNode for FloatNode {
let bw = theme.sizes.border_width.get(); let bw = theme.sizes.border_width.get();
let (x, y) = (x - bw, y - bw); let (x, y) = (x - bw, y - bw);
let pos = self.position.get(); let pos = self.position.get();
if pos.position() != (x, y) { self.set_position(pos.at_point(x, y));
let new_pos = pos.at_point(x, y);
self.position.set(new_pos);
self.state.damage(pos);
self.state.damage(new_pos);
self.schedule_layout();
}
} }
fn cnode_resize_child( fn cnode_resize_child(
@ -828,14 +876,7 @@ impl ContainingNode for FloatNode {
y2 = (v + bw).max(y1 + bw + bw); y2 = (v + bw).max(y1 + bw + bw);
} }
let new_pos = Rect::new_saturating(x1, y1, x2, y2); let new_pos = Rect::new_saturating(x1, y1, x2, y2);
if new_pos != pos { self.set_position(new_pos);
self.position.set(new_pos);
if self.visible.get() {
self.state.damage(pos);
self.state.damage(new_pos);
}
self.schedule_layout();
}
} }
fn cnode_pinned(&self) -> bool { fn cnode_pinned(&self) -> bool {

View file

@ -1,5 +1,12 @@
use { use {
crate::{ crate::{
animation::{
RetainedExitLayer, RetainedToplevel,
multiphase::{
MultiphaseHierarchyPosition, MultiphaseHierarchyTransition,
MultiphaseWindowHierarchy, PhaseAxis,
},
},
client::{Client, ClientId}, client::{Client, ClientId},
criteria::{ criteria::{
CritDestroyListener, CritMatcherId, CritDestroyListener, CritMatcherId,
@ -117,6 +124,7 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
if parent_was_none { if parent_was_none {
data.mapped_during_iteration.set(data.state.eng.iteration()); 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); data.property_changed(TL_CHANGED_NEW);
} }
let was_floating = data.parent_is_float.get(); let was_floating = data.parent_is_float.get();
@ -184,6 +192,57 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
fn tl_change_extents(self: Rc<Self>, rect: &Rect) { fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
let data = self.tl_data(); let data = self.tl_data();
let prev = data.desired_extents.replace(*rect); 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() { if prev.size() != rect.size() {
for sc in data.jay_screencasts.lock().values() { for sc in data.jay_screencasts.lock().values() {
sc.schedule_realloc_or_reconfigure(); sc.schedule_realloc_or_reconfigure();
@ -275,6 +334,35 @@ pub trait ToplevelNodeBase: Node {
true 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) { fn tl_set_active(&self, active: bool) {
let _ = active; let _ = active;
} }
@ -299,6 +387,11 @@ pub trait ToplevelNodeBase: Node {
fn tl_scanout_surface(&self) -> Option<Rc<WlSurface>> { fn tl_scanout_surface(&self) -> Option<Rc<WlSurface>> {
None None
} }
fn tl_animation_snapshot(&self) -> Option<Rc<RetainedToplevel>> {
None
}
fn tl_restack_popups(&self) { fn tl_restack_popups(&self) {
// nothing // nothing
} }
@ -339,6 +432,31 @@ pub trait ToplevelNodeBase: Node {
} }
} }
fn populate_multiphase_ancestor_splits(
position: &mut MultiphaseHierarchyPosition,
mut parent: Option<Rc<dyn ContainingNode>>,
) {
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 struct FullscreenedData {
pub placeholder: Rc<PlaceholderNode>, pub placeholder: Rc<PlaceholderNode>,
pub workspace: Rc<WorkspaceNode>, pub workspace: Rc<WorkspaceNode>,
@ -377,6 +495,13 @@ impl ToplevelType {
ToplevelType::XWindow { .. } => window::X_WINDOW, ToplevelType::XWindow { .. } => window::X_WINDOW,
} }
} }
pub fn is_app_window(&self) -> bool {
matches!(
self,
ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_)
)
}
} }
pub struct ToplevelData { pub struct ToplevelData {
@ -399,8 +524,10 @@ pub struct ToplevelData {
pub title: RefCell<String>, pub title: RefCell<String>,
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>, pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
pub mapped_during_iteration: Cell<u64>, pub mapped_during_iteration: Cell<u64>,
pub spawn_in_pending: Cell<bool>,
pub pos: Cell<Rect>, pub pos: Cell<Rect>,
pub desired_extents: Cell<Rect>, pub desired_extents: Cell<Rect>,
pub layout_animation_position: Cell<MultiphaseHierarchyPosition>,
pub seat_state: NodeSeatState, pub seat_state: NodeSeatState,
pub wants_attention: Cell<bool>, pub wants_attention: Cell<bool>,
pub requested_attention: Cell<bool>, pub requested_attention: Cell<bool>,
@ -462,8 +589,10 @@ impl ToplevelData {
title: RefCell::new(title), title: RefCell::new(title),
parent: Default::default(), parent: Default::default(),
mapped_during_iteration: Cell::new(0), mapped_during_iteration: Cell::new(0),
spawn_in_pending: Cell::new(false),
pos: Default::default(), pos: Default::default(),
desired_extents: Default::default(), desired_extents: Default::default(),
layout_animation_position: Default::default(),
seat_state: Default::default(), seat_state: Default::default(),
wants_attention: Cell::new(false), wants_attention: Cell::new(false),
requested_attention: Cell::new(false), requested_attention: Cell::new(false),
@ -850,7 +979,7 @@ impl ToplevelData {
} }
fd.workspace.remove_fullscreen_node(); fd.workspace.remove_fullscreen_node();
if fd.placeholder.is_destroyed() { if fd.placeholder.is_destroyed() {
state.map_tiled(node); state.map_tiled_without_autotile(node);
return; return;
} }
let parent = fd.placeholder.tl_data().parent.take().unwrap(); let parent = fd.placeholder.tl_data().parent.take().unwrap();
@ -935,6 +1064,62 @@ impl ToplevelData {
self.mapped_during_iteration.get() == self.state.eng.iteration() self.mapped_during_iteration.get() == self.state.eng.iteration()
} }
pub fn queue_spawn_out(&self, node: &dyn ToplevelNode, retained: Option<Rc<RetainedToplevel>>) {
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<ContentType>) { pub fn set_content_type(&self, content_type: Option<ContentType>) {
if self.content_type.replace(content_type) != content_type { if self.content_type.replace(content_type) != content_type {
self.property_changed(TL_CHANGED_CONTENT_TY); self.property_changed(TL_CHANGED_CONTENT_TY);
@ -1043,6 +1228,26 @@ pub fn toplevel_create_split(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, 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<State>, tl: Rc<dyn ToplevelNode>, floating: bool) { pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floating: bool) {
let data = tl.tl_data(); let data = tl.tl_data();
if data.is_fullscreen.get() { if data.is_fullscreen.get() {
@ -1057,11 +1262,21 @@ pub fn toplevel_set_floating(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, floati
}; };
if !floating { if !floating {
parent.cnode_remove_child2(&*tl, true); parent.cnode_remove_child2(&*tl, true);
state.map_tiled(tl); state.map_tiled_without_autotile(tl);
} else if let Some(ws) = data.workspace.get() { } 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); parent.cnode_remove_child2(&*tl, true);
let (width, height) = data.float_size(&ws); 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);
} }
} }
@ -1108,3 +1323,54 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
tl.tl_set_fullscreen(true, Some(ws.clone())); tl.tl_set_fullscreen(true, Some(ws.clone()));
} }
} }
/// Removes a toplevel from the tree so it can be parked in a scratchpad.
///
/// Returns `true` if the window was hidden. A placeholder, a window without a
/// parent, or a window that refuses to leave fullscreen cannot be parked.
pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> bool {
if tl.node_is_placeholder() {
return false;
}
let data = tl.tl_data();
let workspace = data.workspace.get();
if data.is_fullscreen.get() {
tl.clone().tl_set_fullscreen(false, None);
if data.is_fullscreen.get() {
return false;
}
}
let Some(parent) = data.parent.get() else {
return false;
};
let kb_foci = collect_kb_foci(tl.clone());
parent.cnode_remove_child2(&*tl, true);
data.parent.take();
data.float.take();
if data.parent_is_float.replace(false) {
data.property_changed(TL_CHANGED_FLOATING);
}
if data.workspace.take().is_some() {
data.property_changed(TL_CHANGED_WORKSPACE);
}
tl.tl_set_visible(false);
if let Some(workspace) = &workspace {
for seat in kb_foci {
workspace
.clone()
.node_do_focus(&seat, Direction::Unspecified);
}
}
true
}
/// Maps a parked scratchpad window back onto `ws`. Scratchpad windows always
/// return floating, regardless of how they were laid out before parking.
pub fn toplevel_restore_from_scratchpad(
state: &Rc<State>,
tl: Rc<dyn ToplevelNode>,
ws: &Rc<WorkspaceNode>,
) {
let (width, height) = tl.tl_data().float_size(ws);
state.map_floating(tl.clone(), width, height, ws, None);
}

View file

@ -197,10 +197,10 @@ impl WorkspaceNode {
} }
self.pull_child_properties(&**container); self.pull_child_properties(&**container);
let pos = self.position.get(); let pos = self.position.get();
container.clone().tl_change_extents(&pos);
container.tl_set_parent(self.clone()); container.tl_set_parent(self.clone());
container.tl_set_visible(self.container_visible()); container.tl_set_visible(self.container_visible());
self.container.set(Some(container.clone())); self.container.set(Some(container.clone()));
container.clone().tl_change_extents(&pos);
self.state.damage(self.position.get()); self.state.damage(self.position.get());
} }

View file

@ -2034,6 +2034,7 @@ impl Wm {
self.windows_by_surface_serial.remove(&serial); self.windows_by_surface_serial.remove(&serial);
} }
if let Some(window) = data.window.take() { if let Some(window) = data.window.take() {
window.queue_spawn_out();
window.destroy(); window.destroy();
} }
if let Some(parent) = data.parent.take() { if let Some(parent) = data.parent.take() {

View file

@ -64,6 +64,9 @@ pub enum SimpleCommand {
SetFloating(bool), SetFloating(bool),
ToggleFullscreen, ToggleFullscreen,
SetFullscreen(bool), SetFullscreen(bool),
SendToScratchpad,
ToggleScratchpad,
CycleScratchpad,
Forward(bool), Forward(bool),
EnableWindowManagement(bool), EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool), SetFloatAboveFullscreen(bool),
@ -130,6 +133,15 @@ pub enum Action {
MoveToWorkspace { MoveToWorkspace {
name: String, name: String,
}, },
SendToScratchpad {
name: String,
},
ToggleScratchpad {
name: String,
},
CycleScratchpad {
name: String,
},
Multi { Multi {
actions: Vec<Action>, actions: Vec<Action>,
}, },
@ -266,6 +278,20 @@ pub struct UiDrag {
pub threshold: Option<i32>, pub threshold: Option<i32>,
} }
#[derive(Debug, Clone, Default)]
pub struct Animations {
pub enabled: Option<bool>,
pub duration_ms: Option<u32>,
pub style: Option<String>,
pub curve: Option<AnimationCurveConfig>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AnimationCurveConfig {
Preset(String),
CubicBezier([f32; 4]),
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum OutputMatch { pub enum OutputMatch {
Any(Vec<OutputMatch>), Any(Vec<OutputMatch>),
@ -560,8 +586,6 @@ pub struct Config {
pub inputs: Vec<Input>, pub inputs: Vec<Input>,
pub idle: Option<Duration>, pub idle: Option<Duration>,
pub grace_period: Option<Duration>, pub grace_period: Option<Duration>,
pub key_press_enables_dpms: Option<bool>,
pub mouse_move_enables_dpms: Option<bool>,
pub explicit_sync_enabled: Option<bool>, pub explicit_sync_enabled: Option<bool>,
pub focus_follows_mouse: bool, pub focus_follows_mouse: bool,
pub window_management_key: Option<ModifiedKeySym>, pub window_management_key: Option<ModifiedKeySym>,
@ -569,6 +593,7 @@ pub struct Config {
pub tearing: Option<Tearing>, pub tearing: Option<Tearing>,
pub libei: Libei, pub libei: Libei,
pub ui_drag: UiDrag, pub ui_drag: UiDrag,
pub animations: Animations,
pub xwayland: Option<Xwayland>, pub xwayland: Option<Xwayland>,
pub color_management: Option<ColorManagement>, pub color_management: Option<ColorManagement>,
pub float: Option<Float>, pub float: Option<Float>,
@ -587,6 +612,14 @@ pub struct Config {
pub simple_im: Option<SimpleIm>, pub simple_im: Option<SimpleIm>,
pub fallback_output_mode: Option<FallbackOutputMode>, pub fallback_output_mode: Option<FallbackOutputMode>,
pub mouse_follows_focus: Option<bool>, pub mouse_follows_focus: Option<bool>,
pub scratchpads: Vec<Scratchpad>,
pub autotile: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Scratchpad {
pub name: String,
pub exec: Option<Exec>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -653,3 +686,26 @@ fn default_config_parses() {
let input = include_bytes!("default-config.toml"); let input = include_bytes!("default-config.toml");
parse_config(input, &Default::default(), |_| ()).unwrap(); 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"));
}

View file

@ -8,6 +8,7 @@ use {
pub mod action; pub mod action;
mod actions; mod actions;
mod animations;
mod capabilities; mod capabilities;
mod clean_logs_older_than; mod clean_logs_older_than;
mod client_match; mod client_match;
@ -40,6 +41,7 @@ pub mod modified_keysym;
mod output; mod output;
mod output_match; mod output_match;
mod repeat_rate; mod repeat_rate;
mod scratchpad;
pub mod shortcuts; pub mod shortcuts;
mod simple_im; mod simple_im;
mod status; mod status;

View file

@ -117,6 +117,9 @@ impl ActionParser<'_> {
"toggle-fullscreen" => ToggleFullscreen, "toggle-fullscreen" => ToggleFullscreen,
"enter-fullscreen" => SetFullscreen(true), "enter-fullscreen" => SetFullscreen(true),
"exit-fullscreen" => SetFullscreen(false), "exit-fullscreen" => SetFullscreen(false),
"send-to-scratchpad" => SendToScratchpad,
"toggle-scratchpad" => ToggleScratchpad,
"cycle-scratchpad" => CycleScratchpad,
"focus-parent" => FocusParent, "focus-parent" => FocusParent,
"close" => Close, "close" => Close,
"disable-pointer-constraint" => DisablePointerConstraint, "disable-pointer-constraint" => DisablePointerConstraint,
@ -222,6 +225,33 @@ impl ActionParser<'_> {
Ok(Action::MoveToWorkspace { name }) Ok(Action::MoveToWorkspace { name })
} }
fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext
.extract(opt(str("name")))?
.map(|name| name.value)
.unwrap_or("")
.to_string();
Ok(Action::SendToScratchpad { name })
}
fn parse_toggle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext
.extract(opt(str("name")))?
.map(|name| name.value)
.unwrap_or("")
.to_string();
Ok(Action::ToggleScratchpad { name })
}
fn parse_cycle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext
.extract(opt(str("name")))?
.map(|name| name.value)
.unwrap_or("")
.to_string();
Ok(Action::CycleScratchpad { name })
}
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> { fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let con = ext let con = ext
.extract(val("connector"))? .extract(val("connector"))?
@ -551,6 +581,9 @@ impl Parser for ActionParser<'_> {
"switch-to-vt" => self.parse_switch_to_vt(&mut ext), "switch-to-vt" => self.parse_switch_to_vt(&mut ext),
"show-workspace" => self.parse_show_workspace(&mut ext), "show-workspace" => self.parse_show_workspace(&mut ext),
"move-to-workspace" => self.parse_move_to_workspace(&mut ext), "move-to-workspace" => self.parse_move_to_workspace(&mut ext),
"send-to-scratchpad" => self.parse_send_to_scratchpad(&mut ext),
"toggle-scratchpad" => self.parse_toggle_scratchpad(&mut ext),
"cycle-scratchpad" => self.parse_cycle_scratchpad(&mut ext),
"configure-connector" => self.parse_configure_connector(&mut ext), "configure-connector" => self.parse_configure_connector(&mut ext),
"configure-input" => self.parse_configure_input(&mut ext), "configure-input" => self.parse_configure_input(&mut ext),
"configure-output" => self.parse_configure_output(&mut ext), "configure-output" => self.parse_configure_output(&mut ext),

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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<AnimationCurveConfig, Spanned<AnimationsParserError>> {
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<Value>],
) -> Result<AnimationCurveConfig, Spanned<AnimationsParserError>> {
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))
}

View file

@ -1,13 +1,14 @@
use { use {
crate::{ crate::{
config::{ config::{
Action, Config, Libei, Theme, UiDrag, Action, Animations, Config, Libei, Theme, UiDrag,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val}, extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{ parsers::{
action::ActionParser, action::ActionParser,
actions::ActionsParser, actions::ActionsParser,
animations::AnimationsParser,
clean_logs_older_than::CleanLogsOlderThanParser, clean_logs_older_than::CleanLogsOlderThanParser,
client_rule::ClientRulesParser, client_rule::ClientRulesParser,
color_management::ColorManagementParser, color_management::ColorManagementParser,
@ -27,6 +28,7 @@ use {
log_level::LogLevelParser, log_level::LogLevelParser,
output::OutputsParser, output::OutputsParser,
repeat_rate::RepeatRateParser, repeat_rate::RepeatRateParser,
scratchpad::ScratchpadsParser,
shortcuts::{ shortcuts::{
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError, ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
parse_modified_keysym_str, parse_modified_keysym_str,
@ -153,7 +155,9 @@ impl Parser for ConfigParser<'_> {
fallback_output_mode_val, fallback_output_mode_val,
clean_logs_older_than_val, clean_logs_older_than_val,
mouse_follows_focus, mouse_follows_focus,
animations_val,
), ),
(scratchpads_val, autotile),
) = ext.extract(( ) = ext.extract((
( (
opt(val("keymap")), opt(val("keymap")),
@ -213,7 +217,9 @@ impl Parser for ConfigParser<'_> {
opt(val("fallback-output-mode")), opt(val("fallback-output-mode")),
opt(val("clean-logs-older-than")), opt(val("clean-logs-older-than")),
recover(opt(bol("unstable-mouse-follows-focus"))), recover(opt(bol("unstable-mouse-follows-focus"))),
opt(val("animations")),
), ),
(opt(val("scratchpads")), recover(opt(bol("autotile")))),
))?; ))?;
let mut keymap = None; let mut keymap = None;
if let Some(value) = keymap_val { if let Some(value) = keymap_val {
@ -367,15 +373,11 @@ impl Parser for ConfigParser<'_> {
} }
let mut idle = None; let mut idle = None;
let mut grace_period = 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 { if let Some(value) = idle_val {
match value.parse(&mut IdleParser(self.0)) { match value.parse(&mut IdleParser(self.0)) {
Ok(v) => { Ok(v) => {
idle = v.timeout; idle = v.timeout;
grace_period = v.grace_period; 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) => { Err(e) => {
log::warn!("Could not parse the idle timeout: {}", self.0.error(e)); log::warn!("Could not parse the idle timeout: {}", self.0.error(e));
@ -433,6 +435,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; let mut xwayland = None;
if let Some(value) = xwayland_val { if let Some(value) = xwayland_val {
match value.parse(&mut XwaylandParser(self.0)) { match value.parse(&mut XwaylandParser(self.0)) {
@ -560,6 +571,13 @@ impl Parser for ConfigParser<'_> {
} }
} }
} }
let mut scratchpads = vec![];
if let Some(value) = scratchpads_val {
match value.parse(&mut ScratchpadsParser(self.0)) {
Ok(v) => scratchpads = v,
Err(e) => log::warn!("Could not parse the scratchpads: {}", self.0.error(e)),
}
}
Ok(Config { Ok(Config {
keymap, keymap,
repeat_rate, repeat_rate,
@ -585,14 +603,13 @@ impl Parser for ConfigParser<'_> {
inputs, inputs,
idle, idle,
grace_period, grace_period,
key_press_enables_dpms,
mouse_move_enables_dpms,
focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true),
window_management_key, window_management_key,
vrr, vrr,
tearing, tearing,
libei, libei,
ui_drag, ui_drag,
animations,
xwayland, xwayland,
color_management, color_management,
float, float,
@ -611,6 +628,8 @@ impl Parser for ConfigParser<'_> {
simple_im, simple_im,
fallback_output_mode, fallback_output_mode,
mouse_follows_focus: mouse_follows_focus.despan(), mouse_follows_focus: mouse_follows_focus.despan(),
scratchpads,
autotile: autotile.despan(),
}) })
} }
} }

View file

@ -2,7 +2,7 @@ use {
crate::{ crate::{
config::{ config::{
context::Context, context::Context,
extractor::{Extractor, ExtractorError, bol, n64, opt, recover, val}, extractor::{Extractor, ExtractorError, n64, opt, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
}, },
toml::{ toml::{
@ -28,8 +28,6 @@ pub struct IdleParser<'a>(pub &'a Context<'a>);
pub struct Idle { pub struct Idle {
pub timeout: Option<Duration>, pub timeout: Option<Duration>,
pub grace_period: Option<Duration>, pub grace_period: Option<Duration>,
pub key_press_enables_dpms: Option<bool>,
pub mouse_move_enables_dpms: Option<bool>,
} }
impl Parser for IdleParser<'_> { impl Parser for IdleParser<'_> {
@ -43,18 +41,10 @@ impl Parser for IdleParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table); let mut ext = Extractor::new(self.0, span, table);
let ( let (minutes, seconds, grace_period_val) = ext.extract((
minutes,
seconds,
grace_period_val,
key_press_enables_dpms,
mouse_move_enables_dpms,
) = ext.extract((
opt(n64("minutes")), opt(n64("minutes")),
opt(n64("seconds")), opt(n64("seconds")),
opt(val("grace-period")), opt(val("grace-period")),
recover(opt(bol("key-press-enables-dpms"))),
recover(opt(bol("mouse-move-enables-dpms"))),
))?; ))?;
let mut timeout = None; let mut timeout = None;
if minutes.is_some() || seconds.is_some() { if minutes.is_some() || seconds.is_some() {
@ -67,8 +57,6 @@ impl Parser for IdleParser<'_> {
Ok(Idle { Ok(Idle {
timeout, timeout,
grace_period, grace_period,
key_press_enables_dpms: key_press_enables_dpms.despan(),
mouse_move_enables_dpms: mouse_move_enables_dpms.despan(),
}) })
} }
} }

View file

@ -0,0 +1,87 @@
use {
crate::{
config::{
Scratchpad,
context::Context,
extractor::{Extractor, ExtractorError, opt, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::exec::{ExecParser, ExecParserError},
},
toml::{
toml_span::{Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ScratchpadParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Exec(#[from] ExecParserError),
}
pub struct ScratchpadParser<'a>(pub &'a Context<'a>);
impl Parser for ScratchpadParser<'_> {
type Value = Scratchpad;
type Error = ScratchpadParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (name, exec_val) = ext.extract((str("name"), opt(val("exec"))))?;
let exec = match exec_val {
None => None,
Some(e) => Some(e.parse_map(&mut ExecParser(self.0))?),
};
Ok(Scratchpad {
name: name.value.to_string(),
exec,
})
}
}
pub struct ScratchpadsParser<'a>(pub &'a Context<'a>);
impl Parser for ScratchpadsParser<'_> {
type Value = Vec<Scratchpad>;
type Error = ScratchpadParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut ScratchpadParser(self.0)) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse scratchpad: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`scratchpads` value should be an array: {}",
self.0.error3(span)
);
ScratchpadParser(self.0)
.parse_table(span, table)
.map(|v| vec![v])
}
}

View file

@ -13,9 +13,9 @@ mod toml;
use { use {
crate::{ crate::{
config::{ config::{
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
SimpleCommand, Status, Theme, WindowRule, parse_config, OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config,
}, },
rules::{MatcherTemp, RuleMapper}, rules::{MatcherTemp, RuleMapper},
shortcuts::ModeState, shortcuts::ModeState,
@ -23,11 +23,11 @@ use {
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
error_reporter::Report, error_reporter::Report,
jay_config::{ jay_config::{
Axis, AnimationCurve, AnimationStyle, Axis,
client::Client, client::Client,
config, config_dir, config, config_dir,
exec::{Command, set_env, unset_env}, exec::{Command, set_env, unset_env},
get_workspace, get_autotile, get_workspace,
input::{ input::{
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH, FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, capability::CAP_SWITCH,
get_seat, input_devices, on_input_device_removed, on_new_input_device, get_seat, input_devices, on_input_device_removed, on_new_input_device,
@ -37,12 +37,13 @@ use {
is_reload, is_reload,
keyboard::Keymap, keyboard::Keymap,
logging::{clean_logs_older_than, set_log_level}, logging::{clean_logs_older_than, set_log_level},
on_devices_enumerated, on_idle, on_unload, quit, reload, set_autotile, on_devices_enumerated, on_idle, on_unload, quit, reload, set_animation_cubic_bezier,
set_color_management_enabled, set_corner_radius, set_default_workspace_capture, set_animation_curve, set_animation_duration_ms, set_animation_style,
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, set_animations_enabled, set_autotile, set_color_management_enabled, set_corner_radius,
set_idle_grace_period, set_key_press_enables_dpms, set_middle_click_paste_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen,
set_mouse_move_enables_dpms, set_show_bar, set_show_float_pin_icon, set_show_titles, set_floating_titles, set_idle, set_idle_grace_period, set_middle_click_paste_enabled,
set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold, 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}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
tasks::{self, JoinHandle}, tasks::{self, JoinHandle},
@ -172,6 +173,9 @@ impl Action {
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
SimpleCommand::SendToScratchpad => window_or_seat!(s, s.send_to_scratchpad("")),
SimpleCommand::ToggleScratchpad => b.new(move || s.toggle_scratchpad("")),
SimpleCommand::CycleScratchpad => b.new(move || s.cycle_scratchpad("")),
SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
SimpleCommand::Close => window_or_seat!(s, s.close()), SimpleCommand::Close => window_or_seat!(s, s.close()),
SimpleCommand::DisablePointerConstraint => { SimpleCommand::DisablePointerConstraint => {
@ -268,12 +272,7 @@ impl Action {
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)), SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)), SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)), SimpleCommand::SetAutotile(enabled) => b.new(move || set_autotile(enabled)),
SimpleCommand::ToggleAutotile => { SimpleCommand::ToggleAutotile => b.new(move || set_autotile(!get_autotile())),
b.new(move || {
// Toggle not directly supported; set to true
set_autotile(true)
})
}
}, },
Action::Multi { actions } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -310,6 +309,9 @@ impl Action {
let workspace = get_workspace(&name); let workspace = get_workspace(&name);
window_or_seat!(s, s.set_workspace(workspace)) window_or_seat!(s, s.set_workspace(workspace))
} }
Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)),
Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)),
Action::CycleScratchpad { name } => b.new(move || s.cycle_scratchpad(&name)),
Action::ConfigureConnector { con } => b.new(move || { Action::ConfigureConnector { con } => b.new(move || {
for c in connectors() { for c in connectors() {
if con.match_.matches(c) { if con.match_.matches(c) {
@ -1461,6 +1463,43 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
window: Default::default(), window: Default::default(),
}); });
state.clear_modes_after_reload(); state.clear_modes_after_reload();
// Desugar `[[scratchpads]]` into spawn-on-graphics-init plus an internal
// window rule that parks the spawned window. Each spawned process gets a
// unique tag so only its own windows are captured, never other windows of
// the same application.
if !config.scratchpads.is_empty() {
let mut spawn_actions = vec![];
for (i, sp) in config.scratchpads.drain(..).enumerate() {
let Some(mut exec) = sp.exec else {
continue;
};
let tag = exec
.tag
.clone()
.unwrap_or_else(|| format!("__scratchpad.{i}.{}", sp.name));
exec.tag = Some(tag.clone());
spawn_actions.push(Action::Exec { exec });
config.window_rules.push(WindowRule {
name: None,
match_: WindowMatch {
tag: Some(tag),
..Default::default()
},
action: Some(Action::SendToScratchpad { name: sp.name }),
latch: None,
auto_focus: None,
initial_tile_state: None,
});
}
if !spawn_actions.is_empty() {
let mut actions = Vec::with_capacity(spawn_actions.len() + 1);
if let Some(existing) = config.on_graphics_initialized.take() {
actions.push(existing);
}
actions.extend(spawn_actions);
config.on_graphics_initialized = Some(Action::Multi { actions });
}
}
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules); let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
persistent.client_rules.set(client_rules); persistent.client_rules.set(client_rules);
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper); *state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
@ -1649,6 +1688,38 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
if let Some(threshold) = config.ui_drag.threshold { if let Some(threshold) = config.ui_drag.threshold {
set_ui_drag_threshold(threshold); set_ui_drag_threshold(threshold);
} }
set_animations_enabled(config.animations.enabled.unwrap_or(false));
set_animation_duration_ms(config.animations.duration_ms.unwrap_or(160));
match config.animations.style.as_deref().unwrap_or("multiphase") {
"plain" => 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(xwayland) = config.xwayland {
if let Some(enabled) = xwayland.enabled { if let Some(enabled) = xwayland.enabled {
set_x_wayland_enabled(enabled); set_x_wayland_enabled(enabled);
@ -1657,8 +1728,6 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
set_x_scaling_mode(mode); set_x_scaling_mode(mode);
} }
} }
set_key_press_enables_dpms(config.key_press_enables_dpms.unwrap_or(false));
set_mouse_move_enables_dpms(config.mouse_move_enables_dpms.unwrap_or(false));
if let Some(cm) = config.color_management if let Some(cm) = config.color_management
&& let Some(enabled) = cm.enabled && let Some(enabled) = cm.enabled
{ {
@ -1715,6 +1784,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
.seat .seat
.unstable_set_mouse_follows_focus(mouse_follows_focus); .unstable_set_mouse_follows_focus(mouse_follows_focus);
} }
if let Some(v) = config.autotile {
set_autotile(v);
}
} }
fn create_command(exec: &Exec) -> Command { fn create_command(exec: &Exec) -> Command {

View file

@ -162,6 +162,54 @@
"name" "name"
] ]
}, },
{
"description": "Sends the currently focused window to a scratchpad and hides it.\n\nA scratchpad can hold any number of windows. If `name` is omitted, the\ndefault scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-shift-minus = { type = \"send-to-scratchpad\", name = \"terminal\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "send-to-scratchpad"
},
"name": {
"type": "string",
"description": "The name of the scratchpad."
}
},
"required": [
"type"
]
},
{
"description": "Toggles a scratchpad.\n\nIf the scratchpad has a visible window, that window is hidden. Otherwise, the\nmost recently hidden window in the scratchpad is shown on the current workspace.\nOnly one window of a scratchpad is shown at a time, and scratchpad windows are\nalways shown floating. If `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"toggle-scratchpad\", name = \"terminal\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "toggle-scratchpad"
},
"name": {
"type": "string",
"description": "The name of the scratchpad."
}
},
"required": [
"type"
]
},
{
"description": "Cycles through the windows of a scratchpad, one at a time.\n\nWith no window shown, the first window is brought up. Each further invocation\nhides the current window and shows the next; after the last window the\nscratchpad is hidden again. Scratchpad windows are always shown floating.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"cycle-scratchpad\", name = \"terminal\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "cycle-scratchpad"
},
"name": {
"type": "string",
"description": "The name of the scratchpad."
}
},
"required": [
"type"
]
},
{ {
"description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n", "description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n",
"type": "object", "type": "object",
@ -641,6 +689,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": { "BarPosition": {
"type": "string", "type": "string",
"description": "The position of the bar.", "description": "The position of the bar.",
@ -1085,6 +1188,10 @@
"description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n", "description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n",
"$ref": "#/$defs/UiDrag" "$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": { "xwayland": {
"description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n", "description": "Configures the Xwayland settings.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n",
"$ref": "#/$defs/Xwayland" "$ref": "#/$defs/Xwayland"
@ -1150,6 +1257,10 @@
"type": "boolean", "type": "boolean",
"description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n" "description": "Configures whether middle-click pasting is enabled.\n\nChanging this has no effect on running applications.\n\nThe default is `true`.\n"
}, },
"autotile": {
"type": "boolean",
"description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n"
},
"modes": { "modes": {
"description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n", "description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n",
"type": "object", "type": "object",
@ -1177,6 +1288,14 @@
"egui": { "egui": {
"description": "Sets the egui settings of the compositor.\n", "description": "Sets the egui settings of the compositor.\n",
"$ref": "#/$defs/Egui" "$ref": "#/$defs/Egui"
},
"scratchpads": {
"type": "array",
"description": "An array of pre-configured scratchpads.\n\nEach entry launches a program when the graphics are first initialized and\nimmediately parks its window in the named scratchpad. The window is captured\nvia a unique tag attached to the spawned process, so other windows of the\nsame application are never affected.\n\nUse a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows\nup; they are always shown floating.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n\n [[scratchpads]]\n name = \"notes\"\n exec = [\"obsidian\"]\n ```\n",
"items": {
"description": "",
"$ref": "#/$defs/Scratchpad"
}
} }
}, },
"required": [] "required": []
@ -1991,6 +2110,23 @@
}, },
"required": [] "required": []
}, },
"Scratchpad": {
"description": "A pre-configured scratchpad whose program is launched at startup and parked\nin the scratchpad.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n ```\n",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the scratchpad that the spawned window is parked in."
},
"exec": {
"description": "The program to launch when the graphics are first initialized.\n\nIf omitted, no program is launched and the scratchpad is only created on\ndemand by `send-to-scratchpad`.\n",
"$ref": "#/$defs/Exec"
}
},
"required": [
"name"
]
},
"SimpleActionName": { "SimpleActionName": {
"type": "string", "type": "string",
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", "description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
@ -2009,9 +2145,15 @@
"make-group-tab", "make-group-tab",
"change-group-opposite", "change-group-opposite",
"toggle-tab", "toggle-tab",
"enable-autotile",
"disable-autotile",
"toggle-autotile",
"toggle-fullscreen", "toggle-fullscreen",
"enter-fullscreen", "enter-fullscreen",
"exit-fullscreen", "exit-fullscreen",
"send-to-scratchpad",
"toggle-scratchpad",
"cycle-scratchpad",
"focus-parent", "focus-parent",
"close", "close",
"disable-pointer-constraint", "disable-pointer-constraint",

View file

@ -286,6 +286,76 @@ This table is a tagged union. The variant is determined by the `type` field. It
The value of this field should be a string. The value of this field should be a string.
- `send-to-scratchpad`:
Sends the currently focused window to a scratchpad and hides it.
A scratchpad can hold any number of windows. If `name` is omitted, the
default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" }
```
The table has the following fields:
- `name` (optional):
The name of the scratchpad.
The value of this field should be a string.
- `toggle-scratchpad`:
Toggles a scratchpad.
If the scratchpad has a visible window, that window is hidden. Otherwise, the
most recently hidden window in the scratchpad is shown on the current workspace.
Only one window of a scratchpad is shown at a time, and scratchpad windows are
always shown floating. If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "toggle-scratchpad", name = "terminal" }
```
The table has the following fields:
- `name` (optional):
The name of the scratchpad.
The value of this field should be a string.
- `cycle-scratchpad`:
Cycles through the windows of a scratchpad, one at a time.
With no window shown, the first window is brought up. Each further invocation
hides the current window and shows the next; after the last window the
scratchpad is hidden again. Scratchpad windows are always shown floating.
If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "cycle-scratchpad", name = "terminal" }
```
The table has the following fields:
- `name` (optional):
The name of the scratchpad.
The value of this field should be a string.
- `move-to-output`: - `move-to-output`:
Moves a workspace to a different output. Moves a workspace to a different output.
@ -942,6 +1012,126 @@ This table is a tagged union. The variant is determined by the `type` field. It
The numbers should be integers. The numbers should be integers.
<a name="types-AnimationCurve"></a>
### `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.
<a name="types-AnimationStyle"></a>
### `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.
<a name="types-Animations"></a>
### `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).
<a name="types-BarPosition"></a> <a name="types-BarPosition"></a>
### `BarPosition` ### `BarPosition`
@ -2169,6 +2359,24 @@ The table has the following fields:
The value of this field should be a [UiDrag](#types-UiDrag). 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): - `xwayland` (optional):
Configures the Xwayland settings. Configures the Xwayland settings.
@ -2352,6 +2560,18 @@ The table has the following fields:
The value of this field should be a boolean. The value of this field should be a boolean.
- `autotile` (optional):
Configures whether autotiling is enabled by default.
When enabled, newly mapped tiled windows alternate their split
orientation automatically. This can also be toggled at runtime via the
`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.
The default is `false`.
The value of this field should be a boolean.
- `modes` (optional): - `modes` (optional):
Configures the input modes. Configures the input modes.
@ -2452,6 +2672,32 @@ The table has the following fields:
The value of this field should be a [Egui](#types-Egui). The value of this field should be a [Egui](#types-Egui).
- `scratchpads` (optional):
An array of pre-configured scratchpads.
Each entry launches a program when the graphics are first initialized and
immediately parks its window in the named scratchpad. The window is captured
via a unique tag attached to the spawned process, so other windows of the
same application are never affected.
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
up; they are always shown floating.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
[[scratchpads]]
name = "notes"
exec = ["obsidian"]
```
The value of this field should be an array of [Scratchpads](#types-Scratchpad).
<a name="types-Connector"></a> <a name="types-Connector"></a>
### `Connector` ### `Connector`
@ -4385,6 +4631,40 @@ The table has the following fields:
The value of this field should be a string. The value of this field should be a string.
<a name="types-Scratchpad"></a>
### `Scratchpad`
A pre-configured scratchpad whose program is launched at startup and parked
in the scratchpad.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
```
Values of this type should be tables.
The table has the following fields:
- `name` (required):
The name of the scratchpad that the spawned window is parked in.
The value of this field should be a string.
- `exec` (optional):
The program to launch when the graphics are first initialized.
If omitted, no program is launched and the scratchpad is only created on
demand by `send-to-scratchpad`.
The value of this field should be a [Exec](#types-Exec).
<a name="types-SimpleActionName"></a> <a name="types-SimpleActionName"></a>
### `SimpleActionName` ### `SimpleActionName`
@ -4476,6 +4756,18 @@ The string should have one of the following values:
Toggles the current group between tabbed and split mode. Toggles the current group between tabbed and split mode.
- `enable-autotile`:
Enables alternating split orientation for newly tiled windows.
- `disable-autotile`:
Disables alternating split orientation for newly tiled windows.
- `toggle-autotile`:
Toggles alternating split orientation for newly tiled windows.
- `toggle-fullscreen`: - `toggle-fullscreen`:
Toggle the currently focused window between fullscreen and windowed. Toggle the currently focused window between fullscreen and windowed.
@ -4488,6 +4780,18 @@ The string should have one of the following values:
Makes the currently focused window windowed. Makes the currently focused window windowed.
- `send-to-scratchpad`:
Sends the currently focused window to the default scratchpad.
- `toggle-scratchpad`:
Toggles the default scratchpad.
- `cycle-scratchpad`:
Cycles through the windows of the default scratchpad.
- `focus-parent`: - `focus-parent`:
Focus the parent of the currently focused window. Focus the parent of the currently focused window.

View file

@ -345,6 +345,64 @@ Action:
description: The name of the workspace. description: The name of the workspace.
required: true required: true
kind: string kind: string
send-to-scratchpad:
description: |
Sends the currently focused window to a scratchpad and hides it.
A scratchpad can hold any number of windows. If `name` is omitted, the
default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" }
```
fields:
name:
description: The name of the scratchpad.
required: false
kind: string
toggle-scratchpad:
description: |
Toggles a scratchpad.
If the scratchpad has a visible window, that window is hidden. Otherwise, the
most recently hidden window in the scratchpad is shown on the current workspace.
Only one window of a scratchpad is shown at a time, and scratchpad windows are
always shown floating. If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "toggle-scratchpad", name = "terminal" }
```
fields:
name:
description: The name of the scratchpad.
required: false
kind: string
cycle-scratchpad:
description: |
Cycles through the windows of a scratchpad, one at a time.
With no window shown, the first window is brought up. Each further invocation
hides the current window and shows the next; after the last window the
scratchpad is hidden again. Scratchpad windows are always shown floating.
If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "cycle-scratchpad", name = "terminal" }
```
fields:
name:
description: The name of the scratchpad.
required: false
kind: string
move-to-output: move-to-output:
description: | description: |
Moves a workspace to a different output. Moves a workspace to a different output.
@ -1064,12 +1122,24 @@ SimpleActionName:
description: Toggles the current group's direction. description: Toggles the current group's direction.
- value: toggle-tab - value: toggle-tab
description: Toggles the current group between tabbed and split mode. description: Toggles the current group between tabbed and split mode.
- value: enable-autotile
description: Enables alternating split orientation for newly tiled windows.
- value: disable-autotile
description: Disables alternating split orientation for newly tiled windows.
- value: toggle-autotile
description: Toggles alternating split orientation for newly tiled windows.
- value: toggle-fullscreen - value: toggle-fullscreen
description: Toggle the currently focused window between fullscreen and windowed. description: Toggle the currently focused window between fullscreen and windowed.
- value: enter-fullscreen - value: enter-fullscreen
description: Makes the currently focused window fullscreen. description: Makes the currently focused window fullscreen.
- value: exit-fullscreen - value: exit-fullscreen
description: Makes the currently focused window windowed. description: Makes the currently focused window windowed.
- value: send-to-scratchpad
description: Sends the currently focused window to the default scratchpad.
- value: toggle-scratchpad
description: Toggles the default scratchpad.
- value: cycle-scratchpad
description: Cycles through the windows of the default scratchpad.
- value: focus-parent - value: focus-parent
description: Focus the parent of the currently focused window. description: Focus the parent of the currently focused window.
- value: close - value: close
@ -2942,6 +3012,23 @@ Config:
```toml ```toml
ui-drag = { enabled = false, threshold = 20 } 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: xwayland:
ref: Xwayland ref: Xwayland
required: false required: false
@ -3116,6 +3203,17 @@ Config:
Changing this has no effect on running applications. Changing this has no effect on running applications.
The default is `true`. The default is `true`.
autotile:
kind: boolean
required: false
description: |
Configures whether autotiling is enabled by default.
When enabled, newly mapped tiled windows alternate their split
orientation automatically. This can also be toggled at runtime via the
`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.
The default is `false`.
modes: modes:
kind: map kind: map
values: values:
@ -3212,6 +3310,61 @@ Config:
required: false required: false
description: | description: |
Sets the egui settings of the compositor. Sets the egui settings of the compositor.
scratchpads:
kind: array
items:
ref: Scratchpad
required: false
description: |
An array of pre-configured scratchpads.
Each entry launches a program when the graphics are first initialized and
immediately parks its window in the named scratchpad. The window is captured
via a unique tag attached to the spawned process, so other windows of the
same application are never affected.
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
up; they are always shown floating.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
[[scratchpads]]
name = "notes"
exec = ["obsidian"]
```
Scratchpad:
kind: table
description: |
A pre-configured scratchpad whose program is launched at startup and parked
in the scratchpad.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
```
fields:
name:
kind: string
required: true
description: The name of the scratchpad that the spawned window is parked in.
exec:
ref: Exec
required: false
description: |
The program to launch when the graphics are first initialized.
If omitted, no program is launched and the scratchpad is only created on
demand by `send-to-scratchpad`.
Idle: Idle:
@ -3655,6 +3808,97 @@ UiDrag:
The default is `10`. 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: Xwayland:
kind: table kind: table
description: | description: |

View file

@ -135,10 +135,6 @@ request get_pid (since = 27) {
} }
request set_dpms (since = 31) {
active: u32,
}
# events # events
event client_id { event client_id {