Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5db14936e7 | |||
| f777b4c521 | |||
| b6502e1d8a | |||
| d756c8a6a2 |
52 changed files with 1175 additions and 88 deletions
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
@ -687,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,
|
||||||
|
|
|
||||||
|
|
@ -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!();
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -403,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);
|
||||||
|
|
|
||||||
|
|
@ -1100,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)?;
|
||||||
|
|
@ -1114,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();
|
||||||
|
|
@ -2989,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")?
|
||||||
}
|
}
|
||||||
|
|
@ -3373,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")?,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ mod t0052_bar;
|
||||||
mod t0053_theme;
|
mod t0053_theme;
|
||||||
mod t0054_subsurface_already_attached;
|
mod t0054_subsurface_already_attached;
|
||||||
mod t0055_autotiling;
|
mod t0055_autotiling;
|
||||||
|
mod t0055_scratchpad;
|
||||||
|
|
||||||
pub trait TestCase: Sync {
|
pub trait TestCase: Sync {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
@ -160,5 +161,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
|
||||||
t0053_theme,
|
t0053_theme,
|
||||||
t0054_subsurface_already_attached,
|
t0054_subsurface_already_attached,
|
||||||
t0055_autotiling,
|
t0055_autotiling,
|
||||||
|
t0055_scratchpad,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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()?;
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
107
src/it/tests/t0055_scratchpad.rs
Normal file
107
src/it/tests/t0055_scratchpad.rs
Normal 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(())
|
||||||
|
}
|
||||||
171
src/state.rs
171
src/state.rs
|
|
@ -114,9 +114,11 @@ use {
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
|
||||||
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
|
||||||
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
|
PlaceholderNode, TearingMode, TileState, ToplevelData,
|
||||||
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
|
ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode,
|
||||||
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
|
WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig,
|
||||||
|
generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad,
|
||||||
|
toplevel_restore_from_scratchpad, toplevel_set_workspace,
|
||||||
},
|
},
|
||||||
udmabuf::UdmabufHolder,
|
udmabuf::UdmabufHolder,
|
||||||
utils::{
|
utils::{
|
||||||
|
|
@ -412,6 +414,7 @@ pub struct State {
|
||||||
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
|
||||||
pub virtual_outputs: VirtualOutputs,
|
pub virtual_outputs: VirtualOutputs,
|
||||||
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
pub clean_logs_older_than: Cell<Option<SystemTime>>,
|
||||||
|
pub scratchpads: RefCell<AHashMap<String, Vec<Rc<ScratchpadEntry>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Drop for State {
|
// impl Drop for State {
|
||||||
|
|
@ -459,6 +462,27 @@ pub struct IdleState {
|
||||||
pub in_grace_period: Cell<bool>,
|
pub in_grace_period: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ScratchpadEntry {
|
||||||
|
node: Weak<dyn ToplevelNode>,
|
||||||
|
identifier: ToplevelIdentifier,
|
||||||
|
hidden: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScratchpadEntry {
|
||||||
|
fn alive(&self) -> bool {
|
||||||
|
self.node().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(&self) -> Option<Rc<dyn ToplevelNode>> {
|
||||||
|
let node = self.node.upgrade()?;
|
||||||
|
if node.tl_data().identifier.get() == self.identifier {
|
||||||
|
Some(node)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IdleState {
|
impl IdleState {
|
||||||
pub fn set_timeout(&self, state: &State, timeout: Duration) {
|
pub fn set_timeout(&self, state: &State, timeout: Duration) {
|
||||||
self.timeout.set(timeout);
|
self.timeout.set(timeout);
|
||||||
|
|
@ -1023,6 +1047,146 @@ impl State {
|
||||||
float
|
float
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn send_to_scratchpad(self: &Rc<Self>, name: &str, node: Rc<dyn ToplevelNode>) {
|
||||||
|
if node.node_is_placeholder() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let identifier = node.tl_data().identifier.get();
|
||||||
|
if !toplevel_hide_for_scratchpad(node.clone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = Rc::new(ScratchpadEntry {
|
||||||
|
node: Rc::downgrade(&node),
|
||||||
|
identifier,
|
||||||
|
hidden: Cell::new(true),
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
for entries in scratchpads.values_mut() {
|
||||||
|
entries.retain(|entry| entry.alive() && entry.identifier != identifier);
|
||||||
|
}
|
||||||
|
scratchpads
|
||||||
|
.entry(name.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(entry);
|
||||||
|
}
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_scratchpad(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, name: &str) {
|
||||||
|
let entry = {
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
let Some(entries) = scratchpads.get_mut(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
entries.retain(|entry| entry.alive());
|
||||||
|
// Prefer the currently-shown window; otherwise act on the most recent.
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|entry| !entry.hidden.get())
|
||||||
|
.or_else(|| entries.last())
|
||||||
|
.cloned()
|
||||||
|
};
|
||||||
|
let Some(entry) = entry else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if entry.hidden.get() {
|
||||||
|
self.show_scratchpad_entry(seat, name, &entry);
|
||||||
|
} else if entry.node().is_some_and(|node| !node.node_visible()) {
|
||||||
|
self.move_scratchpad_entry_to_current_workspace(seat, &entry);
|
||||||
|
} else {
|
||||||
|
self.hide_scratchpad_entry(&entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycles through the windows of a scratchpad, one at a time:
|
||||||
|
/// nothing shown -> first window -> ... -> last window -> nothing shown.
|
||||||
|
pub fn cycle_scratchpad(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, name: &str) {
|
||||||
|
let (current, next) = {
|
||||||
|
let mut scratchpads = self.scratchpads.borrow_mut();
|
||||||
|
let Some(entries) = scratchpads.get_mut(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
entries.retain(|entry| entry.alive());
|
||||||
|
match entries.iter().position(|entry| !entry.hidden.get()) {
|
||||||
|
// Nothing shown yet: bring up the first window.
|
||||||
|
None => (None, entries.first().cloned()),
|
||||||
|
// Hide the shown window and advance; on the last window, `next`
|
||||||
|
// is `None`, so the scratchpad toggles off.
|
||||||
|
Some(i) => (entries.get(i).cloned(), entries.get(i + 1).cloned()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(current) = ¤t {
|
||||||
|
self.hide_scratchpad_entry(current);
|
||||||
|
}
|
||||||
|
if let Some(next) = &next {
|
||||||
|
self.show_scratchpad_entry(seat, name, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_scratchpad_entry(self: &Rc<Self>, entry: &Rc<ScratchpadEntry>) {
|
||||||
|
if entry.hidden.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if toplevel_hide_for_scratchpad(node) {
|
||||||
|
entry.hidden.set(true);
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_scratchpad_entry(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
name: &str,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
if !entry.hidden.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Only one window of a scratchpad is visible at a time.
|
||||||
|
let siblings: Vec<_> = {
|
||||||
|
let scratchpads = self.scratchpads.borrow();
|
||||||
|
scratchpads
|
||||||
|
.get(name)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|sibling| !Rc::ptr_eq(sibling, entry) && !sibling.hidden.get())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
for sibling in siblings {
|
||||||
|
self.hide_scratchpad_entry(&sibling);
|
||||||
|
}
|
||||||
|
let ws = seat.get_fallback_output().ensure_workspace();
|
||||||
|
toplevel_restore_from_scratchpad(self, node.clone(), &ws);
|
||||||
|
entry.hidden.set(false);
|
||||||
|
node.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
seat.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_scratchpad_entry_to_current_workspace(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
seat: &Rc<WlSeatGlobal>,
|
||||||
|
entry: &Rc<ScratchpadEntry>,
|
||||||
|
) {
|
||||||
|
let Some(node) = entry.node() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let ws = seat.get_fallback_output().ensure_workspace();
|
||||||
|
toplevel_set_workspace(self, node.clone(), &ws);
|
||||||
|
node.node_do_focus(seat, Direction::Unspecified);
|
||||||
|
seat.maybe_schedule_warp_mouse_to_focus();
|
||||||
|
self.tree_changed();
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
|
||||||
if !node.node_visible() {
|
if !node.node_visible() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1298,6 +1462,7 @@ impl State {
|
||||||
self.node_at_tree.borrow_mut().clear();
|
self.node_at_tree.borrow_mut().clear();
|
||||||
self.position_hint_requests.clear();
|
self.position_hint_requests.clear();
|
||||||
self.pending_warp_mouse_to_focus.clear();
|
self.pending_warp_mouse_to_focus.clear();
|
||||||
|
self.scratchpads.borrow_mut().clear();
|
||||||
self.head_managers.clear();
|
self.head_managers.clear();
|
||||||
self.head_managers_async.clear();
|
self.head_managers_async.clear();
|
||||||
self.const_40hz_latch.clear();
|
self.const_40hz_latch.clear();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -150,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>,
|
||||||
}
|
}
|
||||||
|
|
@ -266,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),
|
||||||
});
|
});
|
||||||
|
|
@ -793,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() {
|
||||||
|
|
@ -1519,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,
|
||||||
|
|
@ -1549,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;
|
||||||
}
|
}
|
||||||
|
|
@ -2066,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) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1323,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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -41,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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -28,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,
|
||||||
|
|
@ -570,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,
|
||||||
|
|
|
||||||
87
toml-config/src/config/parsers/scratchpad.rs
Normal file
87
toml-config/src/config/parsers/scratchpad.rs
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ use {
|
||||||
config::{
|
config::{
|
||||||
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
|
||||||
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
|
||||||
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config,
|
||||||
},
|
},
|
||||||
rules::{MatcherTemp, RuleMapper},
|
rules::{MatcherTemp, RuleMapper},
|
||||||
shortcuts::ModeState,
|
shortcuts::ModeState,
|
||||||
|
|
@ -173,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 => {
|
||||||
|
|
@ -306,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) {
|
||||||
|
|
@ -1457,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);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -1240,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": []
|
||||||
|
|
@ -2054,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",
|
||||||
|
|
@ -2078,6 +2151,9 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -1007,6 +1077,7 @@ The string should have one of the following values:
|
||||||
supported plan exists.
|
supported plan exists.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a name="types-Animations"></a>
|
<a name="types-Animations"></a>
|
||||||
### `Animations`
|
### `Animations`
|
||||||
|
|
||||||
|
|
@ -1029,7 +1100,7 @@ The table has the following fields:
|
||||||
- `enabled` (optional):
|
- `enabled` (optional):
|
||||||
|
|
||||||
Enables or disables window animations.
|
Enables or disables window animations.
|
||||||
|
|
||||||
The default is `false`.
|
The default is `false`.
|
||||||
|
|
||||||
The value of this field should be a boolean.
|
The value of this field should be a boolean.
|
||||||
|
|
@ -1037,7 +1108,7 @@ The table has the following fields:
|
||||||
- `duration-ms` (optional):
|
- `duration-ms` (optional):
|
||||||
|
|
||||||
Sets the animation duration in milliseconds.
|
Sets the animation duration in milliseconds.
|
||||||
|
|
||||||
The default is `160`.
|
The default is `160`.
|
||||||
|
|
||||||
The value of this field should be a number.
|
The value of this field should be a number.
|
||||||
|
|
@ -1047,7 +1118,7 @@ The table has the following fields:
|
||||||
- `style` (optional):
|
- `style` (optional):
|
||||||
|
|
||||||
Sets the animation style used for tiled window movement animations.
|
Sets the animation style used for tiled window movement animations.
|
||||||
|
|
||||||
The default is `multiphase`.
|
The default is `multiphase`.
|
||||||
|
|
||||||
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
|
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
|
||||||
|
|
@ -1055,7 +1126,7 @@ The table has the following fields:
|
||||||
- `curve` (optional):
|
- `curve` (optional):
|
||||||
|
|
||||||
Sets the animation curve.
|
Sets the animation curve.
|
||||||
|
|
||||||
The default is `ease-out`.
|
The default is `ease-out`.
|
||||||
|
|
||||||
The value of this field should be a [AnimationCurve](#types-AnimationCurve).
|
The value of this field should be a [AnimationCurve](#types-AnimationCurve).
|
||||||
|
|
@ -2291,11 +2362,11 @@ The table has the following fields:
|
||||||
- `animations` (optional):
|
- `animations` (optional):
|
||||||
|
|
||||||
Configures window animations.
|
Configures window animations.
|
||||||
|
|
||||||
Animations are disabled by default.
|
Animations are disabled by default.
|
||||||
|
|
||||||
- Example:
|
- Example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[animations]
|
[animations]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
@ -2601,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`
|
||||||
|
|
@ -4534,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`
|
||||||
|
|
||||||
|
|
@ -4649,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.
|
||||||
|
|
@ -5830,3 +5973,5 @@ The table has the following fields:
|
||||||
The scaling mode of X windows.
|
The scaling mode of X windows.
|
||||||
|
|
||||||
The value of this field should be a [XScalingMode](#types-XScalingMode).
|
The value of this field should be a [XScalingMode](#types-XScalingMode).
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -1076,6 +1134,12 @@ SimpleActionName:
|
||||||
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
|
||||||
|
|
@ -3246,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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue