mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-18 22:26:12 +01:00
Add Output::setup
Still needs polishing and more importantly testing, also the Lua impl isn't finished yet
This commit is contained in:
parent
023ebe8a2d
commit
ce8b56eee8
14 changed files with 851 additions and 34 deletions
|
@ -82,12 +82,30 @@ require("pinnacle").setup(function(Pinnacle)
|
||||||
|
|
||||||
local tag_names = { "1", "2", "3", "4", "5" }
|
local tag_names = { "1", "2", "3", "4", "5" }
|
||||||
|
|
||||||
|
Output.setup({
|
||||||
|
{
|
||||||
|
function(_)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
tag_names = tag_names,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DP-2",
|
||||||
|
scale = 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Pinnacle Window",
|
||||||
|
scale = 0.5,
|
||||||
|
loc = { x = 300, y = 450 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
-- `connect_for_all` is useful for performing setup on every monitor you have.
|
-- `connect_for_all` is useful for performing setup on every monitor you have.
|
||||||
-- Here, we add tags with names 1-5 and set tag 1 as active.
|
-- Here, we add tags with names 1-5 and set tag 1 as active.
|
||||||
Output.connect_for_all(function(op)
|
-- Output.connect_for_all(function(op)
|
||||||
local tags = Tag.add(op, tag_names)
|
-- local tags = Tag.add(op, tag_names)
|
||||||
tags[1]:set_active(true)
|
-- tags[1]:set_active(true)
|
||||||
end)
|
-- end)
|
||||||
|
|
||||||
-- Tag keybinds
|
-- Tag keybinds
|
||||||
for _, tag_name in ipairs(tag_names) do
|
for _, tag_name in ipairs(tag_names) do
|
||||||
|
|
|
@ -166,15 +166,231 @@ function output.connect_for_all(callback)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class OutputSetupArgs
|
||||||
|
---@field [1] (string | fun(output: OutputHandle): boolean)
|
||||||
|
---@field loc ({ x: integer, y: integer } | { [1]: (string | fun(output: OutputHandle): boolean), [2]: Alignment })?
|
||||||
|
---@field mode Mode?
|
||||||
|
---@field scale number?
|
||||||
|
---@field tag_names string[]?
|
||||||
|
|
||||||
|
---comment
|
||||||
|
---@param op OutputHandle
|
||||||
|
---@param matcher string | fun(output: OutputHandle): boolean
|
||||||
|
---@return boolean
|
||||||
|
local function output_matches(op, matcher)
|
||||||
|
return (type(matcher) == "string" and matcher == op.name) or (type(matcher) == "function" and matcher(op))
|
||||||
|
end
|
||||||
|
|
||||||
|
---Declaratively setup outputs.
|
||||||
|
---
|
||||||
|
---`Output.setup` allows you to specify output properties that will be applied immediately and
|
||||||
|
---on output connection. These include location, mode, scale, and tags.
|
||||||
|
---
|
||||||
|
---Arguments will be applied from top to bottom.
|
||||||
|
---
|
||||||
|
---`loc` will not be applied to arguments with an output matching function.
|
||||||
|
---
|
||||||
|
---@param setup OutputSetupArgs[]
|
||||||
|
function output.setup(setup)
|
||||||
|
---@param op OutputHandle
|
||||||
|
local function apply_transformers(op)
|
||||||
|
for _, args in ipairs(setup) do
|
||||||
|
if output_matches(op, args[1]) then
|
||||||
|
if args.mode then
|
||||||
|
op:set_mode(args.mode.pixel_width, args.mode.pixel_height, args.mode.refresh_rate_millihz)
|
||||||
|
end
|
||||||
|
if args.scale then
|
||||||
|
op:set_scale(args.scale)
|
||||||
|
end
|
||||||
|
if args.tag_names then
|
||||||
|
local tags = require("pinnacle.tag").add(op, args.tag_names)
|
||||||
|
tags[1]:set_active(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply mode, scale, and transforms first
|
||||||
|
local outputs = output.get_all()
|
||||||
|
|
||||||
|
for _, op in ipairs(outputs) do
|
||||||
|
apply_transformers(op)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function layout_outputs()
|
||||||
|
local outputs = output.get_all()
|
||||||
|
|
||||||
|
---@type table<string, { x: integer, y: integer }>
|
||||||
|
local placed_outputs = {}
|
||||||
|
|
||||||
|
local rightmost_output = {
|
||||||
|
output = nil,
|
||||||
|
x = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
local relative_to_outputs_that_are_not_placed = {}
|
||||||
|
|
||||||
|
-- Place outputs with a specified location first
|
||||||
|
for _, args in ipairs(setup) do
|
||||||
|
for _, op in ipairs(outputs) do
|
||||||
|
if type(args[1]) == "string" and op.name == args[1] then
|
||||||
|
if args.loc and args.loc.x and args.loc.y then
|
||||||
|
local loc = { x = args.loc.x, y = args.loc.y }
|
||||||
|
op:set_location(loc)
|
||||||
|
placed_outputs[op.name] = loc
|
||||||
|
|
||||||
|
local props = op:props()
|
||||||
|
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
|
||||||
|
rightmost_output.output = op
|
||||||
|
rightmost_output.x = props.x + props.logical_width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Place outputs with no specified location in a line to the rightmost
|
||||||
|
for _, op in ipairs(outputs) do
|
||||||
|
local args_contains_op = false
|
||||||
|
|
||||||
|
for _, args in ipairs(setup) do
|
||||||
|
if type(args[1]) == "string" and op.name == args[1] then
|
||||||
|
args_contains_op = true
|
||||||
|
if not args.loc then
|
||||||
|
if not rightmost_output.output then
|
||||||
|
op:set_location({ x = 0, y = 0 })
|
||||||
|
else
|
||||||
|
op:set_loc_adj_to(rightmost_output.output, "right_align_top")
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = op:props()
|
||||||
|
|
||||||
|
local loc = { x = props.x, y = props.y }
|
||||||
|
rightmost_output.output = op
|
||||||
|
rightmost_output.x = props.x
|
||||||
|
|
||||||
|
placed_outputs[op.name] = loc
|
||||||
|
|
||||||
|
goto continue_outer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- No match, still lay it out
|
||||||
|
|
||||||
|
if not args_contains_op and not placed_outputs[op.name] then
|
||||||
|
if not rightmost_output.output then
|
||||||
|
op:set_location({ x = 0, y = 0 })
|
||||||
|
else
|
||||||
|
op:set_loc_adj_to(rightmost_output.output, "right_align_top")
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = op:props()
|
||||||
|
|
||||||
|
local loc = { x = props.x, y = props.y }
|
||||||
|
rightmost_output.output = op
|
||||||
|
rightmost_output.x = props.x
|
||||||
|
|
||||||
|
placed_outputs[op.name] = loc
|
||||||
|
end
|
||||||
|
|
||||||
|
::continue_outer::
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Place outputs that are relative to other outputs
|
||||||
|
for _, args in ipairs(setup) do
|
||||||
|
for _, op in ipairs(outputs) do
|
||||||
|
if type(args[1]) == "string" and op.name == args[1] then
|
||||||
|
if args.loc and args.loc[1] and args.loc[2] then
|
||||||
|
local matcher = args.loc[1]
|
||||||
|
local alignment = args.loc[2]
|
||||||
|
---@type OutputHandle?
|
||||||
|
local relative_to = nil
|
||||||
|
|
||||||
|
for _, op in ipairs(outputs) do
|
||||||
|
if output_matches(op, matcher) then
|
||||||
|
relative_to = op
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not relative_to then
|
||||||
|
table.insert(relative_to_outputs_that_are_not_placed, op)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if not placed_outputs[relative_to.name] then
|
||||||
|
-- The output it's relative to hasn't been placed yet;
|
||||||
|
-- Users must place outputs before ones being placed relative to them
|
||||||
|
table.insert(relative_to_outputs_that_are_not_placed, op)
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
|
||||||
|
op:set_loc_adj_to(relative_to, alignment)
|
||||||
|
|
||||||
|
local props = op:props()
|
||||||
|
|
||||||
|
local loc = { x = props.x, y = props.y }
|
||||||
|
|
||||||
|
if not rightmost_output.output or rightmost_output.x < props.x + props.logical_width then
|
||||||
|
rightmost_output.output = op
|
||||||
|
rightmost_output.x = props.x + props.logical_width
|
||||||
|
end
|
||||||
|
|
||||||
|
placed_outputs[op.name] = loc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Place still-not-placed outputs
|
||||||
|
for _, op in ipairs(relative_to_outputs_that_are_not_placed) do
|
||||||
|
if not rightmost_output.output then
|
||||||
|
op:set_location({ x = 0, y = 0 })
|
||||||
|
else
|
||||||
|
op:set_loc_adj_to(rightmost_output.output, "right_align_top")
|
||||||
|
end
|
||||||
|
|
||||||
|
local props = op:props()
|
||||||
|
|
||||||
|
local loc = { x = props.x, y = props.y }
|
||||||
|
rightmost_output.output = op
|
||||||
|
rightmost_output.x = props.x
|
||||||
|
|
||||||
|
placed_outputs[op.name] = loc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
layout_outputs()
|
||||||
|
|
||||||
|
output.connect_signal({
|
||||||
|
connect = function(op)
|
||||||
|
-- FIXME: This currently does not duplicate tags because the connect signal does not fire for previously connected
|
||||||
|
-- | outputs. However, this is unintended behavior, so fix this when you fix that.
|
||||||
|
apply_transformers(op)
|
||||||
|
layout_outputs()
|
||||||
|
end,
|
||||||
|
disconnect = function(_)
|
||||||
|
layout_outputs()
|
||||||
|
end,
|
||||||
|
resize = function(_, _, _)
|
||||||
|
layout_outputs()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
---@type table<string, SignalServiceMethod>
|
---@type table<string, SignalServiceMethod>
|
||||||
local signal_name_to_SignalName = {
|
local signal_name_to_SignalName = {
|
||||||
connect = "OutputConnect",
|
connect = "OutputConnect",
|
||||||
|
disconnect = "OutputDisconnect",
|
||||||
resize = "OutputResize",
|
resize = "OutputResize",
|
||||||
move = "OutputMove",
|
move = "OutputMove",
|
||||||
}
|
}
|
||||||
|
|
||||||
---@class OutputSignal Signals related to output events.
|
---@class OutputSignal Signals related to output events.
|
||||||
---@field connect fun(output: OutputHandle)? An output was connected. FIXME: This currently does not fire for outputs that have been previously connected and disconnected.
|
---@field connect fun(output: OutputHandle)? An output was connected. FIXME: This currently does not fire for outputs that have been previously connected and disconnected.
|
||||||
|
---@field disconnect fun(output: OutputHandle)? An output was disconnected.
|
||||||
---@field resize fun(output: OutputHandle, logical_width: integer, logical_height: integer)? An output's logical size changed.
|
---@field resize fun(output: OutputHandle, logical_width: integer, logical_height: integer)? An output's logical size changed.
|
||||||
---@field move fun(output: OutputHandle, x: integer, y: integer)? An output moved.
|
---@field move fun(output: OutputHandle, x: integer, y: integer)? An output moved.
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,9 @@ local rpc_types = {
|
||||||
OutputConnect = {
|
OutputConnect = {
|
||||||
response_type = "OutputConnectResponse",
|
response_type = "OutputConnectResponse",
|
||||||
},
|
},
|
||||||
|
OutputDisconnect = {
|
||||||
|
response_type = "OutputDisconnectResponse",
|
||||||
|
},
|
||||||
OutputResize = {
|
OutputResize = {
|
||||||
response_type = "OutputResizeResponse",
|
response_type = "OutputResizeResponse",
|
||||||
},
|
},
|
||||||
|
@ -68,6 +71,17 @@ local signals = {
|
||||||
---@type fun(response: table)
|
---@type fun(response: table)
|
||||||
on_response = nil,
|
on_response = nil,
|
||||||
},
|
},
|
||||||
|
OutputDisconnect = {
|
||||||
|
---@nodoc
|
||||||
|
---@type H2Stream?
|
||||||
|
sender = nil,
|
||||||
|
---@nodoc
|
||||||
|
---@type (fun(output: OutputHandle))[]
|
||||||
|
callbacks = {},
|
||||||
|
---@nodoc
|
||||||
|
---@type fun(response: table)
|
||||||
|
on_response = nil,
|
||||||
|
},
|
||||||
OutputResize = {
|
OutputResize = {
|
||||||
---@nodoc
|
---@nodoc
|
||||||
---@type H2Stream?
|
---@type H2Stream?
|
||||||
|
@ -122,6 +136,14 @@ signals.OutputConnect.on_response = function(response)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
signals.OutputDisconnect.on_response = function(response)
|
||||||
|
---@diagnostic disable-next-line: invisible
|
||||||
|
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||||
|
for _, callback in ipairs(signals.OutputDisconnect.callbacks) do
|
||||||
|
callback(handle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
signals.OutputResize.on_response = function(response)
|
signals.OutputResize.on_response = function(response)
|
||||||
---@diagnostic disable-next-line: invisible
|
---@diagnostic disable-next-line: invisible
|
||||||
local handle = require("pinnacle.output").handle.new(response.output_name)
|
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||||
|
|
|
@ -16,6 +16,12 @@ message OutputConnectRequest {
|
||||||
message OutputConnectResponse {
|
message OutputConnectResponse {
|
||||||
optional string output_name = 1;
|
optional string output_name = 1;
|
||||||
}
|
}
|
||||||
|
message OutputDisconnectRequest {
|
||||||
|
optional StreamControl control = 1;
|
||||||
|
}
|
||||||
|
message OutputDisconnectResponse {
|
||||||
|
optional string output_name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message OutputResizeRequest {
|
message OutputResizeRequest {
|
||||||
optional StreamControl control = 1;
|
optional StreamControl control = 1;
|
||||||
|
@ -55,6 +61,7 @@ message WindowPointerLeaveResponse {
|
||||||
|
|
||||||
service SignalService {
|
service SignalService {
|
||||||
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
||||||
|
rpc OutputDisconnect(stream OutputDisconnectRequest) returns (stream OutputDisconnectResponse);
|
||||||
rpc OutputResize(stream OutputResizeRequest) returns (stream OutputResizeResponse);
|
rpc OutputResize(stream OutputResizeRequest) returns (stream OutputResizeResponse);
|
||||||
rpc OutputMove(stream OutputMoveRequest) returns (stream OutputMoveResponse);
|
rpc OutputMove(stream OutputMoveRequest) returns (stream OutputMoveResponse);
|
||||||
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
||||||
|
|
|
@ -2,6 +2,7 @@ use pinnacle_api::layout::{
|
||||||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||||
MasterStackLayout, SpiralLayout,
|
MasterStackLayout, SpiralLayout,
|
||||||
};
|
};
|
||||||
|
use pinnacle_api::output::OutputSetup;
|
||||||
use pinnacle_api::signal::WindowSignal;
|
use pinnacle_api::signal::WindowSignal;
|
||||||
use pinnacle_api::util::{Axis, Batch};
|
use pinnacle_api::util::{Axis, Batch};
|
||||||
use pinnacle_api::xkbcommon::xkb::Keysym;
|
use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||||
|
@ -206,12 +207,7 @@ async fn main() {
|
||||||
let tag_names = ["1", "2", "3", "4", "5"];
|
let tag_names = ["1", "2", "3", "4", "5"];
|
||||||
|
|
||||||
// Setup all monitors with tags "1" through "5"
|
// Setup all monitors with tags "1" through "5"
|
||||||
output.connect_for_all(move |op| {
|
output.setup([OutputSetup::new_with_matcher(|_| true).with_tags(tag_names)]);
|
||||||
let tags = tag.add(op, tag_names);
|
|
||||||
|
|
||||||
// Be sure to set a tag to active or windows won't display
|
|
||||||
tags.first().unwrap().set_active(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
for tag_name in tag_names {
|
for tag_name in tag_names {
|
||||||
// `mod_key + 1-5` switches to tag "1" to "5"
|
// `mod_key + 1-5` switches to tag "1" to "5"
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
//! 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::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use pinnacle_api_defs::pinnacle::output::{
|
use pinnacle_api_defs::pinnacle::output::{
|
||||||
self,
|
self,
|
||||||
|
@ -24,7 +26,7 @@ use crate::{
|
||||||
signal::{OutputSignal, SignalHandle},
|
signal::{OutputSignal, SignalHandle},
|
||||||
tag::TagHandle,
|
tag::TagHandle,
|
||||||
util::Batch,
|
util::Batch,
|
||||||
SIGNAL, TAG,
|
OUTPUT, SIGNAL, TAG,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A struct that allows you to get handles to connected outputs and set them up.
|
/// A struct that allows you to get handles to connected outputs and set them up.
|
||||||
|
@ -159,10 +161,298 @@ impl Output {
|
||||||
|
|
||||||
match signal {
|
match signal {
|
||||||
OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
|
OutputSignal::Connect(f) => signal_state.output_connect.add_callback(f),
|
||||||
|
OutputSignal::Disconnect(f) => signal_state.output_disconnect.add_callback(f),
|
||||||
OutputSignal::Resize(f) => signal_state.output_resize.add_callback(f),
|
OutputSignal::Resize(f) => signal_state.output_resize.add_callback(f),
|
||||||
OutputSignal::Move(f) => signal_state.output_move.add_callback(f),
|
OutputSignal::Move(f) => signal_state.output_move.add_callback(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Declaratively setup outputs.
|
||||||
|
///
|
||||||
|
/// This method allows you to specify [`OutputSetup`]s that will be applied to outputs already
|
||||||
|
/// connected and that will be connected in the future. It handles the setting of modes,
|
||||||
|
/// scales, tags, and locations of your outputs.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // TODO:
|
||||||
|
/// ```
|
||||||
|
pub fn setup(&self, setups: impl IntoIterator<Item = OutputSetup>) {
|
||||||
|
let setups = setups.into_iter().map(Arc::new).collect::<Vec<_>>();
|
||||||
|
let setups_clone = setups.clone();
|
||||||
|
|
||||||
|
let apply_all_but_loc = move |output: &OutputHandle| {
|
||||||
|
for setup in setups.iter() {
|
||||||
|
if setup.output.matches(output) {
|
||||||
|
setup.apply_all_but_loc(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let outputs = self.get_all();
|
||||||
|
for output in outputs {
|
||||||
|
apply_all_but_loc(&output);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout_outputs = move || {
|
||||||
|
let setups = setups_clone.clone().into_iter().collect::<Vec<_>>();
|
||||||
|
let outputs = OUTPUT.get().unwrap().get_all();
|
||||||
|
|
||||||
|
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
|
||||||
|
|
||||||
|
// `OutputHandle`'s Hash impl only hashes the string, therefore this is a false positive
|
||||||
|
#[allow(clippy::mutable_key_type)]
|
||||||
|
let mut placed_outputs = HashSet::<OutputHandle>::new();
|
||||||
|
|
||||||
|
// Place outputs with OutputSetupLoc::Point
|
||||||
|
for output in outputs.iter() {
|
||||||
|
for setup in setups.iter() {
|
||||||
|
if setup.output.matches(output) {
|
||||||
|
if let Some(OutputSetupLoc::Point(x, y)) = setup.loc {
|
||||||
|
output.set_location(x, y);
|
||||||
|
placed_outputs.insert(output.clone());
|
||||||
|
if rightmost_output_and_x.is_none()
|
||||||
|
|| rightmost_output_and_x
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, rm_x)| x > *rm_x)
|
||||||
|
{
|
||||||
|
rightmost_output_and_x = Some((output.clone(), x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place everything without an explicit location to the right of the rightmost output
|
||||||
|
for output in outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|op| !placed_outputs.contains(op))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
for setup in setups.iter() {
|
||||||
|
if setup.output.matches(output) && setup.loc.is_none() {
|
||||||
|
if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() {
|
||||||
|
output.set_loc_adj_to(rm_op, Alignment::RightAlignTop);
|
||||||
|
} else {
|
||||||
|
output.set_location(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
placed_outputs.insert(output.clone());
|
||||||
|
let x = output.x().unwrap();
|
||||||
|
if rightmost_output_and_x.is_none()
|
||||||
|
|| rightmost_output_and_x
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, rm_x)| x > *rm_x)
|
||||||
|
{
|
||||||
|
rightmost_output_and_x = Some((output.clone(), x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to place relative outputs
|
||||||
|
while let Some((output, relative_to, alignment)) = setups.iter().find_map(|setup| {
|
||||||
|
outputs.iter().find_map(|op| {
|
||||||
|
if !placed_outputs.contains(op) && setup.output.matches(op) {
|
||||||
|
match &setup.loc {
|
||||||
|
Some(OutputSetupLoc::RelativeTo(matcher, alignment)) => {
|
||||||
|
let first_matched_op = outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| matcher.matches(o) && placed_outputs.contains(o))?;
|
||||||
|
Some((op, first_matched_op, alignment))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
output.set_loc_adj_to(relative_to, *alignment);
|
||||||
|
|
||||||
|
placed_outputs.insert(output.clone());
|
||||||
|
let x = output.x().unwrap();
|
||||||
|
if rightmost_output_and_x.is_none()
|
||||||
|
|| rightmost_output_and_x
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, rm_x)| x > *rm_x)
|
||||||
|
{
|
||||||
|
rightmost_output_and_x = Some((output.clone(), x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place all remaining outputs right of the rightmost one
|
||||||
|
for output in outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|op| !placed_outputs.contains(op))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() {
|
||||||
|
output.set_loc_adj_to(rm_op, Alignment::RightAlignTop);
|
||||||
|
} else {
|
||||||
|
output.set_location(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
placed_outputs.insert(output.clone());
|
||||||
|
let x = output.x().unwrap();
|
||||||
|
if rightmost_output_and_x.is_none()
|
||||||
|
|| rightmost_output_and_x
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, rm_x)| x > *rm_x)
|
||||||
|
{
|
||||||
|
rightmost_output_and_x = Some((output.clone(), x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
layout_outputs();
|
||||||
|
|
||||||
|
let layout_outputs_clone1 = layout_outputs.clone();
|
||||||
|
let layout_outputs_clone2 = layout_outputs.clone();
|
||||||
|
|
||||||
|
self.connect_signal(OutputSignal::Connect(Box::new(move |output| {
|
||||||
|
apply_all_but_loc(output);
|
||||||
|
layout_outputs_clone2();
|
||||||
|
})));
|
||||||
|
|
||||||
|
self.connect_signal(OutputSignal::Disconnect(Box::new(move |_| {
|
||||||
|
layout_outputs_clone1();
|
||||||
|
})));
|
||||||
|
|
||||||
|
self.connect_signal(OutputSignal::Resize(Box::new(move |_, _, _| {
|
||||||
|
layout_outputs();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A matcher for outputs.
|
||||||
|
pub enum OutputMatcher {
|
||||||
|
/// Match outputs by name.
|
||||||
|
Name(String),
|
||||||
|
/// Match outputs by a function returning a bool.
|
||||||
|
Fn(Box<dyn Fn(&OutputHandle) -> bool + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputMatcher {
|
||||||
|
/// Returns whether this matcher matches the given output.
|
||||||
|
pub fn matches(&self, output: &OutputHandle) -> bool {
|
||||||
|
match self {
|
||||||
|
OutputMatcher::Name(name) => output.name() == name,
|
||||||
|
OutputMatcher::Fn(matcher) => matcher(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for OutputMatcher {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Name(name) => f.debug_tuple("Name").field(name).finish(),
|
||||||
|
Self::Fn(_) => f
|
||||||
|
.debug_tuple("Fn")
|
||||||
|
.field(&"<Box<dyn Fn(&OutputHandle)> -> bool>")
|
||||||
|
.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OutputSetupLoc {
|
||||||
|
Point(i32, i32),
|
||||||
|
RelativeTo(OutputMatcher, Alignment),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An output setup for use in [`Output::setup`].
|
||||||
|
pub struct OutputSetup {
|
||||||
|
output: OutputMatcher,
|
||||||
|
loc: Option<OutputSetupLoc>,
|
||||||
|
mode: Option<Mode>,
|
||||||
|
scale: Option<f32>,
|
||||||
|
tag_names: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputSetup {
|
||||||
|
/// Creates a new `OutputSetup` that applies to the output with the given name.
|
||||||
|
pub fn new(output_name: impl ToString) -> Self {
|
||||||
|
Self {
|
||||||
|
output: OutputMatcher::Name(output_name.to_string()),
|
||||||
|
loc: None,
|
||||||
|
mode: None,
|
||||||
|
scale: None,
|
||||||
|
tag_names: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `OutputSetup` that matches outputs according to the given function.
|
||||||
|
pub fn new_with_matcher(
|
||||||
|
matcher: impl Fn(&OutputHandle) -> bool + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
output: OutputMatcher::Fn(Box::new(matcher)),
|
||||||
|
loc: None,
|
||||||
|
mode: None,
|
||||||
|
scale: None,
|
||||||
|
tag_names: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup map outputs to the given location.
|
||||||
|
pub fn with_absolute_loc(self, x: i32, y: i32) -> Self {
|
||||||
|
Self {
|
||||||
|
loc: Some(OutputSetupLoc::Point(x, y)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup map outputs relative to the first output that `relative_to` matches.
|
||||||
|
pub fn with_relative_loc(self, relative_to: OutputMatcher, alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
loc: Some(OutputSetupLoc::RelativeTo(relative_to, alignment)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup apply the given [`Mode`] to its outputs.
|
||||||
|
pub fn with_mode(self, mode: Mode) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: Some(mode),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup apply the given scale to its outputs.
|
||||||
|
pub fn with_scale(self, scale: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
scale: Some(scale),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes this setup add tags with the given names to its outputs.
|
||||||
|
pub fn with_tags(self, tag_names: impl IntoIterator<Item = impl ToString>) -> Self {
|
||||||
|
Self {
|
||||||
|
tag_names: Some(tag_names.into_iter().map(|s| s.to_string()).collect()),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_all_but_loc(&self, output: &OutputHandle) {
|
||||||
|
if let Some(mode) = &self.mode {
|
||||||
|
output.set_mode(
|
||||||
|
mode.pixel_width,
|
||||||
|
mode.pixel_height,
|
||||||
|
Some(mode.refresh_rate_millihertz),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(scale) = self.scale {
|
||||||
|
output.set_scale(scale);
|
||||||
|
}
|
||||||
|
if let Some(tag_names) = &self.tag_names {
|
||||||
|
let tags = TAG.get().unwrap().add(output, tag_names);
|
||||||
|
if let Some(tag) = tags.first() {
|
||||||
|
tag.set_active(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to an output.
|
/// A handle to an output.
|
||||||
|
|
|
@ -140,6 +140,24 @@ signals! {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
/// An output was connected.
|
||||||
|
///
|
||||||
|
/// Callbacks receive the disconnected output.
|
||||||
|
OutputDisconnect = {
|
||||||
|
enum_name = Disconnect,
|
||||||
|
callback_type = SingleOutputFn,
|
||||||
|
client_request = output_disconnect,
|
||||||
|
on_response = |response, callbacks| {
|
||||||
|
if let Some(output_name) = response.output_name {
|
||||||
|
let output = OUTPUT.get().expect("OUTPUT doesn't exist");
|
||||||
|
let handle = output.new_handle(output_name);
|
||||||
|
|
||||||
|
for callback in callbacks {
|
||||||
|
callback(&handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
/// An output's logical size changed.
|
/// An output's logical size changed.
|
||||||
///
|
///
|
||||||
/// Callbacks receive the output and new width and height.
|
/// Callbacks receive the output and new width and height.
|
||||||
|
@ -223,6 +241,7 @@ pub(crate) type SingleWindowFn = Box<dyn FnMut(&WindowHandle) + Send + 'static>;
|
||||||
|
|
||||||
pub(crate) struct SignalState {
|
pub(crate) struct SignalState {
|
||||||
pub(crate) output_connect: SignalData<OutputConnect>,
|
pub(crate) output_connect: SignalData<OutputConnect>,
|
||||||
|
pub(crate) output_disconnect: SignalData<OutputDisconnect>,
|
||||||
pub(crate) output_resize: SignalData<OutputResize>,
|
pub(crate) output_resize: SignalData<OutputResize>,
|
||||||
pub(crate) output_move: SignalData<OutputMove>,
|
pub(crate) output_move: SignalData<OutputMove>,
|
||||||
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
|
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
|
||||||
|
@ -237,6 +256,7 @@ impl SignalState {
|
||||||
let client = SignalServiceClient::new(channel);
|
let client = SignalServiceClient::new(channel);
|
||||||
Self {
|
Self {
|
||||||
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
|
output_connect: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
|
output_disconnect: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
output_resize: SignalData::new(client.clone(), fut_sender.clone()),
|
output_resize: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
output_move: SignalData::new(client.clone(), fut_sender.clone()),
|
output_move: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
window_pointer_enter: SignalData::new(client.clone(), fut_sender.clone()),
|
window_pointer_enter: SignalData::new(client.clone(), fut_sender.clone()),
|
||||||
|
|
|
@ -63,6 +63,7 @@ pub mod pinnacle {
|
||||||
|
|
||||||
impl_signal_request!(
|
impl_signal_request!(
|
||||||
OutputConnectRequest,
|
OutputConnectRequest,
|
||||||
|
OutputDisconnectRequest,
|
||||||
OutputResizeRequest,
|
OutputResizeRequest,
|
||||||
OutputMoveRequest,
|
OutputMoveRequest,
|
||||||
WindowPointerEnterRequest,
|
WindowPointerEnterRequest,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputMoveRequest,
|
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputDisconnectRequest,
|
||||||
OutputMoveResponse, OutputResizeRequest, OutputResizeResponse, SignalRequest, StreamControl,
|
OutputDisconnectResponse, OutputMoveRequest, OutputMoveResponse, OutputResizeRequest,
|
||||||
WindowPointerEnterRequest, WindowPointerEnterResponse, WindowPointerLeaveRequest,
|
OutputResizeResponse, SignalRequest, StreamControl, WindowPointerEnterRequest,
|
||||||
WindowPointerLeaveResponse,
|
WindowPointerEnterResponse, WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
||||||
};
|
};
|
||||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||||
use tonic::{Request, Response, Status, Streaming};
|
use tonic::{Request, Response, Status, Streaming};
|
||||||
|
@ -18,6 +18,7 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
|
||||||
pub struct SignalState {
|
pub struct SignalState {
|
||||||
// Output
|
// Output
|
||||||
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
||||||
|
pub output_disconnect: SignalData<OutputDisconnectResponse, VecDeque<OutputDisconnectResponse>>,
|
||||||
pub output_resize: SignalData<OutputResizeResponse, VecDeque<OutputResizeResponse>>,
|
pub output_resize: SignalData<OutputResizeResponse, VecDeque<OutputResizeResponse>>,
|
||||||
pub output_move: SignalData<OutputMoveResponse, VecDeque<OutputMoveResponse>>,
|
pub output_move: SignalData<OutputMoveResponse, VecDeque<OutputMoveResponse>>,
|
||||||
|
|
||||||
|
@ -31,6 +32,9 @@ pub struct SignalState {
|
||||||
impl SignalState {
|
impl SignalState {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.output_connect.disconnect();
|
self.output_connect.disconnect();
|
||||||
|
self.output_disconnect.disconnect();
|
||||||
|
self.output_resize.disconnect();
|
||||||
|
self.output_move.disconnect();
|
||||||
self.window_pointer_enter.disconnect();
|
self.window_pointer_enter.disconnect();
|
||||||
self.window_pointer_leave.disconnect();
|
self.window_pointer_leave.disconnect();
|
||||||
}
|
}
|
||||||
|
@ -177,6 +181,7 @@ impl SignalService {
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl signal_service_server::SignalService for SignalService {
|
impl signal_service_server::SignalService for SignalService {
|
||||||
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
||||||
|
type OutputDisconnectStream = ResponseStream<OutputDisconnectResponse>;
|
||||||
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
|
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
|
||||||
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
|
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
|
||||||
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
type WindowPointerEnterStream = ResponseStream<WindowPointerEnterResponse>;
|
||||||
|
@ -193,6 +198,17 @@ impl signal_service_server::SignalService for SignalService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn output_disconnect(
|
||||||
|
&self,
|
||||||
|
request: Request<Streaming<OutputDisconnectRequest>>,
|
||||||
|
) -> Result<Response<Self::OutputDisconnectStream>, Status> {
|
||||||
|
let in_stream = request.into_inner();
|
||||||
|
|
||||||
|
start_signal_stream(self.sender.clone(), in_stream, |state| {
|
||||||
|
&mut state.signal_state.output_disconnect
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn output_resize(
|
async fn output_resize(
|
||||||
&self,
|
&self,
|
||||||
request: Request<Streaming<OutputResizeRequest>>,
|
request: Request<Streaming<OutputResizeRequest>>,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
|
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;
|
||||||
|
use smithay::utils::{Physical, Size};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -116,3 +120,45 @@ pub fn setup_dummy(
|
||||||
|
|
||||||
Ok((state, event_loop))
|
Ok((state, event_loop))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new_output(&mut self, name: impl std::fmt::Display, size: Size<i32, Physical>) {
|
||||||
|
let mode = smithay::output::Mode {
|
||||||
|
size,
|
||||||
|
refresh: 144_000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let physical_properties = smithay::output::PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "Pinnacle".to_string(),
|
||||||
|
model: "Dummy Output".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Output::new(name.to_string(), physical_properties);
|
||||||
|
|
||||||
|
output.change_current_state(Some(mode), None, None, Some((0, 0).into()));
|
||||||
|
|
||||||
|
output.set_preferred(mode);
|
||||||
|
|
||||||
|
output.create_global::<State>(&self.display_handle);
|
||||||
|
|
||||||
|
self.space.map_output(&output, (0, 0));
|
||||||
|
|
||||||
|
self.signal_state.output_connect.signal(|buf| {
|
||||||
|
buf.push_back(OutputConnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_output(&mut self, output: &Output) {
|
||||||
|
self.space.unmap_output(output);
|
||||||
|
|
||||||
|
self.signal_state.output_disconnect.signal(|buffer| {
|
||||||
|
buffer.push_back(OutputDisconnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||||
|
OutputConnectResponse, OutputDisconnectResponse,
|
||||||
|
};
|
||||||
use smithay::{
|
use smithay::{
|
||||||
backend::{
|
backend::{
|
||||||
allocator::{
|
allocator::{
|
||||||
|
@ -1184,6 +1186,12 @@ impl State {
|
||||||
);
|
);
|
||||||
self.space.unmap_output(&output);
|
self.space.unmap_output(&output);
|
||||||
self.gamma_control_manager_state.output_removed(&output);
|
self.gamma_control_manager_state.output_removed(&output);
|
||||||
|
|
||||||
|
self.signal_state.output_disconnect.signal(|buffer| {
|
||||||
|
buffer.push_back(OutputDisconnectResponse {
|
||||||
|
output_name: Some(output.name()),
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ pub fn setup_winit(
|
||||||
model: "Winit Window".to_string(),
|
model: "Winit Window".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = Output::new("Pinnacle window".to_string(), physical_properties);
|
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
|
||||||
|
|
||||||
output.change_current_state(
|
output.change_current_state(
|
||||||
Some(mode),
|
Some(mode),
|
||||||
|
|
|
@ -7,6 +7,7 @@ use smithay::{
|
||||||
output::{Mode, Output, Scale},
|
output::{Mode, Output, Scale},
|
||||||
utils::{Logical, Point, Transform},
|
utils::{Logical, Point, Transform},
|
||||||
};
|
};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
focus::WindowKeyboardFocusStack,
|
focus::WindowKeyboardFocusStack,
|
||||||
|
@ -86,6 +87,7 @@ impl State {
|
||||||
) {
|
) {
|
||||||
output.change_current_state(mode, transform, scale, location);
|
output.change_current_state(mode, transform, scale, location);
|
||||||
if let Some(location) = location {
|
if let Some(location) = location {
|
||||||
|
info!(?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| {
|
||||||
buf.push_back(OutputMoveResponse {
|
buf.push_back(OutputMoveResponse {
|
||||||
|
|
209
tests/lua_api.rs
209
tests/lua_api.rs
|
@ -48,8 +48,53 @@ fn run_lua(ident: &str, code: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SetupLuaGuard {
|
||||||
|
child: std::process::Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SetupLuaGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn setup_lua(ident: &str, code: &str) -> SetupLuaGuard {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let code = format!(r#"
|
||||||
|
require("pinnacle").setup(function({ident})
|
||||||
|
local run = function({ident})
|
||||||
|
{code}
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, err = pcall(run, {ident})
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
print(err)
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
"#);
|
||||||
|
|
||||||
|
let mut child = Command::new("lua").stdin(Stdio::piped()).spawn().unwrap();
|
||||||
|
|
||||||
|
let mut stdin = child.stdin.take().unwrap();
|
||||||
|
|
||||||
|
stdin.write_all(code.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
drop(stdin);
|
||||||
|
|
||||||
|
SetupLuaGuard { child }
|
||||||
|
|
||||||
|
// let exit_status = child.wait().unwrap();
|
||||||
|
//
|
||||||
|
// if exit_status.code().is_some_and(|code| code != 0) {
|
||||||
|
// panic!("lua code panicked");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn assert(
|
fn with_state(
|
||||||
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
|
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
|
||||||
assert: impl FnOnce(&mut State) + Send + 'static,
|
assert: impl FnOnce(&mut State) + Send + 'static,
|
||||||
) {
|
) {
|
||||||
|
@ -66,6 +111,12 @@ macro_rules! run_lua {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! setup_lua {
|
||||||
|
{ |$ident:ident| $($body:tt)* } => {
|
||||||
|
let _guard = setup_lua(stringify!($ident), stringify!($($body)*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn test_lua_api(
|
fn test_lua_api(
|
||||||
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
|
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
@ -149,7 +200,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.windows.len(), 1);
|
assert_eq!(state.windows.len(), 1);
|
||||||
assert_eq!(state.windows[0].class(), Some("foot".to_string()));
|
assert_eq!(state.windows[0].class(), Some("foot".to_string()));
|
||||||
});
|
});
|
||||||
|
@ -166,7 +217,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |_state| {
|
with_state(&sender, |_state| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::env::var("PROCESS_SET_ENV"),
|
std::env::var("PROCESS_SET_ENV"),
|
||||||
Ok("env value".to_string())
|
Ok("env value".to_string())
|
||||||
|
@ -234,7 +285,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.config.window_rules.len(), 1);
|
assert_eq!(state.config.window_rules.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.config.window_rules[0],
|
state.config.window_rules[0],
|
||||||
|
@ -271,7 +322,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.config.window_rules.len(), 2);
|
assert_eq!(state.config.window_rules.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.config.window_rules[1],
|
state.config.window_rules[1],
|
||||||
|
@ -312,7 +363,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.windows.len(), 1);
|
assert_eq!(state.windows.len(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -322,7 +373,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.windows.len(), 0);
|
assert_eq!(state.windows.len(), 0);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -341,7 +392,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.windows[0].with_state(|st| st
|
state.windows[0].with_state(|st| st
|
||||||
.tags
|
.tags
|
||||||
|
@ -359,7 +410,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.windows[0].with_state(|st| st
|
state.windows[0].with_state(|st| st
|
||||||
.tags
|
.tags
|
||||||
|
@ -377,7 +428,7 @@ mod coverage {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.windows[0].with_state(|st| st
|
state.windows[0].with_state(|st| st
|
||||||
.tags
|
.tags
|
||||||
|
@ -446,6 +497,130 @@ mod coverage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod output {
|
||||||
|
use smithay::utils::Rectangle;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
#[self::test]
|
||||||
|
async fn setup() -> anyhow::Result<()> {
|
||||||
|
test_lua_api(|sender| {
|
||||||
|
setup_lua! { |Pinnacle|
|
||||||
|
Pinnacle.output.setup({
|
||||||
|
{
|
||||||
|
function(_)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
tag_names = { "First", "Third", "Schmurd" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Pinnacle Window",
|
||||||
|
loc = { x = 300, y = 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Output 1",
|
||||||
|
loc = { "Pinnacle Window", "bottom_align_left" },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
with_state(&sender, |state| {
|
||||||
|
state.new_output("Output 1", (960, 540).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
with_state(&sender, |state| {
|
||||||
|
let original_op = state
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == "Pinnacle Window")
|
||||||
|
.unwrap();
|
||||||
|
let output_1 = state
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == "Output 1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let original_op_geo = state.space.output_geometry(original_op).unwrap();
|
||||||
|
let output_1_geo = state.space.output_geometry(output_1).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
original_op_geo,
|
||||||
|
Rectangle::from_loc_and_size((300, 0), (1920, 1080))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_1_geo,
|
||||||
|
Rectangle::from_loc_and_size((300, 1080), (960, 540))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_1.with_state(|state| state
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|tag| tag.name())
|
||||||
|
.collect::<Vec<_>>()),
|
||||||
|
vec!["First", "Third", "Schmurd"]
|
||||||
|
);
|
||||||
|
|
||||||
|
state.remove_output(&original_op.clone());
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
with_state(&sender, |state| {
|
||||||
|
let output_1 = state
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == "Output 1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output_1_geo = state.space.output_geometry(output_1).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_1_geo,
|
||||||
|
Rectangle::from_loc_and_size((0, 0), (960, 540))
|
||||||
|
);
|
||||||
|
|
||||||
|
state.new_output("Output 2", (300, 500).into());
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep_secs(1);
|
||||||
|
|
||||||
|
with_state(&sender, |state| {
|
||||||
|
let output_1 = state
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == "Output 1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output_2 = state
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.find(|op| op.name() == "Output 2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output_1_geo = state.space.output_geometry(output_1).unwrap();
|
||||||
|
let output_2_geo = state.space.output_geometry(output_2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_2_geo,
|
||||||
|
Rectangle::from_loc_and_size((0, 0), (300, 500))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output_1_geo,
|
||||||
|
Rectangle::from_loc_and_size((300, 0), (960, 540))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -459,7 +634,7 @@ async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| assert_eq!(state.windows.len(), 1));
|
with_state(&sender, |state| assert_eq!(state.windows.len(), 1));
|
||||||
|
|
||||||
run_lua! { |Pinnacle|
|
run_lua! { |Pinnacle|
|
||||||
for i = 1, 20 do
|
for i = 1, 20 do
|
||||||
|
@ -469,7 +644,7 @@ async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| assert_eq!(state.windows.len(), 21));
|
with_state(&sender, |state| assert_eq!(state.windows.len(), 21));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,7 +658,7 @@ async fn window_count_without_tag_is_correct() -> anyhow::Result<()> {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| assert_eq!(state.windows.len(), 1));
|
with_state(&sender, |state| assert_eq!(state.windows.len(), 1));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,7 +673,7 @@ async fn spawned_window_on_active_tag_has_keyboard_focus() -> anyhow::Result<()>
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state
|
state
|
||||||
.focused_window(state.focused_output().unwrap())
|
.focused_window(state.focused_output().unwrap())
|
||||||
|
@ -521,7 +696,7 @@ async fn spawned_window_on_inactive_tag_does_not_have_keyboard_focus() -> anyhow
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.focused_window(state.focused_output().unwrap()), None);
|
assert_eq!(state.focused_window(state.focused_output().unwrap()), None);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -538,7 +713,7 @@ async fn spawned_window_has_correct_tags() -> anyhow::Result<()> {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.windows.len(), 1);
|
assert_eq!(state.windows.len(), 1);
|
||||||
assert_eq!(state.windows[0].with_state(|st| st.tags.len()), 1);
|
assert_eq!(state.windows[0].with_state(|st| st.tags.len()), 1);
|
||||||
});
|
});
|
||||||
|
@ -551,7 +726,7 @@ async fn spawned_window_has_correct_tags() -> anyhow::Result<()> {
|
||||||
|
|
||||||
sleep_secs(1);
|
sleep_secs(1);
|
||||||
|
|
||||||
assert(&sender, |state| {
|
with_state(&sender, |state| {
|
||||||
assert_eq!(state.windows.len(), 2);
|
assert_eq!(state.windows.len(), 2);
|
||||||
assert_eq!(state.windows[1].with_state(|st| st.tags.len()), 2);
|
assert_eq!(state.windows[1].with_state(|st| st.tags.len()), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
Loading…
Reference in a new issue