Skip to content

Commit 804c3b2

Browse files
authored
Merge pull request #340 from Ph4ntomas/324-window_handle-swap
window: Add method to swap two windows
2 parents 7d7da24 + a338894 commit 804c3b2

9 files changed

Lines changed: 472 additions & 3 deletions

File tree

api/lua/pinnacle/grpc/defs.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,12 @@ local pinnacle_window_v1_DecorationMode = {
12101210
---@class pinnacle.window.v1.ResizeGrabRequest
12111211
---@field button integer?
12121212

1213+
---@class pinnacle.window.v1.SwapRequest
1214+
---@field window_id integer?
1215+
---@field target_id integer?
1216+
1217+
---@class pinnacle.window.v1.SwapResponse
1218+
12131219
---@class pinnacle.window.v1.WindowRuleRequest
12141220
---@field finished pinnacle.window.v1.WindowRuleRequest.Finished?
12151221

@@ -1427,6 +1433,8 @@ pinnacle.window.v1.MoveToOutputRequest = {}
14271433
pinnacle.window.v1.RaiseRequest = {}
14281434
pinnacle.window.v1.MoveGrabRequest = {}
14291435
pinnacle.window.v1.ResizeGrabRequest = {}
1436+
pinnacle.window.v1.SwapRequest = {}
1437+
pinnacle.window.v1.SwapResponse = {}
14301438
pinnacle.window.v1.WindowRuleRequest = {}
14311439
pinnacle.window.v1.WindowRuleRequest.Finished = {}
14321440
pinnacle.window.v1.WindowRuleResponse = {}
@@ -3256,6 +3264,23 @@ pinnacle.window.v1.WindowService.ResizeGrab.response = ".google.protobuf.Empty"
32563264
function Client:pinnacle_window_v1_WindowService_ResizeGrab(data)
32573265
return self:unary_request(pinnacle.window.v1.WindowService.ResizeGrab, data)
32583266
end
3267+
pinnacle.window.v1.WindowService.Swap = {}
3268+
pinnacle.window.v1.WindowService.Swap.service = "pinnacle.window.v1.WindowService"
3269+
pinnacle.window.v1.WindowService.Swap.method = "Swap"
3270+
pinnacle.window.v1.WindowService.Swap.request = ".pinnacle.window.v1.SwapRequest"
3271+
pinnacle.window.v1.WindowService.Swap.response = ".pinnacle.window.v1.SwapResponse"
3272+
3273+
---Performs a unary request.
3274+
---
3275+
---@nodiscard
3276+
---
3277+
---@param data pinnacle.window.v1.SwapRequest
3278+
---
3279+
---@return pinnacle.window.v1.SwapResponse | nil response
3280+
---@return string | nil error An error string, if any
3281+
function Client:pinnacle_window_v1_WindowService_Swap(data)
3282+
return self:unary_request(pinnacle.window.v1.WindowService.Swap, data)
3283+
end
32593284
pinnacle.window.v1.WindowService.WindowRule = {}
32603285
pinnacle.window.v1.WindowService.WindowRule.service = "pinnacle.window.v1.WindowService"
32613286
pinnacle.window.v1.WindowService.WindowRule.method = "WindowRule"

api/lua/pinnacle/window.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,23 @@ function WindowHandle:in_direction(direction)
743743
return response and window_handle.new_from_table(response.window_ids or {}) or {}
744744
end
745745

746+
---Swap position with another window.
747+
---
748+
---@param target pinnacle.window.WindowHandle
749+
function WindowHandle:swap(target)
750+
if target == nil or target.id == nil then
751+
log.error("Invalid window handle")
752+
return
753+
end
754+
755+
local _, err =
756+
client:pinnacle_window_v1_WindowService_Swap({ window_id = self.id, target_id = target.id })
757+
758+
if err then
759+
log.error(err)
760+
end
761+
end
762+
746763
---Creates a new `WindowHandle` from an id.
747764
---@param window_id integer
748765
---@return pinnacle.window.WindowHandle

api/protobuf/pinnacle/window/v1/window.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ message ResizeGrabRequest {
165165
uint32 button = 1;
166166
}
167167

168+
message SwapRequest {
169+
uint32 window_id = 1;
170+
uint32 target_id = 2;
171+
}
172+
message SwapResponse {}
173+
168174
message WindowRuleRequest {
169175
message Finished {
170176
uint32 request_id = 1;
@@ -213,6 +219,7 @@ service WindowService {
213219
rpc Raise(RaiseRequest) returns (google.protobuf.Empty);
214220
rpc MoveGrab(MoveGrabRequest) returns (google.protobuf.Empty);
215221
rpc ResizeGrab(ResizeGrabRequest) returns (google.protobuf.Empty);
222+
rpc Swap(SwapRequest) returns (SwapResponse);
216223

217224
rpc WindowRule(stream WindowRuleRequest) returns (stream WindowRuleResponse);
218225
}

api/rust/src/window.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use pinnacle_api_defs::pinnacle::{
2323
MoveGrabRequest, MoveToOutputRequest, MoveToTagRequest, RaiseRequest,
2424
ResizeGrabRequest, ResizeTileRequest, SetDecorationModeRequest, SetFloatingRequest,
2525
SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest,
26-
SetTagRequest, SetTagsRequest,
26+
SetTagRequest, SetTagsRequest, SwapRequest,
2727
},
2828
},
2929
};
@@ -817,6 +817,21 @@ impl WindowHandle {
817817
response.window_ids.into_iter().map(WindowHandle::from_id)
818818
}
819819

820+
/// Swap position with another window.
821+
pub fn swap(&self, target: &WindowHandle) {
822+
self.swap_async(target).block_on_tokio()
823+
}
824+
825+
/// Async impl for [`Self::swap`].
826+
pub async fn swap_async(&self, target: &WindowHandle) {
827+
let request = SwapRequest {
828+
window_id: self.id,
829+
target_id: target.id,
830+
};
831+
832+
Client::window().swap(request).await.unwrap();
833+
}
834+
820835
/// Gets this window's raw compositor id.
821836
pub fn id(&self) -> u32 {
822837
self.id

src/api/window.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,72 @@ pub fn resize_grab(state: &mut State, button: u32) {
366366
state.schedule_render(&output);
367367
}
368368
}
369+
370+
pub fn swap(state: &mut State, window: WindowElement, target: WindowElement) {
371+
if state.pinnacle.layout_state.pending_swap {
372+
return;
373+
}
374+
375+
if window == target {
376+
return;
377+
}
378+
379+
let output = window.output(&state.pinnacle);
380+
let target_output = target.output(&state.pinnacle);
381+
382+
let Some((output, target_output)) = output.zip(target_output) else {
383+
tracing::warn!("Can't swap windows without output");
384+
return;
385+
};
386+
387+
tracing::debug!("Swapping window positions");
388+
state.pinnacle.layout_state.pending_swap = true;
389+
390+
state.pinnacle.swap_window_positions(&window, &target);
391+
if output != target_output {
392+
tracing::debug!("Swapping window outputs");
393+
window.set_tags_to_output(&target_output);
394+
target.set_tags_to_output(&output);
395+
}
396+
397+
// Swap floating attribute. In case of cross-output swap, this prevent window jumping back.
398+
let window_floating_x = window.with_state(|state| state.floating_x);
399+
let window_floating_y = window.with_state(|state| state.floating_y);
400+
let window_floating_size = window.with_state(|state| state.floating_size);
401+
402+
let target_floating_x = target.with_state(|state| state.floating_x);
403+
let target_floating_y = target.with_state(|state| state.floating_y);
404+
let target_floating_size = target.with_state(|state| state.floating_size);
405+
406+
target.with_state_mut(|state| {
407+
state.floating_x = window_floating_x;
408+
state.floating_y = window_floating_y;
409+
state.floating_size = window_floating_size
410+
});
411+
412+
window.with_state_mut(|state| {
413+
state.floating_x = target_floating_x;
414+
state.floating_y = target_floating_y;
415+
state.floating_size = target_floating_size
416+
});
417+
418+
if window.with_state(|state| !state.layout_mode.is_tiled())
419+
|| target.with_state(|state| !state.layout_mode.is_tiled())
420+
{
421+
tracing::debug!("Swapping non tiled window");
422+
let window_layout_mode = window.with_state(|state| state.layout_mode);
423+
let target_layout_mode = target.with_state(|state| state.layout_mode);
424+
425+
state.update_window_layout_mode_and_layout(&window, |layout| {
426+
layout.apply_mode(target_layout_mode)
427+
});
428+
state.update_window_layout_mode_and_layout(&target, |layout| {
429+
layout.apply_mode(window_layout_mode)
430+
});
431+
} else {
432+
state.pinnacle.request_layout(&output);
433+
if output != target_output {
434+
state.pinnacle.request_layout(&target_output);
435+
}
436+
}
437+
}

src/api/window/v1.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use pinnacle_api_defs::pinnacle::{
1717
MoveToTagRequest, RaiseRequest, ResizeGrabRequest, ResizeTileRequest,
1818
SetDecorationModeRequest, SetFloatingRequest, SetFocusedRequest, SetFullscreenRequest,
1919
SetGeometryRequest, SetMaximizedRequest, SetTagRequest, SetTagsRequest,
20-
SetTagsResponse, WindowRuleRequest, WindowRuleResponse,
20+
SetTagsResponse, SwapRequest, SwapResponse, WindowRuleRequest, WindowRuleResponse,
2121
},
2222
},
2323
};
@@ -727,6 +727,25 @@ impl v1::window_service_server::WindowService for super::WindowService {
727727
.await
728728
}
729729

