mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-18 22:26:12 +01:00
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:
parent
13ea0a683b
commit
b3ba9f9393
17 changed files with 802 additions and 40 deletions
|
@ -4,6 +4,7 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
local Output = Pinnacle.output
|
local Output = Pinnacle.output
|
||||||
local Tag = Pinnacle.tag
|
local Tag = Pinnacle.tag
|
||||||
local Window = Pinnacle.window
|
local Window = Pinnacle.window
|
||||||
|
local Layout = Pinnacle.layout
|
||||||
|
|
||||||
local key = Input.key
|
local key = Input.key
|
||||||
|
|
||||||
|
@ -83,35 +84,18 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
tags[1]:set_active(true)
|
tags[1]:set_active(true)
|
||||||
end)
|
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_handler = Layout.new_handler({
|
||||||
local layout_cycler = Tag.new_layout_cycler({
|
Layout.builtins.master_stack,
|
||||||
"master_stack",
|
|
||||||
"dwindle",
|
|
||||||
"spiral",
|
|
||||||
"corner_top_left",
|
|
||||||
"corner_top_right",
|
|
||||||
"corner_bottom_left",
|
|
||||||
"corner_bottom_right",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
-- mod_key + space = Cycle forward one layout on the focused output
|
Layout.set_handler(layout_handler)
|
||||||
Input.keybind({ mod_key }, key.space, function()
|
|
||||||
local focused_op = Output.get_focused()
|
|
||||||
if focused_op then
|
|
||||||
layout_cycler.next(focused_op)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- mod_key + shift + space = Cycle backward one layout on the focused output
|
-- Spawning must happen after you add tags, as Pinnacle currently doesn't render windows without tags.
|
||||||
Input.keybind({ mod_key, "shift" }, key.space, function()
|
Process.spawn_once(terminal)
|
||||||
local focused_op = Output.get_focused()
|
|
||||||
if focused_op then
|
|
||||||
layout_cycler.prev(focused_op)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
for _, tag_name in ipairs(tag_names) do
|
for _, tag_name in ipairs(tag_names) do
|
||||||
-- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above
|
-- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above
|
||||||
|
|
|
@ -28,5 +28,6 @@ build = {
|
||||||
["pinnacle.window"] = "pinnacle/window.lua",
|
["pinnacle.window"] = "pinnacle/window.lua",
|
||||||
["pinnacle.util"] = "pinnacle/util.lua",
|
["pinnacle.util"] = "pinnacle/util.lua",
|
||||||
["pinnacle.signal"] = "pinnacle/signal.lua",
|
["pinnacle.signal"] = "pinnacle/signal.lua",
|
||||||
|
["pinnacle.layout"] = "pinnacle/layout.lua",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ local pinnacle = {
|
||||||
window = require("pinnacle.window"),
|
window = require("pinnacle.window"),
|
||||||
---@type Process
|
---@type Process
|
||||||
process = require("pinnacle.process"),
|
process = require("pinnacle.process"),
|
||||||
|
---@type Util
|
||||||
|
util = require("pinnacle.util"),
|
||||||
|
---@type Layout
|
||||||
|
layout = require("pinnacle.layout"),
|
||||||
}
|
}
|
||||||
|
|
||||||
---Quit Pinnacle.
|
---Quit Pinnacle.
|
||||||
|
@ -44,7 +48,10 @@ function pinnacle.setup(config_fn)
|
||||||
|
|
||||||
config_fn(pinnacle)
|
config_fn(pinnacle)
|
||||||
|
|
||||||
client.loop:loop()
|
local success, err = pcall(client.loop.loop, client.loop)
|
||||||
|
if not success then
|
||||||
|
print(err)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return pinnacle
|
return pinnacle
|
||||||
|
|
|
@ -170,7 +170,7 @@ end
|
||||||
|
|
||||||
---@nodoc
|
---@nodoc
|
||||||
---@param grpc_request_params GrpcRequestParams
|
---@param grpc_request_params GrpcRequestParams
|
||||||
---@param callback fun(response: table)
|
---@param callback fun(response: table, stream: H2Stream)
|
||||||
---
|
---
|
||||||
---@return H2Stream
|
---@return H2Stream
|
||||||
function client.bidirectional_streaming_request(grpc_request_params, callback)
|
function client.bidirectional_streaming_request(grpc_request_params, callback)
|
||||||
|
@ -209,7 +209,7 @@ function client.bidirectional_streaming_request(grpc_request_params, callback)
|
||||||
end
|
end
|
||||||
|
|
||||||
local response = obj
|
local response = obj
|
||||||
callback(response)
|
callback(response, stream)
|
||||||
|
|
||||||
response_body = response_body:sub(msg_len + 1)
|
response_body = response_body:sub(msg_len + 1)
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@ function protobuf.build_protos()
|
||||||
PINNACLE_PROTO_DIR .. "/pinnacle/process/" .. version .. "/process.proto",
|
PINNACLE_PROTO_DIR .. "/pinnacle/process/" .. version .. "/process.proto",
|
||||||
PINNACLE_PROTO_DIR .. "/pinnacle/window/" .. version .. "/window.proto",
|
PINNACLE_PROTO_DIR .. "/pinnacle/window/" .. version .. "/window.proto",
|
||||||
PINNACLE_PROTO_DIR .. "/pinnacle/signal/" .. version .. "/signal.proto",
|
PINNACLE_PROTO_DIR .. "/pinnacle/signal/" .. version .. "/signal.proto",
|
||||||
|
PINNACLE_PROTO_DIR .. "/pinnacle/layout/" .. version .. "/layout.proto",
|
||||||
PINNACLE_PROTO_DIR .. "/google/protobuf/empty.proto",
|
PINNACLE_PROTO_DIR .. "/google/protobuf/empty.proto",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
321
api/lua/pinnacle/layout.lua
Normal file
321
api/lua/pinnacle/layout.lua
Normal 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
|
|
@ -250,7 +250,7 @@ end
|
||||||
---layout_cycler.prev(Output.get_by_name("HDMI-1"))
|
---layout_cycler.prev(Output.get_by_name("HDMI-1"))
|
||||||
---```
|
---```
|
||||||
---
|
---
|
||||||
---@param layouts Layout[]
|
---@param layouts LayoutOld[]
|
||||||
---
|
---
|
||||||
---@return LayoutCycler
|
---@return LayoutCycler
|
||||||
function tag.new_layout_cycler(layouts)
|
function tag.new_layout_cycler(layouts)
|
||||||
|
@ -393,7 +393,7 @@ local layout_name_to_code = {
|
||||||
corner_bottom_left = 6,
|
corner_bottom_left = 6,
|
||||||
corner_bottom_right = 7,
|
corner_bottom_right = 7,
|
||||||
}
|
}
|
||||||
---@alias Layout
|
---@alias LayoutOld
|
||||||
---| "master_stack" # One master window on the left with all other windows stacked to the right.
|
---| "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.
|
---| "dwindle" # Windows split in half towards the bottom right corner.
|
||||||
---| "spiral" # Windows split in half in a spiral.
|
---| "spiral" # Windows split in half in a spiral.
|
||||||
|
@ -412,7 +412,7 @@ local layout_name_to_code = {
|
||||||
---Tag.get("Tag"):set_layout("dwindle")
|
---Tag.get("Tag"):set_layout("dwindle")
|
||||||
---```
|
---```
|
||||||
---
|
---
|
||||||
---@param layout Layout
|
---@param layout LayoutOld
|
||||||
function TagHandle:set_layout(layout)
|
function TagHandle:set_layout(layout)
|
||||||
---@diagnostic disable-next-line: redefined-local
|
---@diagnostic disable-next-line: redefined-local
|
||||||
local layout = layout_name_to_code[layout]
|
local layout = layout_name_to_code[layout]
|
||||||
|
|
|
@ -77,4 +77,126 @@ function util.batch(requests)
|
||||||
return responses
|
return responses
|
||||||
end
|
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
|
return util
|
||||||
|
|
|
@ -425,7 +425,7 @@ end
|
||||||
--- focused:set_geometry({}) -- Do nothing useful
|
--- focused:set_geometry({}) -- Do nothing useful
|
||||||
---end
|
---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)
|
function WindowHandle:set_geometry(geo)
|
||||||
client.unary_request(build_grpc_request_params("SetGeometry", { window_id = self.id, geometry = geo }))
|
client.unary_request(build_grpc_request_params("SetGeometry", { window_id = self.id, geometry = geo }))
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,20 +7,22 @@ import "pinnacle/v0alpha1/pinnacle.proto";
|
||||||
// Love how the response is the request and the request is the response
|
// Love how the response is the request and the request is the response
|
||||||
|
|
||||||
message LayoutRequest {
|
message LayoutRequest {
|
||||||
// Respond to a layout request from the compositor.
|
// A response to a layout request from the compositor.
|
||||||
message Geometries {
|
message Geometries {
|
||||||
// The id of the request this layout response is responding to.
|
// The id of the request this layout response is responding to.
|
||||||
//
|
//
|
||||||
// Responding with a request_id that has already been responded to
|
// Responding with a request_id that has already been responded to
|
||||||
// or that doesn't exist will return an error.
|
// or that doesn't exist will return an error.
|
||||||
optional uint32 request_id = 1;
|
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.
|
// Target geometries of all windows being laid out.
|
||||||
//
|
//
|
||||||
// Responding with a different number of geometries than
|
// Responding with a different number of geometries than
|
||||||
// requested windows will return an error.
|
// 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 {}
|
message ExplicitLayout {}
|
||||||
|
|
||||||
oneof body {
|
oneof body {
|
||||||
|
|
|
@ -14,6 +14,7 @@ fn main() {
|
||||||
formatcp!("../api/protocol/pinnacle/tag/{VERSION}/tag.proto"),
|
formatcp!("../api/protocol/pinnacle/tag/{VERSION}/tag.proto"),
|
||||||
formatcp!("../api/protocol/pinnacle/window/{VERSION}/window.proto"),
|
formatcp!("../api/protocol/pinnacle/window/{VERSION}/window.proto"),
|
||||||
formatcp!("../api/protocol/pinnacle/signal/{VERSION}/signal.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");
|
let descriptor_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("pinnacle.bin");
|
||||||
|
|
|
@ -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");
|
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("pinnacle");
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod layout;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
||||||
|
|
56
src/api/layout.rs
Normal file
56
src/api/layout.rs
Normal 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);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
signal::SignalService, window::WindowService, InputService, OutputService, PinnacleService,
|
layout::LayoutService, signal::SignalService, window::WindowService, InputService,
|
||||||
ProcessService, TagService,
|
OutputService, PinnacleService, ProcessService, TagService,
|
||||||
},
|
},
|
||||||
input::ModifierMask,
|
input::ModifierMask,
|
||||||
output::OutputName,
|
output::OutputName,
|
||||||
|
@ -17,6 +17,7 @@ use std::{
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use pinnacle_api_defs::pinnacle::{
|
use pinnacle_api_defs::pinnacle::{
|
||||||
input::v0alpha1::input_service_server::InputServiceServer,
|
input::v0alpha1::input_service_server::InputServiceServer,
|
||||||
|
layout::v0alpha1::layout_service_server::LayoutServiceServer,
|
||||||
output::v0alpha1::output_service_server::OutputServiceServer,
|
output::v0alpha1::output_service_server::OutputServiceServer,
|
||||||
process::v0alpha1::process_service_server::ProcessServiceServer,
|
process::v0alpha1::process_service_server::ProcessServiceServer,
|
||||||
signal::v0alpha1::signal_service_server::SignalServiceServer,
|
signal::v0alpha1::signal_service_server::SignalServiceServer,
|
||||||
|
@ -477,6 +478,7 @@ impl State {
|
||||||
let output_service = OutputService::new(grpc_sender.clone());
|
let output_service = OutputService::new(grpc_sender.clone());
|
||||||
let window_service = WindowService::new(grpc_sender.clone());
|
let window_service = WindowService::new(grpc_sender.clone());
|
||||||
let signal_service = SignalService::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()
|
let refl_service = tonic_reflection::server::Builder::configure()
|
||||||
.register_encoded_file_descriptor_set(pinnacle_api_defs::FILE_DESCRIPTOR_SET)
|
.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(TagServiceServer::new(tag_service))
|
||||||
.add_service(OutputServiceServer::new(output_service))
|
.add_service(OutputServiceServer::new(output_service))
|
||||||
.add_service(WindowServiceServer::new(window_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() {
|
match self.xdisplay.as_ref() {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
|
|
254
src/layout.rs
254
src/layout.rs
|
@ -1,13 +1,20 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// 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::{
|
use smithay::{
|
||||||
desktop::{layer_map_for_output, WindowSurface},
|
desktop::{layer_map_for_output, WindowSurface},
|
||||||
output::Output,
|
output::Output,
|
||||||
utils::{Logical, Point, Rectangle, Serial, Size},
|
utils::{Logical, Point, Rectangle, Serial, Size},
|
||||||
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
|
wayland::{compositor, shell::xdg::XdgToplevelSurfaceData},
|
||||||
};
|
};
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
output::OutputName,
|
||||||
state::{State, WithState},
|
state::{State, WithState},
|
||||||
window::{
|
window::{
|
||||||
window_state::{FloatingOrTiled, FullscreenOrMaximized},
|
window_state::{FloatingOrTiled, FullscreenOrMaximized},
|
||||||
|
@ -34,7 +41,7 @@ impl State {
|
||||||
}) else {
|
}) else {
|
||||||
// TODO: maybe default to something like 800x800 like in anvil so people still see
|
// TODO: maybe default to something like 800x800 like in anvil so people still see
|
||||||
// | windows open
|
// | windows open
|
||||||
tracing::error!("Failed to get output geometry");
|
error!("Failed to get output geometry");
|
||||||
return;
|
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) {
|
pub fn update_windows(&mut self, output: &Output) {
|
||||||
let Some(layout) =
|
let Some(layout) =
|
||||||
output.with_state(|state| state.focused_tags().next().map(|tag| tag.layout()))
|
output.with_state(|state| state.focused_tags().next().map(|tag| tag.layout()))
|
||||||
|
@ -78,6 +192,10 @@ impl State {
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.layout_request(output.clone(), tiled_windows);
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
self.tile_windows(output, tiled_windows, layout);
|
self.tile_windows(output, tiled_windows, layout);
|
||||||
|
|
||||||
let output_geo = self.space.output_geometry(output).expect("no output geo");
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::signal::SignalState, backend::Backend, config::Config, cursor::Cursor,
|
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 anyhow::Context;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
|
@ -98,6 +99,8 @@ pub struct State {
|
||||||
pub xdg_base_dirs: BaseDirectories,
|
pub xdg_base_dirs: BaseDirectories,
|
||||||
|
|
||||||
pub signal_state: SignalState,
|
pub signal_state: SignalState,
|
||||||
|
|
||||||
|
pub layout_state: LayoutState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -277,6 +280,8 @@ impl State {
|
||||||
.context("couldn't create xdg BaseDirectories")?,
|
.context("couldn't create xdg BaseDirectories")?,
|
||||||
|
|
||||||
signal_state: SignalState::default(),
|
signal_state: SignalState::default(),
|
||||||
|
|
||||||
|
layout_state: LayoutState::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(state)
|
Ok(state)
|
||||||
|
|
Loading…
Reference in a new issue