Add new master stack layout

Currently only this layout for the Lua client works, and there's no cycling layouts yet
This commit is contained in:
Ottatop 2024-03-14 17:31:32 -05:00
parent 13ea0a683b
commit b3ba9f9393
17 changed files with 802 additions and 40 deletions

View file

@ -4,6 +4,7 @@ require("pinnacle").setup(function(Pinnacle)
local Output = Pinnacle.output
local Tag = Pinnacle.tag
local Window = Pinnacle.window
local Layout = Pinnacle.layout
local key = Input.key
@ -83,35 +84,18 @@ require("pinnacle").setup(function(Pinnacle)
tags[1]:set_active(true)
end)
-- Spawning must happen after you add tags, as Pinnacle currently doesn't render windows without tags.
Process.spawn_once(terminal)
--------------------
-- Layouts --
--------------------
-- Create a layout cycler to cycle layouts on an output.
local layout_cycler = Tag.new_layout_cycler({
"master_stack",
"dwindle",
"spiral",
"corner_top_left",
"corner_top_right",
"corner_bottom_left",
"corner_bottom_right",
local layout_handler = Layout.new_handler({
Layout.builtins.master_stack,
})
-- mod_key + space = Cycle forward one layout on the focused output
Input.keybind({ mod_key }, key.space, function()
local focused_op = Output.get_focused()
if focused_op then
layout_cycler.next(focused_op)
end
end)
Layout.set_handler(layout_handler)
-- mod_key + shift + space = Cycle backward one layout on the focused output
Input.keybind({ mod_key, "shift" }, key.space, function()
local focused_op = Output.get_focused()
if focused_op then
layout_cycler.prev(focused_op)
end
end)
-- Spawning must happen after you add tags, as Pinnacle currently doesn't render windows without tags.
Process.spawn_once(terminal)
for _, tag_name in ipairs(tag_names) do
-- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above

View file

@ -28,5 +28,6 @@ build = {
["pinnacle.window"] = "pinnacle/window.lua",
["pinnacle.util"] = "pinnacle/util.lua",
["pinnacle.signal"] = "pinnacle/signal.lua",
["pinnacle.layout"] = "pinnacle/layout.lua",
},
}

View file

@ -19,6 +19,10 @@ local pinnacle = {
window = require("pinnacle.window"),
---@type Process
process = require("pinnacle.process"),
---@type Util
util = require("pinnacle.util"),
---@type Layout
layout = require("pinnacle.layout"),
}
---Quit Pinnacle.
@ -44,7 +48,10 @@ function pinnacle.setup(config_fn)
config_fn(pinnacle)
client.loop:loop()
local success, err = pcall(client.loop.loop, client.loop)
if not success then
print(err)
end
end
return pinnacle

View file

@ -170,7 +170,7 @@ end
---@nodoc
---@param grpc_request_params GrpcRequestParams
---@param callback fun(response: table)
---@param callback fun(response: table, stream: H2Stream)
---
---@return H2Stream
function client.bidirectional_streaming_request(grpc_request_params, callback)
@ -209,7 +209,7 @@ function client.bidirectional_streaming_request(grpc_request_params, callback)
end
local response = obj
callback(response)
callback(response, stream)
response_body = response_body:sub(msg_len + 1)
end

View file

@ -18,6 +18,7 @@ function protobuf.build_protos()
PINNACLE_PROTO_DIR .. "/pinnacle/process/" .. version .. "/process.proto",
PINNACLE_PROTO_DIR .. "/pinnacle/window/" .. version .. "/window.proto",
PINNACLE_PROTO_DIR .. "/pinnacle/signal/" .. version .. "/signal.proto",
PINNACLE_PROTO_DIR .. "/pinnacle/layout/" .. version .. "/layout.proto",
PINNACLE_PROTO_DIR .. "/google/protobuf/empty.proto",
}

321
api/lua/pinnacle/layout.lua Normal file
View file

@ -0,0 +1,321 @@
local client = require("pinnacle.grpc.client")
local protobuf = require("pinnacle.grpc.protobuf")
---The protobuf absolute path prefix
local prefix = "pinnacle.layout." .. client.version .. "."
local service = prefix .. "LayoutService"
---@type table<string, { request_type: string?, response_type: string? }>
---@enum (key) LayoutServiceMethod
local rpc_types = {
Layout = {
response_type = "LayoutResponse",
},
}
---Build GrpcRequestParams
---@param method LayoutServiceMethod
---@param data table
---@return GrpcRequestParams
local function build_grpc_request_params(method, data)
local req_type = rpc_types[method].request_type
local resp_type = rpc_types[method].response_type
---@type GrpcRequestParams
return {
service = service,
method = method,
request_type = req_type and prefix .. req_type or prefix .. method .. "Request",
response_type = resp_type and prefix .. resp_type,
data = data,
}
end
---@class LayoutArgs
---@field output OutputHandle
---@field windows WindowHandle[]
---@field tags TagHandle[]
---@field output_width integer
---@field output_height integer
---@class Builtin
---@field layout fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[]
---@class Builtin.MasterStack : Builtin
---Gaps between windows, in pixels.
---
---This can be an integer or the table { inner: integer, outer: integer }.
---If it is an integer, all gaps will be that amount of pixels wide.
---If it is a table, `outer` denotes the amount of pixels around the
---edge of the output area that will become a gap, and
---`inner` denotes the amount of pixels around each window that
---will become a gap.
---
---This means that, for example, `inner = 2` will cause the gap
---width between windows to be 4; 2 around each window.
---
---Defaults to 4.
---@field gaps integer | { inner: integer, outer: integer }
---The proportion of the output taken up by the master window(s).
---
---This is a float that will be clamped between 0.1 and 0.9
---similarly to River.
---
---Defaults to 0.5.
---@field master_factor number
---The side the master window(s) will be on.
---
---Defaults to `"left"`.
---@field master_side "left"|"right"|"top"|"bottom"
---How many windows the master side will have.
---
---Defaults to 1.
---@field master_count integer
local builtins = {
---@type Builtin.MasterStack
master_stack = {
gaps = 4,
master_factor = 0.5,
master_side = "left",
master_count = 1,
},
}
---@param args LayoutArgs
---
---@return { x: integer, y: integer, width: integer, height: integer }[]
function builtins.master_stack:layout(args)
local win_count = #args.windows
if win_count == 0 then
return {}
end
local width = args.output_width
local height = args.output_height
---@type { x: integer, y: integer, width: integer, height: integer }[]
local geos = {}
local master_factor = math.max(math.min(self.master_factor, 0.9), 0.1)
if win_count <= self.master_count then
master_factor = 1
end
local rect = require("pinnacle.util").rectangle.new(0, 0, width, height)
local master_rect
local stack_rect
if type(self.gaps) == "number" then
local gaps = self.gaps --[[@as integer]]
rect = rect:split_at("horizontal", 0, gaps)
rect = rect:split_at("horizontal", height - gaps, gaps)
rect = rect:split_at("vertical", 0, gaps)
rect = rect:split_at("vertical", width - gaps, gaps)
if self.master_side == "left" then
master_rect, stack_rect = rect:split_at("vertical", math.floor(width * master_factor) - gaps // 2, gaps)
elseif self.master_side == "right" then
stack_rect, master_rect = rect:split_at("vertical", math.floor(width * master_factor) - gaps // 2, gaps)
elseif self.master_side == "top" then
master_rect, stack_rect = rect:split_at("horizontal", math.floor(height * master_factor) - gaps // 2, gaps)
else
stack_rect, master_rect = rect:split_at("horizontal", math.floor(height * master_factor) - gaps // 2, gaps)
end
if not master_rect then
assert(stack_rect)
master_rect = stack_rect
stack_rect = nil
end
local master_slice_count
local stack_slice_count = nil
if win_count > self.master_count then
master_slice_count = self.master_count - 1
stack_slice_count = win_count - self.master_count - 1
else
master_slice_count = win_count - 1
end
-- layout the master side
if master_slice_count > 0 then
local coord
local len
local axis
if self.master_side == "left" or self.master_side == "right" then
coord = master_rect.y
len = master_rect.height
axis = "horizontal"
else
coord = master_rect.x
len = master_rect.width
axis = "vertical"
end
for i = 1, master_slice_count do
local slice_point = coord + math.floor(len * i + 0.5)
slice_point = slice_point - gaps // 2
local to_push, rest = master_rect:split_at(axis, slice_point, gaps)
table.insert(geos, to_push)
master_rect = rest
end
end
table.insert(geos, master_rect)
if stack_slice_count then
assert(stack_rect)
if stack_slice_count > 0 then
local coord
local len
local axis
if self.master_side == "left" or self.master_side == "right" then
coord = stack_rect.y
len = stack_rect.height / (stack_slice_count + 1)
axis = "horizontal"
else
coord = stack_rect.x
len = stack_rect.width / (stack_slice_count + 1)
axis = "vertical"
end
for i = 1, stack_slice_count do
local slice_point = coord + math.floor(len * i + 0.5)
slice_point = slice_point - gaps // 2
local to_push, rest = stack_rect:split_at(axis, slice_point, gaps)
table.insert(geos, to_push)
stack_rect = rest
end
end
table.insert(geos, stack_rect)
end
return geos
else
local origin_x = self.gaps.outer
local origin_y = self.gaps.outer
width = width - self.gaps.outer * 2
height = height - self.gaps.outer * 2
if win_count == 1 then
table.insert(geos, {
x = origin_x + self.gaps.inner,
y = origin_y + self.gaps.inner,
width = width - self.gaps.inner * 2,
height = height - self.gaps.inner * 2,
})
return geos
end
local h = height / win_count
local y_s = {}
for i = 0, win_count - 1 do
table.insert(y_s, math.floor(i * h + 0.5))
end
local heights = {}
for i = 1, win_count - 1 do
table.insert(heights, y_s[i + 1] - y_s[i])
end
table.insert(heights, height - y_s[win_count])
for i = 1, win_count do
table.insert(geos, { x = origin_x, y = origin_y + y_s[i], width = width, height = heights[i] })
end
for i = 1, #geos do
geos[i].x = geos[i].x + self.gaps.inner
geos[i].y = geos[i].y + self.gaps.inner
geos[i].width = geos[i].width - self.gaps.inner * 2
geos[i].height = geos[i].height - self.gaps.inner * 2
end
return geos
end
end
---@class Layout
local layout = {
builtins = builtins,
}
---@param handler LayoutHandler
function layout.set_handler(handler)
client.bidirectional_streaming_request(
build_grpc_request_params("Layout", {
layout = {},
}),
function(response, stream)
local request_id = response.request_id
local index = handler.index
---@diagnostic disable-next-line: invisible
local output_handle = require("pinnacle.output").handle.new(response.output_name)
---@diagnostic disable-next-line: invisible
local window_handles = require("pinnacle.window").handle.new_from_table(response.window_ids or {})
---@diagnostic disable-next-line: invisible
local tag_handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {})
---@type LayoutArgs
local args = {
output = output_handle,
windows = window_handles,
tags = tag_handles,
output_width = response.output_width,
output_height = response.output_height,
}
local geos = handler.layouts[index]:layout(args)
local body = protobuf.encode(".pinnacle.layout.v0alpha1.LayoutRequest", {
geometries = {
request_id = request_id,
geometries = geos,
output_name = response.output_name,
},
})
stream:write_chunk(body, false)
end
)
end
---@class LayoutHandlerModule
local layout_handler = {}
---@class LayoutHandler
---@field index integer
---@field layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[]
local LayoutHandler = {}
---@param layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[]
---@return LayoutHandler
function layout_handler.new(layouts)
---@type LayoutHandler
local self = {
index = 1,
layouts = layouts,
}
setmetatable(self, { __index = LayoutHandler })
return self
end
---@param layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[]
---
---@return LayoutHandler
function layout.new_handler(layouts)
return layout_handler.new(layouts)
end
return layout

View file

@ -250,7 +250,7 @@ end
---layout_cycler.prev(Output.get_by_name("HDMI-1"))
---```
---
---@param layouts Layout[]
---@param layouts LayoutOld[]
---
---@return LayoutCycler
function tag.new_layout_cycler(layouts)
@ -393,7 +393,7 @@ local layout_name_to_code = {
corner_bottom_left = 6,
corner_bottom_right = 7,
}
---@alias Layout
---@alias LayoutOld
---| "master_stack" # One master window on the left with all other windows stacked to the right.
---| "dwindle" # Windows split in half towards the bottom right corner.
---| "spiral" # Windows split in half in a spiral.
@ -412,7 +412,7 @@ local layout_name_to_code = {
---Tag.get("Tag"):set_layout("dwindle")
---```
---
---@param layout Layout
---@param layout LayoutOld
function TagHandle:set_layout(layout)
---@diagnostic disable-next-line: redefined-local
local layout = layout_name_to_code[layout]

View file

@ -77,4 +77,126 @@ function util.batch(requests)
return responses
end
-- Geometry stuff
---@class RectangleModule
local rectangle = {}
---@class Rectangle
---@field x number The x-position of the top-left corner
---@field y number The y-position of the top-left corner
---@field width number The width of the rectangle
---@field height number The height of the rectangle
local Rectangle = {}
---Split this rectangle along `axis` at `at`.
---
---If `at2` is specified, the split will chop off a section of this
---rectangle from `at` to `at2`.
---
---`at` and `at2` are relative to the space this rectangle is in, not
---this rectangle's origin.
---
---@param axis "horizontal" | "vertical"
---@param at number
---@param thickness? number
---
---@return Rectangle rect1 The first rectangle.
---@return Rectangle? rect2 The seoond rectangle, if there is one.
function Rectangle:split_at(axis, at, thickness)
---@diagnostic disable-next-line: redefined-local
local thickness = thickness or 0
if axis == "horizontal" then
-- Split is off to the top, at most chop off to `thickness`
if at <= self.y then
local diff = at - self.y + thickness
if diff > 0 then
self.y = self.y + diff
self.height = self.height - diff
end
return self
-- Split is to the bottom, then do nothing
elseif at >= self.y + self.height then
return self
-- Split only chops bottom off
elseif at + thickness >= self.y + self.height then
local diff = (self.y + self.height) - at
self.height = self.height - diff
return self
-- Do a split
else
local x = self.x
local top_y = self.y
local width = self.width
local top_height = at - self.y
local bot_y = at + thickness
local bot_height = self.y + self.height - at - thickness
local rect1 = rectangle.new(x, top_y, width, top_height)
local rect2 = rectangle.new(x, bot_y, width, bot_height)
return rect1, rect2
end
elseif axis == "vertical" then
-- Split is off to the left, at most chop off to `thickness`
if at <= self.x then
local diff = at - self.x + thickness
if diff > 0 then
self.x = self.x + diff
self.width = self.width - diff
end
return self
-- Split is to the right, then do nothing
elseif at >= self.x + self.width then
return self
-- Split only chops bottom off
elseif at + thickness >= self.x + self.width then
local diff = (self.x + self.width) - at
self.width = self.width - diff
return self
-- Do a split
else
local left_x = self.x
local y = self.y
local left_width = at - self.x
local height = self.height
local right_x = at + thickness
local right_width = self.x + self.width - at - thickness
local rect1 = rectangle.new(left_x, y, left_width, height)
local rect2 = rectangle.new(right_x, y, right_width, height)
return rect1, rect2
end
end -- TODO: handle error if neither
os.exit(1)
end
---@return Rectangle
function rectangle.new(x, y, width, height)
---@type Rectangle
local self = {
x = x,
y = y,
width = width,
height = height,
}
setmetatable(self, { __index = Rectangle })
return self
end
local r = rectangle.new(0, 0, 100, 100)
local r1, r2 = r:split_at("horizontal", 96, 4)
print(require("inspect")(r1))
print(require("inspect")(r2))
util.rectangle = rectangle
return util

View file

@ -425,7 +425,7 @@ end
--- focused:set_geometry({}) -- Do nothing useful
---end
---```
---@param geo { x: integer?, y: integer, width: integer?, height: integer? } The new location and/or size
---@param geo { x: integer?, y: integer?, width: integer?, height: integer? } The new location and/or size
function WindowHandle:set_geometry(geo)
client.unary_request(build_grpc_request_params("SetGeometry", { window_id = self.id, geometry = geo }))
end

View file

@ -7,20 +7,22 @@ import "pinnacle/v0alpha1/pinnacle.proto";
// Love how the response is the request and the request is the response
message LayoutRequest {
// Respond to a layout request from the compositor.
// A response to a layout request from the compositor.
message Geometries {
// The id of the request this layout response is responding to.
//
// Responding with a request_id that has already been responded to
// or that doesn't exist will return an error.
optional uint32 request_id = 1;
// The output this request is responding to.
optional string output_name = 2;
// Target geometries of all windows being laid out.
//
// Responding with a different number of geometries than
// requested windows will return an error.
repeated .pinnacle.v0alpha1.Geometry geometries = 2;
repeated .pinnacle.v0alpha1.Geometry geometries = 3;
}
// Request a layout explicitly.
// An explicit layout request.
message ExplicitLayout {}
oneof body {

View file

@ -14,6 +14,7 @@ fn main() {
formatcp!("../api/protocol/pinnacle/tag/{VERSION}/tag.proto"),
formatcp!("../api/protocol/pinnacle/window/{VERSION}/window.proto"),
formatcp!("../api/protocol/pinnacle/signal/{VERSION}/signal.proto"),
formatcp!("../api/protocol/pinnacle/layout/{VERSION}/layout.proto"),
];
let descriptor_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("pinnacle.bin");

View file

@ -68,6 +68,12 @@ pub mod pinnacle {
);
}
}
pub mod layout {
pub mod v0alpha1 {
tonic::include_proto!("pinnacle.layout.v0alpha1");
}
}
}
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("pinnacle");

View file

@ -1,3 +1,4 @@
pub mod layout;
pub mod signal;
pub mod window;

56
src/api/layout.rs Normal file
View file

@ -0,0 +1,56 @@
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
layout_request::{self, ExplicitLayout},
layout_service_server, LayoutRequest, LayoutResponse,
};
use tonic::{Request, Response, Status, Streaming};
use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
pub struct LayoutService {
sender: StateFnSender,
}
impl LayoutService {
pub fn new(sender: StateFnSender) -> Self {
Self { sender }
}
}
#[tonic::async_trait]
impl layout_service_server::LayoutService for LayoutService {
type LayoutStream = ResponseStream<LayoutResponse>;
async fn layout(
&self,
request: Request<Streaming<LayoutRequest>>,
) -> Result<Response<Self::LayoutStream>, Status> {
let in_stream = request.into_inner();
run_bidirectional_streaming(
self.sender.clone(),
in_stream,
|state, request| match request {
Ok(request) => {
if let Some(body) = request.body {
match body {
layout_request::Body::Geometries(geos) => {
// dbg!(&geos);
if let Err(err) = state.apply_layout(geos) {
// TODO: send a Status and handle the error client side
tracing::error!("{err}")
}
}
layout_request::Body::Layout(ExplicitLayout {}) => {
// TODO: state.layout_request(output, windows)
}
}
}
}
Err(_) => (),
},
|state, sender, join_handle| {
state.layout_state.layout_request_sender = Some(sender);
},
)
}
}

View file

@ -1,7 +1,7 @@
use crate::{
api::{
signal::SignalService, window::WindowService, InputService, OutputService, PinnacleService,
ProcessService, TagService,
layout::LayoutService, signal::SignalService, window::WindowService, InputService,
OutputService, PinnacleService, ProcessService, TagService,
},
input::ModifierMask,
output::OutputName,
@ -17,6 +17,7 @@ use std::{
use anyhow::Context;
use pinnacle_api_defs::pinnacle::{
input::v0alpha1::input_service_server::InputServiceServer,
layout::v0alpha1::layout_service_server::LayoutServiceServer,
output::v0alpha1::output_service_server::OutputServiceServer,
process::v0alpha1::process_service_server::ProcessServiceServer,
signal::v0alpha1::signal_service_server::SignalServiceServer,
@ -477,6 +478,7 @@ impl State {
let output_service = OutputService::new(grpc_sender.clone());
let window_service = WindowService::new(grpc_sender.clone());
let signal_service = SignalService::new(grpc_sender.clone());
let layout_service = LayoutService::new(grpc_sender.clone());
let refl_service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(pinnacle_api_defs::FILE_DESCRIPTOR_SET)
@ -495,7 +497,8 @@ impl State {
.add_service(TagServiceServer::new(tag_service))
.add_service(OutputServiceServer::new(output_service))
.add_service(WindowServiceServer::new(window_service))
.add_service(SignalServiceServer::new(signal_service));
.add_service(SignalServiceServer::new(signal_service))
.add_service(LayoutServiceServer::new(layout_service));
match self.xdisplay.as_ref() {
Some(_) => {

View file

@ -1,13 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::collections::{HashMap, HashSet};
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{layout_request::Geometries, LayoutResponse};
use smithay::{
desktop::{layer_map_for_output, WindowSurface},
output::Output,
utils::{Logical, Point, Rectangle, Serial, Size},
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
};
use tokio::sync::mpsc::UnboundedSender;
use tonic::Status;
use tracing::error;
use crate::{
output::OutputName,
state::{State, WithState},
window::{
window_state::{FloatingOrTiled, FullscreenOrMaximized},
@ -34,7 +41,7 @@ impl State {
}) else {
// TODO: maybe default to something like 800x800 like in anvil so people still see
// | windows open
tracing::error!("Failed to get output geometry");
error!("Failed to get output geometry");
return;
};
@ -49,6 +56,113 @@ impl State {
}
}
pub fn update_windows_with_geometries(
&mut self,
output: &Output,
geometries: Vec<Rectangle<i32, Logical>>,
) {
let windows_on_foc_tags = output.with_state(|state| {
let focused_tags = state.focused_tags().collect::<Vec<_>>();
self.windows
.iter()
.filter(|win| !win.is_x11_override_redirect())
.filter(|win| {
win.with_state(|state| state.tags.iter().any(|tg| focused_tags.contains(&tg)))
})
.cloned()
.collect::<Vec<_>>()
});
let tiled_windows = windows_on_foc_tags
.iter()
.filter(|win| {
win.with_state(|state| {
state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither()
})
})
.cloned();
let output_geo = self.space.output_geometry(output).expect("no output geo");
for (win, geo) in tiled_windows.zip(geometries.into_iter().map(|mut geo| {
geo.loc += output_geo.loc;
geo
})) {
win.change_geometry(geo);
}
for window in windows_on_foc_tags.iter() {
match window.with_state(|state| state.fullscreen_or_maximized) {
FullscreenOrMaximized::Fullscreen => {
window.change_geometry(output_geo);
}
FullscreenOrMaximized::Maximized => {
let map = layer_map_for_output(output);
let geo = if map.layers().next().is_none() {
// INFO: Sometimes the exclusive zone is some weird number that doesn't match the
// | output res, even when there are no layer surfaces mapped. In this case, we
// | just return the output geometry.
output_geo
} else {
let zone = map.non_exclusive_zone();
tracing::debug!("non_exclusive_zone is {zone:?}");
Rectangle::from_loc_and_size(output_geo.loc + zone.loc, zone.size)
};
window.change_geometry(geo);
}
FullscreenOrMaximized::Neither => {
if let FloatingOrTiled::Floating(rect) =
window.with_state(|state| state.floating_or_tiled)
{
window.change_geometry(rect);
}
}
}
}
let mut pending_wins = Vec::<(WindowElement, Serial)>::new();
let mut non_pending_wins = Vec::<(Point<i32, Logical>, WindowElement)>::new();
for win in windows_on_foc_tags.iter() {
if win.with_state(|state| state.target_loc.is_some()) {
match win.underlying_surface() {
WindowSurface::Wayland(toplevel) => {
let pending = compositor::with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.expect("XdgToplevelSurfaceData wasn't in surface's data map")
.lock()
.expect("Failed to lock Mutex<XdgToplevelSurfaceData>")
.has_pending_changes()
});
if pending {
pending_wins.push((win.clone(), toplevel.send_configure()))
} else {
let loc = win.with_state_mut(|state| state.target_loc.take());
if let Some(loc) = loc {
non_pending_wins.push((loc, win.clone()));
}
}
}
WindowSurface::X11(_) => {
let loc = win.with_state_mut(|state| state.target_loc.take());
if let Some(loc) = loc {
self.space.map_element(win.clone(), loc, false);
}
}
}
}
}
for (loc, window) in non_pending_wins {
self.space.map_element(window, loc, false);
}
self.fixup_z_layering();
}
pub fn update_windows(&mut self, output: &Output) {
let Some(layout) =
output.with_state(|state| state.focused_tags().next().map(|tag| tag.layout()))
@ -78,6 +192,10 @@ impl State {
.cloned()
.collect::<Vec<_>>();
self.layout_request(output.clone(), tiled_windows);
return;
self.tile_windows(output, tiled_windows, layout);
let output_geo = self.space.output_geometry(output).expect("no output geo");
@ -468,3 +586,137 @@ impl State {
}
}
}
// New layout system stuff
/// A monotonically increasing identifier for layout requests.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct LayoutRequestId(pub u32);
#[derive(Debug, Default)]
pub struct LayoutState {
pub layout_request_sender: Option<UnboundedSender<Result<LayoutResponse, Status>>>,
id_maps: HashMap<Output, LayoutRequestId>,
pending_requests: HashMap<Output, Vec<(LayoutRequestId, Vec<WindowElement>)>>,
old_requests: HashMap<Output, HashSet<LayoutRequestId>>,
}
impl State {
pub fn layout_request(&mut self, output: Output, windows: Vec<WindowElement>) {
let Some(sender) = self.layout_state.layout_request_sender.as_ref() else {
error!("Layout requested but no client has connected to the layout service");
return;
};
let Some((output_width, output_height)) = self
.space
.output_geometry(&output)
.map(|geo| (geo.size.w, geo.size.h))
else {
error!("Called `output_geometry` on an unmapped output");
return;
};
let window_ids = windows
.iter()
.map(|win| win.with_state(|state| state.id.0))
.collect::<Vec<_>>();
let tag_ids =
output.with_state(|state| state.focused_tags().map(|tag| tag.id().0).collect());
let id = self
.layout_state
.id_maps
.entry(output.clone())
.or_insert(LayoutRequestId(0));
self.layout_state
.pending_requests
.entry(output.clone())
.or_default()
.push((*id, windows));
// TODO: error
let _ = sender.send(Ok(LayoutResponse {
request_id: Some(id.0),
output_name: Some(output.name()),
window_ids,
tag_ids,
output_width: Some(output_width as u32),
output_height: Some(output_height as u32),
}));
*id = LayoutRequestId(id.0 + 1);
}
pub fn apply_layout(&mut self, geometries: Geometries) -> anyhow::Result<()> {
tracing::info!("Applying layout");
let Geometries {
request_id: Some(request_id),
output_name: Some(output_name),
geometries,
} = geometries
else {
anyhow::bail!("One or more `geometries` fields were None");
};
let request_id = LayoutRequestId(request_id);
let Some(output) = OutputName(output_name).output(self) else {
anyhow::bail!("Output was invalid");
};
let old_requests = self
.layout_state
.old_requests
.entry(output.clone())
.or_default();
if old_requests.contains(&request_id) {
anyhow::bail!("Attempted to layout but the request was already fulfilled");
}
let pending = self
.layout_state
.pending_requests
.entry(output.clone())
.or_default();
let Some(latest) = pending.last().map(|(id, _)| *id) else {
anyhow::bail!("Attempted to layout but the request was nonexistent A");
};
if dbg!(latest) == dbg!(request_id) {
pending.pop();
} else if let Some(pos) = pending
.split_last()
.and_then(|(_, rest)| rest.iter().position(|(id, _)| id == &request_id))
{
// Ignore stale requests
old_requests.insert(request_id);
pending.remove(pos);
return Ok(());
} else {
anyhow::bail!("Attempted to layout but the request was nonexistent B");
};
let geometries = geometries
.into_iter()
.map(|geo| {
Some(Rectangle::<i32, Logical>::from_loc_and_size(
(geo.x?, geo.y?),
(geo.width?, geo.height?),
))
})
.collect::<Option<Vec<_>>>();
let Some(geometries) = geometries else {
anyhow::bail!("Attempted to layout but one or more dimensions were null");
};
self.update_windows_with_geometries(&output, geometries);
Ok(())
}
}

View file

@ -2,7 +2,8 @@
use crate::{
api::signal::SignalState, backend::Backend, config::Config, cursor::Cursor,
focus::OutputFocusStack, grab::resize_grab::ResizeSurfaceState, window::WindowElement,
focus::OutputFocusStack, grab::resize_grab::ResizeSurfaceState, layout::LayoutState,
window::WindowElement,
};
use anyhow::Context;
use smithay::{
@ -98,6 +99,8 @@ pub struct State {
pub xdg_base_dirs: BaseDirectories,
pub signal_state: SignalState,
pub layout_state: LayoutState,
}
impl State {
@ -277,6 +280,8 @@ impl State {
.context("couldn't create xdg BaseDirectories")?,
signal_state: SignalState::default(),
layout_state: LayoutState::default(),
};
Ok(state)