diff --git a/api/lua/examples/default/default_config.lua b/api/lua/examples/default/default_config.lua index 252ed5c..f9b6c5e 100644 --- a/api/lua/examples/default/default_config.lua +++ b/api/lua/examples/default/default_config.lua @@ -4,6 +4,7 @@ require("pinnacle").setup(function(Pinnacle) local Output = Pinnacle.output local Tag = Pinnacle.tag local Window = Pinnacle.window + local Layout = Pinnacle.layout local key = Input.key @@ -83,35 +84,18 @@ require("pinnacle").setup(function(Pinnacle) tags[1]:set_active(true) end) - -- Spawning must happen after you add tags, as Pinnacle currently doesn't render windows without tags. - Process.spawn_once(terminal) + -------------------- + -- Layouts -- + -------------------- - -- Create a layout cycler to cycle layouts on an output. - local layout_cycler = Tag.new_layout_cycler({ - "master_stack", - "dwindle", - "spiral", - "corner_top_left", - "corner_top_right", - "corner_bottom_left", - "corner_bottom_right", + local layout_handler = Layout.new_handler({ + Layout.builtins.master_stack, }) - -- mod_key + space = Cycle forward one layout on the focused output - Input.keybind({ mod_key }, key.space, function() - local focused_op = Output.get_focused() - if focused_op then - layout_cycler.next(focused_op) - end - end) + Layout.set_handler(layout_handler) - -- mod_key + shift + space = Cycle backward one layout on the focused output - Input.keybind({ mod_key, "shift" }, key.space, function() - local focused_op = Output.get_focused() - if focused_op then - layout_cycler.prev(focused_op) - end - end) + -- Spawning must happen after you add tags, as Pinnacle currently doesn't render windows without tags. + Process.spawn_once(terminal) for _, tag_name in ipairs(tag_names) do -- nil-safety: tags are guaranteed to be on the outputs due to connect_for_all above diff --git a/api/lua/pinnacle-api-dev-1.rockspec b/api/lua/pinnacle-api-dev-1.rockspec index ef7d345..01c9a14 100644 --- a/api/lua/pinnacle-api-dev-1.rockspec +++ b/api/lua/pinnacle-api-dev-1.rockspec @@ -28,5 +28,6 @@ build = { ["pinnacle.window"] = "pinnacle/window.lua", ["pinnacle.util"] = "pinnacle/util.lua", ["pinnacle.signal"] = "pinnacle/signal.lua", + ["pinnacle.layout"] = "pinnacle/layout.lua", }, } diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index 249bcd4..51e3028 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.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 diff --git a/api/lua/pinnacle/grpc/client.lua b/api/lua/pinnacle/grpc/client.lua index 6142d26..c01d800 100644 --- a/api/lua/pinnacle/grpc/client.lua +++ b/api/lua/pinnacle/grpc/client.lua @@ -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 diff --git a/api/lua/pinnacle/grpc/protobuf.lua b/api/lua/pinnacle/grpc/protobuf.lua index 0fdaa38..a6b6bef 100644 --- a/api/lua/pinnacle/grpc/protobuf.lua +++ b/api/lua/pinnacle/grpc/protobuf.lua @@ -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", } diff --git a/api/lua/pinnacle/layout.lua b/api/lua/pinnacle/layout.lua new file mode 100644 index 0000000..7068547 --- /dev/null +++ b/api/lua/pinnacle/layout.lua @@ -0,0 +1,321 @@ +local client = require("pinnacle.grpc.client") +local protobuf = require("pinnacle.grpc.protobuf") + +---The protobuf absolute path prefix +local prefix = "pinnacle.layout." .. client.version .. "." +local service = prefix .. "LayoutService" + +---@type table +---@enum (key) LayoutServiceMethod +local rpc_types = { + Layout = { + response_type = "LayoutResponse", + }, +} + +---Build GrpcRequestParams +---@param method LayoutServiceMethod +---@param data table +---@return GrpcRequestParams +local function build_grpc_request_params(method, data) + local req_type = rpc_types[method].request_type + local resp_type = rpc_types[method].response_type + + ---@type GrpcRequestParams + return { + service = service, + method = method, + request_type = req_type and prefix .. req_type or prefix .. method .. "Request", + response_type = resp_type and prefix .. resp_type, + data = data, + } +end + +---@class LayoutArgs +---@field output OutputHandle +---@field windows WindowHandle[] +---@field tags TagHandle[] +---@field output_width integer +---@field output_height integer + +---@class Builtin +---@field layout fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] + +---@class Builtin.MasterStack : Builtin +---Gaps between windows, in pixels. +--- +---This can be an integer or the table { inner: integer, outer: integer }. +---If it is an integer, all gaps will be that amount of pixels wide. +---If it is a table, `outer` denotes the amount of pixels around the +---edge of the output area that will become a gap, and +---`inner` denotes the amount of pixels around each window that +---will become a gap. +--- +---This means that, for example, `inner = 2` will cause the gap +---width between windows to be 4; 2 around each window. +--- +---Defaults to 4. +---@field gaps integer | { inner: integer, outer: integer } +---The proportion of the output taken up by the master window(s). +--- +---This is a float that will be clamped between 0.1 and 0.9 +---similarly to River. +--- +---Defaults to 0.5. +---@field master_factor number +---The side the master window(s) will be on. +--- +---Defaults to `"left"`. +---@field master_side "left"|"right"|"top"|"bottom" +---How many windows the master side will have. +--- +---Defaults to 1. +---@field master_count integer + +local builtins = { + ---@type Builtin.MasterStack + master_stack = { + gaps = 4, + master_factor = 0.5, + master_side = "left", + master_count = 1, + }, +} + +---@param args LayoutArgs +--- +---@return { x: integer, y: integer, width: integer, height: integer }[] +function builtins.master_stack:layout(args) + local win_count = #args.windows + + if win_count == 0 then + return {} + end + + local width = args.output_width + local height = args.output_height + + ---@type { x: integer, y: integer, width: integer, height: integer }[] + local geos = {} + + local master_factor = math.max(math.min(self.master_factor, 0.9), 0.1) + if win_count <= self.master_count then + master_factor = 1 + end + + local rect = require("pinnacle.util").rectangle.new(0, 0, width, height) + + local master_rect + local stack_rect + + if type(self.gaps) == "number" then + local gaps = self.gaps --[[@as integer]] + + rect = rect:split_at("horizontal", 0, gaps) + rect = rect:split_at("horizontal", height - gaps, gaps) + rect = rect:split_at("vertical", 0, gaps) + rect = rect:split_at("vertical", width - gaps, gaps) + + if self.master_side == "left" then + master_rect, stack_rect = rect:split_at("vertical", math.floor(width * master_factor) - gaps // 2, gaps) + elseif self.master_side == "right" then + stack_rect, master_rect = rect:split_at("vertical", math.floor(width * master_factor) - gaps // 2, gaps) + elseif self.master_side == "top" then + master_rect, stack_rect = rect:split_at("horizontal", math.floor(height * master_factor) - gaps // 2, gaps) + else + stack_rect, master_rect = rect:split_at("horizontal", math.floor(height * master_factor) - gaps // 2, gaps) + end + + if not master_rect then + assert(stack_rect) + master_rect = stack_rect + stack_rect = nil + end + + local master_slice_count + local stack_slice_count = nil + + if win_count > self.master_count then + master_slice_count = self.master_count - 1 + stack_slice_count = win_count - self.master_count - 1 + else + master_slice_count = win_count - 1 + end + + -- layout the master side + if master_slice_count > 0 then + local coord + local len + local axis + + if self.master_side == "left" or self.master_side == "right" then + coord = master_rect.y + len = master_rect.height + axis = "horizontal" + else + coord = master_rect.x + len = master_rect.width + axis = "vertical" + end + + for i = 1, master_slice_count do + local slice_point = coord + math.floor(len * i + 0.5) + slice_point = slice_point - gaps // 2 + local to_push, rest = master_rect:split_at(axis, slice_point, gaps) + table.insert(geos, to_push) + master_rect = rest + end + end + + table.insert(geos, master_rect) + + if stack_slice_count then + assert(stack_rect) + + if stack_slice_count > 0 then + local coord + local len + local axis + if self.master_side == "left" or self.master_side == "right" then + coord = stack_rect.y + len = stack_rect.height / (stack_slice_count + 1) + axis = "horizontal" + else + coord = stack_rect.x + len = stack_rect.width / (stack_slice_count + 1) + axis = "vertical" + end + + for i = 1, stack_slice_count do + local slice_point = coord + math.floor(len * i + 0.5) + slice_point = slice_point - gaps // 2 + local to_push, rest = stack_rect:split_at(axis, slice_point, gaps) + table.insert(geos, to_push) + stack_rect = rest + end + end + + table.insert(geos, stack_rect) + end + + return geos + else + local origin_x = self.gaps.outer + local origin_y = self.gaps.outer + width = width - self.gaps.outer * 2 + height = height - self.gaps.outer * 2 + + if win_count == 1 then + table.insert(geos, { + x = origin_x + self.gaps.inner, + y = origin_y + self.gaps.inner, + width = width - self.gaps.inner * 2, + height = height - self.gaps.inner * 2, + }) + return geos + end + + local h = height / win_count + local y_s = {} + for i = 0, win_count - 1 do + table.insert(y_s, math.floor(i * h + 0.5)) + end + local heights = {} + for i = 1, win_count - 1 do + table.insert(heights, y_s[i + 1] - y_s[i]) + end + table.insert(heights, height - y_s[win_count]) + + for i = 1, win_count do + table.insert(geos, { x = origin_x, y = origin_y + y_s[i], width = width, height = heights[i] }) + end + + for i = 1, #geos do + geos[i].x = geos[i].x + self.gaps.inner + geos[i].y = geos[i].y + self.gaps.inner + geos[i].width = geos[i].width - self.gaps.inner * 2 + geos[i].height = geos[i].height - self.gaps.inner * 2 + end + + return geos + end +end + +---@class Layout +local layout = { + builtins = builtins, +} + +---@param handler LayoutHandler +function layout.set_handler(handler) + client.bidirectional_streaming_request( + build_grpc_request_params("Layout", { + layout = {}, + }), + function(response, stream) + local request_id = response.request_id + local index = handler.index + + ---@diagnostic disable-next-line: invisible + local output_handle = require("pinnacle.output").handle.new(response.output_name) + + ---@diagnostic disable-next-line: invisible + local window_handles = require("pinnacle.window").handle.new_from_table(response.window_ids or {}) + + ---@diagnostic disable-next-line: invisible + local tag_handles = require("pinnacle.tag").handle.new_from_table(response.tag_ids or {}) + + ---@type LayoutArgs + local args = { + output = output_handle, + windows = window_handles, + tags = tag_handles, + output_width = response.output_width, + output_height = response.output_height, + } + + local geos = handler.layouts[index]:layout(args) + + local body = protobuf.encode(".pinnacle.layout.v0alpha1.LayoutRequest", { + geometries = { + request_id = request_id, + geometries = geos, + output_name = response.output_name, + }, + }) + + stream:write_chunk(body, false) + end + ) +end + +---@class LayoutHandlerModule +local layout_handler = {} + +---@class LayoutHandler +---@field index integer +---@field layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[] +local LayoutHandler = {} + +---@param layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[] +---@return LayoutHandler +function layout_handler.new(layouts) + ---@type LayoutHandler + local self = { + index = 1, + layouts = layouts, + } + + setmetatable(self, { __index = LayoutHandler }) + + return self +end + +---@param layouts { layout: fun(self: self, args: LayoutArgs): { x: integer, y: integer, width: integer, height: integer }[] }[] +--- +---@return LayoutHandler +function layout.new_handler(layouts) + return layout_handler.new(layouts) +end + +return layout diff --git a/api/lua/pinnacle/tag.lua b/api/lua/pinnacle/tag.lua index a29b626..bfad68d 100644 --- a/api/lua/pinnacle/tag.lua +++ b/api/lua/pinnacle/tag.lua @@ -250,7 +250,7 @@ end ---layout_cycler.prev(Output.get_by_name("HDMI-1")) ---``` --- ----@param layouts Layout[] +---@param layouts LayoutOld[] --- ---@return LayoutCycler function tag.new_layout_cycler(layouts) @@ -393,7 +393,7 @@ local layout_name_to_code = { corner_bottom_left = 6, corner_bottom_right = 7, } ----@alias Layout +---@alias LayoutOld ---| "master_stack" # One master window on the left with all other windows stacked to the right. ---| "dwindle" # Windows split in half towards the bottom right corner. ---| "spiral" # Windows split in half in a spiral. @@ -412,7 +412,7 @@ local layout_name_to_code = { ---Tag.get("Tag"):set_layout("dwindle") ---``` --- ----@param layout Layout +---@param layout LayoutOld function TagHandle:set_layout(layout) ---@diagnostic disable-next-line: redefined-local local layout = layout_name_to_code[layout] diff --git a/api/lua/pinnacle/util.lua b/api/lua/pinnacle/util.lua index 661f919..ba4e8eb 100644 --- a/api/lua/pinnacle/util.lua +++ b/api/lua/pinnacle/util.lua @@ -77,4 +77,126 @@ function util.batch(requests) return responses end +-- Geometry stuff + +---@class RectangleModule +local rectangle = {} + +---@class Rectangle +---@field x number The x-position of the top-left corner +---@field y number The y-position of the top-left corner +---@field width number The width of the rectangle +---@field height number The height of the rectangle +local Rectangle = {} + +---Split this rectangle along `axis` at `at`. +--- +---If `at2` is specified, the split will chop off a section of this +---rectangle from `at` to `at2`. +--- +---`at` and `at2` are relative to the space this rectangle is in, not +---this rectangle's origin. +--- +---@param axis "horizontal" | "vertical" +---@param at number +---@param thickness? number +--- +---@return Rectangle rect1 The first rectangle. +---@return Rectangle? rect2 The seoond rectangle, if there is one. +function Rectangle:split_at(axis, at, thickness) + ---@diagnostic disable-next-line: redefined-local + local thickness = thickness or 0 + + if axis == "horizontal" then + -- Split is off to the top, at most chop off to `thickness` + if at <= self.y then + local diff = at - self.y + thickness + if diff > 0 then + self.y = self.y + diff + self.height = self.height - diff + end + + return self + -- Split is to the bottom, then do nothing + elseif at >= self.y + self.height then + return self + -- Split only chops bottom off + elseif at + thickness >= self.y + self.height then + local diff = (self.y + self.height) - at + self.height = self.height - diff + return self + -- Do a split + else + local x = self.x + local top_y = self.y + local width = self.width + local top_height = at - self.y + + local bot_y = at + thickness + local bot_height = self.y + self.height - at - thickness + + local rect1 = rectangle.new(x, top_y, width, top_height) + local rect2 = rectangle.new(x, bot_y, width, bot_height) + + return rect1, rect2 + end + elseif axis == "vertical" then + -- Split is off to the left, at most chop off to `thickness` + if at <= self.x then + local diff = at - self.x + thickness + if diff > 0 then + self.x = self.x + diff + self.width = self.width - diff + end + + return self + -- Split is to the right, then do nothing + elseif at >= self.x + self.width then + return self + -- Split only chops bottom off + elseif at + thickness >= self.x + self.width then + local diff = (self.x + self.width) - at + self.width = self.width - diff + return self + -- Do a split + else + local left_x = self.x + local y = self.y + local left_width = at - self.x + local height = self.height + + local right_x = at + thickness + local right_width = self.x + self.width - at - thickness + + local rect1 = rectangle.new(left_x, y, left_width, height) + local rect2 = rectangle.new(right_x, y, right_width, height) + + return rect1, rect2 + end + end -- TODO: handle error if neither + + os.exit(1) +end + +---@return Rectangle +function rectangle.new(x, y, width, height) + ---@type Rectangle + local self = { + x = x, + y = y, + width = width, + height = height, + } + setmetatable(self, { __index = Rectangle }) + return self +end + +local r = rectangle.new(0, 0, 100, 100) +local r1, r2 = r:split_at("horizontal", 96, 4) + +print(require("inspect")(r1)) +print(require("inspect")(r2)) + +util.rectangle = rectangle + return util diff --git a/api/lua/pinnacle/window.lua b/api/lua/pinnacle/window.lua index 8fae79a..c7c49bb 100644 --- a/api/lua/pinnacle/window.lua +++ b/api/lua/pinnacle/window.lua @@ -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 diff --git a/api/protocol/pinnacle/layout/v0alpha1/layout.proto b/api/protocol/pinnacle/layout/v0alpha1/layout.proto index 393bebd..bf0905f 100644 --- a/api/protocol/pinnacle/layout/v0alpha1/layout.proto +++ b/api/protocol/pinnacle/layout/v0alpha1/layout.proto @@ -7,20 +7,22 @@ import "pinnacle/v0alpha1/pinnacle.proto"; // Love how the response is the request and the request is the response message LayoutRequest { - // Respond to a layout request from the compositor. + // A response to a layout request from the compositor. message Geometries { // The id of the request this layout response is responding to. // // Responding with a request_id that has already been responded to // or that doesn't exist will return an error. optional uint32 request_id = 1; + // The output this request is responding to. + optional string output_name = 2; // Target geometries of all windows being laid out. // // Responding with a different number of geometries than // requested windows will return an error. - repeated .pinnacle.v0alpha1.Geometry geometries = 2; + repeated .pinnacle.v0alpha1.Geometry geometries = 3; } - // Request a layout explicitly. + // An explicit layout request. message ExplicitLayout {} oneof body { diff --git a/pinnacle-api-defs/build.rs b/pinnacle-api-defs/build.rs index 62d8aaa..adae15b 100644 --- a/pinnacle-api-defs/build.rs +++ b/pinnacle-api-defs/build.rs @@ -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"); diff --git a/pinnacle-api-defs/src/lib.rs b/pinnacle-api-defs/src/lib.rs index a1abc55..30ef083 100644 --- a/pinnacle-api-defs/src/lib.rs +++ b/pinnacle-api-defs/src/lib.rs @@ -68,6 +68,12 @@ pub mod pinnacle { ); } } + + pub mod layout { + pub mod v0alpha1 { + tonic::include_proto!("pinnacle.layout.v0alpha1"); + } + } } pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("pinnacle"); diff --git a/src/api.rs b/src/api.rs index 2fa3895..61dc44c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,3 +1,4 @@ +pub mod layout; pub mod signal; pub mod window; diff --git a/src/api/layout.rs b/src/api/layout.rs new file mode 100644 index 0000000..c033d0d --- /dev/null +++ b/src/api/layout.rs @@ -0,0 +1,56 @@ +use pinnacle_api_defs::pinnacle::layout::v0alpha1::{ + layout_request::{self, ExplicitLayout}, + layout_service_server, LayoutRequest, LayoutResponse, +}; +use tonic::{Request, Response, Status, Streaming}; + +use super::{run_bidirectional_streaming, ResponseStream, StateFnSender}; + +pub struct LayoutService { + sender: StateFnSender, +} + +impl LayoutService { + pub fn new(sender: StateFnSender) -> Self { + Self { sender } + } +} + +#[tonic::async_trait] +impl layout_service_server::LayoutService for LayoutService { + type LayoutStream = ResponseStream; + + async fn layout( + &self, + request: Request>, + ) -> Result, Status> { + let in_stream = request.into_inner(); + + run_bidirectional_streaming( + self.sender.clone(), + in_stream, + |state, request| match request { + Ok(request) => { + if let Some(body) = request.body { + match body { + layout_request::Body::Geometries(geos) => { + // dbg!(&geos); + if let Err(err) = state.apply_layout(geos) { + // TODO: send a Status and handle the error client side + tracing::error!("{err}") + } + } + layout_request::Body::Layout(ExplicitLayout {}) => { + // TODO: state.layout_request(output, windows) + } + } + } + } + Err(_) => (), + }, + |state, sender, join_handle| { + state.layout_state.layout_request_sender = Some(sender); + }, + ) + } +} diff --git a/src/config.rs b/src/config.rs index 66a17c8..fbb12ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use crate::{ api::{ - signal::SignalService, window::WindowService, InputService, OutputService, PinnacleService, - ProcessService, TagService, + layout::LayoutService, signal::SignalService, window::WindowService, InputService, + OutputService, PinnacleService, ProcessService, TagService, }, input::ModifierMask, output::OutputName, @@ -17,6 +17,7 @@ use std::{ use anyhow::Context; use pinnacle_api_defs::pinnacle::{ input::v0alpha1::input_service_server::InputServiceServer, + layout::v0alpha1::layout_service_server::LayoutServiceServer, output::v0alpha1::output_service_server::OutputServiceServer, process::v0alpha1::process_service_server::ProcessServiceServer, signal::v0alpha1::signal_service_server::SignalServiceServer, @@ -477,6 +478,7 @@ impl State { let output_service = OutputService::new(grpc_sender.clone()); let window_service = WindowService::new(grpc_sender.clone()); let signal_service = SignalService::new(grpc_sender.clone()); + let layout_service = LayoutService::new(grpc_sender.clone()); let refl_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(pinnacle_api_defs::FILE_DESCRIPTOR_SET) @@ -495,7 +497,8 @@ impl State { .add_service(TagServiceServer::new(tag_service)) .add_service(OutputServiceServer::new(output_service)) .add_service(WindowServiceServer::new(window_service)) - .add_service(SignalServiceServer::new(signal_service)); + .add_service(SignalServiceServer::new(signal_service)) + .add_service(LayoutServiceServer::new(layout_service)); match self.xdisplay.as_ref() { Some(_) => { diff --git a/src/layout.rs b/src/layout.rs index 1011798..3ed6d9c 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,13 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later +use std::collections::{HashMap, HashSet}; + +use pinnacle_api_defs::pinnacle::layout::v0alpha1::{layout_request::Geometries, LayoutResponse}; use smithay::{ desktop::{layer_map_for_output, WindowSurface}, output::Output, utils::{Logical, Point, Rectangle, Serial, Size}, wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, }; +use tokio::sync::mpsc::UnboundedSender; +use tonic::Status; +use tracing::error; use crate::{ + output::OutputName, state::{State, WithState}, window::{ window_state::{FloatingOrTiled, FullscreenOrMaximized}, @@ -34,7 +41,7 @@ impl State { }) else { // TODO: maybe default to something like 800x800 like in anvil so people still see // | windows open - tracing::error!("Failed to get output geometry"); + error!("Failed to get output geometry"); return; }; @@ -49,6 +56,113 @@ impl State { } } + pub fn update_windows_with_geometries( + &mut self, + output: &Output, + geometries: Vec>, + ) { + let windows_on_foc_tags = output.with_state(|state| { + let focused_tags = state.focused_tags().collect::>(); + 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::>() + }); + + let tiled_windows = windows_on_foc_tags + .iter() + .filter(|win| { + win.with_state(|state| { + state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither() + }) + }) + .cloned(); + + let output_geo = self.space.output_geometry(output).expect("no output geo"); + + for (win, geo) in tiled_windows.zip(geometries.into_iter().map(|mut geo| { + geo.loc += output_geo.loc; + geo + })) { + win.change_geometry(geo); + } + + for window in windows_on_foc_tags.iter() { + match window.with_state(|state| state.fullscreen_or_maximized) { + FullscreenOrMaximized::Fullscreen => { + window.change_geometry(output_geo); + } + FullscreenOrMaximized::Maximized => { + let map = layer_map_for_output(output); + let geo = if map.layers().next().is_none() { + // INFO: Sometimes the exclusive zone is some weird number that doesn't match the + // | output res, even when there are no layer surfaces mapped. In this case, we + // | just return the output geometry. + output_geo + } else { + let zone = map.non_exclusive_zone(); + tracing::debug!("non_exclusive_zone is {zone:?}"); + Rectangle::from_loc_and_size(output_geo.loc + zone.loc, zone.size) + }; + window.change_geometry(geo); + } + FullscreenOrMaximized::Neither => { + if let FloatingOrTiled::Floating(rect) = + window.with_state(|state| state.floating_or_tiled) + { + window.change_geometry(rect); + } + } + } + } + + let mut pending_wins = Vec::<(WindowElement, Serial)>::new(); + let mut non_pending_wins = Vec::<(Point, WindowElement)>::new(); + + for win in windows_on_foc_tags.iter() { + if win.with_state(|state| state.target_loc.is_some()) { + match win.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + let pending = compositor::with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .lock() + .expect("Failed to lock Mutex") + .has_pending_changes() + }); + + if pending { + pending_wins.push((win.clone(), toplevel.send_configure())) + } else { + let loc = win.with_state_mut(|state| state.target_loc.take()); + if let Some(loc) = loc { + non_pending_wins.push((loc, win.clone())); + } + } + } + WindowSurface::X11(_) => { + let loc = win.with_state_mut(|state| state.target_loc.take()); + if let Some(loc) = loc { + self.space.map_element(win.clone(), loc, false); + } + } + } + } + } + + for (loc, window) in non_pending_wins { + self.space.map_element(window, loc, false); + } + + self.fixup_z_layering(); + } + pub fn update_windows(&mut self, output: &Output) { let Some(layout) = output.with_state(|state| state.focused_tags().next().map(|tag| tag.layout())) @@ -78,6 +192,10 @@ impl State { .cloned() .collect::>(); + self.layout_request(output.clone(), tiled_windows); + + return; + self.tile_windows(output, tiled_windows, layout); let output_geo = self.space.output_geometry(output).expect("no output geo"); @@ -468,3 +586,137 @@ impl State { } } } + +// New layout system stuff + +/// A monotonically increasing identifier for layout requests. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct LayoutRequestId(pub u32); + +#[derive(Debug, Default)] +pub struct LayoutState { + pub layout_request_sender: Option>>, + id_maps: HashMap, + pending_requests: HashMap)>>, + old_requests: HashMap>, +} + +impl State { + pub fn layout_request(&mut self, output: Output, windows: Vec) { + let Some(sender) = self.layout_state.layout_request_sender.as_ref() else { + error!("Layout requested but no client has connected to the layout service"); + return; + }; + + let Some((output_width, output_height)) = self + .space + .output_geometry(&output) + .map(|geo| (geo.size.w, geo.size.h)) + else { + error!("Called `output_geometry` on an unmapped output"); + return; + }; + + let window_ids = windows + .iter() + .map(|win| win.with_state(|state| state.id.0)) + .collect::>(); + + let tag_ids = + output.with_state(|state| state.focused_tags().map(|tag| tag.id().0).collect()); + + let id = self + .layout_state + .id_maps + .entry(output.clone()) + .or_insert(LayoutRequestId(0)); + + self.layout_state + .pending_requests + .entry(output.clone()) + .or_default() + .push((*id, windows)); + + // TODO: error + let _ = sender.send(Ok(LayoutResponse { + request_id: Some(id.0), + output_name: Some(output.name()), + window_ids, + tag_ids, + output_width: Some(output_width as u32), + output_height: Some(output_height as u32), + })); + + *id = LayoutRequestId(id.0 + 1); + } + + pub fn apply_layout(&mut self, geometries: Geometries) -> anyhow::Result<()> { + tracing::info!("Applying layout"); + + let Geometries { + request_id: Some(request_id), + output_name: Some(output_name), + geometries, + } = geometries + else { + anyhow::bail!("One or more `geometries` fields were None"); + }; + + let request_id = LayoutRequestId(request_id); + let Some(output) = OutputName(output_name).output(self) else { + anyhow::bail!("Output was invalid"); + }; + + let old_requests = self + .layout_state + .old_requests + .entry(output.clone()) + .or_default(); + + if old_requests.contains(&request_id) { + anyhow::bail!("Attempted to layout but the request was already fulfilled"); + } + + let pending = self + .layout_state + .pending_requests + .entry(output.clone()) + .or_default(); + + let Some(latest) = pending.last().map(|(id, _)| *id) else { + anyhow::bail!("Attempted to layout but the request was nonexistent A"); + }; + + if dbg!(latest) == dbg!(request_id) { + pending.pop(); + } else if let Some(pos) = pending + .split_last() + .and_then(|(_, rest)| rest.iter().position(|(id, _)| id == &request_id)) + { + // Ignore stale requests + old_requests.insert(request_id); + pending.remove(pos); + return Ok(()); + } else { + anyhow::bail!("Attempted to layout but the request was nonexistent B"); + }; + + let geometries = geometries + .into_iter() + .map(|geo| { + Some(Rectangle::::from_loc_and_size( + (geo.x?, geo.y?), + (geo.width?, geo.height?), + )) + }) + .collect::>>(); + + 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(()) + } +} diff --git a/src/state.rs b/src/state.rs index a69e3c9..cb5b5d5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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)