730+
async fn swap(&self, request: Request<SwapRequest>) -> TonicResult<SwapResponse> {
731+
let inner = request.into_inner();
732+
let window_id = WindowId(inner.window_id);
733+
let target_id = WindowId(inner.target_id);
734+
735+
run_unary(&self.sender, move |state| {
736+
let window = window_id.window(&state.pinnacle);
737+
let target = target_id.window(&state.pinnacle);
738+
739+
// Both window & target must be mapped
740+
if let Some((window, target)) = window.zip(target) {
741+
crate::api::window::swap(state, window, target);
742+
};
743+
744+
Ok(SwapResponse {})
745+
})
746+
.await
747+
}
748+
730749
async fn window_rule(
731750
&self,
732751
request: Request<Streaming<WindowRuleRequest>>,

src/handlers/window.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ impl State {
6666
.pending_transactions
6767
.add_for_output(
6868
output,
69-
transaction_builder.into_pending(Vec::new(), false, false),
69+
transaction_builder.into_pending(
70+
Vec::new(),
71+
self.pinnacle.layout_state.pending_swap,
72+
false,
73+
),
7074
);
7175
} else {
7276
// No changes were needed, we can map immediately here

src/window/window_state.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,23 @@ impl LayoutMode {
350350
}
351351
}
352352
}
353+
354+
/// Apply attributes from another LayoutMode.
355+
///
356+
/// If other was Maximized or Fullscreen due to a client request, elevated_mode is use instead,
357+
/// since the new value doesn't come from a client request.
358+
pub fn apply_mode(&mut self, other: LayoutMode) {
359+
self.elevated_mode = None;
360+
self.client_requested_mode = None;
361+
362+
match other.current() {
363+
LayoutModeKind::Tiled | LayoutModeKind::Floating | LayoutModeKind::Spilled => {
364+
self.base_mode = other.base_mode;
365+
}
366+
LayoutModeKind::Maximized => self.set_maximized(true),
367+
LayoutModeKind::Fullscreen => self.set_fullscreen(true),
368+
}
369+
}
353370
}
354371

355372
/// State of a [`WindowElement`]

0 commit comments

Comments
 (0)