mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-26 21:58:10 +01:00
Merge pull request #244 from pinnacle-comp/output_management
Better output management
This commit is contained in:
commit
8eff64e1bc
26 changed files with 2501 additions and 204 deletions
14
.github/workflows/ci.pinnacle.yml
vendored
14
.github/workflows/ci.pinnacle.yml
vendored
|
@ -12,7 +12,7 @@ env:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
name: Build
|
name: Build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -22,7 +22,7 @@ jobs:
|
||||||
- name: Cache stuff
|
- name: Cache stuff
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev
|
run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev
|
||||||
- name: Setup Lua
|
- name: Setup Lua
|
||||||
uses: leafo/gh-actions-lua@v10
|
uses: leafo/gh-actions-lua@v10
|
||||||
with:
|
with:
|
||||||
|
@ -34,7 +34,7 @@ jobs:
|
||||||
- name: Celebratory yahoo
|
- name: Celebratory yahoo
|
||||||
run: echo yahoo
|
run: echo yahoo
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
name: Run tests
|
name: Run tests
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -44,7 +44,7 @@ jobs:
|
||||||
- name: Cache stuff
|
- name: Cache stuff
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev
|
run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev libdisplay-info-dev
|
||||||
- name: Setup Lua
|
- name: Setup Lua
|
||||||
uses: leafo/gh-actions-lua@v10
|
uses: leafo/gh-actions-lua@v10
|
||||||
with:
|
with:
|
||||||
|
@ -60,7 +60,7 @@ jobs:
|
||||||
if: ${{ runner.debug == '1' }}
|
if: ${{ runner.debug == '1' }}
|
||||||
run: RUST_LOG=debug RUST_BACKTRACE=1 just install test -- --nocapture --test-threads=1
|
run: RUST_LOG=debug RUST_BACKTRACE=1 just install test -- --nocapture --test-threads=1
|
||||||
check-format:
|
check-format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
name: Check formatting
|
name: Check formatting
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -72,7 +72,7 @@ jobs:
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
clippy-check:
|
clippy-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
name: Clippy check
|
name: Clippy check
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
@ -84,7 +84,7 @@ jobs:
|
||||||
- name: Cache stuff
|
- name: Cache stuff
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev
|
run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev
|
||||||
- name: Setup Lua
|
- name: Setup Lua
|
||||||
uses: leafo/gh-actions-lua@v10
|
uses: leafo/gh-actions-lua@v10
|
||||||
with:
|
with:
|
||||||
|
|
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -1393,6 +1393,11 @@ version = "0.2.154"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libdisplay-info-sys"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/Smithay/libdisplay-info-rs?rev=a482d0d#a482d0d4b71762c13d40fa394efe04473916f31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
@ -1851,8 +1856,11 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"cliclack",
|
"cliclack",
|
||||||
"dircpy",
|
"dircpy",
|
||||||
|
"drm-sys",
|
||||||
"gag",
|
"gag",
|
||||||
"image",
|
"image",
|
||||||
|
"indexmap 2.2.6",
|
||||||
|
"libdisplay-info-sys",
|
||||||
"pinnacle",
|
"pinnacle",
|
||||||
"pinnacle-api",
|
"pinnacle-api",
|
||||||
"pinnacle-api-defs",
|
"pinnacle-api-defs",
|
||||||
|
|
|
@ -115,6 +115,9 @@ chrono = "0.4.38"
|
||||||
bytemuck = "1.16.0"
|
bytemuck = "1.16.0"
|
||||||
pinnacle-api = { path = "./api/rust" }
|
pinnacle-api = { path = "./api/rust" }
|
||||||
gag = "1.0.0"
|
gag = "1.0.0"
|
||||||
|
drm-sys = "0.7.0"
|
||||||
|
libdisplay-info-sys = { git = "https://github.com/Smithay/libdisplay-info-rs", rev = "a482d0d" }
|
||||||
|
indexmap = "2.2.6"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] }
|
vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] }
|
||||||
|
|
|
@ -66,6 +66,7 @@ You will need:
|
||||||
`LD_LIBRARY_PATH` so the dynamically loaded libraries are found.
|
`LD_LIBRARY_PATH` so the dynamically loaded libraries are found.
|
||||||
> Luarocks currently doesn't install the Lua library and its dependencies due to openssh directory
|
> Luarocks currently doesn't install the Lua library and its dependencies due to openssh directory
|
||||||
> shenanigans. Fix soon, hopefully. In the meantime you can use the Rust API.
|
> shenanigans. Fix soon, hopefully. In the meantime you can use the Rust API.
|
||||||
|
- `libdisplay-info`, for monitor display information
|
||||||
- [protoc](https://grpc.io/docs/protoc-installation/), the Protocol Buffer Compiler, for configuration
|
- [protoc](https://grpc.io/docs/protoc-installation/), the Protocol Buffer Compiler, for configuration
|
||||||
- Arch:
|
- Arch:
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -65,6 +65,20 @@ local pinnacle_output_v0alpha1_Transform = {
|
||||||
---@field pixel_height integer?
|
---@field pixel_height integer?
|
||||||
---@field refresh_rate_millihz integer?
|
---@field refresh_rate_millihz integer?
|
||||||
|
|
||||||
|
---@class pinnacle.output.v0alpha1.SetModelineRequest
|
||||||
|
---@field output_name string?
|
||||||
|
---@field clock number?
|
||||||
|
---@field hdisplay integer?
|
||||||
|
---@field hsync_start integer?
|
||||||
|
---@field hsync_end integer?
|
||||||
|
---@field htotal integer?
|
||||||
|
---@field vdisplay integer?
|
||||||
|
---@field vsync_start integer?
|
||||||
|
---@field vsync_end integer?
|
||||||
|
---@field vtotal integer?
|
||||||
|
---@field hsync_pos boolean?
|
||||||
|
---@field vsync_pos boolean?
|
||||||
|
|
||||||
---@class pinnacle.output.v0alpha1.SetScaleRequest
|
---@class pinnacle.output.v0alpha1.SetScaleRequest
|
||||||
---@field output_name string?
|
---@field output_name string?
|
||||||
---@field absolute number?
|
---@field absolute number?
|
||||||
|
@ -104,6 +118,8 @@ local pinnacle_output_v0alpha1_Transform = {
|
||||||
---@field transform pinnacle.output.v0alpha1.Transform?
|
---@field transform pinnacle.output.v0alpha1.Transform?
|
||||||
---@field serial integer?
|
---@field serial integer?
|
||||||
---@field keyboard_focus_stack_window_ids integer[]?
|
---@field keyboard_focus_stack_window_ids integer[]?
|
||||||
|
---@field enabled boolean?
|
||||||
|
---@field powered boolean?
|
||||||
|
|
||||||
-- Window
|
-- Window
|
||||||
|
|
||||||
|
@ -476,6 +492,13 @@ defs.pinnacle = {
|
||||||
response = "google.protobuf.Empty",
|
response = "google.protobuf.Empty",
|
||||||
},
|
},
|
||||||
---@type GrpcRequestArgs
|
---@type GrpcRequestArgs
|
||||||
|
SetModeline = {
|
||||||
|
service = "pinnacle.output.v0alpha1.OutputService",
|
||||||
|
method = "SetModeline",
|
||||||
|
request = "pinnacle.output.v0alpha1.SetModelineRequest",
|
||||||
|
response = "google.protobuf.Empty",
|
||||||
|
},
|
||||||
|
---@type GrpcRequestArgs
|
||||||
SetScale = {
|
SetScale = {
|
||||||
service = "pinnacle.output.v0alpha1.OutputService",
|
service = "pinnacle.output.v0alpha1.OutputService",
|
||||||
method = "SetScale",
|
method = "SetScale",
|
||||||
|
|
|
@ -40,8 +40,6 @@ output.handle = output_handle
|
||||||
---
|
---
|
||||||
---@return OutputHandle[]
|
---@return OutputHandle[]
|
||||||
function output.get_all()
|
function output.get_all()
|
||||||
-- Not going to batch these because I doubt people would have that many monitors
|
|
||||||
|
|
||||||
local response = client.unary_request(output_service.Get, {})
|
local response = client.unary_request(output_service.Get, {})
|
||||||
|
|
||||||
---@type OutputHandle[]
|
---@type OutputHandle[]
|
||||||
|
@ -54,6 +52,27 @@ function output.get_all()
|
||||||
return handles
|
return handles
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get all enabled outputs.
|
||||||
|
---
|
||||||
|
---### Example
|
||||||
|
---```lua
|
||||||
|
---local outputs = Output.get_all_enabled()
|
||||||
|
---```
|
||||||
|
---
|
||||||
|
---@return OutputHandle[]
|
||||||
|
function output.get_all_enabled()
|
||||||
|
local outputs = output.get_all()
|
||||||
|
|
||||||
|
local enabled_handles = {}
|
||||||
|
for _, handle in ipairs(outputs) do
|
||||||
|
if handle:enabled() then
|
||||||
|
table.insert(enabled_handles, handle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return enabled_handles
|
||||||
|
end
|
||||||
|
|
||||||
---Get an output by its name (the connector it's plugged into).
|
---Get an output by its name (the connector it's plugged into).
|
||||||
---
|
---
|
||||||
---### Example
|
---### Example
|
||||||
|
@ -144,6 +163,7 @@ end
|
||||||
---@class OutputSetup
|
---@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 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 mode Mode? -- Makes this setup apply the given mode to outputs.
|
||||||
|
---@field modeline (string|Modeline)? -- Makes this setup apply the given modeline to outputs. This takes precedence over `mode`.
|
||||||
---@field scale number? -- Makes this setup apply the given scale 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 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.
|
---@field transform Transform? -- Makes this setup applt the given transform to outputs.
|
||||||
|
@ -270,7 +290,9 @@ function output.setup(setups)
|
||||||
goto continue
|
goto continue
|
||||||
end
|
end
|
||||||
|
|
||||||
if setup.mode then
|
if setup.modeline then
|
||||||
|
op:set_modeline(setup.modeline)
|
||||||
|
elseif setup.mode then
|
||||||
op:set_mode(
|
op:set_mode(
|
||||||
setup.mode.pixel_width,
|
setup.mode.pixel_width,
|
||||||
setup.mode.pixel_height,
|
setup.mode.pixel_height,
|
||||||
|
@ -421,7 +443,7 @@ function output.setup_locs(update_locs_on, locs)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function layout_outputs()
|
local function layout_outputs()
|
||||||
local outputs = output.get_all()
|
local outputs = output.get_all_enabled()
|
||||||
|
|
||||||
---@type OutputHandle[]
|
---@type OutputHandle[]
|
||||||
local placed_outputs = {}
|
local placed_outputs = {}
|
||||||
|
@ -813,6 +835,54 @@ function OutputHandle:set_mode(pixel_width, pixel_height, refresh_rate_millihz)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class Modeline
|
||||||
|
---@field clock number
|
||||||
|
---@field hdisplay integer
|
||||||
|
---@field hsync_start integer
|
||||||
|
---@field hsync_end integer
|
||||||
|
---@field htotal integer
|
||||||
|
---@field vdisplay integer
|
||||||
|
---@field vsync_start integer
|
||||||
|
---@field vsync_end integer
|
||||||
|
---@field vtotal integer
|
||||||
|
---@field hsync boolean
|
||||||
|
---@field vsync boolean
|
||||||
|
|
||||||
|
---Set a custom modeline for this output.
|
||||||
|
---
|
||||||
|
---This accepts a `Modeline` table or a string of the modeline.
|
||||||
|
---
|
||||||
|
---@param modeline string|Modeline
|
||||||
|
function OutputHandle:set_modeline(modeline)
|
||||||
|
if type(modeline) == "string" then
|
||||||
|
local ml, err = require("pinnacle.util").output.parse_modeline(modeline)
|
||||||
|
if ml then
|
||||||
|
modeline = ml
|
||||||
|
else
|
||||||
|
print("invalid modeline: " .. tostring(err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type pinnacle.output.v0alpha1.SetModelineRequest
|
||||||
|
local request = {
|
||||||
|
output_name = self.name,
|
||||||
|
clock = modeline.clock,
|
||||||
|
hdisplay = modeline.hdisplay,
|
||||||
|
hsync_start = modeline.hsync_start,
|
||||||
|
hsync_end = modeline.hsync_end,
|
||||||
|
htotal = modeline.htotal,
|
||||||
|
vdisplay = modeline.vdisplay,
|
||||||
|
vsync_start = modeline.vsync_start,
|
||||||
|
vsync_end = modeline.vsync_end,
|
||||||
|
vtotal = modeline.vtotal,
|
||||||
|
hsync_pos = modeline.hsync,
|
||||||
|
vsync_pos = modeline.vsync,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.unary_request(output_service.SetModeline, request)
|
||||||
|
end
|
||||||
|
|
||||||
---Set this output's scaling factor.
|
---Set this output's scaling factor.
|
||||||
---
|
---
|
||||||
---@param scale number
|
---@param scale number
|
||||||
|
@ -900,6 +970,8 @@ end
|
||||||
---@field transform Transform?
|
---@field transform Transform?
|
||||||
---@field serial integer?
|
---@field serial integer?
|
||||||
---@field keyboard_focus_stack WindowHandle[]
|
---@field keyboard_focus_stack WindowHandle[]
|
||||||
|
---@field enabled boolean?
|
||||||
|
---@field powered boolean?
|
||||||
|
|
||||||
---Get all properties of this output.
|
---Get all properties of this output.
|
||||||
---
|
---
|
||||||
|
@ -972,6 +1044,8 @@ end
|
||||||
|
|
||||||
---Get this output's logical width in pixels.
|
---Get this output's logical width in pixels.
|
||||||
---
|
---
|
||||||
|
---If the output is disabled, this returns nil.
|
||||||
|
---
|
||||||
---Shorthand for `handle:props().logical_width`.
|
---Shorthand for `handle:props().logical_width`.
|
||||||
---
|
---
|
||||||
---@return integer?
|
---@return integer?
|
||||||
|
@ -981,6 +1055,8 @@ end
|
||||||
|
|
||||||
---Get this output's logical height in pixels.
|
---Get this output's logical height in pixels.
|
||||||
---
|
---
|
||||||
|
---If the output is disabled, this returns nil.
|
||||||
|
---
|
||||||
---Shorthand for `handle:props().y`.
|
---Shorthand for `handle:props().y`.
|
||||||
---
|
---
|
||||||
---@return integer?
|
---@return integer?
|
||||||
|
@ -1094,6 +1170,25 @@ function OutputHandle:keyboard_focus_stack()
|
||||||
return self:props().keyboard_focus_stack
|
return self:props().keyboard_focus_stack
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Get whether this output is enabled.
|
||||||
|
---
|
||||||
|
---Disabled outputs are not mapped to the global space and cannot be used.
|
||||||
|
---
|
||||||
|
---@return boolean?
|
||||||
|
function OutputHandle:enabled()
|
||||||
|
return self:props().enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get whether this output is powered.
|
||||||
|
---
|
||||||
|
---Unpowered outputs that are enabled will be off, but they will still be
|
||||||
|
---mapped to the global space, meaning you can still interact with them.
|
||||||
|
---
|
||||||
|
---@return boolean?
|
||||||
|
function OutputHandle:powered()
|
||||||
|
return self:props().powered
|
||||||
|
end
|
||||||
|
|
||||||
---Get this output's keyboard focus stack.
|
---Get this output's keyboard focus stack.
|
||||||
---
|
---
|
||||||
---This only includes windows on active tags.
|
---This only includes windows on active tags.
|
||||||
|
|
|
@ -118,10 +118,93 @@ function rectangle.new(x, y, width, height)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Parse a modeline string.
|
||||||
|
---
|
||||||
|
---@param modeline string
|
||||||
|
---
|
||||||
|
---@return Modeline|nil modeline A modeline if successful
|
||||||
|
---@return string|nil error An error message if any
|
||||||
|
local function parse_modeline(modeline)
|
||||||
|
local args = modeline:gmatch("[^%s]+")
|
||||||
|
|
||||||
|
local targs = {}
|
||||||
|
|
||||||
|
for arg in args do
|
||||||
|
table.insert(targs, arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local clock = tonumber(targs[1])
|
||||||
|
local hdisplay = tonumber(targs[2])
|
||||||
|
local hsync_start = tonumber(targs[3])
|
||||||
|
local hsync_end = tonumber(targs[4])
|
||||||
|
local htotal = tonumber(targs[5])
|
||||||
|
local vdisplay = tonumber(targs[6])
|
||||||
|
local vsync_start = tonumber(targs[7])
|
||||||
|
local vsync_end = tonumber(targs[8])
|
||||||
|
local vtotal = tonumber(targs[9])
|
||||||
|
local hsync = targs[10]
|
||||||
|
local vsync = targs[11]
|
||||||
|
|
||||||
|
if
|
||||||
|
not (
|
||||||
|
clock
|
||||||
|
and hdisplay
|
||||||
|
and hsync_start
|
||||||
|
and hsync_end
|
||||||
|
and htotal
|
||||||
|
and vdisplay
|
||||||
|
and vsync_start
|
||||||
|
and vsync_end
|
||||||
|
and vtotal
|
||||||
|
and hsync
|
||||||
|
and vsync
|
||||||
|
)
|
||||||
|
then
|
||||||
|
return nil, "one or more fields was missing"
|
||||||
|
end
|
||||||
|
|
||||||
|
local hsync_lower = string.lower(hsync)
|
||||||
|
local vsync_lower = string.lower(vsync)
|
||||||
|
|
||||||
|
if hsync_lower == "+hsync" then
|
||||||
|
hsync = true
|
||||||
|
elseif hsync_lower == "-hsync" then
|
||||||
|
hsync = false
|
||||||
|
else
|
||||||
|
return nil, "invalid hsync: " .. hsync
|
||||||
|
end
|
||||||
|
|
||||||
|
if vsync_lower == "+vsync" then
|
||||||
|
vsync = true
|
||||||
|
elseif vsync_lower == "-vsync" then
|
||||||
|
vsync = false
|
||||||
|
else
|
||||||
|
return nil, "invalid vsync: " .. vsync
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type Modeline
|
||||||
|
return {
|
||||||
|
clock = clock,
|
||||||
|
hdisplay = hdisplay,
|
||||||
|
hsync_start = hsync_start,
|
||||||
|
hsync_end = hsync_end,
|
||||||
|
htotal = htotal,
|
||||||
|
vdisplay = vdisplay,
|
||||||
|
vsync_start = vsync_start,
|
||||||
|
vsync_end = vsync_end,
|
||||||
|
vtotal = vtotal,
|
||||||
|
hsync = hsync,
|
||||||
|
vsync = vsync,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
---Utility functions.
|
---Utility functions.
|
||||||
---@class Util
|
---@class Util
|
||||||
local util = {
|
local util = {
|
||||||
rectangle = rectangle,
|
rectangle = rectangle,
|
||||||
|
output = {
|
||||||
|
parse_modeline = parse_modeline,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---Batch a set of requests that will be sent to the compositor all at once.
|
---Batch a set of requests that will be sent to the compositor all at once.
|
||||||
|
|
|
@ -36,6 +36,21 @@ message SetModeRequest {
|
||||||
optional uint32 refresh_rate_millihz = 4;
|
optional uint32 refresh_rate_millihz = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetModelineRequest {
|
||||||
|
optional string output_name = 1;
|
||||||
|
optional float clock = 2;
|
||||||
|
optional uint32 hdisplay = 3;
|
||||||
|
optional uint32 hsync_start = 4;
|
||||||
|
optional uint32 hsync_end = 5;
|
||||||
|
optional uint32 htotal = 6;
|
||||||
|
optional uint32 vdisplay = 7;
|
||||||
|
optional uint32 vsync_start = 8;
|
||||||
|
optional uint32 vsync_end = 9;
|
||||||
|
optional uint32 vtotal = 10;
|
||||||
|
optional bool hsync_pos = 11;
|
||||||
|
optional bool vsync_pos = 12;
|
||||||
|
}
|
||||||
|
|
||||||
message SetScaleRequest {
|
message SetScaleRequest {
|
||||||
optional string output_name = 1;
|
optional string output_name = 1;
|
||||||
oneof absolute_or_relative {
|
oneof absolute_or_relative {
|
||||||
|
@ -101,11 +116,14 @@ message GetPropertiesResponse {
|
||||||
optional uint32 serial = 16;
|
optional uint32 serial = 16;
|
||||||
// Window ids of the keyboard focus stack for this output.
|
// Window ids of the keyboard focus stack for this output.
|
||||||
repeated uint32 keyboard_focus_stack_window_ids = 17;
|
repeated uint32 keyboard_focus_stack_window_ids = 17;
|
||||||
|
optional bool enabled = 18;
|
||||||
|
optional bool powered = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
service OutputService {
|
service OutputService {
|
||||||
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
|
rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
|
rpc SetMode(SetModeRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc SetModeline(SetModelineRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
|
rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty);
|
rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty);
|
||||||
rpc SetPowered(SetPoweredRequest) returns (google.protobuf.Empty);
|
rpc SetPowered(SetPoweredRequest) returns (google.protobuf.Empty);
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
//! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different
|
||||||
//! connected monitors and set them up.
|
//! connected monitors and set them up.
|
||||||
|
|
||||||
use std::{num::NonZeroU32, sync::OnceLock};
|
use std::{num::NonZeroU32, str::FromStr, sync::OnceLock};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use pinnacle_api_defs::pinnacle::output::{
|
use pinnacle_api_defs::pinnacle::output::{
|
||||||
self,
|
self,
|
||||||
v0alpha1::{
|
v0alpha1::{
|
||||||
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
||||||
SetLocationRequest, SetModeRequest, SetPoweredRequest, SetScaleRequest,
|
SetLocationRequest, SetModeRequest, SetModelineRequest, SetPoweredRequest, SetScaleRequest,
|
||||||
SetTransformRequest,
|
SetTransformRequest,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -60,7 +60,7 @@ impl Output {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to all connected outputs.
|
/// Get handles to all connected outputs.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
@ -82,10 +82,35 @@ impl Output {
|
||||||
.into_inner()
|
.into_inner()
|
||||||
.output_names
|
.output_names
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(move |name| self.new_handle(name))
|
.map(|name| self.new_handle(name))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get handles to all outputs that are connected and enabled.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let enabled = output.get_all_enabled();
|
||||||
|
/// ```
|
||||||
|
pub fn get_all_enabled(&self) -> Vec<OutputHandle> {
|
||||||
|
block_on_tokio(self.get_all_enabled_async())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The async version of [`Output::get_all_enabled`].
|
||||||
|
pub async fn get_all_enabled_async(&self) -> Vec<OutputHandle> {
|
||||||
|
let outputs = self.get_all_async().await;
|
||||||
|
|
||||||
|
let mut enabled_outputs = Vec::new();
|
||||||
|
for output in outputs {
|
||||||
|
if output.enabled_async().await.unwrap_or_default() {
|
||||||
|
enabled_outputs.push(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled_outputs
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a handle to the output with the given name.
|
/// Get a handle to the output with the given name.
|
||||||
///
|
///
|
||||||
/// By "name", we mean the name of the connector the output is connected to.
|
/// By "name", we mean the name of the connector the output is connected to.
|
||||||
|
@ -272,7 +297,7 @@ impl Output {
|
||||||
|
|
||||||
let api = self.api.get().unwrap().clone();
|
let api = self.api.get().unwrap().clone();
|
||||||
let layout_outputs = move || {
|
let layout_outputs = move || {
|
||||||
let outputs = api.output.get_all();
|
let outputs = api.output.get_all_enabled();
|
||||||
|
|
||||||
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
|
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
|
||||||
|
|
||||||
|
@ -418,10 +443,15 @@ impl std::fmt::Debug for OutputMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OutputMode {
|
||||||
|
Mode(Mode),
|
||||||
|
Modeline(Modeline),
|
||||||
|
}
|
||||||
|
|
||||||
/// An output setup for use in [`Output::setup`].
|
/// An output setup for use in [`Output::setup`].
|
||||||
pub struct OutputSetup {
|
pub struct OutputSetup {
|
||||||
output: OutputMatcher,
|
output: OutputMatcher,
|
||||||
mode: Option<Mode>,
|
mode: Option<OutputMode>,
|
||||||
scale: Option<f32>,
|
scale: Option<f32>,
|
||||||
tag_names: Option<Vec<String>>,
|
tag_names: Option<Vec<String>>,
|
||||||
transform: Option<Transform>,
|
transform: Option<Transform>,
|
||||||
|
@ -453,9 +483,24 @@ impl OutputSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes this setup apply the given [`Mode`] to its outputs.
|
/// Makes this setup apply the given [`Mode`] to its outputs.
|
||||||
|
///
|
||||||
|
/// This will overwrite [`OutputSetup::with_modeline`] if called after it.
|
||||||
pub fn with_mode(self, mode: Mode) -> Self {
|
pub fn with_mode(self, mode: Mode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mode: Some(mode),
|
mode: Some(OutputMode::Mode(mode)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup apply the given [`Modeline`] to its outputs.
|
||||||
|
///
|
||||||
|
/// You can parse a modeline string into a modeline. See [`OutputHandle::set_modeline`] for
|
||||||
|
/// specifics.
|
||||||
|
///
|
||||||
|
/// This will overwrite [`OutputSetup::with_mode`] if called after it.
|
||||||
|
pub fn with_modeline(self, modeline: Modeline) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: Some(OutputMode::Modeline(modeline)),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -486,11 +531,18 @@ impl OutputSetup {
|
||||||
|
|
||||||
fn apply(&self, output: &OutputHandle, tag: &Tag) {
|
fn apply(&self, output: &OutputHandle, tag: &Tag) {
|
||||||
if let Some(mode) = &self.mode {
|
if let Some(mode) = &self.mode {
|
||||||
output.set_mode(
|
match mode {
|
||||||
mode.pixel_width,
|
OutputMode::Mode(mode) => {
|
||||||
mode.pixel_height,
|
output.set_mode(
|
||||||
Some(mode.refresh_rate_millihertz),
|
mode.pixel_width,
|
||||||
);
|
mode.pixel_height,
|
||||||
|
Some(mode.refresh_rate_millihertz),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OutputMode::Modeline(modeline) => {
|
||||||
|
output.set_modeline(*modeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(scale) = self.scale {
|
if let Some(scale) = self.scale {
|
||||||
output.set_scale(scale);
|
output.set_scale(scale);
|
||||||
|
@ -505,7 +557,7 @@ impl OutputSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A location for an output.
|
/// A location for an output.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum OutputLoc {
|
pub enum OutputLoc {
|
||||||
/// A specific point in the global space of the form (x, y).
|
/// A specific point in the global space of the form (x, y).
|
||||||
Point(i32, i32),
|
Point(i32, i32),
|
||||||
|
@ -812,6 +864,37 @@ impl OutputHandle {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a custom modeline for this output.
|
||||||
|
///
|
||||||
|
/// See `xorg.conf(5)` for more information.
|
||||||
|
///
|
||||||
|
/// You can parse a modeline from a string of the form
|
||||||
|
/// `<clock> <hdisplay> <hsync_start> <hsync_end> <htotal> <vdisplay> <vsync_start> <vsync_end> <hsync> <vsync>`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// output.set_modeline("173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync".parse()?);
|
||||||
|
/// ```
|
||||||
|
pub fn set_modeline(&self, modeline: Modeline) {
|
||||||
|
let mut client = self.output_client.clone();
|
||||||
|
block_on_tokio(client.set_modeline(SetModelineRequest {
|
||||||
|
output_name: Some(self.name.clone()),
|
||||||
|
clock: Some(modeline.clock),
|
||||||
|
hdisplay: Some(modeline.hdisplay),
|
||||||
|
hsync_start: Some(modeline.hsync_start),
|
||||||
|
hsync_end: Some(modeline.hsync_end),
|
||||||
|
htotal: Some(modeline.htotal),
|
||||||
|
vdisplay: Some(modeline.vdisplay),
|
||||||
|
vsync_start: Some(modeline.vsync_start),
|
||||||
|
vsync_end: Some(modeline.vsync_end),
|
||||||
|
vtotal: Some(modeline.vtotal),
|
||||||
|
hsync_pos: Some(modeline.hsync),
|
||||||
|
vsync_pos: Some(modeline.vsync),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set this output's scaling factor.
|
/// Set this output's scaling factor.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -970,6 +1053,8 @@ impl OutputHandle {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| self.api.window.new_handle(id))
|
.map(|id| self.api.window.new_handle(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
enabled: response.enabled,
|
||||||
|
powered: response.powered,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1025,6 +1110,8 @@ impl OutputHandle {
|
||||||
|
|
||||||
/// Get this output's logical width in pixels.
|
/// Get this output's logical width in pixels.
|
||||||
///
|
///
|
||||||
|
/// If the output is disabled, this returns None.
|
||||||
|
///
|
||||||
/// Shorthand for `self.props().logical_width`.
|
/// Shorthand for `self.props().logical_width`.
|
||||||
pub fn logical_width(&self) -> Option<u32> {
|
pub fn logical_width(&self) -> Option<u32> {
|
||||||
self.props().logical_width
|
self.props().logical_width
|
||||||
|
@ -1037,6 +1124,8 @@ impl OutputHandle {
|
||||||
|
|
||||||
/// Get this output's logical height in pixels.
|
/// Get this output's logical height in pixels.
|
||||||
///
|
///
|
||||||
|
/// If the output is disabled, this returns None.
|
||||||
|
///
|
||||||
/// Shorthand for `self.props().logical_height`.
|
/// Shorthand for `self.props().logical_height`.
|
||||||
pub fn logical_height(&self) -> Option<u32> {
|
pub fn logical_height(&self) -> Option<u32> {
|
||||||
self.props().logical_height
|
self.props().logical_height
|
||||||
|
@ -1197,6 +1286,34 @@ impl OutputHandle {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get whether this output is enabled.
|
||||||
|
///
|
||||||
|
/// Disabled outputs act as if you unplugged them.
|
||||||
|
pub fn enabled(&self) -> Option<bool> {
|
||||||
|
self.props().enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The async version of [`OutputHandle::enabled`].
|
||||||
|
pub async fn enabled_async(&self) -> Option<bool> {
|
||||||
|
self.props_async().await.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get whether this output is powered.
|
||||||
|
///
|
||||||
|
/// Unpowered outputs will be turned off but you can still interact with them.
|
||||||
|
///
|
||||||
|
/// Outputs can be disabled but still powered; this just means
|
||||||
|
/// they will turn on when powered. Disabled and unpowered outputs
|
||||||
|
/// will not power on when enabled, but will still be interactable.
|
||||||
|
pub fn powered(&self) -> Option<bool> {
|
||||||
|
self.props().powered
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The async version of [`OutputHandle::powered`].
|
||||||
|
pub async fn powered_async(&self) -> Option<bool> {
|
||||||
|
self.props_async().await.powered
|
||||||
|
}
|
||||||
|
|
||||||
/// Get this output's unique name (the name of its connector).
|
/// Get this output's unique name (the name of its connector).
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> String {
|
||||||
self.name.to_string()
|
self.name.to_string()
|
||||||
|
@ -1204,7 +1321,7 @@ impl OutputHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A possible output pixel dimension and refresh rate configuration.
|
/// A possible output pixel dimension and refresh rate configuration.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct Mode {
|
pub struct Mode {
|
||||||
/// The width of the output, in pixels.
|
/// The width of the output, in pixels.
|
||||||
pub pixel_width: u32,
|
pub pixel_width: u32,
|
||||||
|
@ -1261,4 +1378,161 @@ pub struct OutputProperties {
|
||||||
pub serial: Option<u32>,
|
pub serial: Option<u32>,
|
||||||
/// This output's window keyboard focus stack.
|
/// This output's window keyboard focus stack.
|
||||||
pub keyboard_focus_stack: Vec<WindowHandle>,
|
pub keyboard_focus_stack: Vec<WindowHandle>,
|
||||||
|
/// Whether this output is enabled.
|
||||||
|
///
|
||||||
|
/// Enabled outputs are mapped in the global space and usable.
|
||||||
|
/// Disabled outputs function as if you unplugged them.
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
/// Whether this output is powered.
|
||||||
|
///
|
||||||
|
/// Unpowered outputs will be off but you can still interact with them.
|
||||||
|
pub powered: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom modeline.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Default)]
|
||||||
|
pub struct Modeline {
|
||||||
|
pub clock: f32,
|
||||||
|
pub hdisplay: u32,
|
||||||
|
pub hsync_start: u32,
|
||||||
|
pub hsync_end: u32,
|
||||||
|
pub htotal: u32,
|
||||||
|
pub vdisplay: u32,
|
||||||
|
pub vsync_start: u32,
|
||||||
|
pub vsync_end: u32,
|
||||||
|
pub vtotal: u32,
|
||||||
|
pub hsync: bool,
|
||||||
|
pub vsync: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error for the `FromStr` implementation for [`Modeline`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ParseModelineError(ParseModelineErrorKind);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ParseModelineErrorKind {
|
||||||
|
NoClock,
|
||||||
|
InvalidClock,
|
||||||
|
NoHdisplay,
|
||||||
|
InvalidHdisplay,
|
||||||
|
NoHsyncStart,
|
||||||
|
InvalidHsyncStart,
|
||||||
|
NoHsyncEnd,
|
||||||
|
InvalidHsyncEnd,
|
||||||
|
NoHtotal,
|
||||||
|
InvalidHtotal,
|
||||||
|
NoVdisplay,
|
||||||
|
InvalidVdisplay,
|
||||||
|
NoVsyncStart,
|
||||||
|
InvalidVsyncStart,
|
||||||
|
NoVsyncEnd,
|
||||||
|
InvalidVsyncEnd,
|
||||||
|
NoVtotal,
|
||||||
|
InvalidVtotal,
|
||||||
|
NoHsync,
|
||||||
|
InvalidHsync,
|
||||||
|
NoVsync,
|
||||||
|
InvalidVsync,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ParseModelineError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Debug::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseModelineErrorKind> for ParseModelineError {
|
||||||
|
fn from(value: ParseModelineErrorKind) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Modeline {
|
||||||
|
type Err = ParseModelineError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut args = s.split_whitespace();
|
||||||
|
|
||||||
|
let clock = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoClock)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidClock)?;
|
||||||
|
let hdisplay = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoHdisplay)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidHdisplay)?;
|
||||||
|
let hsync_start = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoHsyncStart)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidHsyncStart)?;
|
||||||
|
let hsync_end = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoHsyncEnd)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidHsyncEnd)?;
|
||||||
|
let htotal = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoHtotal)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidHtotal)?;
|
||||||
|
let vdisplay = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoVdisplay)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidVdisplay)?;
|
||||||
|
let vsync_start = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoVsyncStart)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidVsyncStart)?;
|
||||||
|
let vsync_end = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoVsyncEnd)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidVsyncEnd)?;
|
||||||
|
let vtotal = args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoVtotal)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ParseModelineErrorKind::InvalidVtotal)?;
|
||||||
|
|
||||||
|
let hsync = match args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoHsync)?
|
||||||
|
.to_lowercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"+hsync" => true,
|
||||||
|
"-hsync" => false,
|
||||||
|
_ => Err(ParseModelineErrorKind::InvalidHsync)?,
|
||||||
|
};
|
||||||
|
let vsync = match args
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseModelineErrorKind::NoVsync)?
|
||||||
|
.to_lowercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"+vsync" => true,
|
||||||
|
"-vsync" => false,
|
||||||
|
_ => Err(ParseModelineErrorKind::InvalidVsync)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Modeline {
|
||||||
|
clock,
|
||||||
|
hdisplay,
|
||||||
|
hsync_start,
|
||||||
|
hsync_end,
|
||||||
|
htotal,
|
||||||
|
vdisplay,
|
||||||
|
vsync_start,
|
||||||
|
vsync_end,
|
||||||
|
vtotal,
|
||||||
|
hsync,
|
||||||
|
vsync,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
123
src/api.rs
123
src/api.rs
|
@ -16,7 +16,8 @@ use pinnacle_api_defs::pinnacle::{
|
||||||
self,
|
self,
|
||||||
v0alpha1::{
|
v0alpha1::{
|
||||||
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
|
output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest,
|
||||||
SetModeRequest, SetPoweredRequest, SetScaleRequest, SetTransformRequest,
|
SetModeRequest, SetModelineRequest, SetPoweredRequest, SetScaleRequest,
|
||||||
|
SetTransformRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
|
process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse},
|
||||||
|
@ -52,10 +53,10 @@ use tonic::{Request, Response, Status, Streaming};
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::BackendData,
|
backend::{udev::drm_mode_from_api_modeline, BackendData},
|
||||||
config::ConnectorSavedState,
|
config::ConnectorSavedState,
|
||||||
input::ModifierMask,
|
input::ModifierMask,
|
||||||
output::OutputName,
|
output::{OutputMode, OutputName},
|
||||||
render::util::snapshot::capture_snapshots_on_output,
|
render::util::snapshot::capture_snapshots_on_output,
|
||||||
state::{State, WithState},
|
state::{State, WithState},
|
||||||
tag::{Tag, TagId},
|
tag::{Tag, TagId},
|
||||||
|
@ -885,7 +886,7 @@ impl tag_service_server::TagService for TagService {
|
||||||
.flat_map(|id| id.tag(&state.pinnacle))
|
.flat_map(|id| id.tag(&state.pinnacle))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for output in state.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
for output in state.pinnacle.outputs.keys().cloned().collect::<Vec<_>>() {
|
||||||
// TODO: seriously, convert state.tags into a hashset
|
// TODO: seriously, convert state.tags into a hashset
|
||||||
output.with_state_mut(|state| {
|
output.with_state_mut(|state| {
|
||||||
for tag_to_remove in tags_to_remove.iter() {
|
for tag_to_remove in tags_to_remove.iter() {
|
||||||
|
@ -915,8 +916,8 @@ impl tag_service_server::TagService for TagService {
|
||||||
run_unary(&self.sender, move |state| {
|
run_unary(&self.sender, move |state| {
|
||||||
let tag_ids = state
|
let tag_ids = state
|
||||||
.pinnacle
|
.pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||||
.map(|tag| tag.id())
|
.map(|tag| tag.id())
|
||||||
.map(|id| id.0)
|
.map(|id| id.0)
|
||||||
|
@ -1035,11 +1036,20 @@ impl output_service_server::OutputService for OutputService {
|
||||||
if let Some(y) = y {
|
if let Some(y) = y {
|
||||||
loc.y = y;
|
loc.y = y;
|
||||||
}
|
}
|
||||||
state
|
state.pinnacle.change_output_state(
|
||||||
.pinnacle
|
&mut state.backend,
|
||||||
.change_output_state(&output, None, None, None, Some(loc));
|
&output,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(loc),
|
||||||
|
);
|
||||||
debug!("Mapping output {} to {loc:?}", output.name());
|
debug!("Mapping output {} to {loc:?}", output.name());
|
||||||
state.pinnacle.request_layout(&output);
|
state.pinnacle.request_layout(&output);
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1061,13 +1071,64 @@ impl output_service_server::OutputService for OutputService {
|
||||||
let Some(mode) = Some(request).and_then(|request| {
|
let Some(mode) = Some(request).and_then(|request| {
|
||||||
Some(smithay::output::Mode {
|
Some(smithay::output::Mode {
|
||||||
size: (request.pixel_width? as i32, request.pixel_height? as i32).into(),
|
size: (request.pixel_width? as i32, request.pixel_height? as i32).into(),
|
||||||
|
// FIXME: this is nullable, pick a mode with highest refresh if None
|
||||||
refresh: request.refresh_rate_millihz? as i32,
|
refresh: request.refresh_rate_millihz? as i32,
|
||||||
})
|
})
|
||||||
}) else {
|
}) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
state.resize_output(&output, mode);
|
state.pinnacle.change_output_state(
|
||||||
|
&mut state.backend,
|
||||||
|
&output,
|
||||||
|
Some(OutputMode::Smithay(mode)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
state.pinnacle.request_layout(&output);
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_modeline(
|
||||||
|
&self,
|
||||||
|
request: Request<SetModelineRequest>,
|
||||||
|
) -> Result<Response<()>, Status> {
|
||||||
|
let request = request.into_inner();
|
||||||
|
|
||||||
|
run_unary_no_response(&self.sender, |state| {
|
||||||
|
let Some(output) = request
|
||||||
|
.output_name
|
||||||
|
.clone()
|
||||||
|
.map(OutputName)
|
||||||
|
.and_then(|name| name.output(&state.pinnacle))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(mode) = drm_mode_from_api_modeline(request) else {
|
||||||
|
// TODO: raise error
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.pinnacle.change_output_state(
|
||||||
|
&mut state.backend,
|
||||||
|
&output,
|
||||||
|
Some(OutputMode::Drm(mode)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
state.pinnacle.request_layout(&output);
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1102,6 +1163,7 @@ impl output_service_server::OutputService for OutputService {
|
||||||
});
|
});
|
||||||
|
|
||||||
state.pinnacle.change_output_state(
|
state.pinnacle.change_output_state(
|
||||||
|
&mut state.backend,
|
||||||
&output,
|
&output,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
@ -1121,6 +1183,10 @@ impl output_service_server::OutputService for OutputService {
|
||||||
|
|
||||||
state.pinnacle.request_layout(&output);
|
state.pinnacle.request_layout(&output);
|
||||||
state.schedule_render(&output);
|
state.schedule_render(&output);
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1154,11 +1220,20 @@ impl output_service_server::OutputService for OutputService {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
state
|
state.pinnacle.change_output_state(
|
||||||
.pinnacle
|
&mut state.backend,
|
||||||
.change_output_state(&output, None, Some(smithay_transform), None, None);
|
&output,
|
||||||
|
None,
|
||||||
|
Some(smithay_transform),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
state.pinnacle.request_layout(&output);
|
state.pinnacle.request_layout(&output);
|
||||||
state.schedule_render(&output);
|
state.schedule_render(&output);
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1197,8 +1272,8 @@ impl output_service_server::OutputService for OutputService {
|
||||||
run_unary(&self.sender, move |state| {
|
run_unary(&self.sender, move |state| {
|
||||||
let output_names = state
|
let output_names = state
|
||||||
.pinnacle
|
.pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.map(|output| output.name())
|
.map(|output| output.name())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -1325,6 +1400,18 @@ impl output_service_server::OutputService for OutputService {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let enabled = output.as_ref().map(|output| {
|
||||||
|
state
|
||||||
|
.pinnacle
|
||||||
|
.outputs
|
||||||
|
.get(output)
|
||||||
|
.is_some_and(|global| global.is_some())
|
||||||
|
});
|
||||||
|
|
||||||
|
let powered = output
|
||||||
|
.as_ref()
|
||||||
|
.map(|output| output.with_state(|state| state.powered));
|
||||||
|
|
||||||
output::v0alpha1::GetPropertiesResponse {
|
output::v0alpha1::GetPropertiesResponse {
|
||||||
make,
|
make,
|
||||||
model,
|
model,
|
||||||
|
@ -1343,6 +1430,8 @@ impl output_service_server::OutputService for OutputService {
|
||||||
transform,
|
transform,
|
||||||
serial,
|
serial,
|
||||||
keyboard_focus_stack_window_ids,
|
keyboard_focus_stack_window_ids,
|
||||||
|
enabled,
|
||||||
|
powered,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
@ -1378,7 +1467,7 @@ impl render_service_server::RenderService for RenderService {
|
||||||
|
|
||||||
run_unary_no_response(&self.sender, move |state| {
|
run_unary_no_response(&self.sender, move |state| {
|
||||||
state.backend.set_upscale_filter(filter);
|
state.backend.set_upscale_filter(filter);
|
||||||
for output in state.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
for output in state.pinnacle.outputs.keys().cloned().collect::<Vec<_>>() {
|
||||||
state.backend.reset_buffers(&output);
|
state.backend.reset_buffers(&output);
|
||||||
state.schedule_render(&output);
|
state.schedule_render(&output);
|
||||||
}
|
}
|
||||||
|
@ -1403,7 +1492,7 @@ impl render_service_server::RenderService for RenderService {
|
||||||
|
|
||||||
run_unary_no_response(&self.sender, move |state| {
|
run_unary_no_response(&self.sender, move |state| {
|
||||||
state.backend.set_downscale_filter(filter);
|
state.backend.set_downscale_filter(filter);
|
||||||
for output in state.pinnacle.space.outputs().cloned().collect::<Vec<_>>() {
|
for output in state.pinnacle.outputs.keys().cloned().collect::<Vec<_>>() {
|
||||||
state.backend.reset_buffers(&output);
|
state.backend.reset_buffers(&output);
|
||||||
state.schedule_render(&output);
|
state.schedule_render(&output);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ use smithay::{
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
output::OutputMode,
|
||||||
state::{Pinnacle, State, SurfaceDmabufFeedback, WithState},
|
state::{Pinnacle, State, SurfaceDmabufFeedback, WithState},
|
||||||
window::WindowElement,
|
window::WindowElement,
|
||||||
};
|
};
|
||||||
|
@ -151,6 +152,8 @@ pub trait BackendData: 'static {
|
||||||
|
|
||||||
// INFO: only for udev in anvil, maybe shouldn't be a trait fn?
|
// INFO: only for udev in anvil, maybe shouldn't be a trait fn?
|
||||||
fn early_import(&mut self, surface: &WlSurface);
|
fn early_import(&mut self, surface: &WlSurface);
|
||||||
|
|
||||||
|
fn set_output_mode(&mut self, output: &Output, mode: OutputMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackendData for Backend {
|
impl BackendData for Backend {
|
||||||
|
@ -180,6 +183,15 @@ impl BackendData for Backend {
|
||||||
Backend::Dummy(dummy) => dummy.early_import(surface),
|
Backend::Dummy(dummy) => dummy.early_import(surface),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_output_mode(&mut self, output: &Output, mode: OutputMode) {
|
||||||
|
match self {
|
||||||
|
Backend::Winit(winit) => winit.set_output_mode(output, mode),
|
||||||
|
Backend::Udev(udev) => udev.set_output_mode(output, mode),
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
Backend::Dummy(dummy) => dummy.set_output_mode(output, mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update surface primary scanout outputs and send frames and dmabuf feedback to visible windows
|
/// Update surface primary scanout outputs and send frames and dmabuf feedback to visible windows
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||||
OutputConnectResponse, OutputDisconnectResponse,
|
|
||||||
};
|
|
||||||
use smithay::backend::renderer::test::DummyRenderer;
|
use smithay::backend::renderer::test::DummyRenderer;
|
||||||
use smithay::backend::renderer::ImportMemWl;
|
use smithay::backend::renderer::ImportMemWl;
|
||||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
@ -12,6 +10,7 @@ use smithay::{
|
||||||
utils::Transform,
|
utils::Transform,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::output::OutputMode;
|
||||||
use crate::state::{Pinnacle, State, WithState};
|
use crate::state::{Pinnacle, State, WithState};
|
||||||
|
|
||||||
use super::BackendData;
|
use super::BackendData;
|
||||||
|
@ -30,6 +29,7 @@ pub struct Dummy {
|
||||||
// pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
// pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||||
#[cfg(feature = "wlcs")]
|
#[cfg(feature = "wlcs")]
|
||||||
pub wlcs_state: Wlcs,
|
pub wlcs_state: Wlcs,
|
||||||
|
pub output: Output,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
|
@ -50,6 +50,10 @@ impl BackendData for Dummy {
|
||||||
fn reset_buffers(&mut self, _output: &Output) {}
|
fn reset_buffers(&mut self, _output: &Output) {}
|
||||||
|
|
||||||
fn early_import(&mut self, _surface: &WlSurface) {}
|
fn early_import(&mut self, _surface: &WlSurface) {}
|
||||||
|
|
||||||
|
fn set_output_mode(&mut self, output: &Output, mode: OutputMode) {
|
||||||
|
output.change_current_state(Some(mode.into()), None, None, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dummy {
|
impl Dummy {
|
||||||
|
@ -85,15 +89,18 @@ impl Dummy {
|
||||||
// dmabuf_state,
|
// dmabuf_state,
|
||||||
#[cfg(feature = "wlcs")]
|
#[cfg(feature = "wlcs")]
|
||||||
wlcs_state: Wlcs::default(),
|
wlcs_state: Wlcs::default(),
|
||||||
|
output: output.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
UninitBackend {
|
UninitBackend {
|
||||||
seat_name: dummy.seat_name(),
|
seat_name: dummy.seat_name(),
|
||||||
init: Box::new(move |pinnacle| {
|
init: Box::new(move |pinnacle| {
|
||||||
output.create_global::<State>(&display_handle);
|
let global = output.create_global::<State>(&display_handle);
|
||||||
|
|
||||||
pinnacle.output_focus_stack.set_focus(output.clone());
|
pinnacle.output_focus_stack.set_focus(output.clone());
|
||||||
|
|
||||||
|
pinnacle.outputs.insert(output.clone(), Some(global));
|
||||||
|
|
||||||
pinnacle
|
pinnacle
|
||||||
.shm_state
|
.shm_state
|
||||||
.update_formats(dummy.renderer.shm_formats());
|
.update_formats(dummy.renderer.shm_formats());
|
||||||
|
@ -131,7 +138,9 @@ impl Pinnacle {
|
||||||
output.set_preferred(mode);
|
output.set_preferred(mode);
|
||||||
output.with_state_mut(|state| state.modes = vec![mode]);
|
output.with_state_mut(|state| state.modes = vec![mode]);
|
||||||
|
|
||||||
output.create_global::<State>(&self.display_handle);
|
let global = output.create_global::<State>(&self.display_handle);
|
||||||
|
|
||||||
|
self.outputs.insert(output.clone(), Some(global));
|
||||||
|
|
||||||
self.space.map_output(&output, (0, 0));
|
self.space.map_output(&output, (0, 0));
|
||||||
|
|
||||||
|
@ -141,14 +150,4 @@ impl Pinnacle {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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()),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
mod drm;
|
mod drm;
|
||||||
mod gamma;
|
mod gamma;
|
||||||
|
|
||||||
|
pub use drm::util::drm_mode_from_api_modeline;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::Path,
|
path::Path,
|
||||||
|
@ -10,10 +12,8 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use drm::set_crtc_active;
|
use drm::{set_crtc_active, util::create_drm_mode};
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||||
OutputConnectResponse, OutputDisconnectResponse,
|
|
||||||
};
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
allocator::{
|
allocator::{
|
||||||
|
@ -51,10 +51,7 @@ use smithay::{
|
||||||
vulkan::{self, version::Version, PhysicalDevice},
|
vulkan::{self, version::Version, PhysicalDevice},
|
||||||
SwapBuffersError,
|
SwapBuffersError,
|
||||||
},
|
},
|
||||||
desktop::{
|
desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||||
layer_map_for_output,
|
|
||||||
utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
|
||||||
},
|
|
||||||
input::pointer::CursorImageStatus,
|
input::pointer::CursorImageStatus,
|
||||||
output::{Output, PhysicalProperties, Subpixel},
|
output::{Output, PhysicalProperties, Subpixel},
|
||||||
reexports::{
|
reexports::{
|
||||||
|
@ -71,7 +68,6 @@ use smithay::{
|
||||||
presentation_time::server::wp_presentation_feedback,
|
presentation_time::server::wp_presentation_feedback,
|
||||||
},
|
},
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
backend::GlobalId,
|
|
||||||
protocol::{wl_shm, wl_surface::WlSurface},
|
protocol::{wl_shm, wl_surface::WlSurface},
|
||||||
DisplayHandle,
|
DisplayHandle,
|
||||||
},
|
},
|
||||||
|
@ -88,7 +84,7 @@ use tracing::{debug, error, info, trace, warn};
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
config::ConnectorSavedState,
|
config::ConnectorSavedState,
|
||||||
output::{BlankingState, OutputName},
|
output::{BlankingState, OutputMode, OutputName},
|
||||||
render::{
|
render::{
|
||||||
pointer::PointerElement, pointer_render_elements, take_presentation_feedback,
|
pointer::PointerElement, pointer_render_elements, take_presentation_feedback,
|
||||||
OutputRenderElement, CLEAR_COLOR, CLEAR_COLOR_LOCKED,
|
OutputRenderElement, CLEAR_COLOR, CLEAR_COLOR_LOCKED,
|
||||||
|
@ -527,9 +523,12 @@ impl Udev {
|
||||||
/// Schedule a new render that will cause the compositor to redraw everything.
|
/// Schedule a new render that will cause the compositor to redraw everything.
|
||||||
pub fn schedule_render(&mut self, loop_handle: &LoopHandle<State>, output: &Output) {
|
pub fn schedule_render(&mut self, loop_handle: &LoopHandle<State>, output: &Output) {
|
||||||
let Some(surface) = render_surface_for_output(output, &mut self.backends) else {
|
let Some(surface) = render_surface_for_output(output, &mut self.backends) else {
|
||||||
|
tracing::info!("no render surface on output {}", output.name());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// tracing::info!(state = ?surface.render_state, name = output.name());
|
||||||
|
|
||||||
match &surface.render_state {
|
match &surface.render_state {
|
||||||
RenderState::Idle => {
|
RenderState::Idle => {
|
||||||
let output = output.clone();
|
let output = output.clone();
|
||||||
|
@ -577,6 +576,12 @@ impl Udev {
|
||||||
{
|
{
|
||||||
warn!("Failed to reset compositor state on crtc {crtc:?}: {err}");
|
warn!("Failed to reset compositor state on crtc {crtc:?}: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(surface) = render_surface_for_output(output, &mut self.backends) {
|
||||||
|
if let RenderState::Scheduled(idle) = std::mem::take(&mut surface.render_state) {
|
||||||
|
idle.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -638,51 +643,6 @@ impl State {
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the output with the given mode.
|
|
||||||
///
|
|
||||||
/// TODO: This is in udev.rs but is also used in winit.rs.
|
|
||||||
/// | I've got no clue how to make things public without making a mess.
|
|
||||||
pub fn resize_output(&mut self, output: &Output, mode: smithay::output::Mode) {
|
|
||||||
if let Backend::Udev(udev) = &mut self.backend {
|
|
||||||
let drm_mode = udev.backends.iter().find_map(|(_, backend)| {
|
|
||||||
backend
|
|
||||||
.drm_scanner
|
|
||||||
.crtcs()
|
|
||||||
.find(|(_, handle)| {
|
|
||||||
output
|
|
||||||
.user_data()
|
|
||||||
.get::<UdevOutputData>()
|
|
||||||
.is_some_and(|data| &data.crtc == handle)
|
|
||||||
})
|
|
||||||
.and_then(|(info, _)| {
|
|
||||||
info.modes()
|
|
||||||
.iter()
|
|
||||||
.find(|m| smithay::output::Mode::from(**m) == mode)
|
|
||||||
})
|
|
||||||
.copied()
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(drm_mode) = drm_mode {
|
|
||||||
if let Some(render_surface) = render_surface_for_output(output, &mut udev.backends)
|
|
||||||
{
|
|
||||||
match render_surface.compositor.use_mode(drm_mode) {
|
|
||||||
Ok(()) => {
|
|
||||||
self.pinnacle
|
|
||||||
.change_output_state(output, Some(mode), None, None, None);
|
|
||||||
}
|
|
||||||
Err(err) => error!("Failed to resize output: {err}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.pinnacle
|
|
||||||
.change_output_state(output, Some(mode), None, None, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pinnacle.request_layout(output);
|
|
||||||
self.schedule_render(output);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackendData for Udev {
|
impl BackendData for Udev {
|
||||||
|
@ -705,6 +665,61 @@ impl BackendData for Udev {
|
||||||
warn!("early buffer import failed: {}", err);
|
warn!("early buffer import failed: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_output_mode(&mut self, output: &Output, mode: OutputMode) {
|
||||||
|
let drm_mode = self
|
||||||
|
.backends
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, backend)| {
|
||||||
|
backend
|
||||||
|
.drm_scanner
|
||||||
|
.crtcs()
|
||||||
|
.find(|(_, handle)| {
|
||||||
|
output
|
||||||
|
.user_data()
|
||||||
|
.get::<UdevOutputData>()
|
||||||
|
.is_some_and(|data| &data.crtc == handle)
|
||||||
|
})
|
||||||
|
.and_then(|(info, _)| {
|
||||||
|
info.modes()
|
||||||
|
.iter()
|
||||||
|
.find(|m| smithay::output::Mode::from(**m) == mode.into())
|
||||||
|
})
|
||||||
|
.copied()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
info!("Unknown mode for {}, creating new one", output.name());
|
||||||
|
match mode {
|
||||||
|
OutputMode::Smithay(mode) => {
|
||||||
|
create_drm_mode(mode.size.w, mode.size.h, Some(mode.refresh as u32))
|
||||||
|
}
|
||||||
|
OutputMode::Drm(mode) => mode,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) {
|
||||||
|
match render_surface.compositor.use_mode(drm_mode) {
|
||||||
|
Ok(()) => {
|
||||||
|
let mode = smithay::output::Mode::from(mode);
|
||||||
|
info!(
|
||||||
|
"Set {}'s mode to {}x{}@{:.3}Hz",
|
||||||
|
output.name(),
|
||||||
|
mode.size.w,
|
||||||
|
mode.size.h,
|
||||||
|
mode.refresh as f64 / 1000.0
|
||||||
|
);
|
||||||
|
output.change_current_state(Some(mode), None, None, None);
|
||||||
|
output.with_state_mut(|state| {
|
||||||
|
// TODO: push or no?
|
||||||
|
if !state.modes.contains(&mode) {
|
||||||
|
state.modes.push(mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => warn!("Failed to set output mode for {}: {err}", output.name()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: document desperately
|
// TODO: document desperately
|
||||||
|
@ -810,7 +825,6 @@ enum RenderState {
|
||||||
/// The idle token from a render being scheduled.
|
/// The idle token from a render being scheduled.
|
||||||
/// This is used to cancel renders if, for example,
|
/// This is used to cancel renders if, for example,
|
||||||
/// the output being rendered is removed.
|
/// the output being rendered is removed.
|
||||||
#[allow(dead_code)] // TODO:
|
|
||||||
Idle<'static>,
|
Idle<'static>,
|
||||||
),
|
),
|
||||||
/// A frame was rendered and scheduled and we are waiting for vblank.
|
/// A frame was rendered and scheduled and we are waiting for vblank.
|
||||||
|
@ -823,10 +837,6 @@ enum RenderState {
|
||||||
|
|
||||||
/// Render surface for an output.
|
/// Render surface for an output.
|
||||||
struct RenderSurface {
|
struct RenderSurface {
|
||||||
/// The output global id.
|
|
||||||
global: Option<GlobalId>,
|
|
||||||
/// A display handle used to remove the global on drop.
|
|
||||||
display_handle: DisplayHandle,
|
|
||||||
/// The node from `connector_connected`.
|
/// The node from `connector_connected`.
|
||||||
device_id: DrmNode,
|
device_id: DrmNode,
|
||||||
/// The node rendering to the screen? idk
|
/// The node rendering to the screen? idk
|
||||||
|
@ -862,15 +872,6 @@ struct ScreencopyCommitState {
|
||||||
_cursor: CommitCounter,
|
_cursor: CommitCounter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RenderSurface {
|
|
||||||
// Stop advertising this output to clients on drop.
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Some(global) = self.global.take() {
|
|
||||||
self.display_handle.remove_global::<State>(global);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GbmDrmCompositor = DrmCompositor<
|
type GbmDrmCompositor = DrmCompositor<
|
||||||
GbmAllocator<DrmDeviceFd>,
|
GbmAllocator<DrmDeviceFd>,
|
||||||
GbmDevice<DrmDeviceFd>,
|
GbmDevice<DrmDeviceFd>,
|
||||||
|
@ -1036,7 +1037,7 @@ impl Udev {
|
||||||
|
|
||||||
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
|
let (phys_w, phys_h) = connector.size().unwrap_or((0, 0));
|
||||||
|
|
||||||
if pinnacle.space.outputs().any(|op| {
|
if pinnacle.outputs.keys().any(|op| {
|
||||||
op.user_data()
|
op.user_data()
|
||||||
.get::<UdevOutputData>()
|
.get::<UdevOutputData>()
|
||||||
.is_some_and(|op_id| op_id.crtc == crtc)
|
.is_some_and(|op_id| op_id.crtc == crtc)
|
||||||
|
@ -1055,7 +1056,11 @@ impl Udev {
|
||||||
);
|
);
|
||||||
let global = output.create_global::<State>(&self.display_handle);
|
let global = output.create_global::<State>(&self.display_handle);
|
||||||
|
|
||||||
output.with_state_mut(|state| state.serial = serial);
|
pinnacle.outputs.insert(output.clone(), Some(global));
|
||||||
|
|
||||||
|
output.with_state_mut(|state| {
|
||||||
|
state.serial = serial;
|
||||||
|
});
|
||||||
|
|
||||||
output.set_preferred(wl_mode);
|
output.set_preferred(wl_mode);
|
||||||
|
|
||||||
|
@ -1067,6 +1072,10 @@ impl Udev {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
output.with_state_mut(|state| state.modes = modes);
|
output.with_state_mut(|state| state.modes = modes);
|
||||||
|
|
||||||
|
pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.add_head::<State>(&output);
|
||||||
|
|
||||||
let x = pinnacle.space.outputs().fold(0, |acc, o| {
|
let x = pinnacle.space.outputs().fold(0, |acc, o| {
|
||||||
let Some(geo) = pinnacle.space.output_geometry(o) else {
|
let Some(geo) = pinnacle.space.output_geometry(o) else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
@ -1127,10 +1136,8 @@ impl Udev {
|
||||||
);
|
);
|
||||||
|
|
||||||
let surface = RenderSurface {
|
let surface = RenderSurface {
|
||||||
display_handle: self.display_handle.clone(),
|
|
||||||
device_id: node,
|
device_id: node,
|
||||||
render_node: device.render_node,
|
render_node: device.render_node,
|
||||||
global: Some(global),
|
|
||||||
compositor,
|
compositor,
|
||||||
dmabuf_feedback,
|
dmabuf_feedback,
|
||||||
render_state: RenderState::Idle,
|
render_state: RenderState::Idle,
|
||||||
|
@ -1141,7 +1148,14 @@ impl Udev {
|
||||||
|
|
||||||
device.surfaces.insert(crtc, surface);
|
device.surfaces.insert(crtc, surface);
|
||||||
|
|
||||||
pinnacle.change_output_state(&output, Some(wl_mode), None, None, Some(position));
|
pinnacle.change_output_state(
|
||||||
|
self,
|
||||||
|
&output,
|
||||||
|
Some(OutputMode::Smithay(wl_mode)),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(position),
|
||||||
|
);
|
||||||
|
|
||||||
// If there is saved connector state, the connector was previously plugged in.
|
// If there is saved connector state, the connector was previously plugged in.
|
||||||
// In this case, restore its tags and location.
|
// In this case, restore its tags and location.
|
||||||
|
@ -1153,7 +1167,7 @@ impl Udev {
|
||||||
{
|
{
|
||||||
let ConnectorSavedState { loc, tags, scale } = saved_state;
|
let ConnectorSavedState { loc, tags, scale } = saved_state;
|
||||||
output.with_state_mut(|state| state.tags.clone_from(tags));
|
output.with_state_mut(|state| state.tags.clone_from(tags));
|
||||||
pinnacle.change_output_state(&output, None, None, *scale, Some(*loc));
|
pinnacle.change_output_state(self, &output, None, None, *scale, Some(*loc));
|
||||||
} else {
|
} else {
|
||||||
pinnacle.signal_state.output_connect.signal(|buffer| {
|
pinnacle.signal_state.output_connect.signal(|buffer| {
|
||||||
buffer.push_back(OutputConnectResponse {
|
buffer.push_back(OutputConnectResponse {
|
||||||
|
@ -1161,6 +1175,8 @@ impl Udev {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinnacle.output_management_manager_state.update::<State>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A display was unplugged.
|
/// A display was unplugged.
|
||||||
|
@ -1181,8 +1197,8 @@ impl Udev {
|
||||||
device.surfaces.remove(&crtc);
|
device.surfaces.remove(&crtc);
|
||||||
|
|
||||||
let output = pinnacle
|
let output = pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.find(|o| {
|
.find(|o| {
|
||||||
o.user_data()
|
o.user_data()
|
||||||
.get::<UdevOutputData>()
|
.get::<UdevOutputData>()
|
||||||
|
@ -1192,29 +1208,7 @@ impl Udev {
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
if let Some(output) = output {
|
if let Some(output) = output {
|
||||||
// Save this output's state. It will be restored if the monitor gets replugged.
|
pinnacle.remove_output(&output);
|
||||||
pinnacle.config.connector_saved_states.insert(
|
|
||||||
OutputName(output.name()),
|
|
||||||
ConnectorSavedState {
|
|
||||||
loc: output.current_location(),
|
|
||||||
tags: output.with_state(|state| state.tags.clone()),
|
|
||||||
scale: Some(output.current_scale()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: extract into a `remove_output` function and unify with dummy backend
|
|
||||||
for layer in layer_map_for_output(&output).layers() {
|
|
||||||
layer.layer_surface().send_close();
|
|
||||||
}
|
|
||||||
|
|
||||||
pinnacle.space.unmap_output(&output);
|
|
||||||
pinnacle.gamma_control_manager_state.output_removed(&output);
|
|
||||||
|
|
||||||
pinnacle.signal_state.output_disconnect.signal(|buffer| {
|
|
||||||
buffer.push_back(OutputDisconnectResponse {
|
|
||||||
output_name: Some(output.name()),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1290,7 +1284,7 @@ impl Udev {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = if let Some(output) = pinnacle.space.outputs().find(|o| {
|
let output = if let Some(output) = pinnacle.outputs.keys().find(|o| {
|
||||||
let udev_op_data = o.user_data().get::<UdevOutputData>();
|
let udev_op_data = o.user_data().get::<UdevOutputData>();
|
||||||
udev_op_data
|
udev_op_data
|
||||||
.is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc)
|
.is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc)
|
||||||
|
@ -1386,6 +1380,11 @@ impl Udev {
|
||||||
|
|
||||||
assert!(matches!(surface.render_state, RenderState::Scheduled(_)));
|
assert!(matches!(surface.render_state, RenderState::Scheduled(_)));
|
||||||
|
|
||||||
|
if !pinnacle.outputs.contains_key(output) {
|
||||||
|
surface.render_state = RenderState::Idle;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: possibly lift this out and make it so that scheduling a render
|
// TODO: possibly lift this out and make it so that scheduling a render
|
||||||
// does nothing on powered off outputs
|
// does nothing on powered off outputs
|
||||||
if output.with_state(|state| !state.powered) {
|
if output.with_state(|state| !state.powered) {
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
use std::num::NonZeroU32;
|
use std::{ffi::CString, io::Write, mem::MaybeUninit, num::NonZeroU32};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle};
|
use drm_sys::{
|
||||||
|
drm_mode_modeinfo, DRM_MODE_FLAG_NHSYNC, DRM_MODE_FLAG_NVSYNC, DRM_MODE_FLAG_PHSYNC,
|
||||||
|
DRM_MODE_FLAG_PVSYNC, DRM_MODE_TYPE_USERDEF,
|
||||||
|
};
|
||||||
|
use libdisplay_info_sys::cvt::{
|
||||||
|
di_cvt_compute, di_cvt_options, di_cvt_reduced_blanking_version_DI_CVT_REDUCED_BLANKING_NONE,
|
||||||
|
di_cvt_timing,
|
||||||
|
};
|
||||||
|
use pinnacle_api_defs::pinnacle::output::v0alpha1::SetModelineRequest;
|
||||||
|
use smithay::reexports::drm::{
|
||||||
|
self,
|
||||||
|
control::{connector, property, Device, ResourceHandle},
|
||||||
|
};
|
||||||
|
|
||||||
use super::edid_manus::get_manufacturer;
|
use super::edid_manus::get_manufacturer;
|
||||||
|
|
||||||
|
@ -129,3 +141,131 @@ pub(super) fn get_drm_property(
|
||||||
}
|
}
|
||||||
anyhow::bail!("No prop found for {}", name)
|
anyhow::bail!("No prop found for {}", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn drm_mode_from_api_modeline(modeline: SetModelineRequest) -> Option<drm::control::Mode> {
|
||||||
|
let SetModelineRequest {
|
||||||
|
output_name: _,
|
||||||
|
clock: Some(clock),
|
||||||
|
hdisplay: Some(hdisplay),
|
||||||
|
hsync_start: Some(hsync_start),
|
||||||
|
hsync_end: Some(hsync_end),
|
||||||
|
htotal: Some(htotal),
|
||||||
|
vdisplay: Some(vdisplay),
|
||||||
|
vsync_start: Some(vsync_start),
|
||||||
|
vsync_end: Some(vsync_end),
|
||||||
|
vtotal: Some(vtotal),
|
||||||
|
hsync_pos: Some(hsync_pos),
|
||||||
|
vsync_pos: Some(vsync_pos),
|
||||||
|
} = modeline
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let clock = clock * 1000.0;
|
||||||
|
|
||||||
|
let vrefresh = (clock * 1000.0 * 1000.0 / htotal as f32 / vtotal as f32) as u32;
|
||||||
|
|
||||||
|
let mut flags = 0;
|
||||||
|
match hsync_pos {
|
||||||
|
true => flags |= DRM_MODE_FLAG_PHSYNC,
|
||||||
|
false => flags |= DRM_MODE_FLAG_NHSYNC,
|
||||||
|
};
|
||||||
|
match vsync_pos {
|
||||||
|
true => flags |= DRM_MODE_FLAG_PVSYNC,
|
||||||
|
false => flags |= DRM_MODE_FLAG_NVSYNC,
|
||||||
|
};
|
||||||
|
|
||||||
|
let type_ = DRM_MODE_TYPE_USERDEF;
|
||||||
|
|
||||||
|
let name = CString::new(format!(
|
||||||
|
"{}x{}@{:.3}",
|
||||||
|
hdisplay,
|
||||||
|
vdisplay,
|
||||||
|
vrefresh as f64 / 1000.0
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let mut name_buf = [0u8; 32];
|
||||||
|
let _ = name_buf.as_mut_slice().write_all(name.as_bytes_with_nul());
|
||||||
|
let name: [i8; 32] = bytemuck::cast(name_buf);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
drm_mode_modeinfo {
|
||||||
|
clock: clock as u32,
|
||||||
|
hdisplay: hdisplay as u16,
|
||||||
|
hsync_start: hsync_start as u16,
|
||||||
|
hsync_end: hsync_end as u16,
|
||||||
|
htotal: htotal as u16,
|
||||||
|
hskew: 0,
|
||||||
|
vdisplay: vdisplay as u16,
|
||||||
|
vsync_start: vsync_start as u16,
|
||||||
|
vsync_end: vsync_end as u16,
|
||||||
|
vtotal: vtotal as u16,
|
||||||
|
vscan: 0,
|
||||||
|
vrefresh,
|
||||||
|
flags,
|
||||||
|
type_,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new drm mode from a given width, height, and optional refresh rate (defaults to 60Hz).
|
||||||
|
pub fn create_drm_mode(width: i32, height: i32, refresh_mhz: Option<u32>) -> drm::control::Mode {
|
||||||
|
drm::control::Mode::from(generate_cvt_mode(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
refresh_mhz.map(|refresh| refresh as f64 / 1000.0),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/95ac3e99242b4e7f59f00dd073ede405ff8e9e26/backend/drm/util.c#L247
|
||||||
|
fn generate_cvt_mode(hdisplay: i32, vdisplay: i32, vrefresh: Option<f64>) -> drm_mode_modeinfo {
|
||||||
|
let options: di_cvt_options = di_cvt_options {
|
||||||
|
red_blank_ver: di_cvt_reduced_blanking_version_DI_CVT_REDUCED_BLANKING_NONE,
|
||||||
|
h_pixels: hdisplay,
|
||||||
|
v_lines: vdisplay,
|
||||||
|
ip_freq_rqd: vrefresh.unwrap_or(60.0),
|
||||||
|
video_opt: false,
|
||||||
|
vblank: 0.0,
|
||||||
|
additional_hblank: 0,
|
||||||
|
early_vsync_rqd: false,
|
||||||
|
int_rqd: false,
|
||||||
|
margins_rqd: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut timing = MaybeUninit::<di_cvt_timing>::zeroed();
|
||||||
|
// SAFETY: is an ffi function
|
||||||
|
unsafe { di_cvt_compute(timing.as_mut_ptr(), &options as *const _) };
|
||||||
|
|
||||||
|
// SAFETY: Initialized in the function above
|
||||||
|
let timing = unsafe { timing.assume_init() };
|
||||||
|
|
||||||
|
let hsync_start = (hdisplay + timing.h_front_porch as i32) as u16;
|
||||||
|
let vsync_start = (timing.v_lines_rnd + timing.v_front_porch) as u16;
|
||||||
|
let hsync_end = hsync_start + timing.h_sync as u16;
|
||||||
|
let vsync_end = vsync_start + timing.v_sync as u16;
|
||||||
|
|
||||||
|
let name = CString::new(format!("{}x{}", hdisplay, vdisplay)).unwrap();
|
||||||
|
let mut name_buf = [0u8; 32];
|
||||||
|
let _ = name_buf.as_mut_slice().write_all(name.as_bytes_with_nul());
|
||||||
|
let name: [i8; 32] = bytemuck::cast(name_buf);
|
||||||
|
|
||||||
|
drm_mode_modeinfo {
|
||||||
|
clock: f64::round(timing.act_pixel_freq * 1000.0) as u32,
|
||||||
|
hdisplay: hdisplay as u16,
|
||||||
|
hsync_start,
|
||||||
|
hsync_end,
|
||||||
|
htotal: hsync_end + timing.h_back_porch as u16,
|
||||||
|
hskew: 0,
|
||||||
|
vdisplay: timing.v_lines_rnd as u16,
|
||||||
|
vsync_start,
|
||||||
|
vsync_end,
|
||||||
|
vtotal: vsync_end + timing.v_back_porch as u16,
|
||||||
|
vscan: 0,
|
||||||
|
vrefresh: f64::round(timing.act_frame_rate) as u32,
|
||||||
|
flags: DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC,
|
||||||
|
type_: DRM_MODE_TYPE_USERDEF,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ use smithay::{
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
output::BlankingState,
|
output::{BlankingState, OutputMode},
|
||||||
render::{
|
render::{
|
||||||
pointer::PointerElement, pointer_render_elements, take_presentation_feedback, CLEAR_COLOR,
|
pointer::PointerElement, pointer_render_elements, take_presentation_feedback, CLEAR_COLOR,
|
||||||
CLEAR_COLOR_LOCKED,
|
CLEAR_COLOR_LOCKED,
|
||||||
|
@ -67,6 +67,10 @@ impl BackendData for Winit {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn early_import(&mut self, _surface: &WlSurface) {}
|
fn early_import(&mut self, _surface: &WlSurface) {}
|
||||||
|
|
||||||
|
fn set_output_mode(&mut self, output: &Output, mode: OutputMode) {
|
||||||
|
output.change_current_state(Some(mode.into()), None, None, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
|
@ -181,10 +185,12 @@ impl Winit {
|
||||||
|
|
||||||
let init = Box::new(move |pinnacle: &mut Pinnacle| {
|
let init = Box::new(move |pinnacle: &mut Pinnacle| {
|
||||||
let output = winit.output.clone();
|
let output = winit.output.clone();
|
||||||
output.create_global::<State>(&display_handle);
|
let global = output.create_global::<State>(&display_handle);
|
||||||
|
|
||||||
pinnacle.output_focus_stack.set_focus(output.clone());
|
pinnacle.output_focus_stack.set_focus(output.clone());
|
||||||
|
|
||||||
|
pinnacle.outputs.insert(output.clone(), Some(global));
|
||||||
|
|
||||||
pinnacle
|
pinnacle
|
||||||
.shm_state
|
.shm_state
|
||||||
.update_formats(winit.backend.renderer().shm_formats());
|
.update_formats(winit.backend.renderer().shm_formats());
|
||||||
|
@ -201,8 +207,9 @@ impl Winit {
|
||||||
refresh: 144_000,
|
refresh: 144_000,
|
||||||
};
|
};
|
||||||
state.pinnacle.change_output_state(
|
state.pinnacle.change_output_state(
|
||||||
|
&mut state.backend,
|
||||||
&output,
|
&output,
|
||||||
Some(mode),
|
Some(OutputMode::Smithay(mode)),
|
||||||
None,
|
None,
|
||||||
Some(Scale::Fractional(scale_factor)),
|
Some(Scale::Fractional(scale_factor)),
|
||||||
// None,
|
// None,
|
||||||
|
|
|
@ -347,6 +347,7 @@ pub struct ConnectorSavedState {
|
||||||
pub tags: Vec<Tag>,
|
pub tags: Vec<Tag>,
|
||||||
/// The output's previous scale
|
/// The output's previous scale
|
||||||
pub scale: Option<smithay::output::Scale>,
|
pub scale: Option<smithay::output::Scale>,
|
||||||
|
// TODO: transform
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a metaconfig file in `config_dir`, if any.
|
/// Parse a metaconfig file in `config_dir`, if any.
|
||||||
|
@ -380,7 +381,7 @@ impl Pinnacle {
|
||||||
// Clear state
|
// Clear state
|
||||||
|
|
||||||
debug!("Clearing tags");
|
debug!("Clearing tags");
|
||||||
for output in self.space.outputs() {
|
for output in self.outputs.keys() {
|
||||||
output.with_state_mut(|state| state.tags.clear());
|
output.with_state_mut(|state| state.tags.clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
125
src/handlers.rs
125
src/handlers.rs
|
@ -1,17 +1,18 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
pub mod idle;
|
||||||
pub mod session_lock;
|
pub mod session_lock;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
mod xdg_shell;
|
mod xdg_shell;
|
||||||
mod xwayland;
|
mod xwayland;
|
||||||
|
|
||||||
use std::{mem, os::fd::OwnedFd, sync::Arc};
|
use std::{collections::HashMap, mem, os::fd::OwnedFd, sync::Arc};
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::renderer::utils::{self, with_renderer_surface_state},
|
backend::renderer::utils::{self, with_renderer_surface_state},
|
||||||
delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale,
|
delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale,
|
||||||
delegate_idle_notify, delegate_layer_shell, delegate_output, delegate_pointer_constraints,
|
delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_presentation,
|
||||||
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||||
delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell,
|
delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell,
|
||||||
desktop::{
|
desktop::{
|
||||||
self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind,
|
self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind,
|
||||||
|
@ -21,7 +22,7 @@ use smithay::{
|
||||||
pointer::{CursorImageStatus, PointerHandle},
|
pointer::{CursorImageStatus, PointerHandle},
|
||||||
Seat, SeatHandler, SeatState,
|
Seat, SeatHandler, SeatState,
|
||||||
},
|
},
|
||||||
output::Output,
|
output::{Mode, Output, Scale},
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::Interest,
|
calloop::Interest,
|
||||||
wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment,
|
wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment,
|
||||||
|
@ -42,7 +43,6 @@ use smithay::{
|
||||||
},
|
},
|
||||||
dmabuf,
|
dmabuf,
|
||||||
fractional_scale::{self, FractionalScaleHandler},
|
fractional_scale::{self, FractionalScaleHandler},
|
||||||
idle_notify::{IdleNotifierHandler, IdleNotifierState},
|
|
||||||
output::OutputHandler,
|
output::OutputHandler,
|
||||||
pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler},
|
pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler},
|
||||||
seat::WaylandFocus,
|
seat::WaylandFocus,
|
||||||
|
@ -69,16 +69,22 @@ use smithay::{
|
||||||
},
|
},
|
||||||
xwayland::{X11Wm, XWaylandClientData},
|
xwayland::{X11Wm, XWaylandClientData},
|
||||||
};
|
};
|
||||||
use tracing::{error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy,
|
delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management,
|
||||||
|
delegate_output_power_management, delegate_screencopy,
|
||||||
focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget},
|
focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget},
|
||||||
handlers::xdg_shell::snapshot_pre_commit_hook,
|
handlers::xdg_shell::snapshot_pre_commit_hook,
|
||||||
|
output::OutputMode,
|
||||||
protocol::{
|
protocol::{
|
||||||
foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState},
|
foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState},
|
||||||
gamma_control::{GammaControlHandler, GammaControlManagerState},
|
gamma_control::{GammaControlHandler, GammaControlManagerState},
|
||||||
|
output_management::{
|
||||||
|
OutputConfiguration, OutputManagementHandler, OutputManagementManagerState,
|
||||||
|
},
|
||||||
|
output_power_management::{OutputPowerManagementHandler, OutputPowerManagementState},
|
||||||
screencopy::{Screencopy, ScreencopyHandler},
|
screencopy::{Screencopy, ScreencopyHandler},
|
||||||
},
|
},
|
||||||
render::util::snapshot::capture_snapshots_on_output,
|
render::util::snapshot::capture_snapshots_on_output,
|
||||||
|
@ -918,12 +924,109 @@ impl XWaylandShellHandler for State {
|
||||||
}
|
}
|
||||||
delegate_xwayland_shell!(State);
|
delegate_xwayland_shell!(State);
|
||||||
|
|
||||||
impl IdleNotifierHandler for State {
|
impl OutputManagementHandler for State {
|
||||||
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
fn output_management_manager_state(&mut self) -> &mut OutputManagementManagerState {
|
||||||
&mut self.pinnacle.idle_notifier_state
|
&mut self.pinnacle.output_management_manager_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_configuration(&mut self, config: HashMap<Output, OutputConfiguration>) -> bool {
|
||||||
|
for (output, config) in config {
|
||||||
|
match config {
|
||||||
|
OutputConfiguration::Disabled => {
|
||||||
|
self.pinnacle.set_output_enabled(&output, false);
|
||||||
|
// TODO: split
|
||||||
|
self.backend.set_output_powered(&output, false);
|
||||||
|
}
|
||||||
|
OutputConfiguration::Enabled {
|
||||||
|
mode,
|
||||||
|
position,
|
||||||
|
transform,
|
||||||
|
scale,
|
||||||
|
adaptive_sync: _,
|
||||||
|
} => {
|
||||||
|
self.pinnacle.set_output_enabled(&output, true);
|
||||||
|
// TODO: split
|
||||||
|
self.backend.set_output_powered(&output, true);
|
||||||
|
|
||||||
|
self.schedule_render(&output);
|
||||||
|
|
||||||
|
let snapshots = self.backend.with_renderer(|renderer| {
|
||||||
|
capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, [])
|
||||||
|
});
|
||||||
|
|
||||||
|
let mode = mode.map(|(size, refresh)| {
|
||||||
|
if let Some(refresh) = refresh {
|
||||||
|
Mode {
|
||||||
|
size,
|
||||||
|
refresh: refresh.get() as i32,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output
|
||||||
|
.with_state(|state| {
|
||||||
|
state
|
||||||
|
.modes
|
||||||
|
.iter()
|
||||||
|
.filter(|mode| mode.size == size)
|
||||||
|
.max_by_key(|mode| mode.refresh)
|
||||||
|
.copied()
|
||||||
|
})
|
||||||
|
.unwrap_or(Mode {
|
||||||
|
size,
|
||||||
|
refresh: 60_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.pinnacle.change_output_state(
|
||||||
|
&mut self.backend,
|
||||||
|
&output,
|
||||||
|
mode.map(OutputMode::Smithay),
|
||||||
|
transform,
|
||||||
|
scale.map(Scale::Fractional),
|
||||||
|
position,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some((a, b)) = snapshots {
|
||||||
|
output.with_state_mut(|state| {
|
||||||
|
state.new_wait_layout_transaction(
|
||||||
|
self.pinnacle.loop_handle.clone(),
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pinnacle.request_layout(&output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.pinnacle
|
||||||
|
.output_management_manager_state
|
||||||
|
.update::<State>();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_configuration(&mut self, config: HashMap<Output, OutputConfiguration>) -> bool {
|
||||||
|
debug!(?config);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate_idle_notify!(State);
|
delegate_output_management!(State);
|
||||||
|
|
||||||
|
impl OutputPowerManagementHandler for State {
|
||||||
|
fn output_power_management_state(&mut self) -> &mut OutputPowerManagementState {
|
||||||
|
&mut self.pinnacle.output_power_management_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_mode(&mut self, output: &Output, powered: bool) {
|
||||||
|
self.backend.set_output_powered(output, powered);
|
||||||
|
|
||||||
|
if powered {
|
||||||
|
self.schedule_render(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate_output_power_management!(State);
|
||||||
|
|
||||||
impl Pinnacle {
|
impl Pinnacle {
|
||||||
fn position_popup(&self, popup: &PopupSurface) {
|
fn position_popup(&self, popup: &PopupSurface) {
|
||||||
|
|
|
@ -32,20 +32,24 @@ impl Pinnacle {
|
||||||
output: &Output,
|
output: &Output,
|
||||||
geometries: Vec<Rectangle<i32, Logical>>,
|
geometries: Vec<Rectangle<i32, Logical>>,
|
||||||
) -> Vec<(WindowElement, Serial)> {
|
) -> Vec<(WindowElement, Serial)> {
|
||||||
let windows_on_foc_tags = output.with_state(|state| {
|
let (windows_on_foc_tags, to_unmap) = output.with_state(|state| {
|
||||||
let focused_tags = state.focused_tags().collect::<Vec<_>>();
|
let focused_tags = state.focused_tags().collect::<Vec<_>>();
|
||||||
self.windows
|
self.windows
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|win| !win.is_x11_override_redirect())
|
.filter(|win| win.output(self).as_ref() == Some(output))
|
||||||
.filter(|win| {
|
.cloned()
|
||||||
|
.partition::<Vec<_>, _>(|win| {
|
||||||
win.with_state(|state| state.tags.iter().any(|tg| focused_tags.contains(&tg)))
|
win.with_state(|state| state.tags.iter().any(|tg| focused_tags.contains(&tg)))
|
||||||
})
|
})
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for win in to_unmap {
|
||||||
|
self.space.unmap_elem(&win);
|
||||||
|
}
|
||||||
|
|
||||||
let tiled_windows = windows_on_foc_tags
|
let tiled_windows = windows_on_foc_tags
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|win| !win.is_x11_override_redirect())
|
||||||
.filter(|win| {
|
.filter(|win| {
|
||||||
win.with_state(|state| {
|
win.with_state(|state| {
|
||||||
state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither()
|
state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither()
|
||||||
|
@ -154,11 +158,19 @@ impl LayoutState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pinnacle {
|
impl Pinnacle {
|
||||||
pub fn request_layout(&mut self, output: &Output) -> Option<LayoutRequestId> {
|
pub fn request_layout(&mut self, output: &Output) {
|
||||||
|
if self
|
||||||
|
.outputs
|
||||||
|
.get(output)
|
||||||
|
.is_some_and(|global| global.is_none())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let id = self.layout_state.next_id();
|
let id = self.layout_state.next_id();
|
||||||
let Some(sender) = self.layout_state.layout_request_sender.as_ref() else {
|
let Some(sender) = self.layout_state.layout_request_sender.as_ref() else {
|
||||||
warn!("Layout requested but no client has connected to the layout service");
|
warn!("Layout requested but no client has connected to the layout service");
|
||||||
return None;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let windows_on_foc_tags = output.with_state(|state| {
|
let windows_on_foc_tags = output.with_state(|state| {
|
||||||
|
@ -209,8 +221,6 @@ impl Pinnacle {
|
||||||
output_width: Some(output_width as u32),
|
output_width: Some(output_width as u32),
|
||||||
output_height: Some(output_height as u32),
|
output_height: Some(output_height as u32),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Some(id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
140
src/output.rs
140
src/output.rs
|
@ -2,16 +2,20 @@
|
||||||
|
|
||||||
use std::{cell::RefCell, num::NonZeroU32};
|
use std::{cell::RefCell, num::NonZeroU32};
|
||||||
|
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputResizeResponse};
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
|
OutputConnectResponse, OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse,
|
||||||
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::layer_map_for_output,
|
desktop::layer_map_for_output,
|
||||||
output::{Mode, Output, Scale},
|
output::{Mode, Output, Scale},
|
||||||
reexports::calloop::LoopHandle,
|
reexports::{calloop::LoopHandle, drm},
|
||||||
utils::{Logical, Point, Transform},
|
utils::{Logical, Point, Transform},
|
||||||
wayland::session_lock::LockSurface,
|
wayland::session_lock::LockSurface,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
backend::BackendData,
|
||||||
|
config::ConnectorSavedState,
|
||||||
focus::WindowKeyboardFocusStack,
|
focus::WindowKeyboardFocusStack,
|
||||||
layout::transaction::{LayoutTransaction, SnapshotTarget},
|
layout::transaction::{LayoutTransaction, SnapshotTarget},
|
||||||
protocol::screencopy::Screencopy,
|
protocol::screencopy::Screencopy,
|
||||||
|
@ -31,8 +35,8 @@ impl OutputName {
|
||||||
/// Get the output with this name.
|
/// Get the output with this name.
|
||||||
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
||||||
pinnacle
|
pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.find(|output| output.name() == self.0)
|
.find(|output| output.name() == self.0)
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
@ -134,20 +138,34 @@ impl OutputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum OutputMode {
|
||||||
|
Smithay(Mode),
|
||||||
|
Drm(drm::control::Mode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OutputMode> for Mode {
|
||||||
|
fn from(value: OutputMode) -> Self {
|
||||||
|
match value {
|
||||||
|
OutputMode::Smithay(mode) => mode,
|
||||||
|
OutputMode::Drm(mode) => Mode::from(mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Pinnacle {
|
impl Pinnacle {
|
||||||
/// A wrapper around [`Output::change_current_state`] that additionally sends an output
|
|
||||||
/// geometry signal.
|
|
||||||
pub fn change_output_state(
|
pub fn change_output_state(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
backend: &mut impl BackendData,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
mode: Option<Mode>,
|
mode: Option<OutputMode>,
|
||||||
transform: Option<Transform>,
|
transform: Option<Transform>,
|
||||||
scale: Option<Scale>,
|
scale: Option<Scale>,
|
||||||
location: Option<Point<i32, Logical>>,
|
location: Option<Point<i32, Logical>>,
|
||||||
) {
|
) {
|
||||||
let old_scale = output.current_scale().fractional_scale();
|
let old_scale = output.current_scale().fractional_scale();
|
||||||
|
|
||||||
output.change_current_state(mode, transform, scale, location);
|
output.change_current_state(None, transform, scale, location);
|
||||||
if let Some(location) = location {
|
if let Some(location) = location {
|
||||||
self.space.map_output(output, location);
|
self.space.map_output(output, location);
|
||||||
self.signal_state.output_move.signal(|buf| {
|
self.signal_state.output_move.signal(|buf| {
|
||||||
|
@ -158,6 +176,11 @@ impl Pinnacle {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(mode) = mode {
|
||||||
|
backend.set_output_mode(output, mode);
|
||||||
|
}
|
||||||
|
|
||||||
if mode.is_some() || transform.is_some() || scale.is_some() {
|
if mode.is_some() || transform.is_some() || scale.is_some() {
|
||||||
layer_map_for_output(output).arrange();
|
layer_map_for_output(output).arrange();
|
||||||
self.signal_state.output_resize.signal(|buf| {
|
self.signal_state.output_resize.signal(|buf| {
|
||||||
|
@ -169,10 +192,6 @@ impl Pinnacle {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(mode) = mode {
|
|
||||||
output.set_preferred(mode);
|
|
||||||
output.with_state_mut(|state| state.modes.push(mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(scale) = scale {
|
if let Some(scale) = scale {
|
||||||
let pos_multiplier = old_scale / scale.fractional_scale();
|
let pos_multiplier = old_scale / scale.fractional_scale();
|
||||||
|
@ -220,4 +239,101 @@ impl Pinnacle {
|
||||||
lock_surface.send_configure();
|
lock_surface.send_configure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_output_enabled(&mut self, output: &Output, enabled: bool) {
|
||||||
|
if enabled {
|
||||||
|
match self.outputs.entry(output.clone()) {
|
||||||
|
indexmap::map::Entry::Occupied(entry) => {
|
||||||
|
let global = entry.into_mut();
|
||||||
|
if global.is_none() {
|
||||||
|
*global = Some(output.create_global::<State>(&self.display_handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indexmap::map::Entry::Vacant(entry) => {
|
||||||
|
let global = output.create_global::<State>(&self.display_handle);
|
||||||
|
entry.insert(Some(global));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.space.map_output(output, output.current_location());
|
||||||
|
|
||||||
|
// Trigger the connect signal here for configs to reposition outputs
|
||||||
|
//
|
||||||
|
// TODO: Create a new output_disable/enable signal and trigger it here
|
||||||
|
self.signal_state.output_connect.signal(|buffer| {
|
||||||
|
buffer.push_back(OutputConnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let global = self.outputs.get_mut(output);
|
||||||
|
if let Some(global) = global {
|
||||||
|
if let Some(global) = global.take() {
|
||||||
|
self.display_handle.remove_global::<State>(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.space.unmap_output(output);
|
||||||
|
|
||||||
|
// Trigger the disconnect signal here for configs to reposition outputs
|
||||||
|
//
|
||||||
|
// TODO: Create a new output_disable/enable signal and trigger it here
|
||||||
|
self.signal_state.output_disconnect.signal(|buffer| {
|
||||||
|
buffer.push_back(OutputDisconnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.gamma_control_manager_state.output_removed(output);
|
||||||
|
|
||||||
|
self.config.connector_saved_states.insert(
|
||||||
|
OutputName(output.name()),
|
||||||
|
ConnectorSavedState {
|
||||||
|
loc: output.current_location(),
|
||||||
|
tags: output.with_state(|state| state.tags.clone()),
|
||||||
|
scale: Some(output.current_scale()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for layer in layer_map_for_output(output).layers() {
|
||||||
|
layer.layer_surface().send_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completely remove an output, for example when a monitor is unplugged
|
||||||
|
pub fn remove_output(&mut self, output: &Output) {
|
||||||
|
let global = self.outputs.shift_remove(output);
|
||||||
|
if let Some(mut global) = global {
|
||||||
|
if let Some(global) = global.take() {
|
||||||
|
self.display_handle.remove_global::<State>(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for layer in layer_map_for_output(output).layers() {
|
||||||
|
layer.layer_surface().send_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.space.unmap_output(output);
|
||||||
|
|
||||||
|
self.gamma_control_manager_state.output_removed(output);
|
||||||
|
|
||||||
|
self.output_power_management_state.output_removed(output);
|
||||||
|
|
||||||
|
self.output_management_manager_state.remove_head(output);
|
||||||
|
self.output_management_manager_state.update::<State>();
|
||||||
|
|
||||||
|
self.signal_state.output_disconnect.signal(|buffer| {
|
||||||
|
buffer.push_back(OutputDisconnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
self.config.connector_saved_states.insert(
|
||||||
|
OutputName(output.name()),
|
||||||
|
ConnectorSavedState {
|
||||||
|
loc: output.current_location(),
|
||||||
|
tags: output.with_state(|state| state.tags.clone()),
|
||||||
|
scale: Some(output.current_scale()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
pub mod foreign_toplevel;
|
pub mod foreign_toplevel;
|
||||||
pub mod gamma_control;
|
pub mod gamma_control;
|
||||||
|
pub mod output_management;
|
||||||
|
pub mod output_power_management;
|
||||||
pub mod screencopy;
|
pub mod screencopy;
|
||||||
|
|
|
@ -150,7 +150,6 @@ pub trait GammaControlHandler {
|
||||||
fn gamma_control_destroyed(&mut self, output: &Output);
|
fn gamma_control_destroyed(&mut self, output: &Output);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! delegate_gamma_control {
|
macro_rules! delegate_gamma_control {
|
||||||
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
|
1080
src/protocol/output_management.rs
Normal file
1080
src/protocol/output_management.rs
Normal file
File diff suppressed because it is too large
Load diff
206
src/protocol/output_power_management.rs
Normal file
206
src/protocol/output_power_management.rs
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use smithay::{
|
||||||
|
output::Output,
|
||||||
|
reexports::{
|
||||||
|
wayland_protocols_wlr::output_power_management::v1::server::{
|
||||||
|
zwlr_output_power_manager_v1::{self, ZwlrOutputPowerManagerV1},
|
||||||
|
zwlr_output_power_v1::{self, ZwlrOutputPowerV1},
|
||||||
|
},
|
||||||
|
wayland_server::{
|
||||||
|
self, backend::ClientId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch,
|
||||||
|
Resource, WEnum,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::state::WithState;
|
||||||
|
|
||||||
|
const VERSION: u32 = 1;
|
||||||
|
|
||||||
|
pub struct OutputPowerManagementState {
|
||||||
|
clients: HashMap<Output, ZwlrOutputPowerV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OutputPowerManagementGlobalData {
|
||||||
|
filter: Box<dyn Fn(&Client) -> bool + Send + Sync + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OutputPowerManagementHandler {
|
||||||
|
fn output_power_management_state(&mut self) -> &mut OutputPowerManagementState;
|
||||||
|
fn set_mode(&mut self, output: &Output, powered: bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputPowerManagementState {
|
||||||
|
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrOutputPowerManagerV1, OutputPowerManagementGlobalData> + 'static,
|
||||||
|
F: Fn(&Client) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let data = OutputPowerManagementGlobalData {
|
||||||
|
filter: Box::new(filter),
|
||||||
|
};
|
||||||
|
|
||||||
|
display.create_global::<D, ZwlrOutputPowerManagerV1, _>(VERSION, data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
clients: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_removed(&mut self, output: &Output) {
|
||||||
|
if let Some(power) = self.clients.remove(output) {
|
||||||
|
power.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> GlobalDispatch<ZwlrOutputPowerManagerV1, OutputPowerManagementGlobalData, D>
|
||||||
|
for OutputPowerManagementState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrOutputPowerManagerV1, ()> + OutputPowerManagementHandler,
|
||||||
|
{
|
||||||
|
fn bind(
|
||||||
|
_state: &mut D,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
resource: wayland_server::New<ZwlrOutputPowerManagerV1>,
|
||||||
|
_global_data: &OutputPowerManagementGlobalData,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
data_init.init(resource, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_view(client: Client, global_data: &OutputPowerManagementGlobalData) -> bool {
|
||||||
|
(global_data.filter)(&client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrOutputPowerManagerV1, (), D> for OutputPowerManagementState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrOutputPowerV1, ()> + OutputPowerManagementHandler,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
_resource: &ZwlrOutputPowerManagerV1,
|
||||||
|
request: <ZwlrOutputPowerManagerV1 as wayland_server::Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_output_power_manager_v1::Request::GetOutputPower { id, output } => {
|
||||||
|
let Some(output) = Output::from_resource(&output) else {
|
||||||
|
warn!("wlr-output-power-management: no output for wl_output {output:?}");
|
||||||
|
let power = data_init.init(id, ());
|
||||||
|
power.failed();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if state
|
||||||
|
.output_power_management_state()
|
||||||
|
.clients
|
||||||
|
.contains_key(&output)
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"wlr-output-power-management: {} already has an active power manager",
|
||||||
|
output.name()
|
||||||
|
);
|
||||||
|
let power = data_init.init(id, ());
|
||||||
|
power.failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let power = data_init.init(id, ());
|
||||||
|
let is_powered = output.with_state(|state| state.powered);
|
||||||
|
power.mode(match is_powered {
|
||||||
|
true => zwlr_output_power_v1::Mode::On,
|
||||||
|
false => zwlr_output_power_v1::Mode::Off,
|
||||||
|
});
|
||||||
|
|
||||||
|
state
|
||||||
|
.output_power_management_state()
|
||||||
|
.clients
|
||||||
|
.insert(output, power);
|
||||||
|
}
|
||||||
|
zwlr_output_power_manager_v1::Request::Destroy => (),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> Dispatch<ZwlrOutputPowerV1, (), D> for OutputPowerManagementState
|
||||||
|
where
|
||||||
|
D: Dispatch<ZwlrOutputPowerV1, ()> + OutputPowerManagementHandler,
|
||||||
|
{
|
||||||
|
fn request(
|
||||||
|
state: &mut D,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrOutputPowerV1,
|
||||||
|
request: <ZwlrOutputPowerV1 as wayland_server::Resource>::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, D>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_output_power_v1::Request::SetMode { mode } => {
|
||||||
|
let Some(output) = state
|
||||||
|
.output_power_management_state()
|
||||||
|
.clients
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, power)| (power == resource).then_some(output.clone()))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.set_mode(
|
||||||
|
&output,
|
||||||
|
match mode {
|
||||||
|
WEnum::Value(zwlr_output_power_v1::Mode::On) => true,
|
||||||
|
WEnum::Value(zwlr_output_power_v1::Mode::Off) => false,
|
||||||
|
mode => {
|
||||||
|
resource.post_error(
|
||||||
|
zwlr_output_power_v1::Error::InvalidMode,
|
||||||
|
format!("invalid mode {mode:?}"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
zwlr_output_power_v1::Request::Destroy => {
|
||||||
|
state
|
||||||
|
.output_power_management_state()
|
||||||
|
.clients
|
||||||
|
.retain(|_, power| power == resource);
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrOutputPowerV1, _data: &()) {
|
||||||
|
state
|
||||||
|
.output_power_management_state()
|
||||||
|
.clients
|
||||||
|
.retain(|_, power| power == resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! delegate_output_power_management {
|
||||||
|
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
|
||||||
|
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: $crate::protocol::output_power_management::OutputPowerManagementGlobalData
|
||||||
|
] => $crate::protocol::output_power_management::OutputPowerManagementState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: ()
|
||||||
|
] => $crate::protocol::output_power_management::OutputPowerManagementState);
|
||||||
|
|
||||||
|
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
|
||||||
|
smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_v1::ZwlrOutputPowerV1: ()
|
||||||
|
] => $crate::protocol::output_power_management::OutputPowerManagementState);
|
||||||
|
};
|
||||||
|
}
|
33
src/state.rs
33
src/state.rs
|
@ -12,19 +12,23 @@ use crate::{
|
||||||
protocol::{
|
protocol::{
|
||||||
foreign_toplevel::{self, ForeignToplevelManagerState},
|
foreign_toplevel::{self, ForeignToplevelManagerState},
|
||||||
gamma_control::GammaControlManagerState,
|
gamma_control::GammaControlManagerState,
|
||||||
|
output_management::OutputManagementManagerState,
|
||||||
|
output_power_management::OutputPowerManagementState,
|
||||||
screencopy::ScreencopyManagerState,
|
screencopy::ScreencopyManagerState,
|
||||||
},
|
},
|
||||||
window::WindowElement,
|
window::WindowElement,
|
||||||
};
|
};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use indexmap::IndexMap;
|
||||||
use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse;
|
use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse;
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::{PopupManager, Space},
|
desktop::{PopupManager, Space},
|
||||||
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
||||||
|
output::Output,
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction},
|
calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction},
|
||||||
wayland_server::{
|
wayland_server::{
|
||||||
backend::{ClientData, ClientId, DisconnectReason},
|
backend::{ClientData, ClientId, DisconnectReason, GlobalId},
|
||||||
protocol::wl_surface::WlSurface,
|
protocol::wl_surface::WlSurface,
|
||||||
Client, Display, DisplayHandle,
|
Client, Display, DisplayHandle,
|
||||||
},
|
},
|
||||||
|
@ -52,7 +56,12 @@ use smithay::{
|
||||||
},
|
},
|
||||||
xwayland::{X11Wm, XWaylandClientData},
|
xwayland::{X11Wm, XWaylandClientData},
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::HashMap, path::PathBuf, sync::Arc};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use sysinfo::{ProcessRefreshKind, RefreshKind};
|
use sysinfo::{ProcessRefreshKind, RefreshKind};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
use xdg::BaseDirectories;
|
use xdg::BaseDirectories;
|
||||||
|
@ -101,6 +110,8 @@ pub struct Pinnacle {
|
||||||
pub session_lock_manager_state: SessionLockManagerState,
|
pub session_lock_manager_state: SessionLockManagerState,
|
||||||
pub xwayland_shell_state: XWaylandShellState,
|
pub xwayland_shell_state: XWaylandShellState,
|
||||||
pub idle_notifier_state: IdleNotifierState<State>,
|
pub idle_notifier_state: IdleNotifierState<State>,
|
||||||
|
pub output_management_manager_state: OutputManagementManagerState,
|
||||||
|
pub output_power_management_state: OutputPowerManagementState,
|
||||||
|
|
||||||
pub lock_state: LockState,
|
pub lock_state: LockState,
|
||||||
|
|
||||||
|
@ -139,6 +150,11 @@ pub struct Pinnacle {
|
||||||
|
|
||||||
/// A cache of surfaces to their root surface.
|
/// A cache of surfaces to their root surface.
|
||||||
pub root_surface_cache: HashMap<WlSurface, WlSurface>,
|
pub root_surface_cache: HashMap<WlSurface, WlSurface>,
|
||||||
|
|
||||||
|
/// WlSurfaces with an attached idle inhibitor.
|
||||||
|
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
|
||||||
|
|
||||||
|
pub outputs: IndexMap<Output, Option<GlobalId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -148,6 +164,7 @@ impl State {
|
||||||
self.pinnacle.popup_manager.cleanup();
|
self.pinnacle.popup_manager.cleanup();
|
||||||
self.update_pointer_focus();
|
self.update_pointer_focus();
|
||||||
foreign_toplevel::refresh(self);
|
foreign_toplevel::refresh(self);
|
||||||
|
self.pinnacle.refresh_idle_inhibit();
|
||||||
|
|
||||||
if let Backend::Winit(winit) = &mut self.backend {
|
if let Backend::Winit(winit) = &mut self.backend {
|
||||||
winit.render_if_scheduled(&mut self.pinnacle);
|
winit.render_if_scheduled(&mut self.pinnacle);
|
||||||
|
@ -290,6 +307,14 @@ impl Pinnacle {
|
||||||
),
|
),
|
||||||
xwayland_shell_state: XWaylandShellState::new::<State>(&display_handle),
|
xwayland_shell_state: XWaylandShellState::new::<State>(&display_handle),
|
||||||
idle_notifier_state: IdleNotifierState::new(&display_handle, loop_handle),
|
idle_notifier_state: IdleNotifierState::new(&display_handle, loop_handle),
|
||||||
|
output_management_manager_state: OutputManagementManagerState::new::<State, _>(
|
||||||
|
&display_handle,
|
||||||
|
filter_restricted_client,
|
||||||
|
),
|
||||||
|
output_power_management_state: OutputPowerManagementState::new::<State, _>(
|
||||||
|
&display_handle,
|
||||||
|
filter_restricted_client,
|
||||||
|
),
|
||||||
|
|
||||||
lock_state: LockState::default(),
|
lock_state: LockState::default(),
|
||||||
|
|
||||||
|
@ -326,6 +351,10 @@ impl Pinnacle {
|
||||||
layout_state: LayoutState::default(),
|
layout_state: LayoutState::default(),
|
||||||
|
|
||||||
root_surface_cache: HashMap::new(),
|
root_surface_cache: HashMap::new(),
|
||||||
|
|
||||||
|
idle_inhibiting_surfaces: HashSet::new(),
|
||||||
|
|
||||||
|
outputs: IndexMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(pinnacle)
|
Ok(pinnacle)
|
||||||
|
|
|
@ -26,8 +26,8 @@ impl TagId {
|
||||||
/// Get the tag associated with this id.
|
/// Get the tag associated with this id.
|
||||||
pub fn tag(&self, pinnacle: &Pinnacle) -> Option<Tag> {
|
pub fn tag(&self, pinnacle: &Pinnacle) -> Option<Tag> {
|
||||||
pinnacle
|
pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||||
.find(|tag| &tag.id() == self)
|
.find(|tag| &tag.id() == self)
|
||||||
}
|
}
|
||||||
|
@ -118,8 +118,8 @@ impl Tag {
|
||||||
/// RefCell Safety: This uses RefCells on every mapped output.
|
/// RefCell Safety: This uses RefCells on every mapped output.
|
||||||
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
||||||
pinnacle
|
pinnacle
|
||||||
.space
|
.outputs
|
||||||
.outputs()
|
.keys()
|
||||||
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self)))
|
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self)))
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
|
@ -308,7 +308,7 @@ impl Pinnacle {
|
||||||
|
|
||||||
self.z_index_stack.retain(|win| win != window);
|
self.z_index_stack.retain(|win| win != window);
|
||||||
|
|
||||||
for output in self.space.outputs() {
|
for output in self.outputs.keys() {
|
||||||
output.with_state_mut(|state| state.focus_stack.stack.retain(|win| win != window));
|
output.with_state_mut(|state| state.focus_stack.stack.retain(|win| win != window));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue