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 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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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"))
|
||||
---```
|
||||
---
|
||||
---@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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod layout;
|
||||
pub mod signal;
|
||||
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::{
|
||||
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(_) => {
|
||||
|
|
254
src/layout.rs
254
src/layout.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue