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:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
name: Build
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -22,7 +22,7 @@ jobs:
|
|||
- name: Cache stuff
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- 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
|
||||
uses: leafo/gh-actions-lua@v10
|
||||
with:
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
- name: Celebratory yahoo
|
||||
run: echo yahoo
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
name: Run tests
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
- name: Cache stuff
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- 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
|
||||
uses: leafo/gh-actions-lua@v10
|
||||
with:
|
||||
|
@ -60,7 +60,7 @@ jobs:
|
|||
if: ${{ runner.debug == '1' }}
|
||||
run: RUST_LOG=debug RUST_BACKTRACE=1 just install test -- --nocapture --test-threads=1
|
||||
check-format:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
name: Check formatting
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -72,7 +72,7 @@ jobs:
|
|||
- name: Check formatting
|
||||
run: cargo fmt -- --check
|
||||
clippy-check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
name: Clippy check
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -84,7 +84,7 @@ jobs:
|
|||
- name: Cache stuff
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- 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
|
||||
uses: leafo/gh-actions-lua@v10
|
||||
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"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "libdisplay-info-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Smithay/libdisplay-info-rs?rev=a482d0d#a482d0d4b71762c13d40fa394efe04473916f31c"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
|
@ -1851,8 +1856,11 @@ dependencies = [
|
|||
"clap",
|
||||
"cliclack",
|
||||
"dircpy",
|
||||
"drm-sys",
|
||||
"gag",
|
||||
"image",
|
||||
"indexmap 2.2.6",
|
||||
"libdisplay-info-sys",
|
||||
"pinnacle",
|
||||
"pinnacle-api",
|
||||
"pinnacle-api-defs",
|
||||
|
|
|
@ -115,6 +115,9 @@ chrono = "0.4.38"
|
|||
bytemuck = "1.16.0"
|
||||
pinnacle-api = { path = "./api/rust" }
|
||||
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]
|
||||
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.
|
||||
> 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.
|
||||
- `libdisplay-info`, for monitor display information
|
||||
- [protoc](https://grpc.io/docs/protoc-installation/), the Protocol Buffer Compiler, for configuration
|
||||
- Arch:
|
||||
```sh
|
||||
|
|
|
@ -65,6 +65,20 @@ local pinnacle_output_v0alpha1_Transform = {
|
|||
---@field pixel_height 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
|
||||
---@field output_name string?
|
||||
---@field absolute number?
|
||||
|
@ -104,6 +118,8 @@ local pinnacle_output_v0alpha1_Transform = {
|
|||
---@field transform pinnacle.output.v0alpha1.Transform?
|
||||
---@field serial integer?
|
||||
---@field keyboard_focus_stack_window_ids integer[]?
|
||||
---@field enabled boolean?
|
||||
---@field powered boolean?
|
||||
|
||||
-- Window
|
||||
|
||||
|
@ -476,6 +492,13 @@ defs.pinnacle = {
|
|||
response = "google.protobuf.Empty",
|
||||
},
|
||||
---@type GrpcRequestArgs
|
||||
SetModeline = {
|
||||
service = "pinnacle.output.v0alpha1.OutputService",
|
||||
method = "SetModeline",
|
||||
request = "pinnacle.output.v0alpha1.SetModelineRequest",
|
||||
response = "google.protobuf.Empty",
|
||||
},
|
||||
---@type GrpcRequestArgs
|
||||
SetScale = {
|
||||
service = "pinnacle.output.v0alpha1.OutputService",
|
||||
method = "SetScale",
|
||||
|
|
|
@ -40,8 +40,6 @@ output.handle = output_handle
|
|||
---
|
||||
---@return OutputHandle[]
|
||||
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, {})
|
||||
|
||||
---@type OutputHandle[]
|
||||
|
@ -54,6 +52,27 @@ function output.get_all()
|
|||
return handles
|
||||
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).
|
||||
---
|
||||
---### Example
|
||||
|
@ -144,6 +163,7 @@ end
|
|||
---@class OutputSetup
|
||||
---@field filter (fun(output: OutputHandle): boolean)? -- A filter for wildcard matches that should return true if this setup should apply to the passed in output.
|
||||
---@field mode Mode? -- Makes this setup apply the given mode to outputs.
|
||||
---@field 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 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.
|
||||
|
@ -270,7 +290,9 @@ function output.setup(setups)
|
|||
goto continue
|
||||
end
|
||||
|
||||
if setup.mode then
|
||||
if setup.modeline then
|
||||
op:set_modeline(setup.modeline)
|
||||
elseif setup.mode then
|
||||
op:set_mode(
|
||||
setup.mode.pixel_width,
|
||||
setup.mode.pixel_height,
|
||||
|
@ -421,7 +443,7 @@ function output.setup_locs(update_locs_on, locs)
|
|||
end
|
||||
|
||||
local function layout_outputs()
|
||||
local outputs = output.get_all()
|
||||
local outputs = output.get_all_enabled()
|
||||
|
||||
---@type OutputHandle[]
|
||||
local placed_outputs = {}
|
||||
|
@ -813,6 +835,54 @@ function OutputHandle:set_mode(pixel_width, pixel_height, refresh_rate_millihz)
|
|||
})
|
||||
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.
|
||||
---
|
||||
---@param scale number
|
||||
|
@ -900,6 +970,8 @@ end
|
|||
---@field transform Transform?
|
||||
---@field serial integer?
|
||||
---@field keyboard_focus_stack WindowHandle[]
|
||||
---@field enabled boolean?
|
||||
---@field powered boolean?
|
||||
|
||||
---Get all properties of this output.
|
||||
---
|
||||
|
@ -972,6 +1044,8 @@ end
|
|||
|
||||
---Get this output's logical width in pixels.
|
||||
---
|
||||
---If the output is disabled, this returns nil.
|
||||
---
|
||||
---Shorthand for `handle:props().logical_width`.
|
||||
---
|
||||
---@return integer?
|
||||
|
@ -981,6 +1055,8 @@ end
|
|||
|
||||
---Get this output's logical height in pixels.
|
||||
---
|
||||
---If the output is disabled, this returns nil.
|
||||
---
|
||||
---Shorthand for `handle:props().y`.
|
||||
---
|
||||
---@return integer?
|
||||
|
@ -1094,6 +1170,25 @@ function OutputHandle:keyboard_focus_stack()
|
|||
return self:props().keyboard_focus_stack
|
||||
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.
|
||||
---
|
||||
---This only includes windows on active tags.
|
||||
|
|
|
@ -118,10 +118,93 @@ function rectangle.new(x, y, width, height)
|
|||
return self
|
||||
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.
|
||||
---@class Util
|
||||
local util = {
|
||||
rectangle = rectangle,
|
||||
output = {
|
||||
parse_modeline = parse_modeline,
|
||||
},
|
||||
}
|
||||
|
||||
---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;
|
||||
}
|
||||
|
||||
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 {
|
||||
optional string output_name = 1;
|
||||
oneof absolute_or_relative {
|
||||
|
@ -101,11 +116,14 @@ message GetPropertiesResponse {
|
|||
optional uint32 serial = 16;
|
||||
// Window ids of the keyboard focus stack for this output.
|
||||
repeated uint32 keyboard_focus_stack_window_ids = 17;
|
||||
optional bool enabled = 18;
|
||||
optional bool powered = 19;
|
||||
}
|
||||
|
||||
service OutputService {
|
||||
rpc SetLocation(SetLocationRequest) 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 SetTransform(SetTransformRequest) 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
|
||||
//! connected monitors and set them up.
|
||||
|
||||
use std::{num::NonZeroU32, sync::OnceLock};
|
||||
use std::{num::NonZeroU32, str::FromStr, sync::OnceLock};
|
||||
|
||||
use futures::FutureExt;
|
||||
use pinnacle_api_defs::pinnacle::output::{
|
||||
self,
|
||||
v0alpha1::{
|
||||
output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative,
|
||||
SetLocationRequest, SetModeRequest, SetPoweredRequest, SetScaleRequest,
|
||||
SetLocationRequest, SetModeRequest, SetModelineRequest, SetPoweredRequest, SetScaleRequest,
|
||||
SetTransformRequest,
|
||||
},
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ impl Output {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get a handle to all connected outputs.
|
||||
/// Get handles to all connected outputs.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -82,10 +82,35 @@ impl Output {
|
|||
.into_inner()
|
||||
.output_names
|
||||
.into_iter()
|
||||
.map(move |name| self.new_handle(name))
|
||||
.map(|name| self.new_handle(name))
|
||||
.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.
|
||||
///
|
||||
/// 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 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;
|
||||
|
||||
|
@ -418,10 +443,15 @@ impl std::fmt::Debug for OutputMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
enum OutputMode {
|
||||
Mode(Mode),
|
||||
Modeline(Modeline),
|
||||
}
|
||||
|
||||
/// An output setup for use in [`Output::setup`].
|
||||
pub struct OutputSetup {
|
||||
output: OutputMatcher,
|
||||
mode: Option<Mode>,
|
||||
mode: Option<OutputMode>,
|
||||
scale: Option<f32>,
|
||||
tag_names: Option<Vec<String>>,
|
||||
transform: Option<Transform>,
|
||||
|
@ -453,9 +483,24 @@ impl OutputSetup {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -486,12 +531,19 @@ impl OutputSetup {
|
|||
|
||||
fn apply(&self, output: &OutputHandle, tag: &Tag) {
|
||||
if let Some(mode) = &self.mode {
|
||||
match mode {
|
||||
OutputMode::Mode(mode) => {
|
||||
output.set_mode(
|
||||
mode.pixel_width,
|
||||
mode.pixel_height,
|
||||
Some(mode.refresh_rate_millihertz),
|
||||
);
|
||||
}
|
||||
OutputMode::Modeline(modeline) => {
|
||||
output.set_modeline(*modeline);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(scale) = self.scale {
|
||||
output.set_scale(scale);
|
||||
}
|
||||
|
@ -505,7 +557,7 @@ impl OutputSetup {
|
|||
}
|
||||
|
||||
/// A location for an output.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OutputLoc {
|
||||
/// A specific point in the global space of the form (x, y).
|
||||
Point(i32, i32),
|
||||
|
@ -812,6 +864,37 @@ impl OutputHandle {
|
|||
.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.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -970,6 +1053,8 @@ impl OutputHandle {
|
|||
.into_iter()
|
||||
.map(|id| self.api.window.new_handle(id))
|
||||
.collect(),
|
||||
enabled: response.enabled,
|
||||
powered: response.powered,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1025,6 +1110,8 @@ impl OutputHandle {
|
|||
|
||||
/// Get this output's logical width in pixels.
|
||||
///
|
||||
/// If the output is disabled, this returns None.
|
||||
///
|
||||
/// Shorthand for `self.props().logical_width`.
|
||||
pub fn logical_width(&self) -> Option<u32> {
|
||||
self.props().logical_width
|
||||
|
@ -1037,6 +1124,8 @@ impl OutputHandle {
|
|||
|
||||
/// Get this output's logical height in pixels.
|
||||
///
|
||||
/// If the output is disabled, this returns None.
|
||||
///
|
||||
/// Shorthand for `self.props().logical_height`.
|
||||
pub fn logical_height(&self) -> Option<u32> {
|
||||
self.props().logical_height
|
||||
|
@ -1197,6 +1286,34 @@ impl OutputHandle {
|
|||
.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).
|
||||
pub fn name(&self) -> String {
|
||||
self.name.to_string()
|
||||
|
@ -1204,7 +1321,7 @@ impl OutputHandle {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
/// The width of the output, in pixels.
|
||||
pub pixel_width: u32,
|
||||
|
@ -1261,4 +1378,161 @@ pub struct OutputProperties {
|
|||
pub serial: Option<u32>,
|
||||
/// This output's window keyboard focus stack.
|
||||
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,
|
||||
v0alpha1::{
|
||||
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},
|
||||
|
@ -52,10 +53,10 @@ use tonic::{Request, Response, Status, Streaming};
|
|||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use crate::{
|
||||
backend::BackendData,
|
||||
backend::{udev::drm_mode_from_api_modeline, BackendData},
|
||||
config::ConnectorSavedState,
|
||||
input::ModifierMask,
|
||||
output::OutputName,
|
||||
output::{OutputMode, OutputName},
|
||||
render::util::snapshot::capture_snapshots_on_output,
|
||||
state::{State, WithState},
|
||||
tag::{Tag, TagId},
|
||||
|
@ -885,7 +886,7 @@ impl tag_service_server::TagService for TagService {
|
|||
.flat_map(|id| id.tag(&state.pinnacle))
|
||||
.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
|
||||
output.with_state_mut(|state| {
|
||||
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| {
|
||||
let tag_ids = state
|
||||
.pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||
.map(|tag| tag.id())
|
||||
.map(|id| id.0)
|
||||
|
@ -1035,11 +1036,20 @@ impl output_service_server::OutputService for OutputService {
|
|||
if let Some(y) = y {
|
||||
loc.y = y;
|
||||
}
|
||||
state
|
||||
.pinnacle
|
||||
.change_output_state(&output, None, None, None, Some(loc));
|
||||
state.pinnacle.change_output_state(
|
||||
&mut state.backend,
|
||||
&output,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(loc),
|
||||
);
|
||||
debug!("Mapping output {} to {loc:?}", output.name());
|
||||
state.pinnacle.request_layout(&output);
|
||||
state
|
||||
.pinnacle
|
||||
.output_management_manager_state
|
||||
.update::<State>();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -1061,13 +1071,64 @@ impl output_service_server::OutputService for OutputService {
|
|||
let Some(mode) = Some(request).and_then(|request| {
|
||||
Some(smithay::output::Mode {
|
||||
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,
|
||||
})
|
||||
}) else {
|
||||
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
|
||||
}
|
||||
|
@ -1102,6 +1163,7 @@ impl output_service_server::OutputService for OutputService {
|
|||
});
|
||||
|
||||
state.pinnacle.change_output_state(
|
||||
&mut state.backend,
|
||||
&output,
|
||||
None,
|
||||
None,
|
||||
|
@ -1121,6 +1183,10 @@ impl output_service_server::OutputService for OutputService {
|
|||
|
||||
state.pinnacle.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
state
|
||||
.pinnacle
|
||||
.output_management_manager_state
|
||||
.update::<State>();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -1154,11 +1220,20 @@ impl output_service_server::OutputService for OutputService {
|
|||
return;
|
||||
};
|
||||
|
||||
state
|
||||
.pinnacle
|
||||
.change_output_state(&output, None, Some(smithay_transform), None, None);
|
||||
state.pinnacle.change_output_state(
|
||||
&mut state.backend,
|
||||
&output,
|
||||
None,
|
||||
Some(smithay_transform),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
state.pinnacle.request_layout(&output);
|
||||
state.schedule_render(&output);
|
||||
state
|
||||
.pinnacle
|
||||
.output_management_manager_state
|
||||
.update::<State>();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -1197,8 +1272,8 @@ impl output_service_server::OutputService for OutputService {
|
|||
run_unary(&self.sender, move |state| {
|
||||
let output_names = state
|
||||
.pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.map(|output| output.name())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -1325,6 +1400,18 @@ impl output_service_server::OutputService for OutputService {
|
|||
})
|
||||
.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 {
|
||||
make,
|
||||
model,
|
||||
|
@ -1343,6 +1430,8 @@ impl output_service_server::OutputService for OutputService {
|
|||
transform,
|
||||
serial,
|
||||
keyboard_focus_stack_window_ids,
|
||||
enabled,
|
||||
powered,
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
@ -1378,7 +1467,7 @@ impl render_service_server::RenderService for RenderService {
|
|||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
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.schedule_render(&output);
|
||||
}
|
||||
|
@ -1403,7 +1492,7 @@ impl render_service_server::RenderService for RenderService {
|
|||
|
||||
run_unary_no_response(&self.sender, move |state| {
|
||||
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.schedule_render(&output);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ use smithay::{
|
|||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
output::OutputMode,
|
||||
state::{Pinnacle, State, SurfaceDmabufFeedback, WithState},
|
||||
window::WindowElement,
|
||||
};
|
||||
|
@ -151,6 +152,8 @@ pub trait BackendData: 'static {
|
|||
|
||||
// INFO: only for udev in anvil, maybe shouldn't be a trait fn?
|
||||
fn early_import(&mut self, surface: &WlSurface);
|
||||
|
||||
fn set_output_mode(&mut self, output: &Output, mode: OutputMode);
|
||||
}
|
||||
|
||||
impl BackendData for Backend {
|
||||
|
@ -180,6 +183,15 @@ impl BackendData for Backend {
|
|||
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
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse,
|
||||
};
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||
use smithay::backend::renderer::test::DummyRenderer;
|
||||
use smithay::backend::renderer::ImportMemWl;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
|
@ -12,6 +10,7 @@ use smithay::{
|
|||
utils::Transform,
|
||||
};
|
||||
|
||||
use crate::output::OutputMode;
|
||||
use crate::state::{Pinnacle, State, WithState};
|
||||
|
||||
use super::BackendData;
|
||||
|
@ -30,6 +29,7 @@ pub struct Dummy {
|
|||
// pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||
#[cfg(feature = "wlcs")]
|
||||
pub wlcs_state: Wlcs,
|
||||
pub output: Output,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
|
@ -50,6 +50,10 @@ impl BackendData for Dummy {
|
|||
fn reset_buffers(&mut self, _output: &Output) {}
|
||||
|
||||
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 {
|
||||
|
@ -85,15 +89,18 @@ impl Dummy {
|
|||
// dmabuf_state,
|
||||
#[cfg(feature = "wlcs")]
|
||||
wlcs_state: Wlcs::default(),
|
||||
output: output.clone(),
|
||||
};
|
||||
|
||||
UninitBackend {
|
||||
seat_name: dummy.seat_name(),
|
||||
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.outputs.insert(output.clone(), Some(global));
|
||||
|
||||
pinnacle
|
||||
.shm_state
|
||||
.update_formats(dummy.renderer.shm_formats());
|
||||
|
@ -131,7 +138,9 @@ impl Pinnacle {
|
|||
output.set_preferred(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));
|
||||
|
||||
|
@ -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 gamma;
|
||||
|
||||
pub use drm::util::drm_mode_from_api_modeline;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
|
@ -10,10 +12,8 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context};
|
||||
use drm::set_crtc_active;
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse,
|
||||
};
|
||||
use drm::{set_crtc_active, util::create_drm_mode};
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
|
@ -51,10 +51,7 @@ use smithay::{
|
|||
vulkan::{self, version::Version, PhysicalDevice},
|
||||
SwapBuffersError,
|
||||
},
|
||||
desktop::{
|
||||
layer_map_for_output,
|
||||
utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||
},
|
||||
desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback},
|
||||
input::pointer::CursorImageStatus,
|
||||
output::{Output, PhysicalProperties, Subpixel},
|
||||
reexports::{
|
||||
|
@ -71,7 +68,6 @@ use smithay::{
|
|||
presentation_time::server::wp_presentation_feedback,
|
||||
},
|
||||
wayland_server::{
|
||||
backend::GlobalId,
|
||||
protocol::{wl_shm, wl_surface::WlSurface},
|
||||
DisplayHandle,
|
||||
},
|
||||
|
@ -88,7 +84,7 @@ use tracing::{debug, error, info, trace, warn};
|
|||
use crate::{
|
||||
backend::Backend,
|
||||
config::ConnectorSavedState,
|
||||
output::{BlankingState, OutputName},
|
||||
output::{BlankingState, OutputMode, OutputName},
|
||||
render::{
|
||||
pointer::PointerElement, pointer_render_elements, take_presentation_feedback,
|
||||
OutputRenderElement, CLEAR_COLOR, CLEAR_COLOR_LOCKED,
|
||||
|
@ -527,9 +523,12 @@ impl Udev {
|
|||
/// Schedule a new render that will cause the compositor to redraw everything.
|
||||
pub fn schedule_render(&mut self, loop_handle: &LoopHandle<State>, output: &Output) {
|
||||
let Some(surface) = render_surface_for_output(output, &mut self.backends) else {
|
||||
tracing::info!("no render surface on output {}", output.name());
|
||||
return;
|
||||
};
|
||||
|
||||
// tracing::info!(state = ?surface.render_state, name = output.name());
|
||||
|
||||
match &surface.render_state {
|
||||
RenderState::Idle => {
|
||||
let output = output.clone();
|
||||
|
@ -577,6 +576,12 @@ impl Udev {
|
|||
{
|
||||
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 {
|
||||
|
@ -705,6 +665,61 @@ impl BackendData for Udev {
|
|||
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
|
||||
|
@ -810,7 +825,6 @@ enum RenderState {
|
|||
/// The idle token from a render being scheduled.
|
||||
/// This is used to cancel renders if, for example,
|
||||
/// the output being rendered is removed.
|
||||
#[allow(dead_code)] // TODO:
|
||||
Idle<'static>,
|
||||
),
|
||||
/// A frame was rendered and scheduled and we are waiting for vblank.
|
||||
|
@ -823,10 +837,6 @@ enum RenderState {
|
|||
|
||||
/// Render surface for an output.
|
||||
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`.
|
||||
device_id: DrmNode,
|
||||
/// The node rendering to the screen? idk
|
||||
|
@ -862,15 +872,6 @@ struct ScreencopyCommitState {
|
|||
_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<
|
||||
GbmAllocator<DrmDeviceFd>,
|
||||
GbmDevice<DrmDeviceFd>,
|
||||
|
@ -1036,7 +1037,7 @@ impl Udev {
|
|||
|
||||
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()
|
||||
.get::<UdevOutputData>()
|
||||
.is_some_and(|op_id| op_id.crtc == crtc)
|
||||
|
@ -1055,7 +1056,11 @@ impl Udev {
|
|||
);
|
||||
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);
|
||||
|
||||
|
@ -1067,6 +1072,10 @@ impl Udev {
|
|||
.collect::<Vec<_>>();
|
||||
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 Some(geo) = pinnacle.space.output_geometry(o) else {
|
||||
unreachable!()
|
||||
|
@ -1127,10 +1136,8 @@ impl Udev {
|
|||
);
|
||||
|
||||
let surface = RenderSurface {
|
||||
display_handle: self.display_handle.clone(),
|
||||
device_id: node,
|
||||
render_node: device.render_node,
|
||||
global: Some(global),
|
||||
compositor,
|
||||
dmabuf_feedback,
|
||||
render_state: RenderState::Idle,
|
||||
|
@ -1141,7 +1148,14 @@ impl Udev {
|
|||
|
||||
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.
|
||||
// In this case, restore its tags and location.
|
||||
|
@ -1153,7 +1167,7 @@ impl Udev {
|
|||
{
|
||||
let ConnectorSavedState { loc, tags, scale } = saved_state;
|
||||
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 {
|
||||
pinnacle.signal_state.output_connect.signal(|buffer| {
|
||||
buffer.push_back(OutputConnectResponse {
|
||||
|
@ -1161,6 +1175,8 @@ impl Udev {
|
|||
})
|
||||
});
|
||||
}
|
||||
|
||||
pinnacle.output_management_manager_state.update::<State>();
|
||||
}
|
||||
|
||||
/// A display was unplugged.
|
||||
|
@ -1181,8 +1197,8 @@ impl Udev {
|
|||
device.surfaces.remove(&crtc);
|
||||
|
||||
let output = pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.find(|o| {
|
||||
o.user_data()
|
||||
.get::<UdevOutputData>()
|
||||
|
@ -1192,29 +1208,7 @@ impl Udev {
|
|||
.cloned();
|
||||
|
||||
if let Some(output) = output {
|
||||
// Save this output's state. It will be restored if the monitor gets replugged.
|
||||
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()),
|
||||
})
|
||||
});
|
||||
pinnacle.remove_output(&output);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1290,7 +1284,7 @@ impl Udev {
|
|||
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>();
|
||||
udev_op_data
|
||||
.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(_)));
|
||||
|
||||
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
|
||||
// does nothing on powered off outputs
|
||||
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 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;
|
||||
|
||||
|
@ -129,3 +141,131 @@ pub(super) fn get_drm_property(
|
|||
}
|
||||
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 crate::{
|
||||
output::BlankingState,
|
||||
output::{BlankingState, OutputMode},
|
||||
render::{
|
||||
pointer::PointerElement, pointer_render_elements, take_presentation_feedback, CLEAR_COLOR,
|
||||
CLEAR_COLOR_LOCKED,
|
||||
|
@ -67,6 +67,10 @@ impl BackendData for Winit {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -181,10 +185,12 @@ impl Winit {
|
|||
|
||||
let init = Box::new(move |pinnacle: &mut Pinnacle| {
|
||||
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.outputs.insert(output.clone(), Some(global));
|
||||
|
||||
pinnacle
|
||||
.shm_state
|
||||
.update_formats(winit.backend.renderer().shm_formats());
|
||||
|
@ -201,8 +207,9 @@ impl Winit {
|
|||
refresh: 144_000,
|
||||
};
|
||||
state.pinnacle.change_output_state(
|
||||
&mut state.backend,
|
||||
&output,
|
||||
Some(mode),
|
||||
Some(OutputMode::Smithay(mode)),
|
||||
None,
|
||||
Some(Scale::Fractional(scale_factor)),
|
||||
// None,
|
||||
|
|
|
@ -347,6 +347,7 @@ pub struct ConnectorSavedState {
|
|||
pub tags: Vec<Tag>,
|
||||
/// The output's previous scale
|
||||
pub scale: Option<smithay::output::Scale>,
|
||||
// TODO: transform
|
||||
}
|
||||
|
||||
/// Parse a metaconfig file in `config_dir`, if any.
|
||||
|
@ -380,7 +381,7 @@ impl Pinnacle {
|
|||
// Clear state
|
||||
|
||||
debug!("Clearing tags");
|
||||
for output in self.space.outputs() {
|
||||
for output in self.outputs.keys() {
|
||||
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
|
||||
|
||||
pub mod idle;
|
||||
pub mod session_lock;
|
||||
pub mod window;
|
||||
mod xdg_shell;
|
||||
mod xwayland;
|
||||
|
||||
use std::{mem, os::fd::OwnedFd, sync::Arc};
|
||||
use std::{collections::HashMap, mem, os::fd::OwnedFd, sync::Arc};
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::utils::{self, with_renderer_surface_state},
|
||||
delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale,
|
||||
delegate_idle_notify, delegate_layer_shell, delegate_output, delegate_pointer_constraints,
|
||||
delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||
delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_presentation,
|
||||
delegate_primary_selection, delegate_relative_pointer, delegate_seat,
|
||||
delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell,
|
||||
desktop::{
|
||||
self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind,
|
||||
|
@ -21,7 +22,7 @@ use smithay::{
|
|||
pointer::{CursorImageStatus, PointerHandle},
|
||||
Seat, SeatHandler, SeatState,
|
||||
},
|
||||
output::Output,
|
||||
output::{Mode, Output, Scale},
|
||||
reexports::{
|
||||
calloop::Interest,
|
||||
wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment,
|
||||
|
@ -42,7 +43,6 @@ use smithay::{
|
|||
},
|
||||
dmabuf,
|
||||
fractional_scale::{self, FractionalScaleHandler},
|
||||
idle_notify::{IdleNotifierHandler, IdleNotifierState},
|
||||
output::OutputHandler,
|
||||
pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler},
|
||||
seat::WaylandFocus,
|
||||
|
@ -69,16 +69,22 @@ use smithay::{
|
|||
},
|
||||
xwayland::{X11Wm, XWaylandClientData},
|
||||
};
|
||||
use tracing::{error, trace, warn};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use crate::{
|
||||
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},
|
||||
handlers::xdg_shell::snapshot_pre_commit_hook,
|
||||
output::OutputMode,
|
||||
protocol::{
|
||||
foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState},
|
||||
gamma_control::{GammaControlHandler, GammaControlManagerState},
|
||||
output_management::{
|
||||
OutputConfiguration, OutputManagementHandler, OutputManagementManagerState,
|
||||
},
|
||||
output_power_management::{OutputPowerManagementHandler, OutputPowerManagementState},
|
||||
screencopy::{Screencopy, ScreencopyHandler},
|
||||
},
|
||||
render::util::snapshot::capture_snapshots_on_output,
|
||||
|
@ -918,12 +924,109 @@ impl XWaylandShellHandler for State {
|
|||
}
|
||||
delegate_xwayland_shell!(State);
|
||||
|
||||
impl IdleNotifierHandler for State {
|
||||
fn idle_notifier_state(&mut self) -> &mut IdleNotifierState<Self> {
|
||||
&mut self.pinnacle.idle_notifier_state
|
||||
impl OutputManagementHandler for State {
|
||||
fn output_management_manager_state(&mut self) -> &mut OutputManagementManagerState {
|
||||
&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);
|
||||
}
|
||||
}
|
||||
delegate_idle_notify!(State);
|
||||
}
|
||||
self.pinnacle
|
||||
.output_management_manager_state
|
||||
.update::<State>();
|
||||
true
|
||||
}
|
||||
|
||||
fn test_configuration(&mut self, config: HashMap<Output, OutputConfiguration>) -> bool {
|
||||
debug!(?config);
|
||||
true
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fn position_popup(&self, popup: &PopupSurface) {
|
||||
|
|
|
@ -32,20 +32,24 @@ impl Pinnacle {
|
|||
output: &Output,
|
||||
geometries: Vec<Rectangle<i32, Logical>>,
|
||||
) -> 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<_>>();
|
||||
self.windows
|
||||
.iter()
|
||||
.filter(|win| !win.is_x11_override_redirect())
|
||||
.filter(|win| {
|
||||
.filter(|win| win.output(self).as_ref() == Some(output))
|
||||
.cloned()
|
||||
.partition::<Vec<_>, _>(|win| {
|
||||
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
|
||||
.iter()
|
||||
.filter(|win| !win.is_x11_override_redirect())
|
||||
.filter(|win| {
|
||||
win.with_state(|state| {
|
||||
state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither()
|
||||
|
@ -154,11 +158,19 @@ impl LayoutState {
|
|||
}
|
||||
|
||||
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 Some(sender) = self.layout_state.layout_request_sender.as_ref() else {
|
||||
warn!("Layout requested but no client has connected to the layout service");
|
||||
return None;
|
||||
return;
|
||||
};
|
||||
|
||||
let windows_on_foc_tags = output.with_state(|state| {
|
||||
|
@ -209,8 +221,6 @@ impl Pinnacle {
|
|||
output_width: Some(output_width 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 pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputResizeResponse};
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse,
|
||||
};
|
||||
use smithay::{
|
||||
desktop::layer_map_for_output,
|
||||
output::{Mode, Output, Scale},
|
||||
reexports::calloop::LoopHandle,
|
||||
reexports::{calloop::LoopHandle, drm},
|
||||
utils::{Logical, Point, Transform},
|
||||
wayland::session_lock::LockSurface,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::BackendData,
|
||||
config::ConnectorSavedState,
|
||||
focus::WindowKeyboardFocusStack,
|
||||
layout::transaction::{LayoutTransaction, SnapshotTarget},
|
||||
protocol::screencopy::Screencopy,
|
||||
|
@ -31,8 +35,8 @@ impl OutputName {
|
|||
/// Get the output with this name.
|
||||
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
||||
pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.find(|output| output.name() == self.0)
|
||||
.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 {
|
||||
/// A wrapper around [`Output::change_current_state`] that additionally sends an output
|
||||
/// geometry signal.
|
||||
pub fn change_output_state(
|
||||
&mut self,
|
||||
backend: &mut impl BackendData,
|
||||
output: &Output,
|
||||
mode: Option<Mode>,
|
||||
mode: Option<OutputMode>,
|
||||
transform: Option<Transform>,
|
||||
scale: Option<Scale>,
|
||||
location: Option<Point<i32, Logical>>,
|
||||
) {
|
||||
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 {
|
||||
self.space.map_output(output, location);
|
||||
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() {
|
||||
layer_map_for_output(output).arrange();
|
||||
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 {
|
||||
let pos_multiplier = old_scale / scale.fractional_scale();
|
||||
|
@ -220,4 +239,101 @@ impl Pinnacle {
|
|||
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 gamma_control;
|
||||
pub mod output_management;
|
||||
pub mod output_power_management;
|
||||
pub mod screencopy;
|
||||
|
|
|
@ -150,7 +150,6 @@ pub trait GammaControlHandler {
|
|||
fn gamma_control_destroyed(&mut self, output: &Output);
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[macro_export]
|
||||
macro_rules! delegate_gamma_control {
|
||||
($(@<$( $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::{
|
||||
foreign_toplevel::{self, ForeignToplevelManagerState},
|
||||
gamma_control::GammaControlManagerState,
|
||||
output_management::OutputManagementManagerState,
|
||||
output_power_management::OutputPowerManagementState,
|
||||
screencopy::ScreencopyManagerState,
|
||||
},
|
||||
window::WindowElement,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use indexmap::IndexMap;
|
||||
use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse;
|
||||
use smithay::{
|
||||
desktop::{PopupManager, Space},
|
||||
input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState},
|
||||
output::Output,
|
||||
reexports::{
|
||||
calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction},
|
||||
wayland_server::{
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
backend::{ClientData, ClientId, DisconnectReason, GlobalId},
|
||||
protocol::wl_surface::WlSurface,
|
||||
Client, Display, DisplayHandle,
|
||||
},
|
||||
|
@ -52,7 +56,12 @@ use smithay::{
|
|||
},
|
||||
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 tracing::{info, warn};
|
||||
use xdg::BaseDirectories;
|
||||
|
@ -101,6 +110,8 @@ pub struct Pinnacle {
|
|||
pub session_lock_manager_state: SessionLockManagerState,
|
||||
pub xwayland_shell_state: XWaylandShellState,
|
||||
pub idle_notifier_state: IdleNotifierState<State>,
|
||||
pub output_management_manager_state: OutputManagementManagerState,
|
||||
pub output_power_management_state: OutputPowerManagementState,
|
||||
|
||||
pub lock_state: LockState,
|
||||
|
||||
|
@ -139,6 +150,11 @@ pub struct Pinnacle {
|
|||
|
||||
/// A cache of surfaces to their root surface.
|
||||
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 {
|
||||
|
@ -148,6 +164,7 @@ impl State {
|
|||
self.pinnacle.popup_manager.cleanup();
|
||||
self.update_pointer_focus();
|
||||
foreign_toplevel::refresh(self);
|
||||
self.pinnacle.refresh_idle_inhibit();
|
||||
|
||||
if let Backend::Winit(winit) = &mut self.backend {
|
||||
winit.render_if_scheduled(&mut self.pinnacle);
|
||||
|
@ -290,6 +307,14 @@ impl Pinnacle {
|
|||
),
|
||||
xwayland_shell_state: XWaylandShellState::new::<State>(&display_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(),
|
||||
|
||||
|
@ -326,6 +351,10 @@ impl Pinnacle {
|
|||
layout_state: LayoutState::default(),
|
||||
|
||||
root_surface_cache: HashMap::new(),
|
||||
|
||||
idle_inhibiting_surfaces: HashSet::new(),
|
||||
|
||||
outputs: IndexMap::new(),
|
||||
};
|
||||
|
||||
Ok(pinnacle)
|
||||
|
|
|
@ -26,8 +26,8 @@ impl TagId {
|
|||
/// Get the tag associated with this id.
|
||||
pub fn tag(&self, pinnacle: &Pinnacle) -> Option<Tag> {
|
||||
pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.flat_map(|op| op.with_state(|state| state.tags.clone()))
|
||||
.find(|tag| &tag.id() == self)
|
||||
}
|
||||
|
@ -118,8 +118,8 @@ impl Tag {
|
|||
/// RefCell Safety: This uses RefCells on every mapped output.
|
||||
pub fn output(&self, pinnacle: &Pinnacle) -> Option<Output> {
|
||||
pinnacle
|
||||
.space
|
||||
.outputs()
|
||||
.outputs
|
||||
.keys()
|
||||
.find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self)))
|
||||
.cloned()
|
||||
}
|
||||
|
|
|
@ -308,7 +308,7 @@ impl Pinnacle {
|
|||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue