mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-14 08:01:14 +01:00
Merge pull request #201 from pinnacle-comp/api_enhancements
Add various API enhancements
This commit is contained in:
commit
9af11d7b6a
34 changed files with 2920 additions and 463 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1767,6 +1767,7 @@ dependencies = [
|
|||
"image",
|
||||
"nix",
|
||||
"pinnacle",
|
||||
"pinnacle-api",
|
||||
"pinnacle-api-defs",
|
||||
"prost",
|
||||
"serde",
|
||||
|
@ -1796,6 +1797,7 @@ dependencies = [
|
|||
name = "pinnacle-api"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"futures",
|
||||
"num_enum",
|
||||
"pinnacle-api-defs",
|
||||
|
|
|
@ -20,6 +20,7 @@ pinnacle-api-defs = { path = "./pinnacle-api-defs" }
|
|||
# Misc.
|
||||
xkbcommon = "0.7.0"
|
||||
xdg = "2.5.2"
|
||||
bitflags = "2.5.0"
|
||||
|
||||
########################################################################yo😎###########
|
||||
|
||||
|
@ -59,7 +60,7 @@ tokio-stream = { workspace = true }
|
|||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
cliclack = "0.2.5"
|
||||
# Misc.
|
||||
bitflags = "2.5.0"
|
||||
bitflags = { workspace = true }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
toml = "0.8.12"
|
||||
shellexpand = { version = "3.1.0", features = ["path"] }
|
||||
|
@ -109,6 +110,7 @@ temp-env = "0.3.6"
|
|||
tempfile = "3.10.1"
|
||||
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
|
||||
pinnacle = { path = ".", features = ["testing"] }
|
||||
pinnacle-api = { path = "./api/rust" }
|
||||
|
||||
[features]
|
||||
testing = [
|
||||
|
|
|
@ -36,6 +36,11 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
Pinnacle.quit()
|
||||
end)
|
||||
|
||||
-- mod_key + alt + r = Reload config
|
||||
Input.keybind({ mod_key, "alt" }, "r", function()
|
||||
Pinnacle.reload_config()
|
||||
end)
|
||||
|
||||
-- mod_key + alt + c = Close window
|
||||
Input.keybind({ mod_key, "alt" }, "c", function()
|
||||
local focused = Window.get_focused()
|
||||
|
@ -76,18 +81,26 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
end
|
||||
end)
|
||||
|
||||
--------------------
|
||||
-- Tags --
|
||||
--------------------
|
||||
----------------------
|
||||
-- Tags and Outputs --
|
||||
----------------------
|
||||
|
||||
local tag_names = { "1", "2", "3", "4", "5" }
|
||||
|
||||
-- `connect_for_all` is useful for performing setup on every monitor you have.
|
||||
-- Here, we add tags with names 1-5 and set tag 1 as active.
|
||||
Output.connect_for_all(function(op)
|
||||
local tags = Tag.add(op, tag_names)
|
||||
tags[1]:set_active(true)
|
||||
end)
|
||||
-- Setup outputs.
|
||||
--
|
||||
-- `Output.setup` allows you to declare things like mode, scale, and tags for outputs.
|
||||
-- Here we give all outputs tags 1 through 5.
|
||||
Output.setup({
|
||||
-- "*" matches all outputs
|
||||
["*"] = { tags = tag_names },
|
||||
})
|
||||
|
||||
-- If you want to declare output locations as well, you can use `Output.setup_locs`.
|
||||
-- This will additionally allow you to recalculate output locations on signals like
|
||||
-- output connect, disconnect, and resize.
|
||||
--
|
||||
-- Read the admittedly scuffed docs for more.
|
||||
|
||||
-- Tag keybinds
|
||||
for _, tag_name in ipairs(tag_names) do
|
||||
|
@ -246,6 +259,10 @@ require("pinnacle").setup(function(Pinnacle)
|
|||
end
|
||||
end)
|
||||
|
||||
Input.set_libinput_settings({
|
||||
tap = true,
|
||||
})
|
||||
|
||||
-- Enable sloppy focus
|
||||
Window.connect_signal({
|
||||
pointer_enter = function(window)
|
||||
|
|
|
@ -37,6 +37,16 @@ function pinnacle.quit()
|
|||
})
|
||||
end
|
||||
|
||||
---Reload the active config.
|
||||
function pinnacle.reload_config()
|
||||
client.unary_request({
|
||||
service = "pinnacle.v0alpha1.PinnacleService",
|
||||
method = "ReloadConfig",
|
||||
request_type = "pinnacle.v0alpha1.ReloadConfigRequest",
|
||||
data = {},
|
||||
})
|
||||
end
|
||||
|
||||
---Setup a Pinnacle config.
|
||||
---
|
||||
---You must pass in a function that takes in the `Pinnacle` table. This table is how you'll access the other config modules.
|
||||
|
|
|
@ -14,6 +14,7 @@ local rpc_types = {
|
|||
SetLocation = {},
|
||||
SetMode = {},
|
||||
SetScale = {},
|
||||
SetTransform = {},
|
||||
ConnectForAll = {
|
||||
response_type = "ConnectForAllResponse",
|
||||
},
|
||||
|
@ -166,12 +167,465 @@ function output.connect_for_all(callback)
|
|||
})
|
||||
end
|
||||
|
||||
---@param id_str string
|
||||
---@param op OutputHandle
|
||||
---
|
||||
---@return boolean
|
||||
local function output_id_matches(id_str, op)
|
||||
if id_str:match("^serial:") then
|
||||
local serial = tonumber(id_str:sub(8))
|
||||
return serial and serial == op:serial() or false
|
||||
else
|
||||
return id_str == op.name
|
||||
end
|
||||
end
|
||||
|
||||
---@class OutputSetup
|
||||
---@field filter (fun(output: OutputHandle): boolean)? -- A filter for wildcard matches that should return true if this setup should apply to the passed in output.
|
||||
---@field mode Mode? -- Makes this setup apply the given mode to outputs.
|
||||
---@field scale number? -- Makes this setup apply the given scale to outputs.
|
||||
---@field tags string[]? -- Makes this setup add tags with the given name to outputs.
|
||||
---@field transform Transform? -- Makes this setup applt the given transform to outputs.
|
||||
|
||||
---Declaratively setup outputs.
|
||||
---
|
||||
---`Output.setup` allows you to specify output properties that will be applied immediately and
|
||||
---on output connection. These include mode, scale, tags, and more.
|
||||
---
|
||||
---`setups` is a table of output identifier strings to `OutputSetup`s.
|
||||
---
|
||||
---### Keys
|
||||
---
|
||||
---Keys attempt to match outputs.
|
||||
---
|
||||
---Wildcard keys (`"*"`) will match all outputs. You can additionally filter these outputs
|
||||
---by setting a `filter` function in the setup that returns true if it should apply to the output.
|
||||
---(See the example.)
|
||||
---
|
||||
---Otherwise, keys will attempt to match the exact name of an output.
|
||||
---
|
||||
---Use "serial:<number>" to match outputs by their EDID serial. For example, "serial:143256".
|
||||
---Note that not all displays have EDID serials. Also, serials are not guaranteed to be unique.
|
||||
---If you're unlucky enough to have two displays with the same serial, you'll have to use their names
|
||||
---or filter with wildcards instead.
|
||||
---
|
||||
---### Setups
|
||||
---
|
||||
---If an output is matched, the corresponding `OutputSetup` entry will be applied to it.
|
||||
---Any given `tags` will be added, and things like `transform`s, `scale`s, and `mode`s will be set.
|
||||
---
|
||||
---### Ordering setups
|
||||
---
|
||||
---You may need to specify multiple wildcard matches for different setup applications.
|
||||
---You can't just add another key of `"*"`, because that would overwrite the old `"*"`.
|
||||
---In this case, you can order setups by prepending `n:` to the key, where n is an ordering number.
|
||||
---`n` should be between `1` and `#setups`. Setting higher orders without setting lower ones
|
||||
---will cause entries without orders to fill up lower numbers in an arbitrary order. Setting
|
||||
---orders above `#setups` may cause their entries to not apply.
|
||||
---
|
||||
---
|
||||
---### Example
|
||||
---```lua
|
||||
---Output.setup({
|
||||
--- -- Give all outputs tags 1 through 5
|
||||
--- ["1:*"] = {
|
||||
--- tags = { "1", "2", "3", "4", "5" },
|
||||
--- },
|
||||
--- -- Give outputs with a preferred mode of 4K a scale of 2.0
|
||||
--- ["2:*"] = {
|
||||
--- filter = function(op)
|
||||
--- return op:preferred_mode().pixel_height == 2160
|
||||
--- end,
|
||||
--- scale = 2.0,
|
||||
--- },
|
||||
--- -- Additionally give eDP-1 tags 6 and 7
|
||||
--- ["eDP-1"] = {
|
||||
--- tags = { "6", "7" },
|
||||
--- },
|
||||
--- -- Match an output by its EDID serial number
|
||||
--- ["serial:235987"] = { ... }
|
||||
---})
|
||||
---```
|
||||
---
|
||||
---@param setups table<string, OutputSetup>
|
||||
function output.setup(setups)
|
||||
---@type { [1]: string, setup: OutputSetup }[]
|
||||
local op_setups = {}
|
||||
|
||||
local setup_len = 0
|
||||
|
||||
-- Index entries with an index
|
||||
for op_id, op_setup in pairs(setups) do
|
||||
setup_len = setup_len + 1
|
||||
|
||||
---@type string|nil
|
||||
if op_id:match("^%d+:") then
|
||||
---@type string
|
||||
local index = op_id:match("^%d+")
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local op_id = op_id:sub(index:len() + 2)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local index = tonumber(index)
|
||||
|
||||
---@cast index number
|
||||
|
||||
op_setups[index] = { op_id, setup = op_setup }
|
||||
end
|
||||
end
|
||||
|
||||
-- Insert *s first
|
||||
for op_id, op_setup in pairs(setups) do
|
||||
if op_id:match("^*$") then
|
||||
-- Fill up holes if there are any
|
||||
for i = 1, setup_len do
|
||||
if not op_setups[i] then
|
||||
op_setups[i] = { op_id, setup = op_setup }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Insert rest of the entries
|
||||
for op_id, op_setup in pairs(setups) do
|
||||
if not op_id:match("^%d+:") and op_id ~= "*" then
|
||||
-- Fill up holes if there are any
|
||||
for i = 1, setup_len do
|
||||
if not op_setups[i] then
|
||||
op_setups[i] = { op_id, setup = op_setup }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param op OutputHandle
|
||||
local function apply_setups(op)
|
||||
for _, op_setup in ipairs(op_setups) do
|
||||
if output_id_matches(op_setup[1], op) or op_setup[1] == "*" then
|
||||
local setup = op_setup.setup
|
||||
|
||||
if setup.filter and not setup.filter(op) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if setup.mode then
|
||||
op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz)
|
||||
end
|
||||
if setup.scale then
|
||||
op:set_scale(setup.scale)
|
||||
end
|
||||
if setup.tags then
|
||||
require("pinnacle.tag").add(op, setup.tags)
|
||||
end
|
||||
if setup.transform then
|
||||
op:set_transform(setup.transform)
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
local tags = op:tags() or {}
|
||||
if tags[1] then
|
||||
tags[1]:set_active(true)
|
||||
end
|
||||
end
|
||||
|
||||
output.connect_for_all(function(op)
|
||||
apply_setups(op)
|
||||
end)
|
||||
end
|
||||
|
||||
---@alias OutputLoc
|
||||
---| { [1]: integer, [2]: integer } -- A specific point
|
||||
---| { [1]: string, [2]: Alignment } -- A location relative to another output
|
||||
|
||||
---@alias UpdateLocsOn
|
||||
---| "connect" -- Update output locations on output connect
|
||||
---| "disconnect" -- Update output locations on output disconnect
|
||||
---| "resize" -- Update output locations on output resize
|
||||
|
||||
---Setup locations for outputs.
|
||||
---
|
||||
---This function lets you declare positions for outputs, either as a specific point in the global
|
||||
---space or relative to another output.
|
||||
---
|
||||
---### Choosing when to recompute output positions
|
||||
---
|
||||
---`update_locs_on` specifies when output positions should be recomputed. It can be `"all"`, signaling you
|
||||
---want positions to update on all of output connect, disconnect, and resize, or it can be a table
|
||||
---containing `"connect"`, `"disconnect"`, and/or `"resize"`.
|
||||
---
|
||||
---### Specifying locations
|
||||
---
|
||||
---`locs` should be a table of output identifiers to locations.
|
||||
---
|
||||
---#### Output identifiers
|
||||
---
|
||||
---Keys for `locs` should be output identifiers. These are strings of
|
||||
---the name of the output, for example "eDP-1" or "HDMI-A-1".
|
||||
---Additionally, if you want to match the EDID serial of an output,
|
||||
---prepend the serial with "serial:", for example "serial:174652".
|
||||
---You can find this by doing `get-edid | edid-decode`.
|
||||
---
|
||||
---#### Fallback relative-tos
|
||||
---
|
||||
---Sometimes you have an output with a relative location, but the output
|
||||
---it's relative to isn't connected. In this case you can specify an
|
||||
---order that locations will be placed by prepending "n:" to the key.
|
||||
---For example, "4:HDMI-1" will be applied before "5:HDMI-1", allowing
|
||||
---you to specify more than one relative output. The first connected
|
||||
---relative output will be chosen for placement. See the example below.
|
||||
---
|
||||
---### Example
|
||||
---```lua
|
||||
--- -- vvvvv Relayout on output connect, disconnect, and resize
|
||||
---Output.setup_locs("all", {
|
||||
--- -- Anchor eDP-1 to (0, 0) so we can place other outputs relative to it
|
||||
--- ["eDP-1"] = { 0, 0 },
|
||||
--- -- Place HDMI-A-1 below it centered
|
||||
--- ["HDMI-A-1"] = { "eDP-1", "bottom_align_center" },
|
||||
--- -- Place HDMI-A-2 below HDMI-A-1.
|
||||
--- ["3:HDMI-A-2"] = { "HDMI-A-1", "bottom_align_center" },
|
||||
--- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing it below eDP-1 instead.
|
||||
--- ["4:HDMI-A-2"] = { "eDP-1", "bottom_align_center" },
|
||||
---
|
||||
--- -- Note that the last two have a number followed by a colon. This dictates the order of application.
|
||||
--- -- Because Lua tables with string keys don't index by declaration order, this is needed to specify that.
|
||||
--- -- You can also put a "1:" and "2:" in front of "eDP-1" and "HDMI-A-1" if you want to be explicit
|
||||
--- -- about their ordering.
|
||||
--- --
|
||||
--- -- Just note that orders must be from 1 to the length of the array. Entries without an order
|
||||
--- -- will be filled in from 1 upwards, taking any open slots. Entries with orders above
|
||||
--- -- #locs may not be applied.
|
||||
---})
|
||||
---
|
||||
--- -- Only relayout on output connect and resize
|
||||
---Output.setup_locs({ "connect", "resize" }, { ... })
|
||||
---
|
||||
--- -- Use EDID serials for identification.
|
||||
--- -- You can run
|
||||
--- -- require("pinnacle").run(function(Pinnacle)
|
||||
--- -- print(Pinnacle.output.get_focused():serial())
|
||||
--- -- end)
|
||||
--- -- in a Lua repl to find the EDID serial of the focused output.
|
||||
---Output.setup_locs("all" {
|
||||
--- ["serial:139487"] = { ... },
|
||||
---})
|
||||
---```
|
||||
---
|
||||
---@param update_locs_on (UpdateLocsOn)[] | "all"
|
||||
---@param locs table<string, OutputLoc>
|
||||
function output.setup_locs(update_locs_on, locs)
|
||||
---@type { [1]: string, loc: OutputLoc }[]
|
||||
local setups = {}
|
||||
|
||||
local setup_len = 0
|
||||
|
||||
-- Index entries with an index
|
||||
for op_id, op_loc in pairs(locs) do
|
||||
setup_len = setup_len + 1
|
||||
|
||||
---@type string|nil
|
||||
if op_id:match("^%d+:") then
|
||||
---@type string
|
||||
local index = op_id:match("^%d+")
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local op_id = op_id:sub(index:len() + 2)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local index = tonumber(index)
|
||||
|
||||
---@cast index number
|
||||
|
||||
setups[index] = { op_id, loc = op_loc }
|
||||
end
|
||||
end
|
||||
|
||||
-- Insert rest of the entries
|
||||
for op_id, op_loc in pairs(locs) do
|
||||
if not op_id:match("^%d+:") then
|
||||
-- Fill up holes if there are any
|
||||
for i = 1, setup_len do
|
||||
if not setups[i] then
|
||||
setups[i] = { op_id, loc = op_loc }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function layout_outputs()
|
||||
local outputs = output.get_all()
|
||||
|
||||
---@type OutputHandle[]
|
||||
local placed_outputs = {}
|
||||
|
||||
local rightmost_output = {
|
||||
output = nil,
|
||||
x = nil,
|
||||
}
|
||||
|
||||
-- Place outputs with a specified location first
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
for _, setup in ipairs(setups) do
|
||||
for _, op in ipairs(outputs) do
|
||||
if output_id_matches(setup[1], op) then
|
||||
if type(setup.loc[1]) == "number" then
|
||||
local loc = { x = setup.loc[1], y = setup.loc[2] }
|
||||
op:set_location(loc)
|
||||
table.insert(placed_outputs, op)
|
||||
|
||||
local props = op:props()
|
||||
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
|
||||
rightmost_output.output = op
|
||||
rightmost_output.x = props.x + props.logical_width
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Place outputs that are relative to other outputs
|
||||
local function next_output_with_relative_to()
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
for _, setup in ipairs(setups) do
|
||||
for _, op in ipairs(outputs) do
|
||||
for _, placed_op in ipairs(placed_outputs) do
|
||||
if placed_op.name == op.name then
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
if not output_id_matches(setup[1], op) or type(setup.loc[1]) == "number" then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local relative_to_name = setup.loc[1]
|
||||
local alignment = setup.loc[2]
|
||||
for _, placed_op in ipairs(placed_outputs) do
|
||||
if placed_op.name == relative_to_name then
|
||||
return op, placed_op, alignment
|
||||
end
|
||||
end
|
||||
|
||||
goto continue_outer
|
||||
|
||||
::continue::
|
||||
end
|
||||
::continue_outer::
|
||||
end
|
||||
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
while true do
|
||||
local op, relative_to, alignment = next_output_with_relative_to()
|
||||
if not op then
|
||||
break
|
||||
end
|
||||
|
||||
---@cast relative_to OutputHandle
|
||||
---@cast alignment Alignment
|
||||
|
||||
op:set_loc_adj_to(relative_to, alignment)
|
||||
table.insert(placed_outputs, op)
|
||||
|
||||
local props = op:props()
|
||||
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
|
||||
rightmost_output.output = op
|
||||
rightmost_output.x = props.x + props.logical_width
|
||||
end
|
||||
end
|
||||
|
||||
-- Place still-not-placed outputs
|
||||
for _, op in ipairs(outputs) do
|
||||
for _, placed_op in ipairs(placed_outputs) do
|
||||
if placed_op.name == op.name then
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
if not rightmost_output.output then
|
||||
op:set_location({ x = 0, y = 0 })
|
||||
else
|
||||
op:set_loc_adj_to(rightmost_output.output, "right_align_top")
|
||||
end
|
||||
|
||||
local props = op:props()
|
||||
|
||||
rightmost_output.output = op
|
||||
rightmost_output.x = props.x
|
||||
|
||||
table.insert(placed_outputs, op)
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
layout_outputs()
|
||||
|
||||
local layout_on_connect = false
|
||||
local layout_on_disconnect = false
|
||||
local layout_on_resize = false
|
||||
|
||||
if update_locs_on == "all" then
|
||||
layout_on_connect = true
|
||||
layout_on_disconnect = true
|
||||
layout_on_resize = true
|
||||
else
|
||||
---@cast update_locs_on UpdateLocsOn[]
|
||||
|
||||
for _, update_on in ipairs(update_locs_on) do
|
||||
if update_on == "connect" then
|
||||
layout_on_connect = true
|
||||
elseif update_on == "disconnect" then
|
||||
layout_on_disconnect = true
|
||||
elseif update_on == "resize" then
|
||||
layout_on_resize = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if layout_on_connect then
|
||||
-- FIXME: This currently does not duplicate tags because the connect signal does not fire for
|
||||
-- | previously connected outputs. However, this is unintended behavior, so fix this when you fix that.
|
||||
output.connect_signal({
|
||||
connect = function(_)
|
||||
layout_outputs()
|
||||
end,
|
||||
})
|
||||
end
|
||||
if layout_on_disconnect then
|
||||
output.connect_signal({
|
||||
disconnect = function(_)
|
||||
layout_outputs()
|
||||
end,
|
||||
})
|
||||
end
|
||||
if layout_on_resize then
|
||||
output.connect_signal({
|
||||
resize = function(_)
|
||||
layout_outputs()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<string, SignalServiceMethod>
|
||||
local signal_name_to_SignalName = {
|
||||
connect = "OutputConnect",
|
||||
disconnect = "OutputDisconnect",
|
||||
resize = "OutputResize",
|
||||
move = "OutputMove",
|
||||
}
|
||||
|
||||
---@class OutputSignal Signals related to output events.
|
||||
---@field connect fun(output: OutputHandle)? An output was connected. FIXME: This currently does not fire for outputs that have been previously connected and disconnected.
|
||||
---@field disconnect fun(output: OutputHandle)? An output was disconnected.
|
||||
---@field resize fun(output: OutputHandle, logical_width: integer, logical_height: integer)? An output's logical size changed.
|
||||
---@field move fun(output: OutputHandle, x: integer, y: integer)? An output moved.
|
||||
|
||||
---Connect to an output signal.
|
||||
---
|
||||
|
@ -411,6 +865,41 @@ function OutputHandle:decrease_scale(decrease_by)
|
|||
client.unary_request(build_grpc_request_params("SetScale", { output_name = self.name, relative = -decrease_by }))
|
||||
end
|
||||
|
||||
---@enum (key) Transform
|
||||
local transform_name_to_code = {
|
||||
normal = 1,
|
||||
["90"] = 2,
|
||||
["180"] = 3,
|
||||
["270"] = 4,
|
||||
flipped = 5,
|
||||
flipped_90 = 6,
|
||||
flipped_180 = 7,
|
||||
flipped_270 = 8,
|
||||
}
|
||||
|
||||
local transform_code_to_name = {
|
||||
[1] = "normal",
|
||||
[2] = "90",
|
||||
[3] = "180",
|
||||
[4] = "270",
|
||||
[5] = "flipped",
|
||||
[6] = "flipped_90",
|
||||
[7] = "flipped_180",
|
||||
[8] = "flipped_270",
|
||||
}
|
||||
|
||||
---Set this output's transform.
|
||||
---
|
||||
---@param transform Transform
|
||||
function OutputHandle:set_transform(transform)
|
||||
client.unary_request(
|
||||
build_grpc_request_params(
|
||||
"SetTransform",
|
||||
{ output_name = self.name, transform = transform_name_to_code[transform] }
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
---@class Mode
|
||||
---@field pixel_width integer
|
||||
---@field pixel_height integer
|
||||
|
@ -431,6 +920,8 @@ end
|
|||
---@field focused boolean?
|
||||
---@field tags TagHandle[]
|
||||
---@field scale number?
|
||||
---@field transform Transform?
|
||||
---@field serial integer?
|
||||
|
||||
---Get all properties of this output.
|
||||
---
|
||||
|
@ -444,6 +935,7 @@ function OutputHandle:props()
|
|||
response.tags = handles
|
||||
response.tag_ids = nil
|
||||
response.modes = response.modes or {}
|
||||
response.transform = transform_code_to_name[response.transform]
|
||||
|
||||
return response
|
||||
end
|
||||
|
@ -580,6 +1072,24 @@ function OutputHandle:scale()
|
|||
return self:props().scale
|
||||
end
|
||||
|
||||
---Get this output's transform.
|
||||
---
|
||||
---Shorthand for `handle:props().transform`.
|
||||
---
|
||||
---@return Transform?
|
||||
function OutputHandle:transform()
|
||||
return self:props().transform
|
||||
end
|
||||
|
||||
---Get this output's EDID serial number.
|
||||
---
|
||||
---Shorthand for `handle:props().serial`.
|
||||
---
|
||||
---@return integer?
|
||||
function OutputHandle:serial()
|
||||
return self:props().serial
|
||||
end
|
||||
|
||||
---@nodoc
|
||||
---Create a new `OutputHandle` from its raw name.
|
||||
---@param output_name string
|
||||
|
|
|
@ -14,12 +14,26 @@ local rpc_types = {
|
|||
OutputConnect = {
|
||||
response_type = "OutputConnectResponse",
|
||||
},
|
||||
OutputDisconnect = {
|
||||
response_type = "OutputDisconnectResponse",
|
||||
},
|
||||
OutputResize = {
|
||||
response_type = "OutputResizeResponse",
|
||||
},
|
||||
OutputMove = {
|
||||
response_type = "OutputMoveResponse",
|
||||
},
|
||||
|
||||
WindowPointerEnter = {
|
||||
response_type = "WindowPointerEnterResponse",
|
||||
},
|
||||
WindowPointerLeave = {
|
||||
response_type = "WindowPointerLeaveResponse",
|
||||
},
|
||||
|
||||
TagActive = {
|
||||
response_type = "TagActiveResponse",
|
||||
},
|
||||
}
|
||||
|
||||
---Build GrpcRequestParams
|
||||
|
@ -62,6 +76,39 @@ local signals = {
|
|||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
OutputDisconnect = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(output: OutputHandle))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
OutputResize = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(output: OutputHandle, logical_width: integer, logical_height: integer))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
OutputMove = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(output: OutputHandle, x: integer, y: integer))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
WindowPointerEnter = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
|
@ -84,6 +131,17 @@ local signals = {
|
|||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
TagActive = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(tag: TagHandle, active: boolean))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
}
|
||||
|
||||
signals.OutputConnect.on_response = function(response)
|
||||
|
@ -94,6 +152,30 @@ signals.OutputConnect.on_response = function(response)
|
|||
end
|
||||
end
|
||||
|
||||
signals.OutputDisconnect.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||
for _, callback in ipairs(signals.OutputDisconnect.callbacks) do
|
||||
callback(handle)
|
||||
end
|
||||
end
|
||||
|
||||
signals.OutputResize.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||
for _, callback in ipairs(signals.OutputResize.callbacks) do
|
||||
callback(handle, response.logical_width, response.logical_height)
|
||||
end
|
||||
end
|
||||
|
||||
signals.OutputMove.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||
for _, callback in ipairs(signals.OutputMove.callbacks) do
|
||||
callback(handle, response.x, response.y)
|
||||
end
|
||||
end
|
||||
|
||||
signals.WindowPointerEnter.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local window_handle = require("pinnacle.window").handle.new(response.window_id)
|
||||
|
@ -112,6 +194,15 @@ signals.WindowPointerLeave.on_response = function(response)
|
|||
end
|
||||
end
|
||||
|
||||
signals.TagActive.on_response = function(response)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local tag_handle = require("pinnacle.tag").handle.new(response.tag_id)
|
||||
|
||||
for _, callback in ipairs(signals.TagActive.callbacks) do
|
||||
callback(tag_handle, response.active)
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
---@nodoc
|
||||
|
|
|
@ -210,6 +210,52 @@ function tag.remove(tags)
|
|||
client.unary_request(build_grpc_request_params("Remove", { tag_ids = ids }))
|
||||
end
|
||||
|
||||
---@type table<string, SignalServiceMethod>
|
||||
local signal_name_to_SignalName = {
|
||||
active = "TagActive",
|
||||
}
|
||||
|
||||
---@class TagSignal Signals related to tag events.
|
||||
---@field active fun(tag: TagHandle, active: boolean)? A tag was set to active or not active.
|
||||
|
||||
---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({
|
||||
--- active = function(tag, active)
|
||||
--- print("Activity for " .. tag:name() .. " was set to", active)
|
||||
--- 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
|
||||
|
@ -280,6 +326,7 @@ end
|
|||
---@field active boolean? Whether or not the tag is currently being displayed
|
||||
---@field name string? The name of the tag
|
||||
---@field output OutputHandle? The output the tag is on
|
||||
---@field windows WindowHandle[] The windows that have this tag
|
||||
|
||||
---Get all properties of this tag.
|
||||
---
|
||||
|
@ -292,6 +339,8 @@ function TagHandle:props()
|
|||
name = response.name,
|
||||
---@diagnostic disable-next-line: invisible
|
||||
output = response.output_name and require("pinnacle.output").handle.new(response.output_name),
|
||||
---@diagnostic disable-next-line: invisible
|
||||
windows = require("pinnacle.window").handle.new_from_table(response.window_ids or {}),
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -322,6 +371,15 @@ function TagHandle:output()
|
|||
return self:props().output
|
||||
end
|
||||
|
||||
---Get the windows that have this tag.
|
||||
---
|
||||
---Shorthand for `handle:props().windows`.
|
||||
---
|
||||
---@return WindowHandle[]
|
||||
function TagHandle:windows()
|
||||
return self:props().windows
|
||||
end
|
||||
|
||||
---@nodoc
|
||||
---Create a new `TagHandle` from an id.
|
||||
---@param tag_id integer
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
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
|
||||
|
||||
Input:keybind({ "shift" }, "f", function()
|
||||
local focused = Window:get_focused()
|
||||
if focused then
|
||||
print(focused:fullscreen_or_maximized())
|
||||
-- assert(focused:fullscreen_or_maximized() == "neither")
|
||||
focused:set_fullscreen(true)
|
||||
print(focused:fullscreen_or_maximized())
|
||||
-- assert(focused:fullscreen_or_maximized() == "fullscreen")
|
||||
end
|
||||
end)
|
||||
end)
|
|
@ -10,6 +10,18 @@ message Mode {
|
|||
optional uint32 refresh_rate_millihz = 3;
|
||||
}
|
||||
|
||||
enum Transform {
|
||||
TRANSFORM_UNSPECIFIED = 0;
|
||||
TRANSFORM_NORMAL = 1;
|
||||
TRANSFORM_90 = 2;
|
||||
TRANSFORM_180 = 3;
|
||||
TRANSFORM_270 = 4;
|
||||
TRANSFORM_FLIPPED = 5;
|
||||
TRANSFORM_FLIPPED_90 = 6;
|
||||
TRANSFORM_FLIPPED_180 = 7;
|
||||
TRANSFORM_FLIPPED_270 = 8;
|
||||
}
|
||||
|
||||
message SetLocationRequest {
|
||||
optional string output_name = 1;
|
||||
optional int32 x = 2;
|
||||
|
@ -32,6 +44,11 @@ message SetScaleRequest {
|
|||
}
|
||||
}
|
||||
|
||||
message SetTransformRequest {
|
||||
optional string output_name = 1;
|
||||
optional Transform transform = 2;
|
||||
}
|
||||
|
||||
message GetRequest {}
|
||||
message GetResponse {
|
||||
repeated string output_names = 1;
|
||||
|
@ -72,12 +89,18 @@ message GetPropertiesResponse {
|
|||
optional bool focused = 10;
|
||||
repeated uint32 tag_ids = 11;
|
||||
optional float scale = 12;
|
||||
optional Transform transform = 15;
|
||||
// NULLABLE
|
||||
//
|
||||
// The EDID serial number of this output, if it exists.
|
||||
optional uint32 serial = 16;
|
||||
}
|
||||
|
||||
service OutputService {
|
||||
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
|
||||
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
|
||||
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
|
||||
rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty);
|
||||
rpc Get(GetRequest) returns (GetResponse);
|
||||
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,34 @@ message OutputConnectRequest {
|
|||
message OutputConnectResponse {
|
||||
optional string output_name = 1;
|
||||
}
|
||||
message OutputDisconnectRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
message OutputDisconnectResponse {
|
||||
optional string output_name = 1;
|
||||
}
|
||||
|
||||
message OutputResizeRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
|
||||
// An output's logical size changed
|
||||
message OutputResizeResponse {
|
||||
optional string output_name = 1;
|
||||
optional uint32 logical_width = 2;
|
||||
optional uint32 logical_height = 3;
|
||||
}
|
||||
|
||||
message OutputMoveRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
|
||||
// An output's location in the global space changed
|
||||
message OutputMoveResponse {
|
||||
optional string output_name = 1;
|
||||
optional int32 x = 2;
|
||||
optional int32 y = 3;
|
||||
}
|
||||
|
||||
message WindowPointerEnterRequest {
|
||||
optional StreamControl control = 1;
|
||||
|
@ -33,8 +61,23 @@ message WindowPointerLeaveResponse {
|
|||
optional uint32 window_id = 1;
|
||||
}
|
||||
|
||||
message TagActiveRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
message TagActiveResponse {
|
||||
optional uint32 tag_id = 1;
|
||||
// The tag was set to active or inactive.
|
||||
optional bool active = 2;
|
||||
}
|
||||
|
||||
service SignalService {
|
||||
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
||||
rpc OutputDisconnect(stream OutputDisconnectRequest) returns (stream OutputDisconnectResponse);
|
||||
rpc OutputResize(stream OutputResizeRequest) returns (stream OutputResizeResponse);
|
||||
rpc OutputMove(stream OutputMoveRequest) returns (stream OutputMoveResponse);
|
||||
|
||||
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
||||
rpc WindowPointerLeave(stream WindowPointerLeaveRequest) returns (stream WindowPointerLeaveResponse);
|
||||
|
||||
rpc TagActive(stream TagActiveRequest) returns (stream TagActiveResponse);
|
||||
}
|
||||
|
|
|
@ -35,9 +35,14 @@ message GetPropertiesRequest {
|
|||
optional uint32 tag_id = 1;
|
||||
}
|
||||
message GetPropertiesResponse {
|
||||
// Whether or not this tag is active
|
||||
optional bool active = 1;
|
||||
// The name of this tag
|
||||
optional string name = 2;
|
||||
// The output this tag is on
|
||||
optional string output_name = 3;
|
||||
// All windows that have this tag
|
||||
repeated uint32 window_ids = 4;
|
||||
}
|
||||
|
||||
service TagService {
|
||||
|
|
|
@ -21,6 +21,8 @@ enum SetOrToggle {
|
|||
|
||||
message QuitRequest {}
|
||||
|
||||
message ReloadConfigRequest {}
|
||||
|
||||
// A manual ping request independent of any HTTP keepalive.
|
||||
//
|
||||
// Tonic does not seems to give you the means to run something
|
||||
|
@ -36,5 +38,6 @@ message PingResponse {
|
|||
|
||||
service PinnacleService {
|
||||
rpc Quit(QuitRequest) returns (google.protobuf.Empty);
|
||||
rpc ReloadConfig(ReloadConfigRequest) returns (google.protobuf.Empty);
|
||||
rpc Ping(PingRequest) returns (PingResponse);
|
||||
}
|
||||
|
|
|
@ -20,3 +20,4 @@ futures = "0.3.30"
|
|||
num_enum = "0.7.2"
|
||||
xkbcommon = { workspace = true }
|
||||
rand = "0.8.5"
|
||||
bitflags = { workspace = true }
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use pinnacle_api::input::libinput::LibinputSetting;
|
||||
use pinnacle_api::layout::{
|
||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||
MasterStackLayout, SpiralLayout,
|
||||
};
|
||||
use pinnacle_api::output::OutputSetup;
|
||||
use pinnacle_api::signal::WindowSignal;
|
||||
use pinnacle_api::util::{Axis, Batch};
|
||||
use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||
|
@ -26,6 +28,7 @@ async fn main() {
|
|||
tag,
|
||||
layout,
|
||||
render,
|
||||
..
|
||||
} = modules;
|
||||
|
||||
let mod_key = Mod::Ctrl;
|
||||
|
@ -55,6 +58,11 @@ async fn main() {
|
|||
pinnacle.quit();
|
||||
});
|
||||
|
||||
// `mod_key + alt + r` reloads the config
|
||||
input.keybind([mod_key, Mod::Alt], 'r', || {
|
||||
pinnacle.reload_config();
|
||||
});
|
||||
|
||||
// `mod_key + alt + c` closes the focused window
|
||||
input.keybind([mod_key, Mod::Alt], 'c', || {
|
||||
if let Some(window) = window.get_focused() {
|
||||
|
@ -206,12 +214,7 @@ async fn main() {
|
|||
let tag_names = ["1", "2", "3", "4", "5"];
|
||||
|
||||
// Setup all monitors with tags "1" through "5"
|
||||
output.connect_for_all(move |op| {
|
||||
let tags = tag.add(op, tag_names);
|
||||
|
||||
// Be sure to set a tag to active or windows won't display
|
||||
tags.first().unwrap().set_active(true);
|
||||
});
|
||||
output.setup([OutputSetup::new_with_matcher(|_| true).with_tags(tag_names)]);
|
||||
|
||||
for tag_name in tag_names {
|
||||
// `mod_key + 1-5` switches to tag "1" to "5"
|
||||
|
@ -247,6 +250,8 @@ async fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
input.set_libinput_setting(LibinputSetting::Tap(true));
|
||||
|
||||
// Enable sloppy focus
|
||||
window.connect_signal(WindowSignal::PointerEnter(Box::new(|win| {
|
||||
win.set_focused(true);
|
||||
|
|
|
@ -131,11 +131,13 @@ pub fn config(
|
|||
#(#attrs)*
|
||||
#tokio_attr
|
||||
#vis #sig {
|
||||
let (#module_ident, __fut_receiver) = ::pinnacle_api::connect().await.unwrap();
|
||||
let (__api, __fut_receiver) = ::pinnacle_api::connect().await.unwrap();
|
||||
|
||||
let #module_ident = __api.clone();
|
||||
|
||||
#(#stmts)*
|
||||
|
||||
::pinnacle_api::listen(__fut_receiver).await;
|
||||
::pinnacle_api::listen(__api, __fut_receiver).await;
|
||||
}
|
||||
}
|
||||
.into()
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use pinnacle_api_defs::pinnacle::layout::v0alpha1::{
|
||||
|
@ -26,22 +26,28 @@ use crate::{
|
|||
tag::TagHandle,
|
||||
util::{Axis, Geometry},
|
||||
window::WindowHandle,
|
||||
OUTPUT, TAG, WINDOW,
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
/// A struct that allows you to manage layouts.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Layout {
|
||||
api: OnceLock<ApiModules>,
|
||||
layout_client: LayoutServiceClient<Channel>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub(crate) fn new(channel: Channel) -> Self {
|
||||
Self {
|
||||
api: OnceLock::new(),
|
||||
layout_client: LayoutServiceClient::new(channel.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||
self.api.set(api).unwrap();
|
||||
}
|
||||
|
||||
/// Consume the given [`LayoutManager`] and set it as the global layout handler.
|
||||
///
|
||||
/// This returns a [`LayoutRequester`] that allows you to manually request layouts from
|
||||
|
@ -61,7 +67,10 @@ impl Layout {
|
|||
|
||||
let manager = Arc::new(Mutex::new(manager));
|
||||
|
||||
let api = self.api.get().unwrap().clone();
|
||||
|
||||
let requester = LayoutRequester {
|
||||
api: api.clone(),
|
||||
sender: from_client_clone,
|
||||
manager: manager.clone(),
|
||||
};
|
||||
|
@ -69,16 +78,16 @@ impl Layout {
|
|||
let thing = async move {
|
||||
while let Some(Ok(response)) = from_server.next().await {
|
||||
let args = LayoutArgs {
|
||||
output: OUTPUT.get().unwrap().new_handle(response.output_name()),
|
||||
output: api.output.new_handle(response.output_name()),
|
||||
windows: response
|
||||
.window_ids
|
||||
.into_iter()
|
||||
.map(|id| WINDOW.get().unwrap().new_handle(id))
|
||||
.map(|id| api.window.new_handle(id))
|
||||
.collect(),
|
||||
tags: response
|
||||
.tag_ids
|
||||
.into_iter()
|
||||
.map(|id| TAG.get().unwrap().new_handle(id))
|
||||
.map(|id| api.tag.new_handle(id))
|
||||
.collect(),
|
||||
output_width: response.output_width.unwrap_or_default(),
|
||||
output_height: response.output_height.unwrap_or_default(),
|
||||
|
@ -225,6 +234,7 @@ impl LayoutManager for CyclingLayoutManager {
|
|||
/// A struct that can request layouts and provides access to a consumed [`LayoutManager`].
|
||||
#[derive(Debug)]
|
||||
pub struct LayoutRequester<T> {
|
||||
api: ApiModules,
|
||||
sender: UnboundedSender<LayoutRequest>,
|
||||
/// The manager that was consumed, wrapped in an `Arc<Mutex>`.
|
||||
pub manager: Arc<Mutex<T>>,
|
||||
|
@ -233,6 +243,7 @@ pub struct LayoutRequester<T> {
|
|||
impl<T> Clone for LayoutRequester<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
api: self.api.clone(),
|
||||
sender: self.sender.clone(),
|
||||
manager: self.manager.clone(),
|
||||
}
|
||||
|
@ -245,7 +256,7 @@ impl<T> LayoutRequester<T> {
|
|||
/// This uses the focused output for the request.
|
||||
/// If you want to layout a specific output, see [`LayoutRequester::request_layout_on_output`].
|
||||
pub fn request_layout(&self) {
|
||||
let output_name = OUTPUT.get().unwrap().get_focused().map(|op| op.name);
|
||||
let output_name = self.api.output.get_focused().map(|op| op.name);
|
||||
self.sender
|
||||
.send(LayoutRequest {
|
||||
body: Some(Body::Layout(ExplicitLayout { output_name })),
|
||||
|
|
|
@ -67,12 +67,7 @@
|
|||
//! // `modules` is now available in the function body.
|
||||
//! // You can deconstruct `ApiModules` to get all the config structs.
|
||||
//! let ApiModules {
|
||||
//! pinnacle,
|
||||
//! process,
|
||||
//! window,
|
||||
//! input,
|
||||
//! output,
|
||||
//! tag,
|
||||
//! ..
|
||||
//! } = modules;
|
||||
//! }
|
||||
//! ```
|
||||
|
@ -80,7 +75,7 @@
|
|||
//! ## 5. Begin crafting your config!
|
||||
//! You can peruse the documentation for things to configure.
|
||||
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use futures::{future::BoxFuture, Future, StreamExt};
|
||||
use input::Input;
|
||||
|
@ -115,18 +110,9 @@ pub use pinnacle_api_macros::config;
|
|||
pub use tokio;
|
||||
pub use xkbcommon;
|
||||
|
||||
static PINNACLE: OnceLock<Pinnacle> = OnceLock::new();
|
||||
static PROCESS: OnceLock<Process> = OnceLock::new();
|
||||
static WINDOW: OnceLock<Window> = OnceLock::new();
|
||||
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();
|
||||
static RENDER: OnceLock<Render> = OnceLock::new();
|
||||
|
||||
/// A struct containing static references to all of the configuration structs.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone)]
|
||||
pub struct ApiModules {
|
||||
/// The [`Pinnacle`] struct
|
||||
pub pinnacle: &'static Pinnacle,
|
||||
|
@ -144,6 +130,23 @@ pub struct ApiModules {
|
|||
pub layout: &'static Layout,
|
||||
/// The [`Render`] struct
|
||||
pub render: &'static Render,
|
||||
signal: Arc<RwLock<SignalState>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ApiModules {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApiModules")
|
||||
.field("pinnacle", &self.pinnacle)
|
||||
.field("process", &self.process)
|
||||
.field("window", &self.window)
|
||||
.field("input", &self.input)
|
||||
.field("output", &self.output)
|
||||
.field("tag", &self.tag)
|
||||
.field("layout", &self.layout)
|
||||
.field("render", &self.render)
|
||||
.field("signal", &"...")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to Pinnacle and builds the configuration structs.
|
||||
|
@ -164,21 +167,19 @@ pub async fn connect(
|
|||
|
||||
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()));
|
||||
let window = WINDOW.get_or_init(|| Window::new(channel.clone()));
|
||||
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()));
|
||||
let render = RENDER.get_or_init(|| Render::new(channel.clone()));
|
||||
let signal = Arc::new(RwLock::new(SignalState::new(
|
||||
channel.clone(),
|
||||
fut_sender.clone(),
|
||||
)));
|
||||
|
||||
SIGNAL
|
||||
.set(RwLock::new(SignalState::new(
|
||||
channel.clone(),
|
||||
fut_sender.clone(),
|
||||
)))
|
||||
.map_err(|_| "failed to create SIGNAL")?;
|
||||
let pinnacle = Box::leak(Box::new(Pinnacle::new(channel.clone())));
|
||||
let process = Box::leak(Box::new(Process::new(channel.clone(), fut_sender.clone())));
|
||||
let window = Box::leak(Box::new(Window::new(channel.clone())));
|
||||
let input = Box::leak(Box::new(Input::new(channel.clone(), fut_sender.clone())));
|
||||
let output = Box::leak(Box::new(Output::new(channel.clone())));
|
||||
let tag = Box::leak(Box::new(Tag::new(channel.clone())));
|
||||
let render = Box::leak(Box::new(Render::new(channel.clone())));
|
||||
let layout = Box::leak(Box::new(Layout::new(channel.clone())));
|
||||
|
||||
let modules = ApiModules {
|
||||
pinnacle,
|
||||
|
@ -189,8 +190,15 @@ pub async fn connect(
|
|||
tag,
|
||||
layout,
|
||||
render,
|
||||
signal: signal.clone(),
|
||||
};
|
||||
|
||||
window.finish_init(modules.clone());
|
||||
output.finish_init(modules.clone());
|
||||
tag.finish_init(modules.clone());
|
||||
layout.finish_init(modules.clone());
|
||||
signal.read().await.finish_init(modules.clone());
|
||||
|
||||
Ok((modules, fut_recv))
|
||||
}
|
||||
|
||||
|
@ -201,15 +209,13 @@ 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, ()>>) {
|
||||
pub async fn listen(api: ApiModules, fut_recv: UnboundedReceiver<BoxFuture<'static, ()>>) {
|
||||
let mut fut_recv = UnboundedReceiverStream::new(fut_recv);
|
||||
|
||||
let pinnacle = PINNACLE.get().unwrap();
|
||||
|
||||
let keepalive = async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(err) = pinnacle.ping().await {
|
||||
if let Err(err) = api.pinnacle.ping().await {
|
||||
eprintln!("Failed to ping compositor: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
||||
//! connected monitors and set them up.
|
||||
|
||||
use std::{num::NonZeroU32, sync::OnceLock};
|
||||
|
||||
use futures::FutureExt;
|
||||
use pinnacle_api_defs::pinnacle::output::{
|
||||
self,
|
||||
v0alpha1::{
|
||||
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
||||
SetLocationRequest, SetModeRequest, SetScaleRequest,
|
||||
SetLocationRequest, SetModeRequest, SetScaleRequest, SetTransformRequest,
|
||||
},
|
||||
};
|
||||
use tonic::transport::Channel;
|
||||
|
@ -22,9 +24,9 @@ use tonic::transport::Channel;
|
|||
use crate::{
|
||||
block_on_tokio,
|
||||
signal::{OutputSignal, SignalHandle},
|
||||
tag::TagHandle,
|
||||
tag::{Tag, TagHandle},
|
||||
util::Batch,
|
||||
SIGNAL, TAG,
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
/// A struct that allows you to get handles to connected outputs and set them up.
|
||||
|
@ -33,19 +35,26 @@ use crate::{
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct Output {
|
||||
output_client: OutputServiceClient<Channel>,
|
||||
api: OnceLock<ApiModules>,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub(crate) fn new(channel: Channel) -> Self {
|
||||
Self {
|
||||
output_client: OutputServiceClient::new(channel.clone()),
|
||||
api: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||
self.api.set(api).unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn new_handle(&self, name: impl Into<String>) -> OutputHandle {
|
||||
OutputHandle {
|
||||
name: name.into(),
|
||||
output_client: self.output_client.clone(),
|
||||
api: self.api.get().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,7 +154,7 @@ impl Output {
|
|||
for_all(&output);
|
||||
}
|
||||
|
||||
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
|
||||
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
|
||||
signal_state.output_connect.add_callback(Box::new(for_all));
|
||||
}
|
||||
|
||||
|
@ -155,12 +164,396 @@ impl Output {
|
|||
/// You can pass in an [`OutputSignal`] 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: OutputSignal) -> SignalHandle {
|
||||
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
|
||||
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
|
||||
|
||||
match signal {
|
||||
OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
|
||||
OutputSignal::Disconnect(f) => signal_state.output_disconnect.add_callback(f),
|
||||
OutputSignal::Resize(f) => signal_state.output_resize.add_callback(f),
|
||||
OutputSignal::Move(f) => signal_state.output_move.add_callback(f),
|
||||
}
|
||||
}
|
||||
|
||||
/// Declaratively setup outputs.
|
||||
///
|
||||
/// This method allows you to specify [`OutputSetup`]s that will be applied to outputs already
|
||||
/// connected and that will be connected in the future. It handles the setting of modes,
|
||||
/// scales, tags, and more.
|
||||
///
|
||||
/// Setups will be applied top to bottom.
|
||||
///
|
||||
/// See [`OutputSetup`] for more information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pinnacle_api::output::OutputSetup;
|
||||
/// use pinnacle_api::output::OutputId;
|
||||
///
|
||||
/// output.setup([
|
||||
/// // Give all outputs tags 1 through 5
|
||||
/// OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3", "4", "5"]),
|
||||
/// // Give outputs with a preferred mode of 4K a scale of 2.0
|
||||
/// OutputSetup::new_with_matcher(|op| op.preferred_mode().unwrap().pixel_width == 2160)
|
||||
/// .with_scale(2.0),
|
||||
/// // Additionally give eDP-1 tags 6 and 7
|
||||
/// OutputSetup::new(OutputId::name("eDP-1")).with_tags(["6", "7"]),
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn setup(&self, setups: impl IntoIterator<Item = OutputSetup>) {
|
||||
let setups = setups.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let tag_mod = self.api.get().unwrap().tag.clone();
|
||||
let apply_setups = move |output: &OutputHandle| {
|
||||
for setup in setups.iter() {
|
||||
if setup.output.matches(output) {
|
||||
setup.apply(output, &tag_mod);
|
||||
}
|
||||
}
|
||||
if let Some(tag) = output.tags().first() {
|
||||
tag.set_active(true);
|
||||
}
|
||||
};
|
||||
|
||||
self.connect_for_all(move |output| {
|
||||
apply_setups(output);
|
||||
});
|
||||
}
|
||||
|
||||
/// Specify locations for outputs and when they should be laid out.
|
||||
///
|
||||
/// This method allows you to specify locations for outputs, either as a specific point
|
||||
/// or relative to another output.
|
||||
///
|
||||
/// This will relayout outputs according to the given [`UpdateLocsOn`] flags.
|
||||
///
|
||||
/// Layouts not specified in `setup` or that have cyclic relative-to outputs will be
|
||||
/// laid out in a line to the right of the rightmost output.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pinnacle_api::output::UpdateLocsOn;
|
||||
/// use pinnacle_api::output::OutputLoc;
|
||||
/// use pinnacle_api::output::OutputId;
|
||||
///
|
||||
/// output.setup_locs(
|
||||
/// // Relayout all outputs when outputs are connected, disconnected, and resized
|
||||
/// UpdateLocsOn::all(),
|
||||
/// [
|
||||
/// // Anchor eDP-1 to (0, 0) so other outputs can be placed relative to it
|
||||
/// (OutputId::name("eDP-1"), OutputLoc::Point(0, 0)),
|
||||
/// // Place HDMI-A-1 below it centered
|
||||
/// (
|
||||
/// OutputId::name("HDMI-A-1"),
|
||||
/// OutputLoc::RelativeTo(OutputId::name("eDP-1"), Alignment::BottomAlignCenter),
|
||||
/// ),
|
||||
/// // Place HDMI-A-2 below HDMI-A-1.
|
||||
/// (
|
||||
/// OutputId::name("HDMI-A-2"),
|
||||
/// OutputLoc::RelativeTo(OutputId::name("HDMI-A-1"), Alignment::BottomAlignCenter),
|
||||
/// ),
|
||||
/// // Additionally, if HDMI-A-1 isn't connected, place it below eDP-1 instead.
|
||||
/// (
|
||||
/// OutputId::name("HDMI-A-2"),
|
||||
/// OutputLoc::RelativeTo(OutputId::name("eDP-1"), Alignment::BottomAlignCenter),
|
||||
/// ),
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn setup_locs(
|
||||
&self,
|
||||
update_locs_on: UpdateLocsOn,
|
||||
setup: impl IntoIterator<Item = (OutputId, OutputLoc)>,
|
||||
) {
|
||||
let setup: Vec<_> = setup.into_iter().collect();
|
||||
|
||||
let api = self.api.get().unwrap().clone();
|
||||
let layout_outputs = move || {
|
||||
let outputs = api.output.get_all();
|
||||
|
||||
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
|
||||
|
||||
let mut placed_outputs = Vec::<OutputHandle>::new();
|
||||
|
||||
// Place outputs with OutputSetupLoc::Point
|
||||
for output in outputs.iter() {
|
||||
if let Some(&(_, OutputLoc::Point(x, y))) =
|
||||
setup.iter().find(|(op_id, _)| op_id.matches(output))
|
||||
{
|
||||
output.set_location(x, y);
|
||||
|
||||
placed_outputs.push(output.clone());
|
||||
let props = output.props();
|
||||
let x = props.x.unwrap();
|
||||
let width = props.logical_width.unwrap() as i32;
|
||||
if rightmost_output_and_x.is_none()
|
||||
|| rightmost_output_and_x
|
||||
.as_ref()
|
||||
.is_some_and(|(_, rm_x)| x + width > *rm_x)
|
||||
{
|
||||
rightmost_output_and_x = Some((output.clone(), x + width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to place relative outputs
|
||||
//
|
||||
// Because this code is hideous I'm gonna comment what it does
|
||||
while let Some((output, relative_to, alignment)) =
|
||||
setup.iter().find_map(|(setup_op_id, loc)| {
|
||||
// For every location setup,
|
||||
// find the first unplaced output it refers to that has a relative location
|
||||
outputs
|
||||
.iter()
|
||||
.find(|setup_op| {
|
||||
!placed_outputs.contains(setup_op) && setup_op_id.matches(setup_op)
|
||||
})
|
||||
.and_then(|setup_op| match loc {
|
||||
OutputLoc::RelativeTo(rel_id, alignment) => {
|
||||
placed_outputs.iter().find_map(|placed_op| {
|
||||
(rel_id.matches(placed_op))
|
||||
.then_some((setup_op, placed_op, alignment))
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
{
|
||||
output.set_loc_adj_to(relative_to, *alignment);
|
||||
|
||||
placed_outputs.push(output.clone());
|
||||
let props = output.props();
|
||||
let x = props.x.unwrap();
|
||||
let width = props.logical_width.unwrap() as i32;
|
||||
if rightmost_output_and_x.is_none()
|
||||
|| rightmost_output_and_x
|
||||
.as_ref()
|
||||
.is_some_and(|(_, rm_x)| x + width > *rm_x)
|
||||
{
|
||||
rightmost_output_and_x = Some((output.clone(), x + width));
|
||||
}
|
||||
}
|
||||
|
||||
// Place all remaining outputs right of the rightmost one
|
||||
for output in outputs
|
||||
.iter()
|
||||
.filter(|op| !placed_outputs.contains(op))
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() {
|
||||
output.set_loc_adj_to(rm_op, Alignment::RightAlignTop);
|
||||
} else {
|
||||
output.set_location(0, 0);
|
||||
}
|
||||
|
||||
placed_outputs.push(output.clone());
|
||||
let props = output.props();
|
||||
let x = props.x.unwrap();
|
||||
let width = props.logical_width.unwrap() as i32;
|
||||
if rightmost_output_and_x.is_none()
|
||||
|| rightmost_output_and_x
|
||||
.as_ref()
|
||||
.is_some_and(|(_, rm_x)| x + width > *rm_x)
|
||||
{
|
||||
rightmost_output_and_x = Some((output.clone(), x + width));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
layout_outputs();
|
||||
|
||||
let layout_outputs_clone1 = layout_outputs.clone();
|
||||
let layout_outputs_clone2 = layout_outputs.clone();
|
||||
|
||||
if update_locs_on.contains(UpdateLocsOn::CONNECT) {
|
||||
self.connect_signal(OutputSignal::Connect(Box::new(move |_| {
|
||||
layout_outputs_clone2();
|
||||
})));
|
||||
}
|
||||
|
||||
if update_locs_on.contains(UpdateLocsOn::DISCONNECT) {
|
||||
self.connect_signal(OutputSignal::Disconnect(Box::new(move |_| {
|
||||
layout_outputs_clone1();
|
||||
})));
|
||||
}
|
||||
|
||||
if update_locs_on.contains(UpdateLocsOn::RESIZE) {
|
||||
self.connect_signal(OutputSignal::Resize(Box::new(move |_, _, _| {
|
||||
layout_outputs();
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A matcher for outputs.
|
||||
enum OutputMatcher {
|
||||
/// Match outputs by unique id.
|
||||
Id(OutputId),
|
||||
/// Match outputs using a function that returns a bool.
|
||||
Fn(Box<dyn Fn(&OutputHandle) -> bool + Send + Sync>),
|
||||
}
|
||||
|
||||
impl OutputMatcher {
|
||||
/// Returns whether this matcher matches the given output.
|
||||
fn matches(&self, output: &OutputHandle) -> bool {
|
||||
match self {
|
||||
OutputMatcher::Id(id) => id.matches(output),
|
||||
OutputMatcher::Fn(matcher) => matcher(output),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OutputMatcher {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Id(name) => f.debug_tuple("Name").field(name).finish(),
|
||||
Self::Fn(_) => f
|
||||
.debug_tuple("Fn")
|
||||
.field(&"<Box<dyn Fn(&OutputHandle)> -> bool>")
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An output setup for use in [`Output::setup`].
|
||||
pub struct OutputSetup {
|
||||
output: OutputMatcher,
|
||||
mode: Option<Mode>,
|
||||
scale: Option<f32>,
|
||||
tag_names: Option<Vec<String>>,
|
||||
transform: Option<Transform>,
|
||||
}
|
||||
|
||||
impl OutputSetup {
|
||||
/// Creates a new `OutputSetup` that applies to the output with the given name.
|
||||
pub fn new(id: OutputId) -> Self {
|
||||
Self {
|
||||
output: OutputMatcher::Id(id),
|
||||
mode: None,
|
||||
scale: None,
|
||||
tag_names: None,
|
||||
transform: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `OutputSetup` that matches outputs according to the given function.
|
||||
pub fn new_with_matcher(
|
||||
matcher: impl Fn(&OutputHandle) -> bool + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
output: OutputMatcher::Fn(Box::new(matcher)),
|
||||
mode: None,
|
||||
scale: None,
|
||||
tag_names: None,
|
||||
transform: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes this setup apply the given [`Mode`] to its outputs.
|
||||
pub fn with_mode(self, mode: Mode) -> Self {
|
||||
Self {
|
||||
mode: Some(mode),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes this setup apply the given scale to its outputs.
|
||||
pub fn with_scale(self, scale: f32) -> Self {
|
||||
Self {
|
||||
scale: Some(scale),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes this setup add tags with the given names to its outputs.
|
||||
pub fn with_tags(self, tag_names: impl IntoIterator<Item = impl ToString>) -> Self {
|
||||
Self {
|
||||
tag_names: Some(tag_names.into_iter().map(|s| s.to_string()).collect()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes this setup apply the given transform to its outputs.
|
||||
pub fn with_transform(self, transform: Transform) -> Self {
|
||||
Self {
|
||||
transform: Some(transform),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&self, output: &OutputHandle, tag: &Tag) {
|
||||
if let Some(mode) = &self.mode {
|
||||
output.set_mode(
|
||||
mode.pixel_width,
|
||||
mode.pixel_height,
|
||||
Some(mode.refresh_rate_millihertz),
|
||||
);
|
||||
}
|
||||
if let Some(scale) = self.scale {
|
||||
output.set_scale(scale);
|
||||
}
|
||||
if let Some(tag_names) = &self.tag_names {
|
||||
tag.add(output, tag_names);
|
||||
}
|
||||
if let Some(transform) = self.transform {
|
||||
output.set_transform(transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A location for an output.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum OutputLoc {
|
||||
/// A specific point in the global space of the form (x, y).
|
||||
Point(i32, i32),
|
||||
/// A location relative to another output with an [`Alignment`].
|
||||
RelativeTo(OutputId, Alignment),
|
||||
}
|
||||
|
||||
/// An identifier for an output.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum OutputId {
|
||||
/// Identify using the output's name.
|
||||
Name(String),
|
||||
/// Identify using the output's EDID serial number.
|
||||
///
|
||||
/// Note: some displays (like laptop screens) don't have a serial number, in which case this won't match it.
|
||||
/// Additionally the Rust API assumes monitor serial numbers are unique.
|
||||
/// If you're unlucky enough to have two monitors with the same serial number,
|
||||
/// use [`OutputId::Name`] instead.
|
||||
Serial(NonZeroU32),
|
||||
}
|
||||
|
||||
impl OutputId {
|
||||
/// Creates an [`OutputId::Name`].
|
||||
///
|
||||
/// This is a convenience function so you don't have to call `.into()`
|
||||
/// or `.to_string()`.
|
||||
pub fn name(name: impl ToString) -> Self {
|
||||
Self::Name(name.to_string())
|
||||
}
|
||||
|
||||
/// Returns whether `output` is identified by this `OutputId`.
|
||||
pub fn matches(&self, output: &OutputHandle) -> bool {
|
||||
match self {
|
||||
OutputId::Name(name) => name == output.name(),
|
||||
OutputId::Serial(serial) => Some(serial.get()) == output.serial(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Flags for when [`Output::setup_locs`] should relayout outputs.
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct UpdateLocsOn: u8 {
|
||||
/// Relayout when an output is connected.
|
||||
const CONNECT = 1;
|
||||
/// Relayout when an output is disconnected.
|
||||
const DISCONNECT = 1 << 1;
|
||||
/// Relayout when an output is resized, either through a scale or mode change.
|
||||
const RESIZE = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to an output.
|
||||
|
@ -170,6 +563,7 @@ impl Output {
|
|||
pub struct OutputHandle {
|
||||
pub(crate) name: String,
|
||||
output_client: OutputServiceClient<Channel>,
|
||||
api: ApiModules,
|
||||
}
|
||||
|
||||
impl PartialEq for OutputHandle {
|
||||
|
@ -215,6 +609,31 @@ pub enum Alignment {
|
|||
RightAlignBottom,
|
||||
}
|
||||
|
||||
/// An output transform.
|
||||
///
|
||||
/// This determines what orientation outputs will render at.
|
||||
#[derive(num_enum::TryFromPrimitive, Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[repr(i32)]
|
||||
pub enum Transform {
|
||||
/// No transform.
|
||||
#[default]
|
||||
Normal = 1,
|
||||
/// 90 degrees counter-clockwise.
|
||||
_90,
|
||||
/// 180 degrees counter-clockwise.
|
||||
_180,
|
||||
/// 270 degrees counter-clockwise.
|
||||
_270,
|
||||
/// Flipped vertically (across the horizontal axis).
|
||||
Flipped,
|
||||
/// Flipped vertically and rotated 90 degrees counter-clockwise
|
||||
Flipped90,
|
||||
/// Flipped vertically and rotated 180 degrees counter-clockwise
|
||||
Flipped180,
|
||||
/// Flipped vertically and rotated 270 degrees counter-clockwise
|
||||
Flipped270,
|
||||
}
|
||||
|
||||
impl OutputHandle {
|
||||
/// Set the location of this output in the global space.
|
||||
///
|
||||
|
@ -436,6 +855,25 @@ impl OutputHandle {
|
|||
self.increase_scale(-decrease_by);
|
||||
}
|
||||
|
||||
/// Set this output's transform.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pinnacle_api::output::Transform;
|
||||
///
|
||||
/// // Rotate 90 degrees counter-clockwise
|
||||
/// output.set_transform(Transform::_90);
|
||||
/// ```
|
||||
pub fn set_transform(&self, transform: Transform) {
|
||||
let mut client = self.output_client.clone();
|
||||
block_on_tokio(client.set_transform(SetTransformRequest {
|
||||
output_name: Some(self.name.clone()),
|
||||
transform: Some(transform as i32),
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Get all properties of this output.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -462,8 +900,6 @@ impl OutputHandle {
|
|||
.unwrap()
|
||||
.into_inner();
|
||||
|
||||
let tag = TAG.get().expect("TAG doesn't exist");
|
||||
|
||||
OutputProperties {
|
||||
make: response.make,
|
||||
model: response.model,
|
||||
|
@ -502,9 +938,11 @@ impl OutputHandle {
|
|||
tags: response
|
||||
.tag_ids
|
||||
.into_iter()
|
||||
.map(|id| tag.new_handle(id))
|
||||
.map(|id| self.api.tag.new_handle(id))
|
||||
.collect(),
|
||||
scale: response.scale,
|
||||
transform: response.transform.and_then(|tf| tf.try_into().ok()),
|
||||
serial: response.serial,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -680,6 +1118,30 @@ impl OutputHandle {
|
|||
self.props_async().await.scale
|
||||
}
|
||||
|
||||
/// Get this output's transform.
|
||||
///
|
||||
/// Shorthand for `self.props().transform`
|
||||
pub fn transform(&self) -> Option<Transform> {
|
||||
self.props().transform
|
||||
}
|
||||
|
||||
/// The async version of [`OutputHandle::transform`].
|
||||
pub async fn transform_async(&self) -> Option<Transform> {
|
||||
self.props_async().await.transform
|
||||
}
|
||||
|
||||
/// Get this output's EDID serial number.
|
||||
///
|
||||
/// Shorthand for `self.props().serial`
|
||||
pub fn serial(&self) -> Option<u32> {
|
||||
self.props().serial
|
||||
}
|
||||
|
||||
/// The async version of [`OutputHandle::serial`].
|
||||
pub async fn serial_async(&self) -> Option<u32> {
|
||||
self.props_async().await.serial
|
||||
}
|
||||
|
||||
/// Get this output's unique name (the name of its connector).
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
|
@ -700,6 +1162,7 @@ pub struct Mode {
|
|||
}
|
||||
|
||||
/// The properties of an output.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct OutputProperties {
|
||||
/// The make of the output.
|
||||
|
@ -737,4 +1200,8 @@ pub struct OutputProperties {
|
|||
pub tags: Vec<TagHandle>,
|
||||
/// This output's scaling factor.
|
||||
pub scale: Option<f32>,
|
||||
/// This output's transform.
|
||||
pub transform: Option<Transform>,
|
||||
/// This output's EDID serial number.
|
||||
pub serial: Option<u32>,
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use pinnacle_api_defs::pinnacle::v0alpha1::{
|
||||
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest,
|
||||
pinnacle_service_client::PinnacleServiceClient, PingRequest, QuitRequest, ReloadConfigRequest,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
@ -42,6 +42,12 @@ impl Pinnacle {
|
|||
block_on_tokio(client.quit(QuitRequest {})).unwrap();
|
||||
}
|
||||
|
||||
/// Reload the currently active config.
|
||||
pub fn reload_config(&self) {
|
||||
let mut client = self.client.clone();
|
||||
block_on_tokio(client.reload_config(ReloadConfigRequest {})).unwrap();
|
||||
}
|
||||
|
||||
pub(super) async fn ping(&self) -> Result<(), String> {
|
||||
let mut client = self.client.clone();
|
||||
let mut payload = [0u8; 8];
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
//! Some of the other modules have a `connect_signal` method that will allow you to pass in
|
||||
//! callbacks to run on each signal. Use them to connect to the signals defined here.
|
||||
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use std::{
|
||||
collections::{btree_map, BTreeMap},
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
Arc, OnceLock,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -25,7 +27,9 @@ use tokio::sync::{
|
|||
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||
use tonic::{transport::Channel, Streaming};
|
||||
|
||||
use crate::{block_on_tokio, output::OutputHandle, window::WindowHandle, OUTPUT, WINDOW};
|
||||
use crate::{
|
||||
block_on_tokio, output::OutputHandle, tag::TagHandle, window::WindowHandle, ApiModules,
|
||||
};
|
||||
|
||||
pub(crate) trait Signal {
|
||||
type Callback;
|
||||
|
@ -97,6 +101,7 @@ macro_rules! signals {
|
|||
.into_inner()
|
||||
},
|
||||
$on_resp,
|
||||
self.api.get().unwrap().clone(),
|
||||
);
|
||||
|
||||
self.callback_sender.replace(channels.callback_sender);
|
||||
|
@ -127,10 +132,9 @@ signals! {
|
|||
enum_name = Connect,
|
||||
callback_type = SingleOutputFn,
|
||||
client_request = output_connect,
|
||||
on_response = |response, callbacks| {
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(output_name) = response.output_name {
|
||||
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
|
||||
let handle = output.new_handle(output_name);
|
||||
let handle = api.output.new_handle(output_name);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle);
|
||||
|
@ -138,6 +142,57 @@ signals! {
|
|||
}
|
||||
},
|
||||
}
|
||||
/// An output was connected.
|
||||
///
|
||||
/// Callbacks receive the disconnected output.
|
||||
OutputDisconnect = {
|
||||
enum_name = Disconnect,
|
||||
callback_type = SingleOutputFn,
|
||||
client_request = output_disconnect,
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(output_name) = response.output_name {
|
||||
let handle = api.output.new_handle(output_name);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
/// An output's logical size changed.
|
||||
///
|
||||
/// Callbacks receive the output and new width and height.
|
||||
OutputResize = {
|
||||
enum_name = Resize,
|
||||
callback_type = Box<dyn FnMut(&OutputHandle, u32, u32) + Send + 'static>,
|
||||
client_request = output_resize,
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(output_name) = &response.output_name {
|
||||
let handle = api.output.new_handle(output_name);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle, response.logical_width(), response.logical_height())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
/// An output's location in the global space changed.
|
||||
///
|
||||
/// Callbacks receive the output and new x and y.
|
||||
OutputMove = {
|
||||
enum_name = Move,
|
||||
callback_type = Box<dyn FnMut(&OutputHandle, i32, i32) + Send + 'static>,
|
||||
client_request = output_move,
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(output_name) = &response.output_name {
|
||||
let handle = api.output.new_handle(output_name);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle, response.x(), response.y())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Signals relating to window events.
|
||||
WindowSignal => {
|
||||
|
@ -148,10 +203,9 @@ signals! {
|
|||
enum_name = PointerEnter,
|
||||
callback_type = SingleWindowFn,
|
||||
client_request = window_pointer_enter,
|
||||
on_response = |response, callbacks| {
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(window_id) = response.window_id {
|
||||
let window = WINDOW.get().expect("WINDOW doesn't exist");
|
||||
let handle = window.new_handle(window_id);
|
||||
let handle = api.window.new_handle(window_id);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle);
|
||||
|
@ -166,10 +220,9 @@ signals! {
|
|||
enum_name = PointerLeave,
|
||||
callback_type = SingleWindowFn,
|
||||
client_request = window_pointer_leave,
|
||||
on_response = |response, callbacks| {
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(window_id) = response.window_id {
|
||||
let window = WINDOW.get().expect("WINDOW doesn't exist");
|
||||
let handle = window.new_handle(window_id);
|
||||
let handle = api.window.new_handle(window_id);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle);
|
||||
|
@ -178,6 +231,24 @@ signals! {
|
|||
},
|
||||
}
|
||||
}
|
||||
/// Signals relating to tag events.
|
||||
TagSignal => {
|
||||
/// A tag was set to active or not active.
|
||||
TagActive = {
|
||||
enum_name = Active,
|
||||
callback_type = Box<dyn FnMut(&TagHandle, bool) + Send + 'static>,
|
||||
client_request = tag_active,
|
||||
on_response = |response, callbacks, api| {
|
||||
if let Some(tag_id) = response.tag_id {
|
||||
let handle = api.tag.new_handle(tag_id);
|
||||
|
||||
for callback in callbacks {
|
||||
callback(&handle, response.active.unwrap());
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type SingleOutputFn = Box<dyn FnMut(&OutputHandle) + Send + 'static>;
|
||||
|
@ -185,8 +256,20 @@ pub(crate) type SingleWindowFn = Box<dyn FnMut(&WindowHandle) + Send + 'static>;
|
|||
|
||||
pub(crate) struct SignalState {
|
||||
pub(crate) output_connect: SignalData<OutputConnect>,
|
||||
pub(crate) output_disconnect: SignalData<OutputDisconnect>,
|
||||
pub(crate) output_resize: SignalData<OutputResize>,
|
||||
pub(crate) output_move: SignalData<OutputMove>,
|
||||
|
||||
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
|
||||
pub(crate) window_pointer_leave: SignalData<WindowPointerLeave>,
|
||||
|
||||
pub(crate) tag_active: SignalData<TagActive>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SignalState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SignalState").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalState {
|
||||
|
@ -197,10 +280,24 @@ impl SignalState {
|
|||
let client = SignalServiceClient::new(channel);
|
||||
Self {
|
||||
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
output_disconnect: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
output_resize: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
output_move: 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()),
|
||||
tag_active: SignalData::new(client.clone(), fut_sender.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||
self.output_connect.api.set(api.clone()).unwrap();
|
||||
self.output_disconnect.api.set(api.clone()).unwrap();
|
||||
self.output_resize.api.set(api.clone()).unwrap();
|
||||
self.output_move.api.set(api.clone()).unwrap();
|
||||
self.window_pointer_enter.api.set(api.clone()).unwrap();
|
||||
self.window_pointer_leave.api.set(api.clone()).unwrap();
|
||||
self.tag_active.api.set(api.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
@ -208,6 +305,7 @@ pub(crate) struct SignalConnId(pub(crate) u32);
|
|||
|
||||
pub(crate) struct SignalData<S: Signal> {
|
||||
client: SignalServiceClient<Channel>,
|
||||
api: OnceLock<ApiModules>,
|
||||
fut_sender: UnboundedSender<BoxFuture<'static, ()>>,
|
||||
callback_sender: Option<UnboundedSender<(SignalConnId, S::Callback)>>,
|
||||
remove_callback_sender: Option<UnboundedSender<SignalConnId>>,
|
||||
|
@ -223,6 +321,7 @@ impl<S: Signal> SignalData<S> {
|
|||
) -> Self {
|
||||
Self {
|
||||
client,
|
||||
api: OnceLock::new(),
|
||||
fut_sender,
|
||||
callback_sender: Default::default(),
|
||||
remove_callback_sender: Default::default(),
|
||||
|
@ -244,13 +343,14 @@ fn connect_signal<Req, Resp, F, T, O>(
|
|||
callback_count: Arc<AtomicU32>,
|
||||
to_in_stream: T,
|
||||
mut on_response: O,
|
||||
api: ApiModules,
|
||||
) -> ConnectSignalChannels<F>
|
||||
where
|
||||
Req: SignalRequest + Send + 'static,
|
||||
Resp: Send + 'static,
|
||||
F: Send + 'static,
|
||||
T: FnOnce(UnboundedReceiverStream<Req>) -> Streaming<Resp>,
|
||||
O: FnMut(Resp, btree_map::ValuesMut<'_, SignalConnId, F>) + Send + 'static,
|
||||
O: FnMut(Resp, btree_map::ValuesMut<'_, SignalConnId, F>, &ApiModules) + Send + 'static,
|
||||
{
|
||||
let (control_sender, recv) = unbounded_channel::<Req>();
|
||||
let out_stream = UnboundedReceiverStream::new(recv);
|
||||
|
@ -283,7 +383,7 @@ where
|
|||
|
||||
match response {
|
||||
Ok(response) => {
|
||||
on_response(response, callbacks.values_mut());
|
||||
on_response(response, callbacks.values_mut(), &api);
|
||||
|
||||
control_sender
|
||||
.send(Req::from_control(StreamControl::Ready))
|
||||
|
|
|
@ -29,8 +29,9 @@
|
|||
//!
|
||||
//! These [`TagHandle`]s allow you to manipulate individual tags and get their properties.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use futures::FutureExt;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use pinnacle_api_defs::pinnacle::{
|
||||
tag::{
|
||||
self,
|
||||
|
@ -43,25 +44,39 @@ use pinnacle_api_defs::pinnacle::{
|
|||
};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::{block_on_tokio, output::OutputHandle, util::Batch, OUTPUT};
|
||||
use crate::{
|
||||
block_on_tokio,
|
||||
output::OutputHandle,
|
||||
signal::{SignalHandle, TagSignal},
|
||||
util::Batch,
|
||||
window::WindowHandle,
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
/// A struct that allows you to add and remove tags and get [`TagHandle`]s.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tag {
|
||||
tag_client: TagServiceClient<Channel>,
|
||||
api: OnceLock<ApiModules>,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub(crate) fn new(channel: Channel) -> Self {
|
||||
Self {
|
||||
tag_client: TagServiceClient::new(channel.clone()),
|
||||
api: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||
self.api.set(api).unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn new_handle(&self, id: u32) -> TagHandle {
|
||||
TagHandle {
|
||||
id,
|
||||
tag_client: self.tag_client.clone(),
|
||||
api: self.api.get().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,8 +172,7 @@ impl Tag {
|
|||
/// The async version of [`Tag::get`].
|
||||
pub async fn get_async(&self, name: impl Into<String>) -> Option<TagHandle> {
|
||||
let name = name.into();
|
||||
let output_module = OUTPUT.get().expect("OUTPUT doesn't exist");
|
||||
let focused_output = output_module.get_focused();
|
||||
let focused_output = self.api.get().unwrap().output.get_focused();
|
||||
|
||||
if let Some(output) = focused_output {
|
||||
self.get_on_output_async(name, &output).await
|
||||
|
@ -220,6 +234,19 @@ impl Tag {
|
|||
|
||||
block_on_tokio(client.remove(RemoveRequest { tag_ids })).unwrap();
|
||||
}
|
||||
|
||||
/// 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(self.api.get().unwrap().signal.write());
|
||||
|
||||
match signal {
|
||||
TagSignal::Active(f) => signal_state.tag_active.add_callback(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a tag.
|
||||
|
@ -229,6 +256,7 @@ impl Tag {
|
|||
pub struct TagHandle {
|
||||
pub(crate) id: u32,
|
||||
tag_client: TagServiceClient<Channel>,
|
||||
api: ApiModules,
|
||||
}
|
||||
|
||||
impl PartialEq for TagHandle {
|
||||
|
@ -245,26 +273,6 @@ impl std::hash::Hash for TagHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Various static layouts.
|
||||
#[repr(i32)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, TryFromPrimitive)]
|
||||
pub enum Layout {
|
||||
/// One master window on the left with all other windows stacked to the right
|
||||
MasterStack = 1,
|
||||
/// Windows split in half towards the bottom right corner
|
||||
Dwindle,
|
||||
/// Windows split in half in a spiral
|
||||
Spiral,
|
||||
/// One main corner window in the top left with a column of windows on the right and a row on the bottom
|
||||
CornerTopLeft,
|
||||
/// One main corner window in the top right with a column of windows on the left and a row on the bottom
|
||||
CornerTopRight,
|
||||
/// One main corner window in the bottom left with a column of windows on the right and a row on the top.
|
||||
CornerBottomLeft,
|
||||
/// One main corner window in the bottom right with a column of windows on the left and a row on the top.
|
||||
CornerBottomRight,
|
||||
}
|
||||
|
||||
impl TagHandle {
|
||||
/// Activate this tag and deactivate all other ones on the same output.
|
||||
///
|
||||
|
@ -396,12 +404,18 @@ impl TagHandle {
|
|||
.unwrap()
|
||||
.into_inner();
|
||||
|
||||
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
|
||||
let output = self.api.output;
|
||||
let window = self.api.window;
|
||||
|
||||
TagProperties {
|
||||
active: response.active,
|
||||
name: response.name,
|
||||
output: response.output_name.map(|name| output.new_handle(name)),
|
||||
windows: response
|
||||
.window_ids
|
||||
.into_iter()
|
||||
.map(|id| window.new_handle(id))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,6 +454,18 @@ impl TagHandle {
|
|||
pub async fn output_async(&self) -> Option<OutputHandle> {
|
||||
self.props_async().await.output
|
||||
}
|
||||
|
||||
/// Get all windows with this tag.
|
||||
///
|
||||
/// Shorthand for `self.props().windows`.
|
||||
pub fn windows(&self) -> Vec<WindowHandle> {
|
||||
self.props().windows
|
||||
}
|
||||
|
||||
/// The async version of [`TagHandle::windows`].
|
||||
pub async fn windows_async(&self) -> Vec<WindowHandle> {
|
||||
self.props_async().await.windows
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties of a tag.
|
||||
|
@ -451,4 +477,6 @@ pub struct TagProperties {
|
|||
pub name: Option<String>,
|
||||
/// The output the tag is on
|
||||
pub output: Option<OutputHandle>,
|
||||
/// The windows that have this tag
|
||||
pub windows: Vec<WindowHandle>,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
//!
|
||||
//! This module also allows you to set window rules; see the [rules] module for more information.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use futures::FutureExt;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use pinnacle_api_defs::pinnacle::{
|
||||
|
@ -34,7 +36,7 @@ use crate::{
|
|||
signal::{SignalHandle, WindowSignal},
|
||||
tag::TagHandle,
|
||||
util::{Batch, Geometry},
|
||||
SIGNAL, TAG,
|
||||
ApiModules,
|
||||
};
|
||||
|
||||
use self::rules::{WindowRule, WindowRuleCondition};
|
||||
|
@ -47,19 +49,26 @@ pub mod rules;
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct Window {
|
||||
window_client: WindowServiceClient<Channel>,
|
||||
api: OnceLock<ApiModules>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(channel: Channel) -> Self {
|
||||
Self {
|
||||
window_client: WindowServiceClient::new(channel.clone()),
|
||||
api: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_init(&self, api: ApiModules) {
|
||||
self.api.set(api).unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn new_handle(&self, id: u32) -> WindowHandle {
|
||||
WindowHandle {
|
||||
id,
|
||||
window_client: self.window_client.clone(),
|
||||
api: self.api.get().unwrap().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +189,7 @@ impl Window {
|
|||
/// You can pass in a [`WindowSignal`] 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: WindowSignal) -> SignalHandle {
|
||||
let mut signal_state = block_on_tokio(SIGNAL.get().expect("SIGNAL doesn't exist").write());
|
||||
let mut signal_state = block_on_tokio(self.api.get().unwrap().signal.write());
|
||||
|
||||
match signal {
|
||||
WindowSignal::PointerEnter(f) => signal_state.window_pointer_enter.add_callback(f),
|
||||
|
@ -196,6 +205,7 @@ impl Window {
|
|||
pub struct WindowHandle {
|
||||
id: u32,
|
||||
window_client: WindowServiceClient<Channel>,
|
||||
api: ApiModules,
|
||||
}
|
||||
|
||||
impl PartialEq for WindowHandle {
|
||||
|
@ -572,8 +582,6 @@ impl WindowHandle {
|
|||
height: geo.height() as u32,
|
||||
});
|
||||
|
||||
let tag = TAG.get().expect("TAG doesn't exist");
|
||||
|
||||
WindowProperties {
|
||||
geometry,
|
||||
class: response.class,
|
||||
|
@ -584,7 +592,7 @@ impl WindowHandle {
|
|||
tags: response
|
||||
.tag_ids
|
||||
.into_iter()
|
||||
.map(|id| tag.new_handle(id))
|
||||
.map(|id| self.api.tag.new_handle(id))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,8 +63,12 @@ pub mod pinnacle {
|
|||
|
||||
impl_signal_request!(
|
||||
OutputConnectRequest,
|
||||
OutputDisconnectRequest,
|
||||
OutputResizeRequest,
|
||||
OutputMoveRequest,
|
||||
WindowPointerEnterRequest,
|
||||
WindowPointerLeaveRequest
|
||||
WindowPointerLeaveRequest,
|
||||
TagActiveRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
120
src/api.rs
120
src/api.rs
|
@ -16,7 +16,7 @@ use pinnacle_api_defs::pinnacle::{
|
|||
self,
|
||||
v0alpha1::{
|
||||
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
|
||||
SetModeRequest, SetScaleRequest,
|
||||
SetModeRequest, SetScaleRequest, SetTransformRequest,
|
||||
},
|
||||
},
|
||||
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
|
||||
|
@ -30,11 +30,13 @@ use pinnacle_api_defs::pinnacle::{
|
|||
SwitchToRequest,
|
||||
},
|
||||
},
|
||||
v0alpha1::{pinnacle_service_server, PingRequest, PingResponse, QuitRequest, SetOrToggle},
|
||||
v0alpha1::{
|
||||
pinnacle_service_server, PingRequest, PingResponse, QuitRequest, ReloadConfigRequest,
|
||||
SetOrToggle,
|
||||
},
|
||||
};
|
||||
use smithay::{
|
||||
backend::renderer::TextureFilter,
|
||||
desktop::layer_map_for_output,
|
||||
input::keyboard::XkbConfig,
|
||||
output::Scale,
|
||||
reexports::{calloop, input as libinput},
|
||||
|
@ -193,6 +195,18 @@ impl pinnacle_service_server::PinnacleService for PinnacleService {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn reload_config(
|
||||
&self,
|
||||
_request: Request<ReloadConfigRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
run_unary_no_response(&self.sender, |state| {
|
||||
state
|
||||
.start_config(state.config.dir(&state.xdg_base_dirs))
|
||||
.expect("failed to restart config");
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn ping(&self, request: Request<PingRequest>) -> Result<Response<PingResponse>, Status> {
|
||||
let payload = request.into_inner().payload;
|
||||
Ok(Response::new(PingResponse { payload }))
|
||||
|
@ -707,9 +721,9 @@ impl tag_service_server::TagService for TagService {
|
|||
};
|
||||
|
||||
match set_or_toggle {
|
||||
SetOrToggle::Set => tag.set_active(true),
|
||||
SetOrToggle::Unset => tag.set_active(false),
|
||||
SetOrToggle::Toggle => tag.set_active(!tag.active()),
|
||||
SetOrToggle::Set => tag.set_active(true, state),
|
||||
SetOrToggle::Unset => tag.set_active(false, state),
|
||||
SetOrToggle::Toggle => tag.set_active(!tag.active(), state),
|
||||
SetOrToggle::Unspecified => unreachable!(),
|
||||
}
|
||||
|
||||
|
@ -737,11 +751,11 @@ impl tag_service_server::TagService for TagService {
|
|||
let Some(tag) = tag_id.tag(state) else { return };
|
||||
let Some(output) = tag.output(state) else { return };
|
||||
|
||||
output.with_state_mut(|state| {
|
||||
for op_tag in state.tags.iter_mut() {
|
||||
op_tag.set_active(false);
|
||||
output.with_state_mut(|op_state| {
|
||||
for op_tag in op_state.tags.iter_mut() {
|
||||
op_tag.set_active(false, state);
|
||||
}
|
||||
tag.set_active(true);
|
||||
tag.set_active(true, state);
|
||||
});
|
||||
|
||||
state.request_layout(&output);
|
||||
|
@ -874,11 +888,26 @@ impl tag_service_server::TagService for TagService {
|
|||
.map(|output| output.name());
|
||||
let active = tag.as_ref().map(|tag| tag.active());
|
||||
let name = tag.as_ref().map(|tag| tag.name());
|
||||
let window_ids = tag
|
||||
.as_ref()
|
||||
.map(|tag| {
|
||||
state
|
||||
.windows
|
||||
.iter()
|
||||
.filter_map(|win| {
|
||||
win.with_state(|win_state| {
|
||||
win_state.tags.contains(tag).then_some(win_state.id.0)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
tag::v0alpha1::GetPropertiesResponse {
|
||||
active,
|
||||
name,
|
||||
output_name,
|
||||
window_ids,
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
@ -940,8 +969,7 @@ impl output_service_server::OutputService for OutputService {
|
|||
if let Some(y) = y {
|
||||
loc.y = y;
|
||||
}
|
||||
output.change_current_state(None, None, None, Some(loc));
|
||||
state.space.map_output(&output, loc);
|
||||
state.change_output_state(&output, None, None, None, Some(loc));
|
||||
debug!("Mapping output {} to {loc:?}", output.name());
|
||||
state.request_layout(&output);
|
||||
})
|
||||
|
@ -1001,8 +1029,49 @@ impl output_service_server::OutputService for OutputService {
|
|||
|
||||
current_scale = f64::max(current_scale, 0.25);
|
||||
|
||||
output.change_current_state(None, None, Some(Scale::Fractional(current_scale)), None);
|
||||
layer_map_for_output(&output).arrange();
|
||||
state.change_output_state(
|
||||
&output,
|
||||
None,
|
||||
None,
|
||||
Some(Scale::Fractional(current_scale)),
|
||||
None,
|
||||
);
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn set_transform(
|
||||
&self,
|
||||
request: Request<SetTransformRequest>,
|
||||
) -> Result<Response<()>, Status> {
|
||||
let request = request.into_inner();
|
||||
|
||||
let smithay_transform = match request.transform() {
|
||||
output::v0alpha1::Transform::Unspecified => {
|
||||
return Err(Status::invalid_argument("transform was unspecified"));
|
||||
}
|
||||
output::v0alpha1::Transform::Normal => smithay::utils::Transform::Normal,
|
||||
output::v0alpha1::Transform::Transform90 => smithay::utils::Transform::_90,
|
||||
output::v0alpha1::Transform::Transform180 => smithay::utils::Transform::_180,
|
||||
output::v0alpha1::Transform::Transform270 => smithay::utils::Transform::_270,
|
||||
output::v0alpha1::Transform::Flipped => smithay::utils::Transform::Flipped,
|
||||
output::v0alpha1::Transform::Flipped90 => smithay::utils::Transform::Flipped90,
|
||||
output::v0alpha1::Transform::Flipped180 => smithay::utils::Transform::Flipped180,
|
||||
output::v0alpha1::Transform::Flipped270 => smithay::utils::Transform::Flipped270,
|
||||
};
|
||||
|
||||
let Some(output_name) = request.output_name else {
|
||||
return Err(Status::invalid_argument("output_name was null"));
|
||||
};
|
||||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
let Some(output) = OutputName(output_name).output(state) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.change_output_state(&output, None, Some(smithay_transform), None, None);
|
||||
state.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
})
|
||||
|
@ -1109,6 +1178,27 @@ impl output_service_server::OutputService for OutputService {
|
|||
.as_ref()
|
||||
.map(|output| output.current_scale().fractional_scale() as f32);
|
||||
|
||||
let transform = output.as_ref().map(|output| {
|
||||
(match output.current_transform() {
|
||||
smithay::utils::Transform::Normal => output::v0alpha1::Transform::Normal,
|
||||
smithay::utils::Transform::_90 => output::v0alpha1::Transform::Transform90,
|
||||
smithay::utils::Transform::_180 => output::v0alpha1::Transform::Transform180,
|
||||
smithay::utils::Transform::_270 => output::v0alpha1::Transform::Transform270,
|
||||
smithay::utils::Transform::Flipped => output::v0alpha1::Transform::Flipped,
|
||||
smithay::utils::Transform::Flipped90 => output::v0alpha1::Transform::Flipped90,
|
||||
smithay::utils::Transform::Flipped180 => {
|
||||
output::v0alpha1::Transform::Flipped180
|
||||
}
|
||||
smithay::utils::Transform::Flipped270 => {
|
||||
output::v0alpha1::Transform::Flipped270
|
||||
}
|
||||
}) as i32
|
||||
});
|
||||
|
||||
let serial = output.as_ref().and_then(|output| {
|
||||
output.with_state(|state| state.serial.map(|serial| serial.get()))
|
||||
});
|
||||
|
||||
output::v0alpha1::GetPropertiesResponse {
|
||||
make,
|
||||
model,
|
||||
|
@ -1124,6 +1214,8 @@ impl output_service_server::OutputService for OutputService {
|
|||
focused,
|
||||
tag_ids,
|
||||
scale,
|
||||
transform,
|
||||
serial,
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, SignalRequest,
|
||||
StreamControl, WindowPointerEnterRequest, WindowPointerEnterResponse,
|
||||
WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputDisconnectRequest,
|
||||
OutputDisconnectResponse, OutputMoveRequest, OutputMoveResponse, OutputResizeRequest,
|
||||
OutputResizeResponse, SignalRequest, StreamControl, TagActiveRequest, TagActiveResponse,
|
||||
WindowPointerEnterRequest, WindowPointerEnterResponse, WindowPointerLeaveRequest,
|
||||
WindowPointerLeaveResponse,
|
||||
};
|
||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
@ -15,16 +17,28 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignalState {
|
||||
// Output
|
||||
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
||||
pub output_disconnect: SignalData<OutputDisconnectResponse, VecDeque<OutputDisconnectResponse>>,
|
||||
pub output_resize: SignalData<OutputResizeResponse, VecDeque<OutputResizeResponse>>,
|
||||
pub output_move: SignalData<OutputMoveResponse, VecDeque<OutputMoveResponse>>,
|
||||
|
||||
// Window
|
||||
pub window_pointer_enter:
|
||||
SignalData<WindowPointerEnterResponse, VecDeque<WindowPointerEnterResponse>>,
|
||||
pub window_pointer_leave:
|
||||
SignalData<WindowPointerLeaveResponse, VecDeque<WindowPointerLeaveResponse>>,
|
||||
|
||||
// Tag
|
||||
pub tag_active: SignalData<TagActiveResponse, VecDeque<TagActiveResponse>>,
|
||||
}
|
||||
|
||||
impl SignalState {
|
||||
pub fn clear(&mut self) {
|
||||
self.output_connect.disconnect();
|
||||
self.output_disconnect.disconnect();
|
||||
self.output_resize.disconnect();
|
||||
self.output_move.disconnect();
|
||||
self.window_pointer_enter.disconnect();
|
||||
self.window_pointer_leave.disconnect();
|
||||
}
|
||||
|
@ -171,9 +185,15 @@ impl SignalService {
|
|||
#[tonic::async_trait]
|
||||
impl signal_service_server::SignalService for SignalService {
|
||||
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
||||
type OutputDisconnectStream = ResponseStream<OutputDisconnectResponse>;
|
||||
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
|
||||
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
|
||||
|
||||
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
||||
type WindowPointerLeaveStream = ResponseStream<WindowPointerLeaveResponse>;
|
||||
|
||||
type TagActiveStream = ResponseStream<TagActiveResponse>;
|
||||
|
||||
async fn output_connect(
|
||||
&self,
|
||||
request: Request<Streaming<OutputConnectRequest>>,
|
||||
|
@ -185,6 +205,39 @@ impl signal_service_server::SignalService for SignalService {
|
|||
})
|
||||
}
|
||||
|
||||
async fn output_disconnect(
|
||||
&self,
|
||||
request: Request<Streaming<OutputDisconnectRequest>>,
|
||||
) -> Result<Response<Self::OutputDisconnectStream>, Status> {
|
||||
let in_stream = request.into_inner();
|
||||
|
||||
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||
&mut state.signal_state.output_disconnect
|
||||
})
|
||||
}
|
||||
|
||||
async fn output_resize(
|
||||
&self,
|
||||
request: Request<Streaming<OutputResizeRequest>>,
|
||||
) -> Result<Response<Self::OutputResizeStream>, Status> {
|
||||
let in_stream = request.into_inner();
|
||||
|
||||
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||
&mut state.signal_state.output_resize
|
||||
})
|
||||
}
|
||||
|
||||
async fn output_move(
|
||||
&self,
|
||||
request: Request<Streaming<OutputMoveRequest>>,
|
||||
) -> Result<Response<Self::OutputMoveStream>, Status> {
|
||||
let in_stream = request.into_inner();
|
||||
|
||||
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||
&mut state.signal_state.output_move
|
||||
})
|
||||
}
|
||||
|
||||
async fn window_pointer_enter(
|
||||
&self,
|
||||
request: Request<Streaming<WindowPointerEnterRequest>>,
|
||||
|
@ -206,4 +259,15 @@ impl signal_service_server::SignalService for SignalService {
|
|||
&mut state.signal_state.window_pointer_leave
|
||||
})
|
||||
}
|
||||
|
||||
async fn tag_active(
|
||||
&self,
|
||||
request: Request<Streaming<TagActiveRequest>>,
|
||||
) -> Result<Response<Self::TagActiveStream>, Status> {
|
||||
let in_stream = request.into_inner();
|
||||
|
||||
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||
&mut state.signal_state.tag_active
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse,
|
||||
};
|
||||
use smithay::backend::renderer::test::DummyRenderer;
|
||||
use smithay::backend::renderer::ImportMemWl;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -116,3 +120,45 @@ pub fn setup_dummy(
|
|||
|
||||
Ok((state, event_loop))
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new_output(&mut self, name: impl std::fmt::Display, size: Size<i32, Physical>) {
|
||||
let mode = smithay::output::Mode {
|
||||
size,
|
||||
refresh: 144_000,
|
||||
};
|
||||
|
||||
let physical_properties = smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "Pinnacle".to_string(),
|
||||
model: "Dummy Output".to_string(),
|
||||
};
|
||||
|
||||
let output = Output::new(name.to_string(), physical_properties);
|
||||
|
||||
output.change_current_state(Some(mode), None, None, Some((0, 0).into()));
|
||||
|
||||
output.set_preferred(mode);
|
||||
|
||||
output.create_global::<State>(&self.display_handle);
|
||||
|
||||
self.space.map_output(&output, (0, 0));
|
||||
|
||||
self.signal_state.output_connect.signal(|buf| {
|
||||
buf.push_back(OutputConnectResponse {
|
||||
output_name: Some(output.name()),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, output: &Output) {
|
||||
self.space.unmap_output(output);
|
||||
|
||||
self.signal_state.output_disconnect.signal(|buffer| {
|
||||
buffer.push_back(OutputDisconnectResponse {
|
||||
output_name: Some(output.name()),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context};
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse,
|
||||
};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
|
@ -49,10 +51,7 @@ use smithay::{
|
|||
vulkan::{self, version::Version, PhysicalDevice},
|
||||
SwapBuffersError,
|
||||
},
|
||||
desktop::{
|
||||
layer_map_for_output,
|
||||
utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||
},
|
||||
desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||
input::pointer::CursorImageStatus,
|
||||
output::{Output, PhysicalProperties, Subpixel},
|
||||
reexports::{
|
||||
|
@ -272,16 +271,14 @@ impl State {
|
|||
{
|
||||
match render_surface.compositor.use_mode(drm_mode) {
|
||||
Ok(()) => {
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
layer_map_for_output(output).arrange();
|
||||
self.change_output_state(output, Some(mode), None, None, None);
|
||||
}
|
||||
Err(err) => error!("Failed to resize output: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
layer_map_for_output(output).arrange();
|
||||
self.change_output_state(output, Some(mode), None, None, None);
|
||||
}
|
||||
|
||||
self.schedule_render(output);
|
||||
|
@ -1010,11 +1007,11 @@ impl State {
|
|||
connector.interface_id()
|
||||
);
|
||||
|
||||
let (make, model) = EdidInfo::try_from_connector(&device.drm, connector.handle())
|
||||
.map(|info| (info.manufacturer, info.model))
|
||||
let (make, model, serial) = EdidInfo::try_from_connector(&device.drm, connector.handle())
|
||||
.map(|info| (info.manufacturer, info.model, info.serial))
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to parse EDID info: {err}");
|
||||
("Unknown".into(), "Unknown".into())
|
||||
("Unknown".into(), "Unknown".into(), None)
|
||||
});
|
||||
|
||||
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
|
||||
|
@ -1038,10 +1035,14 @@ impl State {
|
|||
);
|
||||
let global = output.create_global::<State>(&udev.display_handle);
|
||||
|
||||
output.with_state_mut(|state| state.serial = serial);
|
||||
|
||||
for mode in modes {
|
||||
output.add_mode(mode);
|
||||
}
|
||||
|
||||
output.set_preferred(wl_mode);
|
||||
|
||||
self.output_focus_stack.set_focus(output.clone());
|
||||
|
||||
let x = self.space.outputs().fold(0, |acc, o| {
|
||||
|
@ -1052,10 +1053,6 @@ impl State {
|
|||
});
|
||||
let position = (x, 0).into();
|
||||
|
||||
output.set_preferred(wl_mode);
|
||||
output.change_current_state(Some(wl_mode), None, None, Some(position));
|
||||
self.space.map_output(&output, position);
|
||||
|
||||
output.user_data().insert_if_missing(|| UdevOutputData {
|
||||
crtc,
|
||||
device_id: node,
|
||||
|
@ -1122,6 +1119,8 @@ impl State {
|
|||
|
||||
device.surfaces.insert(crtc, surface);
|
||||
|
||||
self.change_output_state(&output, Some(wl_mode), None, None, Some(position));
|
||||
|
||||
// If there is saved connector state, the connector was previously plugged in.
|
||||
// In this case, restore its tags and location.
|
||||
// TODO: instead of checking the connector, check the monitor's edid info instead
|
||||
|
@ -1131,11 +1130,8 @@ impl State {
|
|||
.get(&OutputName(output.name()))
|
||||
{
|
||||
let ConnectorSavedState { loc, tags, scale } = saved_state;
|
||||
|
||||
output.change_current_state(None, None, *scale, Some(*loc));
|
||||
self.space.map_output(&output, *loc);
|
||||
|
||||
output.with_state_mut(|state| state.tags = tags.clone());
|
||||
self.change_output_state(&output, None, None, *scale, Some(*loc));
|
||||
} else {
|
||||
self.signal_state.output_connect.signal(|buffer| {
|
||||
buffer.push_back(OutputConnectResponse {
|
||||
|
@ -1187,6 +1183,12 @@ impl State {
|
|||
);
|
||||
self.space.unmap_output(&output);
|
||||
self.gamma_control_manager_state.output_removed(&output);
|
||||
|
||||
self.signal_state.output_disconnect.signal(|buffer| {
|
||||
buffer.push_back(OutputDisconnectResponse {
|
||||
output_name: Some(output.name()),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle};
|
||||
|
||||
// A bunch of this stuff is from cosmic-comp
|
||||
|
@ -6,6 +8,7 @@ use smithay::reexports::drm::control::{connector, property, Device, ResourceHand
|
|||
pub struct EdidInfo {
|
||||
pub model: String,
|
||||
pub manufacturer: String,
|
||||
pub serial: Option<NonZeroU32>,
|
||||
}
|
||||
|
||||
impl EdidInfo {
|
||||
|
@ -55,6 +58,9 @@ fn parse_edid(buffer: &[u8]) -> anyhow::Result<EdidInfo> {
|
|||
|
||||
let manufacturer = get_manufacturer([char1 as char, char2 as char, char3 as char]);
|
||||
|
||||
// INFO: This probably *isn't* completely unique between all monitors
|
||||
let serial = u32::from_le_bytes(buffer[12..=15].try_into()?);
|
||||
|
||||
// Monitor names are inside of these display/monitor descriptors at bytes 72..=125.
|
||||
// Each descriptor is 18 bytes long.
|
||||
let descriptor1 = &buffer[72..=89];
|
||||
|
@ -99,6 +105,7 @@ fn parse_edid(buffer: &[u8]) -> anyhow::Result<EdidInfo> {
|
|||
Ok(EdidInfo {
|
||||
model,
|
||||
manufacturer,
|
||||
serial: NonZeroU32::new(serial),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ use smithay::{
|
|||
},
|
||||
winit::{self, WinitEvent, WinitGraphicsBackend},
|
||||
},
|
||||
desktop::layer_map_for_output,
|
||||
input::pointer::CursorImageStatus,
|
||||
output::{Output, Scale, Subpixel},
|
||||
reexports::{
|
||||
|
@ -109,7 +108,7 @@ pub fn setup_winit(
|
|||
model: "Winit Window".to_string(),
|
||||
};
|
||||
|
||||
let output = Output::new("Pinnacle window".to_string(), physical_properties);
|
||||
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
|
||||
|
||||
output.change_current_state(
|
||||
Some(mode),
|
||||
|
@ -221,13 +220,13 @@ pub fn setup_winit(
|
|||
size,
|
||||
refresh: 144_000,
|
||||
};
|
||||
output.change_current_state(
|
||||
state.change_output_state(
|
||||
&output,
|
||||
Some(mode),
|
||||
None,
|
||||
Some(Scale::Fractional(scale_factor)),
|
||||
None,
|
||||
);
|
||||
layer_map_for_output(&output).arrange();
|
||||
state.request_layout(&output);
|
||||
}
|
||||
WinitEvent::Focus(focused) => {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::{cell::RefCell, num::NonZeroU32};
|
||||
|
||||
use smithay::output::Output;
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputResizeResponse};
|
||||
use smithay::{
|
||||
desktop::layer_map_for_output,
|
||||
output::{Mode, Output, Scale},
|
||||
utils::{Logical, Point, Transform},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
focus::WindowKeyboardFocusStack,
|
||||
|
@ -35,6 +41,7 @@ pub struct OutputState {
|
|||
pub tags: Vec<Tag>,
|
||||
pub focus_stack: WindowKeyboardFocusStack,
|
||||
pub screencopy: Option<Screencopy>,
|
||||
pub serial: Option<NonZeroU32>,
|
||||
}
|
||||
|
||||
impl WithState for Output {
|
||||
|
@ -68,3 +75,40 @@ impl OutputState {
|
|||
self.tags.iter().filter(|tag| tag.active())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// A wrapper around [`Output::change_current_state`] that additionally sends an output
|
||||
/// geometry signal.
|
||||
pub fn change_output_state(
|
||||
&mut self,
|
||||
output: &Output,
|
||||
mode: Option<Mode>,
|
||||
transform: Option<Transform>,
|
||||
scale: Option<Scale>,
|
||||
location: Option<Point<i32, Logical>>,
|
||||
) {
|
||||
output.change_current_state(mode, transform, scale, location);
|
||||
if let Some(location) = location {
|
||||
info!(?location);
|
||||
self.space.map_output(output, location);
|
||||
self.signal_state.output_move.signal(|buf| {
|
||||
buf.push_back(OutputMoveResponse {
|
||||
output_name: Some(output.name()),
|
||||
x: Some(location.x),
|
||||
y: Some(location.y),
|
||||
});
|
||||
});
|
||||
}
|
||||
if mode.is_some() || transform.is_some() || scale.is_some() {
|
||||
layer_map_for_output(output).arrange();
|
||||
self.signal_state.output_resize.signal(|buf| {
|
||||
let geo = self.space.output_geometry(output);
|
||||
buf.push_back(OutputResizeResponse {
|
||||
output_name: Some(output.name()),
|
||||
logical_width: geo.map(|geo| geo.size.w as u32),
|
||||
logical_height: geo.map(|geo| geo.size.h as u32),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
38
src/tag.rs
38
src/tag.rs
|
@ -1,10 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
hash::Hash,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
use smithay::output::Output;
|
||||
|
@ -63,31 +64,46 @@ impl Eq for TagInner {}
|
|||
///
|
||||
/// A window may have 0 or more tags, and you can display 0 or more tags
|
||||
/// on each output at a time.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Tag(Rc<RefCell<TagInner>>);
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tag(Arc<Mutex<TagInner>>);
|
||||
|
||||
impl PartialEq for Tag {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
// RefCell Safety: These methods should never panic because they are all self-contained or Copy.
|
||||
impl Tag {
|
||||
pub fn id(&self) -> TagId {
|
||||
self.0.borrow().id
|
||||
self.0.lock().expect("tag already locked").id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.0.borrow().name.clone()
|
||||
self.0.lock().expect("tag already locked").name.clone()
|
||||
}
|
||||
|
||||
pub fn active(&self) -> bool {
|
||||
self.0.borrow().active
|
||||
self.0.lock().expect("tag already locked").active
|
||||
}
|
||||
|
||||
pub fn set_active(&self, active: bool) {
|
||||
self.0.borrow_mut().active = active;
|
||||
pub fn set_active(&self, active: bool, state: &mut State) {
|
||||
self.0.lock().expect("tag already locked").active = active;
|
||||
|
||||
state.signal_state.tag_active.signal(|buf| {
|
||||
buf.push_back(
|
||||
pinnacle_api_defs::pinnacle::signal::v0alpha1::TagActiveResponse {
|
||||
tag_id: Some(self.id().0),
|
||||
active: Some(self.active()),
|
||||
},
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self(Rc::new(RefCell::new(TagInner {
|
||||
Self(Arc::new(Mutex::new(TagInner {
|
||||
id: TagId::next(),
|
||||
name,
|
||||
active: false,
|
||||
|
|
88
tests/common/mod.rs
Normal file
88
tests/common/mod.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
use std::{panic::UnwindSafe, time::Duration};
|
||||
|
||||
use pinnacle::{backend::dummy::setup_dummy, state::State};
|
||||
use smithay::{
|
||||
output::Output,
|
||||
reexports::calloop::{
|
||||
self,
|
||||
channel::{Event, Sender},
|
||||
},
|
||||
};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn with_state(
|
||||
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
|
||||
with_state: impl FnOnce(&mut State) + Send + 'static,
|
||||
) {
|
||||
sender.send(Box::new(with_state)).unwrap();
|
||||
}
|
||||
|
||||
pub fn sleep_secs(secs: u64) {
|
||||
std::thread::sleep(Duration::from_secs(secs));
|
||||
}
|
||||
|
||||
pub fn test_api(
|
||||
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
|
||||
) -> anyhow::Result<()> {
|
||||
let (mut state, mut event_loop) = setup_dummy(true, None)?;
|
||||
|
||||
let (sender, recv) = calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
|
||||
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(recv, |event, _, state| match event {
|
||||
Event::Msg(f) => f(state),
|
||||
Event::Closed => (),
|
||||
})
|
||||
.map_err(|_| anyhow::anyhow!("failed to insert source"))?;
|
||||
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
|
||||
state.start_grpc_server(tempdir.path())?;
|
||||
|
||||
let loop_signal = event_loop.get_signal();
|
||||
|
||||
let join_handle = std::thread::spawn(move || {
|
||||
let res = std::panic::catch_unwind(|| {
|
||||
test(sender);
|
||||
});
|
||||
loop_signal.stop();
|
||||
if let Err(err) = res {
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
});
|
||||
|
||||
event_loop.run(None, &mut state, |state| {
|
||||
state.fixup_z_layering();
|
||||
state.space.refresh();
|
||||
state.popup_manager.cleanup();
|
||||
|
||||
state
|
||||
.display_handle
|
||||
.flush_clients()
|
||||
.expect("failed to flush client buffers");
|
||||
|
||||
// TODO: couple these or something, this is really error-prone
|
||||
assert_eq!(
|
||||
state.windows.len(),
|
||||
state.z_index_stack.len(),
|
||||
"Length of `windows` and `z_index_stack` are different. \
|
||||
If you see this, report it to the developer."
|
||||
);
|
||||
})?;
|
||||
|
||||
if let Err(err) = join_handle.join() {
|
||||
panic!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn output_for_name(state: &State, name: &str) -> Output {
|
||||
state
|
||||
.space
|
||||
.outputs()
|
||||
.find(|op| op.name() == name)
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
917
tests/lua_api.rs
917
tests/lua_api.rs
File diff suppressed because it is too large
Load diff
354
tests/rust_api.rs
Normal file
354
tests/rust_api.rs
Normal file
|
@ -0,0 +1,354 @@
|
|||
mod common;
|
||||
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use pinnacle_api::ApiModules;
|
||||
use test_log::test;
|
||||
|
||||
use crate::common::output_for_name;
|
||||
use crate::common::{sleep_secs, test_api, with_state};
|
||||
|
||||
#[tokio::main]
|
||||
async fn run_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) {
|
||||
let (api, _recv) = pinnacle_api::connect().await.unwrap();
|
||||
|
||||
run(api.clone());
|
||||
}
|
||||
|
||||
fn run_rust(run: impl FnOnce(ApiModules) + Send + 'static) {
|
||||
std::thread::spawn(|| {
|
||||
run_rust_inner(run);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn setup_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) {
|
||||
let (api, recv) = pinnacle_api::connect().await.unwrap();
|
||||
|
||||
run(api.clone());
|
||||
|
||||
pinnacle_api::listen(api, recv).await;
|
||||
}
|
||||
|
||||
fn setup_rust(run: impl FnOnce(ApiModules) + Send + 'static) -> JoinHandle<()> {
|
||||
std::thread::spawn(|| {
|
||||
setup_rust_inner(run);
|
||||
})
|
||||
}
|
||||
|
||||
mod output {
|
||||
use pinnacle::state::WithState;
|
||||
use pinnacle_api::output::{Alignment, OutputId, OutputLoc, OutputSetup, UpdateLocsOn};
|
||||
use smithay::{output::Output, utils::Rectangle};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn setup() -> anyhow::Result<()> {
|
||||
test_api(|sender| {
|
||||
setup_rust(|api| {
|
||||
api.output.setup([
|
||||
OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"]),
|
||||
OutputSetup::new_with_matcher(|op| op.name().contains("Test"))
|
||||
.with_tags(["Test 4", "Test 5"]),
|
||||
OutputSetup::new(OutputId::name("Second"))
|
||||
.with_scale(2.0)
|
||||
.with_mode(pinnacle_api::output::Mode {
|
||||
pixel_width: 6900,
|
||||
pixel_height: 420,
|
||||
refresh_rate_millihertz: 69420,
|
||||
})
|
||||
.with_transform(pinnacle_api::output::Transform::_90),
|
||||
]);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
state.new_output("First", (300, 200).into());
|
||||
state.new_output("Second", (300, 200).into());
|
||||
state.new_output("Test Third", (300, 200).into());
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let original_op = output_for_name(state, "Pinnacle Window");
|
||||
let first_op = output_for_name(state, "First");
|
||||
let second_op = output_for_name(state, "Second");
|
||||
let test_third_op = output_for_name(state, "Test Third");
|
||||
|
||||
let tags_for = |output: &Output| {
|
||||
output
|
||||
.with_state(|state| state.tags.iter().map(|t| t.name()).collect::<Vec<_>>())
|
||||
};
|
||||
|
||||
let focused_tags_for = |output: &Output| {
|
||||
output.with_state(|state| {
|
||||
state.focused_tags().map(|t| t.name()).collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
assert_eq!(tags_for(&original_op), vec!["1", "2", "3"]);
|
||||
assert_eq!(tags_for(&first_op), vec!["1", "2", "3"]);
|
||||
assert_eq!(tags_for(&second_op), vec!["1", "2", "3"]);
|
||||
assert_eq!(
|
||||
tags_for(&test_third_op),
|
||||
vec!["1", "2", "3", "Test 4", "Test 5"]
|
||||
);
|
||||
|
||||
assert_eq!(focused_tags_for(&original_op), vec!["1"]);
|
||||
assert_eq!(focused_tags_for(&test_third_op), vec!["1"]);
|
||||
|
||||
assert_eq!(second_op.current_scale().fractional_scale(), 2.0);
|
||||
|
||||
let second_mode = second_op.current_mode().unwrap();
|
||||
assert_eq!(second_mode.size.w, 6900);
|
||||
assert_eq!(second_mode.size.h, 420);
|
||||
assert_eq!(second_mode.refresh, 69420);
|
||||
|
||||
assert_eq!(
|
||||
second_op.current_transform(),
|
||||
smithay::utils::Transform::_90
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> {
|
||||
test_api(|sender| {
|
||||
setup_rust(|api| {
|
||||
api.output.setup_locs(
|
||||
UpdateLocsOn::all(),
|
||||
[
|
||||
(OutputId::name("Pinnacle Window"), OutputLoc::Point(0, 0)),
|
||||
(
|
||||
OutputId::name("First"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("Second"),
|
||||
Alignment::LeftAlignTop,
|
||||
),
|
||||
),
|
||||
(
|
||||
OutputId::name("Second"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("First"),
|
||||
Alignment::RightAlignTop,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
state.new_output("First", (300, 200).into());
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let original_op = output_for_name(state, "Pinnacle Window");
|
||||
let first_op = output_for_name(state, "First");
|
||||
|
||||
let original_geo = state.space.output_geometry(&original_op).unwrap();
|
||||
let first_geo = state.space.output_geometry(&first_op).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
original_geo,
|
||||
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
|
||||
);
|
||||
assert_eq!(
|
||||
first_geo,
|
||||
Rectangle::from_loc_and_size((1920, 0), (300, 200))
|
||||
);
|
||||
|
||||
state.new_output("Second", (500, 500).into());
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let original_op = output_for_name(state, "Pinnacle Window");
|
||||
let first_op = output_for_name(state, "First");
|
||||
let second_op = output_for_name(state, "Second");
|
||||
|
||||
let original_geo = state.space.output_geometry(&original_op).unwrap();
|
||||
let first_geo = state.space.output_geometry(&first_op).unwrap();
|
||||
let second_geo = state.space.output_geometry(&second_op).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
original_geo,
|
||||
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
|
||||
);
|
||||
assert_eq!(
|
||||
first_geo,
|
||||
Rectangle::from_loc_and_size((1920, 0), (300, 200))
|
||||
);
|
||||
assert_eq!(
|
||||
second_geo,
|
||||
Rectangle::from_loc_and_size((1920 + 300, 0), (500, 500))
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn setup_loc_with_relative_locs_with_more_than_one_relative_works() -> anyhow::Result<()>
|
||||
{
|
||||
test_api(|sender| {
|
||||
setup_rust(|api| {
|
||||
api.output.setup_locs(
|
||||
UpdateLocsOn::all(),
|
||||
[
|
||||
(OutputId::name("Pinnacle Window"), OutputLoc::Point(0, 0)),
|
||||
(
|
||||
OutputId::name("First"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("Pinnacle Window"),
|
||||
Alignment::BottomAlignLeft,
|
||||
),
|
||||
),
|
||||
(
|
||||
OutputId::name("Second"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("First"),
|
||||
Alignment::BottomAlignLeft,
|
||||
),
|
||||
),
|
||||
(
|
||||
OutputId::name("Third"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("Second"),
|
||||
Alignment::BottomAlignLeft,
|
||||
),
|
||||
),
|
||||
(
|
||||
OutputId::name("Third"),
|
||||
OutputLoc::RelativeTo(
|
||||
OutputId::name("First"),
|
||||
Alignment::BottomAlignLeft,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
state.new_output("First", (300, 200).into());
|
||||
state.new_output("Second", (300, 700).into());
|
||||
state.new_output("Third", (300, 400).into());
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let original_op = output_for_name(state, "Pinnacle Window");
|
||||
let first_op = output_for_name(state, "First");
|
||||
let second_op = output_for_name(state, "Second");
|
||||
let third_op = output_for_name(state, "Third");
|
||||
|
||||
let original_geo = state.space.output_geometry(&original_op).unwrap();
|
||||
let first_geo = state.space.output_geometry(&first_op).unwrap();
|
||||
let second_geo = state.space.output_geometry(&second_op).unwrap();
|
||||
let third_geo = state.space.output_geometry(&third_op).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
original_geo,
|
||||
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
|
||||
);
|
||||
assert_eq!(
|
||||
first_geo,
|
||||
Rectangle::from_loc_and_size((0, 1080), (300, 200))
|
||||
);
|
||||
assert_eq!(
|
||||
second_geo,
|
||||
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 700))
|
||||
);
|
||||
assert_eq!(
|
||||
third_geo,
|
||||
Rectangle::from_loc_and_size((0, 1080 + 200 + 700), (300, 400))
|
||||
);
|
||||
|
||||
state.remove_output(&second_op);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let original_op = output_for_name(state, "Pinnacle Window");
|
||||
let first_op = output_for_name(state, "First");
|
||||
let third_op = output_for_name(state, "Third");
|
||||
|
||||
let original_geo = state.space.output_geometry(&original_op).unwrap();
|
||||
let first_geo = state.space.output_geometry(&first_op).unwrap();
|
||||
let third_geo = state.space.output_geometry(&third_op).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
original_geo,
|
||||
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
|
||||
);
|
||||
assert_eq!(
|
||||
first_geo,
|
||||
Rectangle::from_loc_and_size((0, 1080), (300, 200))
|
||||
);
|
||||
assert_eq!(
|
||||
third_geo,
|
||||
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 400))
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
mod handle {
|
||||
use pinnacle_api::output::Transform;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::main]
|
||||
#[self::test]
|
||||
async fn set_transform() -> anyhow::Result<()> {
|
||||
test_api(|sender| {
|
||||
run_rust(|api| {
|
||||
api.output
|
||||
.get_focused()
|
||||
.unwrap()
|
||||
.set_transform(Transform::Flipped270);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let op = state.focused_output().unwrap();
|
||||
assert_eq!(
|
||||
op.current_transform(),
|
||||
smithay::utils::Transform::Flipped270
|
||||
);
|
||||
});
|
||||
|
||||
run_rust(|api| {
|
||||
api.output
|
||||
.get_focused()
|
||||
.unwrap()
|
||||
.set_transform(Transform::_180);
|
||||
});
|
||||
|
||||
sleep_secs(1);
|
||||
|
||||
with_state(&sender, |state| {
|
||||
let op = state.focused_output().unwrap();
|
||||
assert_eq!(op.current_transform(), smithay::utils::Transform::_180);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue