Merge pull request #244 from pinnacle-comp/output_management

Better output management
This commit is contained in:
Ottatop 2024-06-04 19:11:34 -05:00 committed by GitHub
commit 8eff64e1bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 2501 additions and 204 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

View file

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