Merge pull request #176 from pinnacle-comp/layout

Add a dynamic and configurable layout system
This commit is contained in:
Ottatop 2024-03-16 22:08:25 -05:00 committed by GitHub
commit 5d117288c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3956 additions and 1802 deletions

View file

@ -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)

View file

@ -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",
},
}

View file

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

View file

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

View file

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

1147
api/lua/pinnacle/layout.lua Normal file

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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

View file

@ -1,4 +0,0 @@
---@class LayoutModule
local layout = {}
return layout

View file

@ -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

View file

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

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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);

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

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

View file

@ -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");

File diff suppressed because it is too large Load diff

63
src/api/layout.rs Normal file
View 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);
},
)
}
}

View file

@ -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
View 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,
}
}
}

View file

@ -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) => {

View file

@ -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(_) => {

View file

@ -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);
});
}
}

View file

@ -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) {

View file

@ -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)

View file

@ -1,13 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use std::collections::{HashMap, HashSet};
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{layout_request::Geometries, LayoutResponse};
use smithay::{
desktop::{layer_map_for_output, WindowSurface},
output::Output,
utils::{Logical, Point, Rectangle, Serial, Size},
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(())
}
}

View file

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

View file

@ -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
})))
}

View file

@ -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()