1
0
Fork 0
forked from wry/wry

feat: implement scratchpad window toggling

This commit is contained in:
atagen 2026-05-31 17:23:56 +10:00
parent 5c2f631fdb
commit d756c8a6a2
17 changed files with 515 additions and 3 deletions

View file

@ -403,6 +403,7 @@ fn start_compositor2(
bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)),
virtual_outputs: Default::default(),
clean_logs_older_than: Default::default(),
scratchpads: Default::default(),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);

View file

@ -1100,6 +1100,24 @@ impl ConfigProxyHandler {
Ok(())
}
fn handle_seat_send_to_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() {
self.state.send_to_scratchpad(name, toplevel);
}
Ok(())
})
}
fn handle_seat_toggle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
self.state.toggle_scratchpad(&seat, name);
Ok(())
})
}
fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> {
let window = self.get_window(window)?;
let name = self.get_workspace(ws)?;
@ -1114,6 +1132,14 @@ impl ConfigProxyHandler {
Ok(())
}
fn handle_window_send_to_scratchpad(&self, window: Window, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let window = self.get_window(window)?;
self.state.send_to_scratchpad(name, window);
Ok(())
})
}
fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> {
let dev = self.get_device_handler_data(device)?;
let name = dev.device.name();
@ -2989,6 +3015,12 @@ impl ConfigProxyHandler {
ClientMessage::SetSeatWorkspace { seat, workspace } => self
.handle_set_seat_workspace(seat, workspace)
.wrn("set_seat_workspace")?,
ClientMessage::SeatSendToScratchpad { seat, name } => self
.handle_seat_send_to_scratchpad(seat, name)
.wrn("seat_send_to_scratchpad")?,
ClientMessage::SeatToggleScratchpad { seat, name } => self
.handle_seat_toggle_scratchpad(seat, name)
.wrn("seat_toggle_scratchpad")?,
ClientMessage::GetConnector { ty, idx } => {
self.handle_get_connector(ty, idx).wrn("get_connector")?
}
@ -3373,6 +3405,9 @@ impl ConfigProxyHandler {
ClientMessage::SetWindowWorkspace { window, workspace } => self
.handle_set_window_workspace(window, workspace)
.wrn("set_window_workspace")?,
ClientMessage::WindowSendToScratchpad { window, name } => self
.handle_window_send_to_scratchpad(window, name)
.wrn("window_send_to_scratchpad")?,
ClientMessage::SetWindowFullscreen { window, fullscreen } => self
.handle_set_window_fullscreen(window, fullscreen)
.wrn("set_window_fullscreen")?,

View file

@ -284,6 +284,20 @@ 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,
})
}
fn clear(&self) {
unsafe {
if let Some(srv) = self.srv.take() {

View file

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

View file

@ -0,0 +1,50 @@
use {
crate::{
it::{test_error::TestResult, testrun::TestRun},
tree::Node,
},
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");
Ok(())
}

View file

@ -114,9 +114,11 @@ use {
tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
PlaceholderNode, ScratchpadToplevelState, TearingMode, TileState, ToplevelData,
ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode,
WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig,
generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad,
toplevel_restore_from_scratchpad, toplevel_set_workspace,
},
udmabuf::UdmabufHolder,
utils::{
@ -412,6 +414,7 @@ pub struct State {
pub bo_drop_queue: Rc<ObjectDropQueue<Rc<dyn BufferObject>>>,
pub virtual_outputs: VirtualOutputs,
pub clean_logs_older_than: Cell<Option<SystemTime>>,
pub scratchpads: RefCell<AHashMap<String, Vec<Rc<ScratchpadEntry>>>>,
}
// impl Drop for State {
@ -459,6 +462,28 @@ pub struct IdleState {
pub in_grace_period: Cell<bool>,
}
pub struct ScratchpadEntry {
node: Weak<dyn ToplevelNode>,
identifier: ToplevelIdentifier,
hidden: Cell<bool>,
restore: RefCell<Option<ScratchpadToplevelState>>,
}
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 {
pub fn set_timeout(&self, state: &State, timeout: Duration) {
self.timeout.set(timeout);
@ -1023,6 +1048,121 @@ impl State {
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();
let entry = Rc::new(ScratchpadEntry {
node: Rc::downgrade(&node),
identifier,
hidden: Cell::new(false),
restore: Default::default(),
});
let Some(restore) = toplevel_hide_for_scratchpad(node) else {
return;
};
*entry.restore.borrow_mut() = Some(restore);
entry.hidden.set(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.clone());
}
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());
entries
.iter()
.rev()
.find(|entry| {
!entry.hidden.get() && entry.node().is_some_and(|node| node.node_visible())
})
.cloned()
.or_else(|| {
entries
.iter()
.rev()
.find(|entry| {
entry.hidden.get()
|| entry.node().is_some_and(|node| !node.node_visible())
})
.cloned()
})
};
let Some(entry) = entry else {
return;
};
if entry.hidden.get() {
self.show_scratchpad_entry(seat, &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);
}
}
fn hide_scratchpad_entry(self: &Rc<Self>, entry: &Rc<ScratchpadEntry>) {
let Some(node) = entry.node() else {
return;
};
if let Some(restore) = toplevel_hide_for_scratchpad(node) {
*entry.restore.borrow_mut() = Some(restore);
entry.hidden.set(true);
self.tree_changed();
}
}
fn show_scratchpad_entry(
self: &Rc<Self>,
seat: &Rc<WlSeatGlobal>,
entry: &Rc<ScratchpadEntry>,
) {
if !entry.hidden.get() {
return;
}
let Some(node) = entry.node() else {
return;
};
let restore = entry.restore.borrow();
let Some(restore) = restore.as_ref() else {
return;
};
let ws = seat.get_fallback_output().ensure_workspace();
toplevel_restore_from_scratchpad(self, node.clone(), &ws, restore);
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>>) {
if !node.node_visible() {
return;
@ -1298,6 +1438,7 @@ impl State {
self.node_at_tree.borrow_mut().clear();
self.position_hint_requests.clear();
self.pending_warp_mouse_to_focus.clear();
self.scratchpads.borrow_mut().clear();
self.head_managers.clear();
self.head_managers_async.clear();
self.const_40hz_latch.clear();

View file

@ -1323,3 +1323,64 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
tl.tl_set_fullscreen(true, Some(ws.clone()));
}
}
pub struct ScratchpadToplevelState {
pub floating: bool,
pub fullscreen: bool,
pub workspace: Option<Rc<WorkspaceNode>>,
}
pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> Option<ScratchpadToplevelState> {
if tl.node_is_placeholder() {
return None;
}
let data = tl.tl_data();
let scratchpad_state = ScratchpadToplevelState {
floating: data.parent_is_float.get(),
fullscreen: data.is_fullscreen.get(),
workspace: data.workspace.get(),
};
if data.is_fullscreen.get() {
tl.clone().tl_set_fullscreen(false, None);
if data.is_fullscreen.get() {
return None;
}
}
let parent = data.parent.get()?;
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) = &scratchpad_state.workspace {
for seat in kb_foci {
workspace
.clone()
.node_do_focus(&seat, Direction::Unspecified);
}
}
Some(scratchpad_state)
}
pub fn toplevel_restore_from_scratchpad(
state: &Rc<State>,
tl: Rc<dyn ToplevelNode>,
ws: &Rc<WorkspaceNode>,
scratchpad_state: &ScratchpadToplevelState,
) {
if scratchpad_state.floating {
let (width, height) = tl.tl_data().float_size(ws);
state.map_floating(tl.clone(), width, height, ws, None);
} else {
state.map_tiled_on(tl.clone(), ws);
}
if scratchpad_state.fullscreen && ws.fullscreen.is_none() {
tl.tl_set_fullscreen(true, Some(ws.clone()));
}
}