mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-14 08:01:14 +01:00
Merge pull request #176 from pinnacle-comp/layout
Add a dynamic and configurable layout system
This commit is contained in:
commit
5d117288c2
35 changed files with 3956 additions and 1802 deletions
|
@ -1,9 +1,12 @@
|
|||
-- neovim users be like
|
||||
require("pinnacle").setup(function(Pinnacle)
|
||||
local Input = Pinnacle.input
|
||||
local Process = Pinnacle.process
|
||||
local Output = Pinnacle.output
|
||||
local Tag = Pinnacle.tag
|
||||
local Window = Pinnacle.window
|
||||
local Layout = Pinnacle.layout
|
||||
local Util = Pinnacle.util
|
||||
|
||||
local key = Input.key
|
||||
|
||||
|
@ -83,36 +86,7 @@ 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)
|
||||
|
||||
-- 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",
|
||||
})
|
||||
|
||||
-- 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)
|
||||
|
||||
-- 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)
|
||||
|
||||
-- Tag keybinds
|
||||
for _, tag_name in ipairs(tag_names) do
|
||||
-- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above
|
||||
|
||||
|
@ -143,10 +117,139 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
end)
|
||||
end
|
||||
|
||||
--------------------
|
||||
-- Layouts --
|
||||
--------------------
|
||||
|
||||
-- Pinnacle does not manage layouts compositor-side.
|
||||
-- Instead, it delegates computation of layouts to your config,
|
||||
-- which provides an interface to calculate the size and location of
|
||||
-- windows that the compositor will use to position windows.
|
||||
--
|
||||
-- If you're familiar with River's layout generators, you'll understand the system here
|
||||
-- a bit better.
|
||||
--
|
||||
-- The Lua API provides two layout system abstractions:
|
||||
-- 1. Layout managers, and
|
||||
-- 2. Layout generators.
|
||||
--
|
||||
-- ### Layout Managers ###
|
||||
-- A layout manager is a table that contains a `get_active` function
|
||||
-- that returns some layout generator.
|
||||
-- A manager is meant to keep track of and choose various layout generators
|
||||
-- across your usage of the compositor.
|
||||
--
|
||||
-- ### Layout generators ###
|
||||
-- A layout generator is a table that holds some state as well as
|
||||
-- the `layout` function, which takes in layout arguments and computes
|
||||
-- an array of geometries that will determine the size and position
|
||||
-- of windows being laid out.
|
||||
--
|
||||
-- There is one built-in layout manager and five built-in layout generators,
|
||||
-- as shown below.
|
||||
--
|
||||
-- Additionally, this system is designed to be user-extensible;
|
||||
-- you are free to create your own layout managers and generators for
|
||||
-- maximum customizability! Docs for doing so are in the works, so sit tight.
|
||||
|
||||
-- Create a cycling layout manager. This provides methods to cycle
|
||||
-- between the given layout generators below.
|
||||
local layout_manager = Layout.new_cycling_manager({
|
||||
-- `Layout.builtins` contains functions that create various layout generators.
|
||||
-- Each of these has settings that can be overridden by passing in a table with
|
||||
-- overriding options.
|
||||
Layout.builtins.master_stack(),
|
||||
Layout.builtins.master_stack({ master_side = "right" }),
|
||||
Layout.builtins.master_stack({ master_side = "top" }),
|
||||
Layout.builtins.master_stack({ master_side = "bottom" }),
|
||||
Layout.builtins.dwindle(),
|
||||
Layout.builtins.spiral(),
|
||||
Layout.builtins.corner(),
|
||||
Layout.builtins.corner({ corner_loc = "top_right" }),
|
||||
Layout.builtins.corner({ corner_loc = "bottom_left" }),
|
||||
Layout.builtins.corner({ corner_loc = "bottom_right" }),
|
||||
Layout.builtins.fair(),
|
||||
Layout.builtins.fair({ direction = "horizontal" }),
|
||||
})
|
||||
|
||||
-- Set the cycling layout manager as the layout manager that will be used.
|
||||
-- This then allows you to call `Layout.request_layout` to manually layout windows.
|
||||
Layout.set_manager(layout_manager)
|
||||
|
||||
-- mod_key + space = Cycle forward one layout on the focused output
|
||||
--
|
||||
-- Yes, this is a bit verbose for my liking.
|
||||
-- You need to cycle the layout on the first active tag
|
||||
-- because that is the one that decides which layout is used.
|
||||
Input.keybind({ mod_key }, key.space, function()
|
||||
local focused_op = Output.get_focused()
|
||||
if focused_op then
|
||||
local tags = focused_op:tags() or {}
|
||||
local tag = nil
|
||||
|
||||
---@type (fun(): (boolean|nil))[]
|
||||
local tag_actives = {}
|
||||
for i, t in ipairs(tags) do
|
||||
tag_actives[i] = function()
|
||||
return t:active()
|
||||
end
|
||||
end
|
||||
|
||||
-- We are batching API calls here for better performance
|
||||
tag_actives = Util.batch(tag_actives)
|
||||
|
||||
for i, active in ipairs(tag_actives) do
|
||||
if active then
|
||||
tag = tags[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if tag then
|
||||
layout_manager:cycle_layout_forward(tag)
|
||||
Layout.request_layout(focused_op)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- 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
|
||||
local tags = focused_op:tags() or {}
|
||||
local tag = nil
|
||||
|
||||
---@type (fun(): (boolean|nil))[]
|
||||
local tag_actives = {}
|
||||
for i, t in ipairs(tags) do
|
||||
tag_actives[i] = function()
|
||||
return t:active()
|
||||
end
|
||||
end
|
||||
|
||||
tag_actives = Util.batch(tag_actives)
|
||||
|
||||
for i, active in ipairs(tag_actives) do
|
||||
if active then
|
||||
tag = tags[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if tag then
|
||||
layout_manager:cycle_layout_backward(tag)
|
||||
Layout.request_layout(focused_op)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Enable sloppy focus
|
||||
Window.connect_signal({
|
||||
pointer_enter = function(window)
|
||||
window:set_focused(true)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Spawning should happen after you add tags, as Pinnacle currently doesn't render windows without tags.
|
||||
Process.spawn_once(terminal)
|
||||
end)
|
||||
|
|
|
@ -24,9 +24,9 @@ build = {
|
|||
["pinnacle.output"] = "pinnacle/output.lua",
|
||||
["pinnacle.process"] = "pinnacle/process.lua",
|
||||
["pinnacle.tag"] = "pinnacle/tag.lua",
|
||||
["pinnacle.tag.layout"] = "pinnacle/tag/layout.lua",
|
||||
["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",
|
||||
}
|
||||
|
||||
|
|
1147
api/lua/pinnacle/layout.lua
Normal file
1147
api/lua/pinnacle/layout.lua
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,9 +14,6 @@ local rpc_types = {
|
|||
OutputConnect = {
|
||||
response_type = "OutputConnectResponse",
|
||||
},
|
||||
Layout = {
|
||||
response_type = "LayoutResponse",
|
||||
},
|
||||
WindowPointerEnter = {
|
||||
response_type = "WindowPointerEnterResponse",
|
||||
},
|
||||
|
@ -65,17 +62,6 @@ local signals = {
|
|||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
Layout = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(tag: TagHandle, windows: WindowHandle[]))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
WindowPointerEnter = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
|
@ -108,17 +94,6 @@ signals.OutputConnect.on_response = function(response)
|
|||
end
|
||||
end
|
||||
|
||||
signals.Layout.on_response = function(response)
|
||||
---@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_handle = require("pinnacle.tag").handle.new(response.tag_id)
|
||||
|
||||
for _, callback in ipairs(signals.Layout.callbacks) do
|
||||
callback(tag_handle, window_handles)
|
||||
end
|
||||
end
|
||||
|
||||
signals.WindowPointerEnter.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local window_handle = require("pinnacle.window").handle.new(response.window_id)
|
||||
|
|
|
@ -17,7 +17,6 @@ local rpc_types = {
|
|||
response_type = "AddResponse",
|
||||
},
|
||||
Remove = {},
|
||||
SetLayout = {},
|
||||
Get = {
|
||||
response_type = "GetResponse",
|
||||
},
|
||||
|
@ -211,165 +210,6 @@ function tag.remove(tags)
|
|||
client.unary_request(build_grpc_request_params("Remove", { tag_ids = ids }))
|
||||
end
|
||||
|
||||
---@class LayoutCycler
|
||||
---@field next fun(output: OutputHandle?)
|
||||
---@field prev fun(output: OutputHandle?)
|
||||
|
||||
---Create a layout cycler that will cycle layouts on the given output.
|
||||
---
|
||||
---This returns a `LayoutCycler` table with two fields, both functions that take in an optional `OutputHandle`:
|
||||
--- - `next`: Cycle to the next layout on the given output
|
||||
--- - `prev`: Cycle to the previous layout on the given output
|
||||
---
|
||||
---If the output isn't specified then the focused one will be used.
|
||||
---
|
||||
---Internally, this will only change the layout of the first active tag on the output
|
||||
---because that is the one that determines the layout.
|
||||
---
|
||||
---### Example
|
||||
---```lua
|
||||
--- ---@type LayoutCycler[]
|
||||
---local layouts = {
|
||||
--- "master_stack",
|
||||
--- "dwindle",
|
||||
--- "corner_top_left",
|
||||
--- "corner_top_right".
|
||||
---} -- Only cycle between these four layouts
|
||||
---
|
||||
---local layout_cycler = Tag.new_layout_cycler()
|
||||
---
|
||||
--- -- Assume the focused output starts with the "master_stack" layout
|
||||
---layout_cycler.next() -- Layout is now "dwindle"
|
||||
---layout_cycler.next() -- Layout is now "corner_top_left"
|
||||
---layout_cycler.next() -- Layout is now "corner_top_right"
|
||||
---layout_cycler.next() -- Layout is now "master_stack"
|
||||
---layout_cycler.next() -- Layout is now "dwindle"
|
||||
---
|
||||
--- -- Cycling on another output
|
||||
---layout_cycler.next(Output.get_by_name("eDP-1"))
|
||||
---layout_cycler.prev(Output.get_by_name("HDMI-1"))
|
||||
---```
|
||||
---
|
||||
---@param layouts Layout[]
|
||||
---
|
||||
---@return LayoutCycler
|
||||
function tag.new_layout_cycler(layouts)
|
||||
local indices = {}
|
||||
|
||||
if #layouts == 0 then
|
||||
return {
|
||||
next = function(_) end,
|
||||
prev = function(_) end,
|
||||
}
|
||||
end
|
||||
|
||||
---@type LayoutCycler
|
||||
return {
|
||||
next = function(output)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local output = output or require("pinnacle.output").get_focused()
|
||||
if not output then
|
||||
return
|
||||
end
|
||||
|
||||
local tags = output:props().tags or {}
|
||||
|
||||
for _, tg in ipairs(tags) do
|
||||
if tg:props().active then
|
||||
local id = tg.id
|
||||
if #layouts == 1 then
|
||||
indices[id] = 1
|
||||
elseif indices[id] == nil then
|
||||
indices[id] = 2
|
||||
else
|
||||
if indices[id] + 1 > #layouts then
|
||||
indices[id] = 1
|
||||
else
|
||||
indices[id] = indices[id] + 1
|
||||
end
|
||||
end
|
||||
|
||||
tg:set_layout(layouts[indices[id]])
|
||||
break
|
||||
end
|
||||
end
|
||||
end,
|
||||
prev = function(output)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local output = output or require("pinnacle.output").get_focused()
|
||||
if not output then
|
||||
return
|
||||
end
|
||||
|
||||
local tags = output:props().tags or {}
|
||||
|
||||
for _, tg in ipairs(tags) do
|
||||
if tg:props().active then
|
||||
local id = tg.id
|
||||
|
||||
if #layouts == 1 then
|
||||
indices[id] = 1
|
||||
elseif indices[id] == nil then
|
||||
indices[id] = #layouts - 1
|
||||
else
|
||||
if indices[id] - 1 < 1 then
|
||||
indices[id] = #layouts
|
||||
else
|
||||
indices[id] = indices[id] - 1
|
||||
end
|
||||
end
|
||||
|
||||
tg:set_layout(layouts[indices[id]])
|
||||
break
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local signal_name_to_SignalName = {
|
||||
layout = "Layout",
|
||||
}
|
||||
|
||||
---@class TagSignal Signals related to tag events.
|
||||
---@field layout fun(tag: TagHandle, windows: WindowHandle[])? The compositor requested a layout of the given tiled windows. You'll also receive the first active tag.
|
||||
|
||||
---Connect to a tag signal.
|
||||
---
|
||||
---The compositor sends signals about various events. Use this function to run a callback when
|
||||
---some tag signal occurs.
|
||||
---
|
||||
---This function returns a table of signal handles with each handle stored at the same key used
|
||||
---to connect to the signal. See `SignalHandles` for more information.
|
||||
---
|
||||
---# Example
|
||||
---```lua
|
||||
---Tag.connect_signal({
|
||||
--- layout = function(tag, windows)
|
||||
--- print("Compositor requested a layout")
|
||||
--- end
|
||||
---})
|
||||
---```
|
||||
---
|
||||
---@param signals TagSignal The signal you want to connect to
|
||||
---
|
||||
---@return SignalHandles signal_handles Handles to every signal you connected to wrapped in a table, with keys being the same as the connected signal.
|
||||
---
|
||||
---@see SignalHandles.disconnect_all - To disconnect from these signals
|
||||
function tag.connect_signal(signals)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handles = require("pinnacle.signal").handles.new({})
|
||||
|
||||
for signal, callback in pairs(signals) do
|
||||
require("pinnacle.signal").add_callback(signal_name_to_SignalName[signal], callback)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handle = require("pinnacle.signal").handle.new(signal_name_to_SignalName[signal], callback)
|
||||
handles[signal] = handle
|
||||
end
|
||||
|
||||
return handles
|
||||
end
|
||||
|
||||
---Remove this tag.
|
||||
---
|
||||
---### Example
|
||||
|
@ -384,45 +224,6 @@ function TagHandle:remove()
|
|||
client.unary_request(build_grpc_request_params("Remove", { tag_ids = { self.id } }))
|
||||
end
|
||||
|
||||
local layout_name_to_code = {
|
||||
master_stack = 1,
|
||||
dwindle = 2,
|
||||
spiral = 3,
|
||||
corner_top_left = 4,
|
||||
corner_top_right = 5,
|
||||
corner_bottom_left = 6,
|
||||
corner_bottom_right = 7,
|
||||
}
|
||||
---@alias Layout
|
||||
---| "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.
|
||||
---| "corner_top_left" # One main corner window in the top left with a column of windows on the right and a row on the bottom.
|
||||
---| "corner_top_right" # One main corner window in the top right with a column of windows on the left and a row on the bottom.
|
||||
---| "corner_bottom_left" # One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
||||
---| "corner_bottom_right" # One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
||||
|
||||
---Set this tag's layout.
|
||||
---
|
||||
---If this is the first active tag on its output, its layout will be used to tile windows.
|
||||
---
|
||||
---### Example
|
||||
---```lua
|
||||
--- -- Assume the focused output has tag "Tag"
|
||||
---Tag.get("Tag"):set_layout("dwindle")
|
||||
---```
|
||||
---
|
||||
---@param layout Layout
|
||||
function TagHandle:set_layout(layout)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local layout = layout_name_to_code[layout]
|
||||
|
||||
client.unary_request(build_grpc_request_params("SetLayout", {
|
||||
tag_id = self.id,
|
||||
layout = layout,
|
||||
}))
|
||||
end
|
||||
|
||||
---Activate this tag and deactivate all other ones on the same output.
|
||||
---
|
||||
---### Example
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
---@class LayoutModule
|
||||
local layout = {}
|
||||
|
||||
return layout
|
|
@ -2,9 +2,127 @@
|
|||
-- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
---Create `Rectangle`s.
|
||||
---@class RectangleModule
|
||||
local rectangle = {}
|
||||
|
||||
---@classmod
|
||||
---A rectangle with a position and size.
|
||||
---@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 `thickness` is specified, the split will chop off a section of this
|
||||
---rectangle from `at` to `at + thickness`.
|
||||
---
|
||||
---`at` is 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|nil rect2 The second 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
|
||||
|
||||
print("Invalid axis:", axis)
|
||||
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
|
||||
|
||||
---Utility functions.
|
||||
---@class Util
|
||||
local util = {}
|
||||
local util = {
|
||||
rectangle = rectangle,
|
||||
}
|
||||
|
||||
---Batch a set of requests that will be sent to the compositor all at once.
|
||||
---
|
||||
|
@ -77,4 +195,41 @@ function util.batch(requests)
|
|||
return responses
|
||||
end
|
||||
|
||||
-- Taken from the following stackoverflow answer:
|
||||
-- https://stackoverflow.com/a/16077650
|
||||
local function deep_copy_rec(obj, seen)
|
||||
seen = seen or {}
|
||||
if obj == nil then
|
||||
return nil
|
||||
end
|
||||
if seen[obj] then
|
||||
return seen[obj]
|
||||
end
|
||||
|
||||
local no
|
||||
if type(obj) == "table" then
|
||||
no = {}
|
||||
seen[obj] = no
|
||||
|
||||
for k, v in next, obj, nil do
|
||||
no[deep_copy_rec(k, seen)] = deep_copy_rec(v, seen)
|
||||
end
|
||||
setmetatable(no, deep_copy_rec(getmetatable(obj), seen))
|
||||
else -- number, string, boolean, etc
|
||||
no = obj
|
||||
end
|
||||
return no
|
||||
end
|
||||
|
||||
---Create a deep copy of an object.
|
||||
---
|
||||
---@generic T
|
||||
---
|
||||
---@param obj T The object to deep copy.
|
||||
---
|
||||
---@return T deep_copy A deep copy of `obj`
|
||||
function util.deep_copy(obj)
|
||||
return deep_copy_rec(obj, nil)
|
||||
end
|
||||
|
||||
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
|
||||
|
|
56
api/protocol/pinnacle/layout/v0alpha1/layout.proto
Normal file
56
api/protocol/pinnacle/layout/v0alpha1/layout.proto
Normal file
|
@ -0,0 +1,56 @@
|
|||
syntax = "proto2";
|
||||
|
||||
package pinnacle.layout.v0alpha1;
|
||||
|
||||
import "pinnacle/v0alpha1/pinnacle.proto";
|
||||
|
||||
// Love how the response is the request and the request is the response
|
||||
|
||||
message LayoutRequest {
|
||||
// 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 = 3;
|
||||
}
|
||||
// An explicit layout request.
|
||||
message ExplicitLayout {
|
||||
// NULLABLE
|
||||
//
|
||||
// Layout this output.
|
||||
//
|
||||
// If it is null, the focused output will be used.
|
||||
optional string output_name = 1;
|
||||
}
|
||||
|
||||
oneof body {
|
||||
Geometries geometries = 1;
|
||||
ExplicitLayout layout = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// The compositor requested a layout.
|
||||
//
|
||||
// The client must respond with `LayoutRequest.geometries`.
|
||||
message LayoutResponse {
|
||||
optional uint32 request_id = 1;
|
||||
optional string output_name = 2;
|
||||
repeated uint32 window_ids = 3;
|
||||
// Ids of all focused tags on the output.
|
||||
repeated uint32 tag_ids = 4;
|
||||
optional uint32 output_width = 5;
|
||||
optional uint32 output_height = 6;
|
||||
}
|
||||
|
||||
service LayoutService {
|
||||
rpc Layout(stream LayoutRequest) returns (stream LayoutResponse);
|
||||
}
|
|
@ -17,16 +17,6 @@ message OutputConnectResponse {
|
|||
optional string output_name = 1;
|
||||
}
|
||||
|
||||
message LayoutRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
message LayoutResponse {
|
||||
// The windows that need to be laid out.
|
||||
repeated uint32 window_ids = 1;
|
||||
// The tag that is being laid out.
|
||||
optional uint32 tag_id = 2;
|
||||
}
|
||||
|
||||
message WindowPointerEnterRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
|
@ -45,7 +35,6 @@ message WindowPointerLeaveResponse {
|
|||
|
||||
service SignalService {
|
||||
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
||||
rpc Layout(stream LayoutRequest) returns (stream LayoutResponse);
|
||||
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
||||
rpc WindowPointerLeave(stream WindowPointerLeaveRequest) returns (stream WindowPointerLeaveResponse);
|
||||
}
|
||||
|
|
|
@ -26,21 +26,6 @@ message RemoveRequest {
|
|||
repeated uint32 tag_ids = 1;
|
||||
}
|
||||
|
||||
message SetLayoutRequest {
|
||||
optional uint32 tag_id = 1;
|
||||
enum Layout {
|
||||
LAYOUT_UNSPECIFIED = 0;
|
||||
LAYOUT_MASTER_STACK = 1;
|
||||
LAYOUT_DWINDLE = 2;
|
||||
LAYOUT_SPIRAL = 3;
|
||||
LAYOUT_CORNER_TOP_LEFT = 4;
|
||||
LAYOUT_CORNER_TOP_RIGHT = 5;
|
||||
LAYOUT_CORNER_BOTTOM_LEFT = 6;
|
||||
LAYOUT_CORNER_BOTTOM_RIGHT = 7;
|
||||
}
|
||||
optional Layout layout = 2;
|
||||
}
|
||||
|
||||
message GetRequest {}
|
||||
message GetResponse {
|
||||
repeated uint32 tag_ids = 1;
|
||||
|
@ -60,7 +45,6 @@ service TagService {
|
|||
rpc SwitchTo(SwitchToRequest) returns (google.protobuf.Empty);
|
||||
rpc Add(AddRequest) returns (AddResponse);
|
||||
rpc Remove(RemoveRequest) returns (google.protobuf.Empty);
|
||||
rpc SetLayout(SetLayoutRequest) returns (google.protobuf.Empty);
|
||||
rpc Get(GetRequest) returns (GetResponse);
|
||||
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
use pinnacle_api::layout::{
|
||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||
MasterStackLayout, SpiralLayout,
|
||||
};
|
||||
use pinnacle_api::signal::WindowSignal;
|
||||
use pinnacle_api::util::{Axis, Batch};
|
||||
use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||
use pinnacle_api::{
|
||||
input::{Mod, MouseButton, MouseEdge},
|
||||
tag::{Layout, LayoutCycler},
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
// Pinnacle needs to perform some setup before and after your config.
|
||||
// The `#[pinnacle_api::config(modules)]` attribute does so and
|
||||
// will bind all the config structs to the provided identifier.
|
||||
#[pinnacle_api::config(modules)]
|
||||
async fn main() {
|
||||
// Deconstruct to get all the APIs.
|
||||
let ApiModules {
|
||||
pinnacle,
|
||||
process,
|
||||
|
@ -15,13 +23,16 @@ async fn main() {
|
|||
input,
|
||||
output,
|
||||
tag,
|
||||
layout,
|
||||
} = modules;
|
||||
|
||||
let mod_key = Mod::Ctrl;
|
||||
|
||||
let terminal = "alacritty";
|
||||
|
||||
// Mousebinds
|
||||
//------------------------
|
||||
// Mousebinds |
|
||||
//------------------------
|
||||
|
||||
// `mod_key + left click` starts moving a window
|
||||
input.mousebind([mod_key], MouseButton::Left, MouseEdge::Press, || {
|
||||
|
@ -33,7 +44,9 @@ async fn main() {
|
|||
window.begin_resize(MouseButton::Right);
|
||||
});
|
||||
|
||||
// Keybinds
|
||||
//------------------------
|
||||
// Keybinds |
|
||||
//------------------------
|
||||
|
||||
// `mod_key + alt + q` quits Pinnacle
|
||||
input.keybind([mod_key, Mod::Alt], 'q', || {
|
||||
|
@ -73,12 +86,117 @@ async fn main() {
|
|||
}
|
||||
});
|
||||
|
||||
// Window rules
|
||||
//
|
||||
//------------------------
|
||||
// Window rules |
|
||||
//------------------------
|
||||
// You can define window rules to get windows to open with desired properties.
|
||||
// See `pinnacle_api::window::rules` in the docs for more information.
|
||||
|
||||
// Tags
|
||||
//------------------------
|
||||
// Layouts |
|
||||
//------------------------
|
||||
|
||||
// Pinnacle does not manage layouts compositor-side.
|
||||
// Instead, it delegates computation of layouts to your config,
|
||||
// which provides an interface to calculate the size and location of
|
||||
// windows that the compositor will use to position windows.
|
||||
//
|
||||
// If you're familiar with River's layout generators, you'll understand the system here
|
||||
// a bit better.
|
||||
//
|
||||
// The Rust API provides two layout system abstractions:
|
||||
// 1. Layout managers, and
|
||||
// 2. Layout generators.
|
||||
//
|
||||
// ### Layout Managers ###
|
||||
// A layout manager is a struct that implements the `LayoutManager` trait.
|
||||
// A manager is meant to keep track of and choose various layout generators
|
||||
// across your usage of the compositor.
|
||||
//
|
||||
// ### Layout generators ###
|
||||
// A layout generator is a struct that implements the `LayoutGenerator` trait.
|
||||
// It takes in layout arguments and computes a vector of geometries that will
|
||||
// determine the size and position of windows being laid out.
|
||||
//
|
||||
// There is one built-in layout manager and five built-in layout generators,
|
||||
// as shown below.
|
||||
//
|
||||
// Additionally, this system is designed to be user-extensible;
|
||||
// you are free to create your own layout managers and generators for
|
||||
// maximum customizability! Docs for doing so are in the works, so sit tight.
|
||||
|
||||
// Create a `CyclingLayoutManager` that can cycle between layouts on different tags.
|
||||
//
|
||||
// It takes in some layout generators that need to be boxed and dyn-coerced.
|
||||
let layout_requester = layout.set_manager(CyclingLayoutManager::new([
|
||||
Box::<MasterStackLayout>::default() as _,
|
||||
Box::new(MasterStackLayout {
|
||||
master_side: MasterSide::Right,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::new(MasterStackLayout {
|
||||
master_side: MasterSide::Top,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::new(MasterStackLayout {
|
||||
master_side: MasterSide::Bottom,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::<DwindleLayout>::default() as _,
|
||||
Box::<SpiralLayout>::default() as _,
|
||||
Box::<CornerLayout>::default() as _,
|
||||
Box::new(CornerLayout {
|
||||
corner_loc: CornerLocation::TopRight,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::new(CornerLayout {
|
||||
corner_loc: CornerLocation::BottomLeft,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::new(CornerLayout {
|
||||
corner_loc: CornerLocation::BottomRight,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
Box::<FairLayout>::default() as _,
|
||||
Box::new(FairLayout {
|
||||
axis: Axis::Horizontal,
|
||||
..Default::default()
|
||||
}) as _,
|
||||
]));
|
||||
|
||||
let mut layout_requester_clone = layout_requester.clone();
|
||||
|
||||
// `mod_key + space` cycles to the next layout
|
||||
input.keybind([mod_key], Keysym::space, move || {
|
||||
let Some(focused_op) = output.get_focused() else { return };
|
||||
let Some(first_active_tag) = focused_op.tags().batch_find(
|
||||
|tg| Box::pin(tg.active_async()),
|
||||
|active| active == &Some(true),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
layout_requester.cycle_layout_forward(&first_active_tag);
|
||||
layout_requester.request_layout_on_output(&focused_op);
|
||||
});
|
||||
|
||||
// `mod_key + shift + space` cycles to the previous layout
|
||||
input.keybind([mod_key, Mod::Shift], Keysym::space, move || {
|
||||
let Some(focused_op) = output.get_focused() else { return };
|
||||
let Some(first_active_tag) = focused_op.tags().batch_find(
|
||||
|tg| Box::pin(tg.active_async()),
|
||||
|active| active == &Some(true),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
layout_requester_clone.cycle_layout_backward(&first_active_tag);
|
||||
layout_requester_clone.request_layout_on_output(&focused_op);
|
||||
});
|
||||
|
||||
//------------------------
|
||||
// Tags |
|
||||
//------------------------
|
||||
|
||||
let tag_names = ["1", "2", "3", "4", "5"];
|
||||
|
||||
|
@ -90,32 +208,6 @@ async fn main() {
|
|||
tags.first().unwrap().set_active(true);
|
||||
});
|
||||
|
||||
process.spawn_once([terminal]);
|
||||
|
||||
// Create a layout cycler to cycle through the given layouts
|
||||
let LayoutCycler {
|
||||
prev: layout_prev,
|
||||
next: layout_next,
|
||||
} = tag.new_layout_cycler([
|
||||
Layout::MasterStack,
|
||||
Layout::Dwindle,
|
||||
Layout::Spiral,
|
||||
Layout::CornerTopLeft,
|
||||
Layout::CornerTopRight,
|
||||
Layout::CornerBottomLeft,
|
||||
Layout::CornerBottomRight,
|
||||
]);
|
||||
|
||||
// `mod_key + space` cycles to the next layout
|
||||
input.keybind([mod_key], Keysym::space, move || {
|
||||
layout_next(None);
|
||||
});
|
||||
|
||||
// `mod_key + shift + space` cycles to the previous layout
|
||||
input.keybind([mod_key, Mod::Shift], Keysym::space, move || {
|
||||
layout_prev(None);
|
||||
});
|
||||
|
||||
for tag_name in tag_names {
|
||||
// `mod_key + 1-5` switches to tag "1" to "5"
|
||||
input.keybind([mod_key], tag_name, move || {
|
||||
|
@ -154,4 +246,6 @@ async fn main() {
|
|||
window.connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
|
||||
win.set_focused(true);
|
||||
})));
|
||||
|
||||
process.spawn_once([terminal]);
|
||||
}
|
||||
|
|
1117
api/rust/src/layout.rs
Normal file
1117
api/rust/src/layout.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,7 @@
|
|||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
#![deny(elided_lifetimes_in_paths)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! The Rust implementation of [Pinnacle](https://github.com/pinnacle-comp/pinnacle)'s
|
||||
|
@ -87,6 +88,7 @@ use futures::{
|
|||
Future, StreamExt,
|
||||
};
|
||||
use input::Input;
|
||||
use layout::Layout;
|
||||
use output::Output;
|
||||
use pinnacle::Pinnacle;
|
||||
use process::Process;
|
||||
|
@ -102,6 +104,7 @@ use tower::service_fn;
|
|||
use window::Window;
|
||||
|
||||
pub mod input;
|
||||
pub mod layout;
|
||||
pub mod output;
|
||||
pub mod pinnacle;
|
||||
pub mod process;
|
||||
|
@ -121,6 +124,7 @@ static INPUT: OnceLock<Input> = OnceLock::new();
|
|||
static OUTPUT: OnceLock<Output> = OnceLock::new();
|
||||
static TAG: OnceLock<Tag> = OnceLock::new();
|
||||
static SIGNAL: OnceLock<RwLock<SignalState>> = OnceLock::new();
|
||||
static LAYOUT: OnceLock<Layout> = OnceLock::new();
|
||||
|
||||
/// A struct containing static references to all of the configuration structs.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
@ -137,6 +141,8 @@ pub struct ApiModules {
|
|||
pub output: &'static Output,
|
||||
/// The [`Tag`] struct
|
||||
pub tag: &'static Tag,
|
||||
/// The [`Layout`] struct
|
||||
pub layout: &'static Layout,
|
||||
}
|
||||
|
||||
/// Connects to Pinnacle and builds the configuration structs.
|
||||
|
@ -154,7 +160,7 @@ pub async fn connect(
|
|||
}))
|
||||
.await?;
|
||||
|
||||
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<()>>();
|
||||
let (fut_sender, fut_recv) = unbounded_channel::<BoxFuture<'static, ()>>();
|
||||
|
||||
let pinnacle = PINNACLE.get_or_init(|| Pinnacle::new(channel.clone()));
|
||||
let process = PROCESS.get_or_init(|| Process::new(channel.clone(), fut_sender.clone()));
|
||||
|
@ -162,6 +168,7 @@ pub async fn connect(
|
|||
let input = INPUT.get_or_init(|| Input::new(channel.clone(), fut_sender.clone()));
|
||||
let tag = TAG.get_or_init(|| Tag::new(channel.clone()));
|
||||
let output = OUTPUT.get_or_init(|| Output::new(channel.clone()));
|
||||
let layout = LAYOUT.get_or_init(|| Layout::new(channel.clone()));
|
||||
|
||||
SIGNAL
|
||||
.set(RwLock::new(SignalState::new(
|
||||
|
@ -177,6 +184,7 @@ pub async fn connect(
|
|||
input,
|
||||
output,
|
||||
tag,
|
||||
layout,
|
||||
};
|
||||
|
||||
Ok((modules, fut_recv))
|
||||
|
@ -190,7 +198,7 @@ pub async fn connect(
|
|||
/// This function is inserted at the end of your config through the [`config`] macro.
|
||||
/// You should use the macro instead of this function directly.
|
||||
pub async fn listen(fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
|
||||
let mut future_set = FuturesUnordered::<BoxFuture<()>>::new();
|
||||
let mut future_set = FuturesUnordered::<BoxFuture<'static, ()>>::new();
|
||||
|
||||
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ use tokio::sync::{
|
|||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
use tonic::{transport::Channel, Streaming};
|
||||
|
||||
use crate::{
|
||||
block_on_tokio, output::OutputHandle, tag::TagHandle, window::WindowHandle, OUTPUT, TAG, WINDOW,
|
||||
};
|
||||
use crate::{block_on_tokio, output::OutputHandle, window::WindowHandle, OUTPUT, WINDOW};
|
||||
|
||||
pub(crate) trait Signal {
|
||||
type Callback;
|
||||
|
@ -117,37 +115,6 @@ macro_rules! signals {
|
|||
}
|
||||
|
||||
signals! {
|
||||
/// Signals relating to tag events.
|
||||
TagSignal => {
|
||||
/// The compositor requested that the given windows be laid out.
|
||||
///
|
||||
/// Callbacks receive the tag that is being laid out and the windows being laid out.
|
||||
///
|
||||
/// Note: if multiple tags are active, only the first will be received, but all windows on those
|
||||
/// active tags will be received.
|
||||
Layout = {
|
||||
enum_name = Layout,
|
||||
callback_type = LayoutFn,
|
||||
client_request = layout,
|
||||
on_response = |response, callbacks| {
|
||||
if let Some(tag_id) = response.tag_id {
|
||||
let tag = TAG.get().expect("TAG doesn't exist");
|
||||
let window = WINDOW.get().expect("WINDOW doesn't exist");
|
||||
let tag = tag.new_handle(tag_id);
|
||||
|
||||
let windows = response
|
||||
.window_ids
|
||||
.into_iter()
|
||||
.map(|id| window.new_handle(id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&tag, windows.as_slice());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Signals relating to output events.
|
||||
OutputSignal => {
|
||||
/// An output was connected.
|
||||
|
@ -213,12 +180,10 @@ signals! {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) type LayoutFn = Box<dyn FnMut(&TagHandle, &[WindowHandle]) + Send + 'static>;
|
||||
pub(crate) type SingleOutputFn = Box<dyn FnMut(&OutputHandle) + Send + 'static>;
|
||||
pub(crate) type SingleWindowFn = Box<dyn FnMut(&WindowHandle) + Send + 'static>;
|
||||
|
||||
pub(crate) struct SignalState {
|
||||
pub(crate) layout: SignalData<Layout>,
|
||||
pub(crate) output_connect: SignalData<OutputConnect>,
|
||||
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
|
||||
pub(crate) window_pointer_leave: SignalData<WindowPointerLeave>,
|
||||
|
@ -231,7 +196,6 @@ impl SignalState {
|
|||
) -> Self {
|
||||
let client = SignalServiceClient::new(channel);
|
||||
Self {
|
||||
layout: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
window_pointer_enter: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
window_pointer_leave: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
|
@ -349,7 +313,6 @@ where
|
|||
}
|
||||
}
|
||||
_dc = dc_ping_recv_fuse => {
|
||||
println!("dc");
|
||||
control_sender.send(Req::from_control(StreamControl::Disconnect)).expect("send failed");
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -29,11 +29,6 @@
|
|||
//!
|
||||
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use futures::FutureExt;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use pinnacle_api_defs::pinnacle::{
|
||||
|
@ -41,20 +36,14 @@ use pinnacle_api_defs::pinnacle::{
|
|||
self,
|
||||
v0alpha1::{
|
||||
tag_service_client::TagServiceClient, AddRequest, RemoveRequest, SetActiveRequest,
|
||||
SetLayoutRequest, SwitchToRequest,
|
||||
SwitchToRequest,
|
||||
},
|
||||
},
|
||||
v0alpha1::SetOrToggle,
|
||||
};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::{
|
||||
block_on_tokio,
|
||||
output::OutputHandle,
|
||||
signal::{SignalHandle, TagSignal},
|
||||
util::Batch,
|
||||
OUTPUT, SIGNAL,
|
||||
};
|
||||
use crate::{block_on_tokio, output::OutputHandle, util::Batch, OUTPUT};
|
||||
|
||||
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -231,139 +220,6 @@ impl Tag {
|
|||
|
||||
block_on_tokio(client.remove(RemoveRequest { tag_ids })).unwrap();
|
||||
}
|
||||
|
||||
/// Create a [`LayoutCycler`] to cycle layouts on outputs.
|
||||
///
|
||||
/// This will create a `LayoutCycler` with two functions: one to cycle forward the layout for
|
||||
/// the first active tag on the specified output, and one to cycle backward.
|
||||
///
|
||||
/// If you do not specify an output for `LayoutCycler` functions, it will default to the
|
||||
/// focused output.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pinnacle_api::tag::{Layout, LayoutCycler};
|
||||
/// use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||
/// use pinnacle_api::input::Mod;
|
||||
///
|
||||
/// // Create a layout cycler that cycles through the listed layouts
|
||||
/// let LayoutCycler {
|
||||
/// prev: layout_prev,
|
||||
/// next: layout_next,
|
||||
/// } = tag.new_layout_cycler([
|
||||
/// Layout::MasterStack,
|
||||
/// Layout::Dwindle,
|
||||
/// Layout::Spiral,
|
||||
/// Layout::CornerTopLeft,
|
||||
/// Layout::CornerTopRight,
|
||||
/// Layout::CornerBottomLeft,
|
||||
/// Layout::CornerBottomRight,
|
||||
/// ]);
|
||||
///
|
||||
/// // Cycle layouts forward on the focused output
|
||||
/// layout_next(None);
|
||||
///
|
||||
/// // Cycle layouts backward on the focused output
|
||||
/// layout_prev(None);
|
||||
///
|
||||
/// // Cycle layouts forward on "eDP-1"
|
||||
/// layout_next(output.get_by_name("eDP-1")?);
|
||||
/// ```
|
||||
pub fn new_layout_cycler(&self, layouts: impl IntoIterator<Item = Layout>) -> LayoutCycler {
|
||||
let indices = Arc::new(Mutex::new(HashMap::<u32, usize>::new()));
|
||||
let indices_clone = indices.clone();
|
||||
|
||||
let layouts = layouts.into_iter().collect::<Vec<_>>();
|
||||
let layouts_clone = layouts.clone();
|
||||
let len = layouts.len();
|
||||
|
||||
let output_module = OUTPUT.get().expect("OUTPUT doesn't exist");
|
||||
let output_module_clone = output_module.clone();
|
||||
|
||||
let next = move |output: Option<&OutputHandle>| {
|
||||
let Some(output) = output.cloned().or_else(|| output_module.get_focused()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(first_tag) = output
|
||||
.props()
|
||||
.tags
|
||||
.into_iter()
|
||||
.find(|tag| tag.active() == Some(true))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut indices = indices.lock().expect("layout next mutex lock failed");
|
||||
let index = indices.entry(first_tag.id).or_insert(0);
|
||||
|
||||
if *index + 1 >= len {
|
||||
*index = 0;
|
||||
} else {
|
||||
*index += 1;
|
||||
}
|
||||
|
||||
first_tag.set_layout(layouts[*index]);
|
||||
};
|
||||
|
||||
let prev = move |output: Option<&OutputHandle>| {
|
||||
let Some(output) = output
|
||||
.cloned()
|
||||
.or_else(|| output_module_clone.get_focused())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(first_tag) = output
|
||||
.props()
|
||||
.tags
|
||||
.into_iter()
|
||||
.find(|tag| tag.active() == Some(true))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut indices = indices_clone.lock().expect("layout next mutex lock failed");
|
||||
let index = indices.entry(first_tag.id).or_insert(0);
|
||||
|
||||
if index.checked_sub(1).is_none() {
|
||||
*index = len - 1;
|
||||
} else {
|
||||
*index -= 1;
|
||||
}
|
||||
|
||||
first_tag.set_layout(layouts_clone[*index]);
|
||||
};
|
||||
|
||||
LayoutCycler {
|
||||
prev: Box::new(prev),
|
||||
next: Box::new(next),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to a tag signal.
|
||||
///
|
||||
/// The compositor will fire off signals that your config can listen for and act upon.
|
||||
/// You can pass in a [`TagSignal`] along with a callback and it will get run
|
||||
/// with the necessary arguments every time a signal of that type is received.
|
||||
pub fn connect_signal(&self, signal: TagSignal) -> SignalHandle {
|
||||
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
|
||||
|
||||
match signal {
|
||||
TagSignal::Layout(layout_fn) => signal_state.layout.add_callback(layout_fn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout cycler that keeps track of tags and their layouts and provides functions to cycle
|
||||
/// layouts on them.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct LayoutCycler {
|
||||
/// Cycle to the next layout on the given output, or the focused output if `None`.
|
||||
pub prev: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
|
||||
/// Cycle to the previous layout on the given output, or the focused output if `None`.
|
||||
pub next: Box<dyn Fn(Option<&OutputHandle>) + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
/// A handle to a tag.
|
||||
|
@ -511,31 +367,6 @@ impl TagHandle {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
/// Set this tag's layout.
|
||||
///
|
||||
/// Layouting only applies to tiled windows (windows that are not floating, maximized, or
|
||||
/// fullscreen). If multiple tags are active on an output, the first active tag's layout will
|
||||
/// determine the layout strategy.
|
||||
///
|
||||
/// See [`Layout`] for the different static layouts Pinnacle currently has to offer.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pinnacle_api::tag::Layout;
|
||||
///
|
||||
/// // Set the layout of tag "1" on the focused output to "corner top left".
|
||||
/// tag.get("1", None)?.set_layout(Layout::CornerTopLeft);
|
||||
/// ```
|
||||
pub fn set_layout(&self, layout: Layout) {
|
||||
let mut client = self.tag_client.clone();
|
||||
block_on_tokio(client.set_layout(SetLayoutRequest {
|
||||
tag_id: Some(self.id),
|
||||
layout: Some(layout as i32),
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Get all properties of this tag.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
|
@ -26,6 +26,105 @@ pub struct Geometry {
|
|||
pub height: u32,
|
||||
}
|
||||
|
||||
/// A horizontal or vertical axis.
|
||||
#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
|
||||
pub enum Axis {
|
||||
/// A horizontal axis.
|
||||
Horizontal,
|
||||
/// A vertical axis.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
/// Split this geometry along the given [`Axis`] at `at`.
|
||||
///
|
||||
/// `thickness` denotes how thick the split will be from `at`.
|
||||
///
|
||||
/// Returns the top/left geometry along with the bottom/right one if it exists.
|
||||
pub fn split_at(mut self, axis: Axis, at: i32, thickness: u32) -> (Geometry, Option<Geometry>) {
|
||||
match axis {
|
||||
Axis::Horizontal => {
|
||||
if at <= self.y {
|
||||
let diff = at - self.y + thickness as i32;
|
||||
if diff > 0 {
|
||||
self.y += diff;
|
||||
self.height = self.height.saturating_sub(diff as u32);
|
||||
}
|
||||
(self, None)
|
||||
} else if at >= self.y + self.height as i32 {
|
||||
(self, None)
|
||||
} else if at + thickness as i32 >= self.y + self.height as i32 {
|
||||
let diff = self.y + self.height as i32 - at;
|
||||
self.height = self.height.saturating_sub(diff as u32);
|
||||
(self, None)
|
||||
} else {
|
||||
let x = self.x;
|
||||
let top_y = self.y;
|
||||
let width = self.width;
|
||||
let top_height = at - self.y;
|
||||
|
||||
let bot_y = at + thickness as i32;
|
||||
let bot_height = self.y + self.height as i32 - at - thickness as i32;
|
||||
|
||||
let geo1 = Geometry {
|
||||
x,
|
||||
y: top_y,
|
||||
width,
|
||||
height: top_height as u32,
|
||||
};
|
||||
let geo2 = Geometry {
|
||||
x,
|
||||
y: bot_y,
|
||||
width,
|
||||
height: bot_height as u32,
|
||||
};
|
||||
|
||||
(geo1, Some(geo2))
|
||||
}
|
||||
}
|
||||
Axis::Vertical => {
|
||||
if at <= self.x {
|
||||
let diff = at - self.x + thickness as i32;
|
||||
if diff > 0 {
|
||||
self.x += diff;
|
||||
self.width = self.width.saturating_sub(diff as u32);
|
||||
}
|
||||
(self, None)
|
||||
} else if at >= self.x + self.width as i32 {
|
||||
(self, None)
|
||||
} else if at + thickness as i32 >= self.x + self.width as i32 {
|
||||
let diff = self.x + self.width as i32 - at;
|
||||
self.width = self.width.saturating_sub(diff as u32);
|
||||
(self, None)
|
||||
} else {
|
||||
let left_x = self.x;
|
||||
let y = self.y;
|
||||
let left_width = at - self.x;
|
||||
let height = self.height;
|
||||
|
||||
let right_x = at + thickness as i32;
|
||||
let right_width = self.x + self.width as i32 - at - thickness as i32;
|
||||
|
||||
let geo1 = Geometry {
|
||||
x: left_x,
|
||||
y,
|
||||
width: left_width as u32,
|
||||
height,
|
||||
};
|
||||
let geo2 = Geometry {
|
||||
x: right_x,
|
||||
y,
|
||||
width: right_width as u32,
|
||||
height,
|
||||
};
|
||||
|
||||
(geo1, Some(geo2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch a set of requests that will be sent ot the compositor all at once.
|
||||
///
|
||||
/// # Rationale
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -63,12 +63,17 @@ pub mod pinnacle {
|
|||
|
||||
impl_signal_request!(
|
||||
OutputConnectRequest,
|
||||
LayoutRequest,
|
||||
WindowPointerEnterRequest,
|
||||
WindowPointerLeaveRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod layout {
|
||||
pub mod v0alpha1 {
|
||||
tonic::include_proto!("pinnacle.layout.v0alpha1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("pinnacle");
|
||||
|
|
879
src/api.rs
879
src/api.rs
File diff suppressed because it is too large
Load diff
63
src/api/layout.rs
Normal file
63
src/api/layout.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
|
||||
layout_request::{self, ExplicitLayout},
|
||||
layout_service_server, LayoutRequest, LayoutResponse,
|
||||
};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
||||
use crate::output::OutputName;
|
||||
|
||||
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) => {
|
||||
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 { output_name }) => {
|
||||
if let Some(output) = output_name
|
||||
.map(OutputName)
|
||||
.and_then(|name| name.output(state))
|
||||
.or_else(|| state.focused_output().cloned())
|
||||
{
|
||||
state.request_layout(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::error!("{err}"),
|
||||
},
|
||||
|state, sender, _join_handle| {
|
||||
state.layout_state.layout_request_sender = Some(sender);
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
signal_service_server, LayoutRequest, LayoutResponse, OutputConnectRequest,
|
||||
OutputConnectResponse, SignalRequest, StreamControl, WindowPointerEnterRequest,
|
||||
WindowPointerEnterResponse, WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, SignalRequest,
|
||||
StreamControl, WindowPointerEnterRequest, WindowPointerEnterResponse,
|
||||
WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
||||
};
|
||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
|
@ -15,7 +16,6 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
|
|||
#[derive(Debug, Default)]
|
||||
pub struct SignalState {
|
||||
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
||||
pub layout: SignalData<LayoutResponse, VecDeque<LayoutResponse>>,
|
||||
pub window_pointer_enter:
|
||||
SignalData<WindowPointerEnterResponse, VecDeque<WindowPointerEnterResponse>>,
|
||||
pub window_pointer_leave:
|
||||
|
@ -25,7 +25,6 @@ pub struct SignalState {
|
|||
impl SignalState {
|
||||
pub fn clear(&mut self) {
|
||||
self.output_connect.disconnect();
|
||||
self.layout.disconnect();
|
||||
self.window_pointer_enter.disconnect();
|
||||
self.window_pointer_leave.disconnect();
|
||||
}
|
||||
|
@ -121,7 +120,7 @@ impl<T, B: SignalBuffer<T>> SignalData<T, B> {
|
|||
fn start_signal_stream<I, O, B, F>(
|
||||
sender: StateFnSender,
|
||||
in_stream: Streaming<I>,
|
||||
with_signal_buffer: F,
|
||||
signal_data_selector: F,
|
||||
) -> Result<Response<ResponseStream<O>>, Status>
|
||||
where
|
||||
I: SignalRequest + std::fmt::Debug + Send + 'static,
|
||||
|
@ -129,7 +128,7 @@ where
|
|||
B: SignalBuffer<O>,
|
||||
F: Fn(&mut State) -> &mut SignalData<O, B> + Clone + Send + 'static,
|
||||
{
|
||||
let with_signal_buffer_clone = with_signal_buffer.clone();
|
||||
let signal_data_selector_clone = signal_data_selector.clone();
|
||||
|
||||
run_bidirectional_streaming(
|
||||
sender,
|
||||
|
@ -138,22 +137,22 @@ where
|
|||
let request = match request {
|
||||
Ok(request) => request,
|
||||
Err(status) => {
|
||||
tracing::error!("Error in output_connect signal in stream: {status}");
|
||||
error!("Error in output_connect signal in stream: {status}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("Got {request:?} from client stream");
|
||||
debug!("Got {request:?} from client stream");
|
||||
|
||||
let signal = with_signal_buffer(state);
|
||||
let signal = signal_data_selector(state);
|
||||
match request.control() {
|
||||
StreamControl::Ready => signal.ready(),
|
||||
StreamControl::Disconnect => signal.disconnect(),
|
||||
StreamControl::Unspecified => tracing::warn!("Received unspecified stream control"),
|
||||
StreamControl::Unspecified => warn!("Received unspecified stream control"),
|
||||
}
|
||||
},
|
||||
move |state, sender, join_handle| {
|
||||
let signal = with_signal_buffer_clone(state);
|
||||
let signal = signal_data_selector_clone(state);
|
||||
signal.connect(sender, join_handle);
|
||||
},
|
||||
)
|
||||
|
@ -172,7 +171,6 @@ impl SignalService {
|
|||
#[tonic::async_trait]
|
||||
impl signal_service_server::SignalService for SignalService {
|
||||
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
||||
type LayoutStream = ResponseStream<LayoutResponse>;
|
||||
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
||||
type WindowPointerLeaveStream = ResponseStream<WindowPointerLeaveResponse>;
|
||||
|
||||
|
@ -187,17 +185,6 @@ impl signal_service_server::SignalService for SignalService {
|
|||
})
|
||||
}
|
||||
|
||||
async fn layout(
|
||||
&self,
|
||||
request: Request<Streaming<LayoutRequest>>,
|
||||
) -> Result<Response<Self::LayoutStream>, Status> {
|
||||
let in_stream = request.into_inner();
|
||||
|
||||
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||
&mut state.signal_state.layout
|
||||
})
|
||||
}
|
||||
|
||||
async fn window_pointer_enter(
|
||||
&self,
|
||||
request: Request<Streaming<WindowPointerEnterRequest>>,
|
||||
|
|
773
src/api/window.rs
Normal file
773
src/api/window.rs
Normal file
|
@ -0,0 +1,773 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use pinnacle_api_defs::pinnacle::{
|
||||
v0alpha1::{Geometry, SetOrToggle},
|
||||
window::{
|
||||
self,
|
||||
v0alpha1::{
|
||||
window_service_server, AddWindowRuleRequest, CloseRequest, FullscreenOrMaximized,
|
||||
MoveGrabRequest, MoveToTagRequest, ResizeGrabRequest, SetFloatingRequest,
|
||||
SetFocusedRequest, SetFullscreenRequest, SetGeometryRequest, SetMaximizedRequest,
|
||||
SetTagRequest, WindowRule, WindowRuleCondition,
|
||||
},
|
||||
},
|
||||
};
|
||||
use smithay::{
|
||||
desktop::{space::SpaceElement, WindowSurface},
|
||||
reexports::wayland_protocols::xdg::shell::server,
|
||||
utils::{Point, Rectangle, SERIAL_COUNTER},
|
||||
wayland::seat::WaylandFocus,
|
||||
};
|
||||
use tonic::{Request, Response, Status};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::{
|
||||
focus::keyboard::KeyboardFocusTarget, output::OutputName, state::WithState, tag::TagId,
|
||||
window::window_state::WindowId,
|
||||
};
|
||||
|
||||
use super::{run_unary, run_unary_no_response, StateFnSender};
|
||||
|
||||
pub struct WindowService {
|
||||
sender: StateFnSender,
|
||||
}
|
||||
|
||||
impl WindowService {
|
||||
pub fn new(sender: StateFnSender) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl window_service_server::WindowService for WindowService {
|
||||
async fn close(&self, request: Request<CloseRequest>) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else { return };
|
||||
|
||||
match window.underlying_surface() {
|
||||
WindowSurface::Wayland(toplevel) => toplevel.send_close(),
|
||||
WindowSurface::X11(surface) => {
|
||||
if !surface.is_override_redirect() {
|
||||
if let Err(err) = surface.close() {
|
||||
error!("failed to close x11 window: {err}");
|
||||
}
|
||||
} else {
|
||||
warn!("tried to close OR window");
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_geometry(
|
||||
&self,
|
||||
request: Request<SetGeometryRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
tracing::info!(request = ?request);
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let geometry = request.geometry.unwrap_or_default();
|
||||
let x = geometry.x;
|
||||
let y = geometry.y;
|
||||
let width = geometry.width;
|
||||
let height = geometry.height;
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else { return };
|
||||
|
||||
// TODO: with no x or y, defaults unmapped windows to 0, 0
|
||||
let mut window_loc = state
|
||||
.space
|
||||
.element_location(&window)
|
||||
.unwrap_or((x.unwrap_or_default(), y.unwrap_or_default()).into());
|
||||
window_loc.x = x.unwrap_or(window_loc.x);
|
||||
window_loc.y = y.unwrap_or(window_loc.y);
|
||||
|
||||
let mut window_size = window.geometry().size;
|
||||
window_size.w = width.unwrap_or(window_size.w);
|
||||
window_size.h = height.unwrap_or(window_size.h);
|
||||
|
||||
let rect = Rectangle::from_loc_and_size(window_loc, window_size);
|
||||
// window.change_geometry(rect);
|
||||
window.with_state_mut(|state| {
|
||||
use crate::window::window_state::FloatingOrTiled;
|
||||
state.floating_or_tiled = match state.floating_or_tiled {
|
||||
FloatingOrTiled::Floating(_) => FloatingOrTiled::Floating(rect),
|
||||
FloatingOrTiled::Tiled(_) => FloatingOrTiled::Tiled(Some(rect)),
|
||||
}
|
||||
});
|
||||
|
||||
for output in state.space.outputs_for_element(&window) {
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_fullscreen(
|
||||
&self,
|
||||
request: Request<SetFullscreenRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let set_or_toggle = request.set_or_toggle();
|
||||
|
||||
if set_or_toggle == SetOrToggle::Unspecified {
|
||||
return Err(Status::invalid_argument("unspecified set or toggle"));
|
||||
}
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => {
|
||||
if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) {
|
||||
window.toggle_fullscreen();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Unset => {
|
||||
if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) {
|
||||
window.toggle_fullscreen();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Toggle => window.toggle_fullscreen(),
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
let Some(output) = window.output(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_maximized(
|
||||
&self,
|
||||
request: Request<SetMaximizedRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let set_or_toggle = request.set_or_toggle();
|
||||
|
||||
if set_or_toggle == SetOrToggle::Unspecified {
|
||||
return Err(Status::invalid_argument("unspecified set or toggle"));
|
||||
}
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => {
|
||||
if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) {
|
||||
window.toggle_maximized();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Unset => {
|
||||
if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) {
|
||||
window.toggle_maximized();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Toggle => window.toggle_maximized(),
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
let Some(output) = window.output(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_floating(
|
||||
&self,
|
||||
request: Request<SetFloatingRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let set_or_toggle = request.set_or_toggle();
|
||||
|
||||
if set_or_toggle == SetOrToggle::Unspecified {
|
||||
return Err(Status::invalid_argument("unspecified set or toggle"));
|
||||
}
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => {
|
||||
if !window.with_state(|state| state.floating_or_tiled.is_floating()) {
|
||||
window.toggle_floating();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Unset => {
|
||||
if window.with_state(|state| state.floating_or_tiled.is_floating()) {
|
||||
window.toggle_floating();
|
||||
}
|
||||
}
|
||||
SetOrToggle::Toggle => window.toggle_floating(),
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
let Some(output) = window.output(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_focused(
|
||||
&self,
|
||||
request: Request<SetFocusedRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let set_or_toggle = request.set_or_toggle();
|
||||
|
||||
if set_or_toggle == SetOrToggle::Unspecified {
|
||||
return Err(Status::invalid_argument("unspecified set or toggle"));
|
||||
}
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if window.is_x11_override_redirect() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(output) = window.output(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for win in state.space.elements() {
|
||||
win.set_activate(false);
|
||||
}
|
||||
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => {
|
||||
window.set_activate(true);
|
||||
output.with_state_mut(|state| state.focus_stack.set_focus(window.clone()));
|
||||
state.output_focus_stack.set_focus(output.clone());
|
||||
if let Some(keyboard) = state.seat.get_keyboard() {
|
||||
keyboard.set_focus(
|
||||
state,
|
||||
Some(KeyboardFocusTarget::Window(window)),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
}
|
||||
}
|
||||
SetOrToggle::Unset => {
|
||||
if state.focused_window(&output) == Some(window) {
|
||||
output.with_state_mut(|state| state.focus_stack.unset_focus());
|
||||
if let Some(keyboard) = state.seat.get_keyboard() {
|
||||
keyboard.set_focus(state, None, SERIAL_COUNTER.next_serial());
|
||||
}
|
||||
}
|
||||
}
|
||||
SetOrToggle::Toggle => {
|
||||
if state.focused_window(&output).as_ref() == Some(&window) {
|
||||
output.with_state_mut(|state| state.focus_stack.unset_focus());
|
||||
if let Some(keyboard) = state.seat.get_keyboard() {
|
||||
keyboard.set_focus(state, None, SERIAL_COUNTER.next_serial());
|
||||
}
|
||||
} else {
|
||||
window.set_activate(true);
|
||||
output.with_state_mut(|state| state.focus_stack.set_focus(window.clone()));
|
||||
state.output_focus_stack.set_focus(output.clone());
|
||||
if let Some(keyboard) = state.seat.get_keyboard() {
|
||||
keyboard.set_focus(
|
||||
state,
|
||||
Some(KeyboardFocusTarget::Window(window)),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
for window in state.space.elements() {
|
||||
if let Some(toplevel) = window.toplevel() {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_to_tag(
|
||||
&self,
|
||||
request: Request<MoveToTagRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let tag_id = TagId(
|
||||
request
|
||||
.tag_id
|
||||
.ok_or_else(|| Status::invalid_argument("no tag specified"))?,
|
||||
);
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else { return };
|
||||
let Some(tag) = tag_id.tag(state) else { return };
|
||||
window.with_state_mut(|state| {
|
||||
state.tags = vec![tag.clone()];
|
||||
});
|
||||
let Some(output) = tag.output(state) else { return };
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_tag(&self, request: Request<SetTagRequest>) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
let tag_id = TagId(
|
||||
request
|
||||
.tag_id
|
||||
.ok_or_else(|| Status::invalid_argument("no tag specified"))?,
|
||||
);
|
||||
|
||||
let set_or_toggle = request.set_or_toggle();
|
||||
|
||||
if set_or_toggle == SetOrToggle::Unspecified {
|
||||
return Err(Status::invalid_argument("unspecified set or toggle"));
|
||||
}
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(window) = window_id.window(state) else { return };
|
||||
let Some(tag) = tag_id.tag(state) else { return };
|
||||
|
||||
// TODO: turn state.tags into a hashset
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => window.with_state_mut(|state| {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
state.tags.push(tag.clone());
|
||||
}),
|
||||
SetOrToggle::Unset => window.with_state_mut(|state| {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
}),
|
||||
SetOrToggle::Toggle => window.with_state_mut(|state| {
|
||||
if !state.tags.contains(&tag) {
|
||||
state.tags.push(tag.clone());
|
||||
} else {
|
||||
state.tags.retain(|tg| tg != &tag);
|
||||
}
|
||||
}),
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
let Some(output) = tag.output(state) else { return };
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_grab(&self, request: Request<MoveGrabRequest>) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let button = request
|
||||
.button
|
||||
.ok_or_else(|| Status::invalid_argument("no button specified"))?;
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(pointer_location) = state.seat.get_pointer().map(|ptr| ptr.current_location())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((pointer_focus, _)) = state.pointer_focus_target_under(pointer_location)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = pointer_focus.window_for(state) else {
|
||||
tracing::info!("Move grabs are currently not implemented for non-windows");
|
||||
return;
|
||||
};
|
||||
let Some(wl_surf) = window.wl_surface() else {
|
||||
return;
|
||||
};
|
||||
let seat = state.seat.clone();
|
||||
|
||||
crate::grab::move_grab::move_request_server(
|
||||
state,
|
||||
&wl_surf,
|
||||
&seat,
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
button,
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn resize_grab(
|
||||
&self,
|
||||
request: Request<ResizeGrabRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let button = request
|
||||
.button
|
||||
.ok_or_else(|| Status::invalid_argument("no button specified"))?;
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(pointer_loc) = state.seat.get_pointer().map(|ptr| ptr.current_location())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((pointer_focus, window_loc)) = state.pointer_focus_target_under(pointer_loc)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = pointer_focus.window_for(state) else {
|
||||
tracing::info!("Move grabs are currently not implemented for non-windows");
|
||||
return;
|
||||
};
|
||||
let Some(wl_surf) = window.wl_surface() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let window_geometry = window.geometry();
|
||||
let window_x = window_loc.x as f64;
|
||||
let window_y = window_loc.y as f64;
|
||||
let window_width = window_geometry.size.w as f64;
|
||||
let window_height = window_geometry.size.h as f64;
|
||||
let half_width = window_x + window_width / 2.0;
|
||||
let half_height = window_y + window_height / 2.0;
|
||||
let full_width = window_x + window_width;
|
||||
let full_height = window_y + window_height;
|
||||
|
||||
let edges = match pointer_loc {
|
||||
Point { x, y, .. }
|
||||
if (window_x..=half_width).contains(&x)
|
||||
&& (window_y..=half_height).contains(&y) =>
|
||||
{
|
||||
server::xdg_toplevel::ResizeEdge::TopLeft
|
||||
}
|
||||
Point { x, y, .. }
|
||||
if (half_width..=full_width).contains(&x)
|
||||
&& (window_y..=half_height).contains(&y) =>
|
||||
{
|
||||
server::xdg_toplevel::ResizeEdge::TopRight
|
||||
}
|
||||
Point { x, y, .. }
|
||||
if (window_x..=half_width).contains(&x)
|
||||
&& (half_height..=full_height).contains(&y) =>
|
||||
{
|
||||
server::xdg_toplevel::ResizeEdge::BottomLeft
|
||||
}
|
||||
Point { x, y, .. }
|
||||
if (half_width..=full_width).contains(&x)
|
||||
&& (half_height..=full_height).contains(&y) =>
|
||||
{
|
||||
server::xdg_toplevel::ResizeEdge::BottomRight
|
||||
}
|
||||
_ => server::xdg_toplevel::ResizeEdge::None,
|
||||
};
|
||||
|
||||
crate::grab::resize_grab::resize_request_server(
|
||||
state,
|
||||
&wl_surf,
|
||||
&state.seat.clone(),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
edges.into(),
|
||||
button,
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get(
|
||||
&self,
|
||||
_request: Request<window::v0alpha1::GetRequest>,
|
||||
) -> Result<Response<window::v0alpha1::GetResponse>, Status> {
|
||||
run_unary(&self.sender, move |state| {
|
||||
let window_ids = state
|
||||
.windows
|
||||
.iter()
|
||||
.map(|win| win.with_state(|state| state.id.0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
window::v0alpha1::GetResponse { window_ids }
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_properties(
|
||||
&self,
|
||||
request: Request<window::v0alpha1::GetPropertiesRequest>,
|
||||
) -> Result<Response<window::v0alpha1::GetPropertiesResponse>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let window_id = WindowId(
|
||||
request
|
||||
.window_id
|
||||
.ok_or_else(|| Status::invalid_argument("no window specified"))?,
|
||||
);
|
||||
|
||||
run_unary(&self.sender, move |state| {
|
||||
let window = window_id.window(state);
|
||||
|
||||
let width = window.as_ref().map(|win| win.geometry().size.w);
|
||||
|
||||
let height = window.as_ref().map(|win| win.geometry().size.h);
|
||||
|
||||
let x = window
|
||||
.as_ref()
|
||||
.and_then(|win| state.space.element_location(win))
|
||||
.map(|loc| loc.x);
|
||||
|
||||
let y = window
|
||||
.as_ref()
|
||||
.and_then(|win| state.space.element_location(win))
|
||||
.map(|loc| loc.y);
|
||||
|
||||
let geometry = if width.is_none() && height.is_none() && x.is_none() && y.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(Geometry {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
};
|
||||
|
||||
let class = window.as_ref().and_then(|win| win.class());
|
||||
let title = window.as_ref().and_then(|win| win.title());
|
||||
|
||||
let focused = window.as_ref().and_then(|win| {
|
||||
state
|
||||
.focused_output()
|
||||
.and_then(|output| state.focused_window(output))
|
||||
.map(|foc_win| win == &foc_win)
|
||||
});
|
||||
|
||||
let floating = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.floating_or_tiled.is_floating()));
|
||||
|
||||
let fullscreen_or_maximized = window
|
||||
.as_ref()
|
||||
.map(|win| win.with_state(|state| state.fullscreen_or_maximized))
|
||||
.map(|fs_or_max| match fs_or_max {
|
||||
// TODO: from impl
|
||||
crate::window::window_state::FullscreenOrMaximized::Neither => {
|
||||
FullscreenOrMaximized::Neither
|
||||
}
|
||||
crate::window::window_state::FullscreenOrMaximized::Fullscreen => {
|
||||
FullscreenOrMaximized::Fullscreen
|
||||
}
|
||||
crate::window::window_state::FullscreenOrMaximized::Maximized => {
|
||||
FullscreenOrMaximized::Maximized
|
||||
}
|
||||
} as i32);
|
||||
|
||||
let tag_ids = window
|
||||
.as_ref()
|
||||
.map(|win| {
|
||||
win.with_state(|state| {
|
||||
state.tags.iter().map(|tag| tag.id().0).collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
window::v0alpha1::GetPropertiesResponse {
|
||||
geometry,
|
||||
class,
|
||||
title,
|
||||
focused,
|
||||
floating,
|
||||
fullscreen_or_maximized,
|
||||
tag_ids,
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn add_window_rule(
|
||||
&self,
|
||||
request: Request<AddWindowRuleRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let cond = request
|
||||
.cond
|
||||
.ok_or_else(|| Status::invalid_argument("no condition specified"))?
|
||||
.into();
|
||||
|
||||
let rule = request
|
||||
.rule
|
||||
.ok_or_else(|| Status::invalid_argument("no rule specified"))?
|
||||
.into();
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
state.config.window_rules.push((cond, rule));
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowRuleCondition> for crate::window::rules::WindowRuleCondition {
|
||||
fn from(cond: WindowRuleCondition) -> Self {
|
||||
let cond_any = match cond.any.is_empty() {
|
||||
true => None,
|
||||
false => Some(
|
||||
cond.any
|
||||
.into_iter()
|
||||
.map(crate::window::rules::WindowRuleCondition::from)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
};
|
||||
|
||||
let cond_all = match cond.all.is_empty() {
|
||||
true => None,
|
||||
false => Some(
|
||||
cond.all
|
||||
.into_iter()
|
||||
.map(crate::window::rules::WindowRuleCondition::from)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
};
|
||||
|
||||
let class = match cond.classes.is_empty() {
|
||||
true => None,
|
||||
false => Some(cond.classes),
|
||||
};
|
||||
|
||||
let title = match cond.titles.is_empty() {
|
||||
true => None,
|
||||
false => Some(cond.titles),
|
||||
};
|
||||
|
||||
let tag = match cond.tags.is_empty() {
|
||||
true => None,
|
||||
false => Some(cond.tags.into_iter().map(TagId).collect::<Vec<_>>()),
|
||||
};
|
||||
|
||||
crate::window::rules::WindowRuleCondition {
|
||||
cond_any,
|
||||
cond_all,
|
||||
class,
|
||||
title,
|
||||
tag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowRule> for crate::window::rules::WindowRule {
|
||||
fn from(rule: WindowRule) -> Self {
|
||||
let fullscreen_or_maximized = match rule.fullscreen_or_maximized() {
|
||||
FullscreenOrMaximized::Unspecified => None,
|
||||
FullscreenOrMaximized::Neither => {
|
||||
Some(crate::window::window_state::FullscreenOrMaximized::Neither)
|
||||
}
|
||||
FullscreenOrMaximized::Fullscreen => {
|
||||
Some(crate::window::window_state::FullscreenOrMaximized::Fullscreen)
|
||||
}
|
||||
FullscreenOrMaximized::Maximized => {
|
||||
Some(crate::window::window_state::FullscreenOrMaximized::Maximized)
|
||||
}
|
||||
};
|
||||
let output = rule.output.map(OutputName);
|
||||
let tags = match rule.tags.is_empty() {
|
||||
true => None,
|
||||
false => Some(rule.tags.into_iter().map(TagId).collect::<Vec<_>>()),
|
||||
};
|
||||
let floating_or_tiled = rule.floating.map(|floating| match floating {
|
||||
true => crate::window::rules::FloatingOrTiled::Floating,
|
||||
false => crate::window::rules::FloatingOrTiled::Tiled,
|
||||
});
|
||||
let size = rule.width.and_then(|w| {
|
||||
rule.height.and_then(|h| {
|
||||
Some((
|
||||
NonZeroU32::try_from(w as u32).ok()?,
|
||||
NonZeroU32::try_from(h as u32).ok()?,
|
||||
))
|
||||
})
|
||||
});
|
||||
let location = rule.x.and_then(|x| rule.y.map(|y| (x, y)));
|
||||
|
||||
crate::window::rules::WindowRule {
|
||||
output,
|
||||
tags,
|
||||
floating_or_tiled,
|
||||
fullscreen_or_maximized,
|
||||
size,
|
||||
location,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -233,7 +233,7 @@ pub fn setup_winit(
|
|||
None,
|
||||
);
|
||||
layer_map_for_output(&output).arrange();
|
||||
state.update_windows(&output);
|
||||
state.request_layout(&output);
|
||||
}
|
||||
WinitEvent::Focus(_) => {}
|
||||
WinitEvent::Input(input_evt) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
api::{
|
||||
signal::SignalService, InputService, OutputService, PinnacleService, ProcessService,
|
||||
TagService, WindowService,
|
||||
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(_) => {
|
||||
|
|
|
@ -161,7 +161,7 @@ impl CompositorHandler for State {
|
|||
self.apply_window_rules(&new_window);
|
||||
|
||||
if let Some(focused_output) = self.focused_output().cloned() {
|
||||
self.update_windows(&focused_output);
|
||||
self.request_layout(&focused_output);
|
||||
new_window.send_frame(
|
||||
&focused_output,
|
||||
self.clock.now(),
|
||||
|
@ -514,7 +514,7 @@ impl WlrLayerShellHandler for State {
|
|||
drop(map); // wow i really love refcells haha
|
||||
|
||||
self.loop_handle.insert_idle(move |state| {
|
||||
state.update_windows(&output);
|
||||
state.request_layout(&output);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -534,7 +534,7 @@ impl WlrLayerShellHandler for State {
|
|||
|
||||
if let Some(output) = output {
|
||||
self.loop_handle.insert_idle(move |state| {
|
||||
state.update_windows(&output);
|
||||
state.request_layout(&output);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ impl XdgShellHandler for State {
|
|||
};
|
||||
|
||||
if let Some(output) = window.output(self) {
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
let focus = self
|
||||
.focused_window(&output)
|
||||
.map(KeyboardFocusTarget::Window);
|
||||
|
@ -766,7 +766,7 @@ impl XdgShellHandler for State {
|
|||
}
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
}
|
||||
|
||||
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
||||
|
@ -779,7 +779,7 @@ impl XdgShellHandler for State {
|
|||
}
|
||||
|
||||
let Some(output) = window.output(self) else { return };
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
}
|
||||
|
||||
fn minimize_request(&mut self, _surface: ToplevelSurface) {
|
||||
|
|
|
@ -103,7 +103,7 @@ impl XwmHandler for State {
|
|||
|
||||
if let Some(output) = window.output(self) {
|
||||
output.with_state_mut(|state| state.focus_stack.set_focus(window.clone()));
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
}
|
||||
|
||||
self.loop_handle.insert_idle(move |state| {
|
||||
|
@ -166,7 +166,7 @@ impl XwmHandler for State {
|
|||
self.space.unmap_elem(&win);
|
||||
|
||||
if let Some(output) = win.output(self) {
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
|
||||
let focus = self
|
||||
.focused_window(&output)
|
||||
|
@ -227,7 +227,7 @@ impl XwmHandler for State {
|
|||
.retain(|elem| win.wl_surface() != elem.wl_surface());
|
||||
|
||||
if let Some(output) = win.output(self) {
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
|
||||
let focus = self
|
||||
.focused_window(&output)
|
||||
|
|
541
src/layout.rs
541
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},
|
||||
utils::{Logical, Point, Rectangle, Serial},
|
||||
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},
|
||||
|
@ -16,46 +23,11 @@ use crate::{
|
|||
};
|
||||
|
||||
impl State {
|
||||
/// Compute the positions and sizes of tiled windows on
|
||||
/// `output` according to the provided [`Layout`].
|
||||
fn tile_windows(&self, output: &Output, windows: Vec<WindowElement>, layout: Layout) {
|
||||
let Some(rect) = self.space.output_geometry(output).map(|op_geo| {
|
||||
let map = layer_map_for_output(output);
|
||||
if map.layers().peekable().peek().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.
|
||||
op_geo
|
||||
} else {
|
||||
let zone = map.non_exclusive_zone();
|
||||
tracing::debug!("non_exclusive_zone is {zone:?}");
|
||||
Rectangle::from_loc_and_size(op_geo.loc + zone.loc, zone.size)
|
||||
}
|
||||
}) else {
|
||||
// TODO: maybe default to something like 800x800 like in anvil so people still see
|
||||
// | windows open
|
||||
tracing::error!("Failed to get output geometry");
|
||||
return;
|
||||
};
|
||||
|
||||
match layout {
|
||||
Layout::MasterStack => master_stack(windows, rect),
|
||||
Layout::Dwindle => dwindle(windows, rect),
|
||||
Layout::Spiral => spiral(windows, rect),
|
||||
layout @ (Layout::CornerTopLeft
|
||||
| Layout::CornerTopRight
|
||||
| Layout::CornerBottomLeft
|
||||
| Layout::CornerBottomRight) => corner(&layout, windows, rect),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_windows(&mut self, output: &Output) {
|
||||
let Some(layout) =
|
||||
output.with_state(|state| state.focused_tags().next().map(|tag| tag.layout()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
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
|
||||
|
@ -75,30 +47,32 @@ impl State {
|
|||
state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither()
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.tile_windows(output, tiled_windows, layout);
|
||||
.cloned();
|
||||
|
||||
let output_geo = self.space.output_geometry(output).expect("no output geo");
|
||||
|
||||
let non_exclusive_geo = {
|
||||
let map = layer_map_for_output(output);
|
||||
map.non_exclusive_zone()
|
||||
};
|
||||
|
||||
for (win, geo) in tiled_windows.zip(geometries.into_iter().map(|mut geo| {
|
||||
geo.loc += output_geo.loc + non_exclusive_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);
|
||||
window.change_geometry(Rectangle::from_loc_and_size(
|
||||
output_geo.loc + non_exclusive_geo.loc,
|
||||
non_exclusive_geo.size,
|
||||
));
|
||||
}
|
||||
FullscreenOrMaximized::Neither => {
|
||||
if let FloatingOrTiled::Floating(rect) =
|
||||
|
@ -152,309 +126,7 @@ impl State {
|
|||
|
||||
self.fixup_z_layering();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Layout {
|
||||
MasterStack,
|
||||
Dwindle,
|
||||
Spiral,
|
||||
CornerTopLeft,
|
||||
CornerTopRight,
|
||||
CornerBottomLeft,
|
||||
CornerBottomRight,
|
||||
}
|
||||
|
||||
fn master_stack(windows: Vec<WindowElement>, rect: Rectangle<i32, Logical>) {
|
||||
let size = rect.size;
|
||||
let loc = rect.loc;
|
||||
|
||||
let master = windows.first();
|
||||
let stack = windows.iter().skip(1);
|
||||
|
||||
let Some(master) = master else { return };
|
||||
|
||||
let stack_count = stack.clone().count();
|
||||
|
||||
if stack_count == 0 {
|
||||
// one window
|
||||
master.change_geometry(Rectangle::from_loc_and_size(loc, size));
|
||||
} else {
|
||||
let loc: Point<i32, Logical> = (loc.x, loc.y).into();
|
||||
let new_master_size: Size<i32, Logical> = (size.w / 2, size.h).into();
|
||||
master.change_geometry(Rectangle::from_loc_and_size(loc, new_master_size));
|
||||
|
||||
let height = size.h as f32 / stack_count as f32;
|
||||
let mut y_s = vec![];
|
||||
for i in 0..stack_count {
|
||||
y_s.push((i as f32 * height).round() as i32);
|
||||
}
|
||||
let heights = y_s
|
||||
.windows(2)
|
||||
.map(|pair| pair[1] - pair[0])
|
||||
.chain(vec![size.h - y_s.last().expect("vec was empty")])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (i, win) in stack.enumerate() {
|
||||
win.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from((size.w / 2 + loc.x, y_s[i] + loc.y)),
|
||||
Size::from((size.w / 2, i32::max(heights[i], 40))),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dwindle(windows: Vec<WindowElement>, rect: Rectangle<i32, Logical>) {
|
||||
let size = rect.size;
|
||||
let loc = rect.loc;
|
||||
|
||||
let mut iter = windows.windows(2).peekable();
|
||||
|
||||
if iter.peek().is_none() {
|
||||
if let Some(window) = windows.first() {
|
||||
window.change_geometry(Rectangle::from_loc_and_size(loc, size));
|
||||
}
|
||||
} else {
|
||||
let mut win1_size = size;
|
||||
let mut win1_loc = loc;
|
||||
for (i, wins) in iter.enumerate() {
|
||||
let win1 = &wins[0];
|
||||
let win2 = &wins[1];
|
||||
|
||||
enum Slice {
|
||||
Right,
|
||||
Below,
|
||||
}
|
||||
|
||||
let slice = if i % 2 == 0 { Slice::Right } else { Slice::Below };
|
||||
|
||||
match slice {
|
||||
Slice::Right => {
|
||||
let width_partition = win1_size.w / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
win1_loc,
|
||||
Size::from((win1_size.w - width_partition, i32::max(win1_size.h, 40))),
|
||||
));
|
||||
|
||||
win1_loc = (win1_loc.x + (win1_size.w - width_partition), win1_loc.y).into();
|
||||
win1_size = (width_partition, i32::max(win1_size.h, 40)).into();
|
||||
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
Slice::Below => {
|
||||
let height_partition = win1_size.h / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
win1_loc,
|
||||
Size::from((win1_size.w, i32::max(win1_size.h - height_partition, 40))),
|
||||
));
|
||||
|
||||
win1_loc = (win1_loc.x, win1_loc.y + (win1_size.h - height_partition)).into();
|
||||
win1_size = (win1_size.w, i32::max(height_partition, 40)).into();
|
||||
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spiral(windows: Vec<WindowElement>, rect: Rectangle<i32, Logical>) {
|
||||
let size = rect.size;
|
||||
let loc = rect.loc;
|
||||
|
||||
let mut window_pairs = windows.windows(2).peekable();
|
||||
|
||||
if window_pairs.peek().is_none() {
|
||||
if let Some(window) = windows.first() {
|
||||
window.change_geometry(Rectangle::from_loc_and_size(loc, size));
|
||||
}
|
||||
} else {
|
||||
let mut win1_loc = loc;
|
||||
let mut win1_size = size;
|
||||
|
||||
for (i, wins) in window_pairs.enumerate() {
|
||||
let win1 = &wins[0];
|
||||
let win2 = &wins[1];
|
||||
|
||||
enum Slice {
|
||||
Above,
|
||||
Below,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
let slice = match i % 4 {
|
||||
0 => Slice::Right,
|
||||
1 => Slice::Below,
|
||||
2 => Slice::Left,
|
||||
3 => Slice::Above,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match slice {
|
||||
Slice::Above => {
|
||||
let height_partition = win1_size.h / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from((win1_loc.x, win1_loc.y + height_partition)),
|
||||
Size::from((win1_size.w, i32::max(win1_size.h - height_partition, 40))),
|
||||
));
|
||||
|
||||
win1_size = (win1_size.w, i32::max(height_partition, 40)).into();
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
Slice::Below => {
|
||||
let height_partition = win1_size.h / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
win1_loc,
|
||||
Size::from((win1_size.w, win1_size.h - i32::max(height_partition, 40))),
|
||||
));
|
||||
|
||||
win1_loc = (win1_loc.x, win1_loc.y + (win1_size.h - height_partition)).into();
|
||||
win1_size = (win1_size.w, i32::max(height_partition, 40)).into();
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
Slice::Left => {
|
||||
let width_partition = win1_size.w / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from((win1_loc.x + width_partition, win1_loc.y)),
|
||||
Size::from((win1_size.w - width_partition, i32::max(win1_size.h, 40))),
|
||||
));
|
||||
|
||||
win1_size = (width_partition, i32::max(win1_size.h, 40)).into();
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
Slice::Right => {
|
||||
let width_partition = win1_size.w / 2;
|
||||
|
||||
win1.change_geometry(Rectangle::from_loc_and_size(
|
||||
win1_loc,
|
||||
Size::from((win1_size.w - width_partition, i32::max(win1_size.h, 40))),
|
||||
));
|
||||
|
||||
win1_loc = (win1_loc.x + (win1_size.w - width_partition), win1_loc.y).into();
|
||||
win1_size = (width_partition, i32::max(win1_size.h, 40)).into();
|
||||
win2.change_geometry(Rectangle::from_loc_and_size(win1_loc, win1_size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn corner(layout: &Layout, windows: Vec<WindowElement>, rect: Rectangle<i32, Logical>) {
|
||||
let size = rect.size;
|
||||
let loc = rect.loc;
|
||||
|
||||
match windows.len() {
|
||||
0 => (),
|
||||
1 => {
|
||||
windows[0].change_geometry(rect);
|
||||
}
|
||||
2 => {
|
||||
windows[0].change_geometry(Rectangle::from_loc_and_size(
|
||||
loc,
|
||||
Size::from((size.w / 2, size.h)),
|
||||
));
|
||||
|
||||
windows[1].change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from((loc.x + size.w / 2, loc.y)),
|
||||
Size::from((size.w / 2, size.h)),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
let mut windows = windows.into_iter();
|
||||
let Some(corner) = windows.next() else { unreachable!() };
|
||||
|
||||
let mut horiz_stack = Vec::<WindowElement>::new();
|
||||
let mut vert_stack = Vec::<WindowElement>::new();
|
||||
|
||||
for (i, win) in windows.enumerate() {
|
||||
if i % 2 == 0 {
|
||||
horiz_stack.push(win);
|
||||
} else {
|
||||
vert_stack.push(win);
|
||||
}
|
||||
}
|
||||
|
||||
let div_factor = 2;
|
||||
|
||||
corner.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from(match layout {
|
||||
Layout::CornerTopLeft => (loc.x, loc.y),
|
||||
Layout::CornerTopRight => (loc.x + size.w - size.w / div_factor, loc.y),
|
||||
Layout::CornerBottomLeft => (loc.x, loc.y + size.h - size.h / div_factor),
|
||||
Layout::CornerBottomRight => (
|
||||
loc.x + size.w - size.w / div_factor,
|
||||
loc.y + size.h - size.h / div_factor,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
Size::from((size.w / div_factor, size.h / div_factor)),
|
||||
));
|
||||
|
||||
let vert_stack_count = vert_stack.len();
|
||||
|
||||
let height = size.h as f32 / vert_stack_count as f32;
|
||||
let mut y_s = vec![];
|
||||
for i in 0..vert_stack_count {
|
||||
y_s.push((i as f32 * height).round() as i32);
|
||||
}
|
||||
let heights = y_s
|
||||
.windows(2)
|
||||
.map(|pair| pair[1] - pair[0])
|
||||
.chain(vec![size.h - y_s.last().expect("vec was empty")])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (i, win) in vert_stack.iter().enumerate() {
|
||||
win.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from((
|
||||
match layout {
|
||||
Layout::CornerTopLeft | Layout::CornerBottomLeft => size.w / 2 + loc.x,
|
||||
Layout::CornerTopRight | Layout::CornerBottomRight => loc.x,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
y_s[i] + loc.y,
|
||||
)),
|
||||
Size::from((size.w / 2, i32::max(heights[i], 40))),
|
||||
));
|
||||
}
|
||||
|
||||
let horiz_stack_count = horiz_stack.len();
|
||||
|
||||
let width = size.w as f32 / 2.0 / horiz_stack_count as f32;
|
||||
let mut x_s = vec![];
|
||||
for i in 0..horiz_stack_count {
|
||||
x_s.push((i as f32 * width).round() as i32);
|
||||
}
|
||||
let widths = x_s
|
||||
.windows(2)
|
||||
.map(|pair| pair[1] - pair[0])
|
||||
.chain(vec![size.w / 2 - x_s.last().expect("vec was empty")])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (i, win) in horiz_stack.iter().enumerate() {
|
||||
win.change_geometry(Rectangle::from_loc_and_size(
|
||||
Point::from(match layout {
|
||||
Layout::CornerTopLeft => (x_s[i] + loc.x, loc.y + size.h / 2),
|
||||
Layout::CornerTopRight => (x_s[i] + loc.x + size.w / 2, loc.y + size.h / 2),
|
||||
Layout::CornerBottomLeft => (x_s[i] + loc.x, loc.y),
|
||||
Layout::CornerBottomRight => (x_s[i] + loc.x + size.w / 2, loc.y),
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
Size::from((i32::max(widths[i], 1), size.h / 2)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Swaps two windows in the main window vec and updates all windows.
|
||||
pub fn swap_window_positions(&mut self, win1: &WindowElement, win2: &WindowElement) {
|
||||
let win1_index = self.windows.iter().position(|win| win == win1);
|
||||
|
@ -463,8 +135,157 @@ impl State {
|
|||
if let (Some(first), Some(second)) = (win1_index, win2_index) {
|
||||
self.windows.swap(first, second);
|
||||
if let Some(output) = win1.output(self) {
|
||||
self.update_windows(&output);
|
||||
self.request_layout(&output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 request_layout(&mut self, output: &Output) {
|
||||
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 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 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()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (output_width, output_height) = {
|
||||
let map = layer_map_for_output(output);
|
||||
let zone = map.non_exclusive_zone();
|
||||
(zone.size.w, zone.size.h)
|
||||
};
|
||||
|
||||
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<()> {
|
||||
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 latest == 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?),
|
||||
(i32::max(geo.width?, 1), i32::max(geo.height?, 1)),
|
||||
))
|
||||
})
|
||||
.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)
|
||||
|
|
25
src/tag.rs
25
src/tag.rs
|
@ -9,26 +9,18 @@ use std::{
|
|||
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::{
|
||||
layout::Layout,
|
||||
state::{State, WithState},
|
||||
};
|
||||
use crate::state::{State, WithState};
|
||||
|
||||
static TAG_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// A unique id for a [`Tag`].
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TagId {
|
||||
/// The tag given was invalid/nonexistent
|
||||
None,
|
||||
#[serde(untagged)]
|
||||
Some(u32),
|
||||
}
|
||||
pub struct TagId(pub u32);
|
||||
|
||||
impl TagId {
|
||||
/// Get the next available `TagId`.
|
||||
fn next() -> Self {
|
||||
Self::Some(TAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||
Self(TAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Get the tag associated with this id.
|
||||
|
@ -57,8 +49,6 @@ struct TagInner {
|
|||
name: String,
|
||||
/// Whether this tag is active or not.
|
||||
active: bool,
|
||||
/// What layout this tag has.
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl PartialEq for TagInner {
|
||||
|
@ -93,14 +83,6 @@ impl Tag {
|
|||
pub fn set_active(&self, active: bool) {
|
||||
self.0.borrow_mut().active = active;
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> Layout {
|
||||
self.0.borrow().layout
|
||||
}
|
||||
|
||||
pub fn set_layout(&self, layout: Layout) {
|
||||
self.0.borrow_mut().layout = layout;
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
|
@ -109,7 +91,6 @@ impl Tag {
|
|||
id: TagId::next(),
|
||||
name,
|
||||
active: false,
|
||||
layout: Layout::MasterStack, // TODO: get from config
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
|
@ -237,7 +237,7 @@ mod coverage {
|
|||
..Default::default()
|
||||
},
|
||||
WindowRule {
|
||||
tags: Some(vec![TagId::Some(0)]),
|
||||
tags: Some(vec![TagId(0)]),
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
|
@ -272,7 +272,7 @@ mod coverage {
|
|||
WindowRuleCondition {
|
||||
cond_all: Some(vec![WindowRuleCondition {
|
||||
class: Some(vec!["steam".to_string()]),
|
||||
tag: Some(vec![TagId::Some(0), TagId::Some(1)]),
|
||||
tag: Some(vec![TagId(0), TagId(1)]),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
|
|
Loading…
Reference in a new issue