Finish Lua setup and setup_locs

This commit is contained in:
Ottatop 2024-04-16 19:25:59 -05:00
parent 3f5a56c201
commit f6ad192c86
6 changed files with 711 additions and 521 deletions

View file

@ -76,12 +76,19 @@ require("pinnacle").setup(function(Pinnacle)
end
end)
--------------------
-- Tags --
--------------------
----------------------
-- Tags and Outputs --
----------------------
local tag_names = { "1", "2", "3", "4", "5" }
-- Setup outputs.
--
-- `Output.setup` allows you to declare things like mode, scale, and tags for outputs.
-- Here we give all outputs tags 1 through 5.
--
-- Note that output matching functions currently don't infer the type of the parameter,
-- so you may need to add `---@param <param name> OutputHandle` above it.
Output.setup({
{
function(_)
@ -89,23 +96,13 @@ require("pinnacle").setup(function(Pinnacle)
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.
-- Here, we add tags with names 1-5 and set tag 1 as active.
-- Output.connect_for_all(function(op)
-- local tags = Tag.add(op, tag_names)
-- tags[1]:set_active(true)
-- end)
-- If you want to declare output locations as well, you can use `Output.setup_locs`.
-- This will additionally allow you to recalculate output locations on signals like
-- output connect, disconnect, and resize.
--
-- Read the admittedly scuffed docs for more.
-- Tag keybinds
for _, tag_name in ipairs(tag_names) do

View file

@ -166,7 +166,7 @@ function output.connect_for_all(callback)
})
end
---@class OutputSetupArgs
---@class OutputSetup
---@field [1] (string | fun(output: OutputHandle): boolean)
---@field loc ({ x: integer, y: integer } | { [1]: (string | fun(output: OutputHandle): boolean), [2]: Alignment })?
---@field mode Mode?
@ -184,43 +184,132 @@ 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.
---on output connection. These include mode, scale, tags, and more.
---
---Arguments will be applied from top to bottom.
---Setups will be applied from top to bottom.
---
---`loc` will not be applied to arguments with an output matching function.
---`setups` is an array of `OutputSetup` tables.
---The table entry at [1] in an `OutputSetup` table should be either a string or a function
---that takes in an `OutputHandle` and returns a boolean. Strings will match output names directly,
---while the function matches outputs based on custom logic. You can specify keys such as
---`tag_names`, `scale`, and others to customize output properties.
---
---@param setup OutputSetupArgs[]
function output.setup(setup)
---### Example
---```lua
---Output.setup({
--- -- Give all outputs tags 1 through 5
--- {
--- function(_) return true end,
--- tag_names = { "1", "2", "3", "4", "5" },
--- }
--- -- Give outputs with a preferred mode of 4K a scale of 2.0
--- {
--- function(op)
--- return op:preferred_mode().pixel_width == 2160
--- end,
--- scale = 2.0,
--- },
--- -- Additionally give eDP-1 tags 6 and 7
--- {
--- "eDP-1",
--- tag_names = { "6", "7" },
--- },
---})
---```
---
---@param setups OutputSetup[]
function output.setup(setups)
---@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)
local function apply_setups(op)
for _, setup in ipairs(setups) do
if output_matches(op, setup[1]) then
if setup.mode then
op:set_mode(setup.mode.pixel_width, setup.mode.pixel_height, setup.mode.refresh_rate_millihz)
end
if args.scale then
op:set_scale(args.scale)
if setup.scale then
op:set_scale(setup.scale)
end
if args.tag_names then
local tags = require("pinnacle.tag").add(op, args.tag_names)
if setup.tag_names then
require("pinnacle.tag").add(op, setup.tag_names)
end
end
end
local tags = op:tags() or {}
if tags[1] then
tags[1]:set_active(true)
end
end
end
end
-- Apply mode, scale, and transforms first
local outputs = output.get_all()
output.connect_for_all(function(op)
apply_setups(op)
end)
end
for _, op in ipairs(outputs) do
apply_transformers(op)
---@alias OutputLoc
---| { x: integer, y: integer } -- A specific point
---| { [1]: string, [2]: Alignment } -- A location relative to another output
---| { [1]: string, [2]: Alignment }[] -- A location relative to another output with fallbacks
---@alias UpdateLocsOn
---| "connect" -- Update output locations on output connect
---| "disconnect" -- Update output locations on output disconnect
---| "resize" -- Update output locations on output resize
---Setup locations for outputs.
---
---This function lets you declare positions for outputs, either as a specific point in the global
---space or relative to another output.
---
---`update_locs_on` specifies when output positions should be recomputed. It can be `"all"`, signaling you
---want positions to update on all of output connect, disconnect, and resize, or it can be a table
---containing `"connect"`, `"disconnect"`, and/or `"resize"`.
---
---`setup` is an array of tables of the form `{ [1]: string, loc: OutputLoc }`, where `OutputLoc` is either
---the table `{ x: integer, y: integer }`, `{ [1]: string, [2]: Alignment }`, or an array of the latter table.
---See the examples for information.
---
---### Example
---```lua
--- -- vvvvv Relayout on output connect, disconnect, and resize
---Output.setup_locs("all", {
--- -- Anchor eDP-1 to (0, 0) so we can place other outputs relative to it
--- { "eDP-1", loc = { x = 0, y = 0 } },
--- -- Place HDMI-A-1 below it centered
--- { "HDMI-A-1", loc = { "eDP-1", "bottom_align_center" } },
--- -- Place HDMI-A-2 below HDMI-A-1.
--- -- Additionally, if HDMI-A-1 isn't connected, fallback to placing
--- -- it below eDP-1 instead.
--- {
--- "HDMI-A-2",
--- loc = {
--- { "HDMI-A-1", "bottom_align_center" },
--- { "eDP-1", "bottom_align_center" },
--- },
--- },
---})
---
--- -- Only relayout on output connect and resize
---Output.setup_locs({ "connect", "resize" }, { ... })
---```
---
---@param update_locs_on (UpdateLocsOn)[] | "all"
---@param setup { [1]: string, loc: OutputLoc }[]
function output.setup_locs(update_locs_on, setup)
---@type { [1]: string, loc: ({ x: integer, y: integer } | { [1]: string, [2]: Alignment }[]) }[]
local setups = {}
for _, s in ipairs(setup) do
if type(s.loc[1]) == "string" then
table.insert(setups, { s[1], loc = { s.loc } })
else
table.insert(setups, s)
end
end
local function layout_outputs()
local outputs = output.get_all()
---@type table<string, { x: integer, y: integer }>
---@type OutputHandle[]
local placed_outputs = {}
local rightmost_output = {
@ -228,16 +317,15 @@ function output.setup(setup)
x = nil,
}
local relative_to_outputs_that_are_not_placed = {}
-- Place outputs with a specified location first
for _, args in ipairs(setup) do
---@diagnostic disable-next-line: redefined-local
for _, setup in ipairs(setups) 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 }
if op.name == setup[1] then
if setup.loc and setup.loc.x and setup.loc.y then
local loc = { x = setup.loc.x, y = setup.loc.y }
op:set_location(loc)
placed_outputs[op.name] = loc
table.insert(placed_outputs, op)
local props = op:props()
if not rightmost_output.x or rightmost_output.x < props.x + props.logical_width then
@ -245,107 +333,73 @@ function output.setup(setup)
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
end
if not relative_to then
table.insert(relative_to_outputs_that_are_not_placed, op)
-- Place outputs that are relative to other outputs
local function next_output_with_relative_to()
---@diagnostic disable-next-line: redefined-local
for _, setup in ipairs(setups) do
for _, op in ipairs(outputs) do
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == op.name then
goto continue
end
end
if op.name ~= setup[1] or type(setup.loc[1]) ~= "table" then
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
for _, loc in ipairs(setup.loc) do
local relative_to_name = loc[1]
local alignment = loc[2]
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == relative_to_name then
return op, placed_op, alignment
end
end
end
goto continue_outer
::continue::
end
::continue_outer::
end
return nil, nil, nil
end
while true do
local op, relative_to, alignment = next_output_with_relative_to()
if not op then
break
end
---@cast relative_to OutputHandle
---@cast alignment Alignment
op:set_loc_adj_to(relative_to, alignment)
table.insert(placed_outputs, op)
local props = op:props()
local loc = { x = props.x, y = props.y }
if not rightmost_output.output or rightmost_output.x < props.x + props.logical_width then
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
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
for _, op in ipairs(outputs) do
for _, placed_op in ipairs(placed_outputs) do
if placed_op.name == op.name then
goto continue
end
end
if not rightmost_output.output then
op:set_location({ x = 0, y = 0 })
else
@ -354,30 +408,62 @@ function output.setup(setup)
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
table.insert(placed_outputs, op)
::continue::
end
end
layout_outputs()
local layout_on_connect = false
local layout_on_disconnect = false
local layout_on_resize = false
if update_locs_on == "all" then
layout_on_connect = true
layout_on_disconnect = true
layout_on_resize = true
else
---@cast update_locs_on UpdateLocsOn[]
for _, update_on in ipairs(update_locs_on) do
if update_on == "connect" then
layout_on_connect = true
elseif update_on == "disconnect" then
layout_on_disconnect = true
elseif update_on == "resize" then
layout_on_resize = true
end
end
end
if layout_on_connect then
-- FIXME: This currently does not duplicate tags because the connect signal does not fire for
-- | previously connected outputs. However, this is unintended behavior, so fix this when you fix that.
output.connect_signal({
connect = function(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(_, _, _)
connect = function(_)
layout_outputs()
end,
})
end
if layout_on_disconnect then
output.connect_signal({
disconnect = function(_)
layout_outputs()
end,
})
end
if layout_on_resize then
output.connect_signal({
resize = function(_)
layout_outputs()
end,
})
end
end
---@type table<string, SignalServiceMethod>

View file

@ -369,9 +369,7 @@ impl Output {
if update_locs_on.contains(UpdateLocsOn::CONNECT) {
self.connect_signal(OutputSignal::Connect(Box::new(move |output| {
println!("GOT CONNECT SIGNAL FOR {}", output.name());
layout_outputs_clone2();
println!("FINISHED CONNECT SIGNAL FOR {}", output.name());
})));
}

View file

@ -70,7 +70,6 @@ macro_rules! signals {
callback_sender
.send((self.current_id, callback))
.map_err(|e| { println!("{e}"); e })
.expect("failed to send callback");
let handle = SignalHandle::new(self.current_id, remove_callback_sender);

View file

@ -5,7 +5,7 @@ use std::{
process::{Command, Stdio},
};
use crate::common::{sleep_secs, test_api, with_state};
use crate::common::{output_for_name, sleep_secs, test_api, with_state};
use pinnacle::state::WithState;
use test_log::test;
@ -99,20 +99,17 @@ macro_rules! setup_lua {
};
}
mod coverage {
use pinnacle::{
use pinnacle::{
tag::TagId,
window::{
rules::{WindowRule, WindowRuleCondition},
window_state::FullscreenOrMaximized,
},
};
};
use super::*;
// Process
// Process
mod process {
mod process {
use super::*;
@ -151,11 +148,11 @@ mod coverage {
});
})
}
}
}
// Window
// Window
mod window {
mod window {
use super::*;
#[tokio::main]
@ -367,9 +364,9 @@ mod coverage {
})
}
}
}
}
mod tag {
mod tag {
use super::*;
mod handle {
@ -416,16 +413,16 @@ mod coverage {
assert(potato_props.name == "Potato")
assert(potato_props.output.name == "Pinnacle Window")
assert(#potato_props.windows == 2)
assert(first_props.windows[1]:class() == "foot")
assert(first_props.windows[2]:class() == "foot")
assert(potato_props.windows[1]:class() == "foot")
assert(potato_props.windows[2]:class() == "foot")
}
})
}
}
}
}
mod output {
use smithay::utils::Rectangle;
mod output {
use smithay::{output::Output, utils::Rectangle};
use super::*;
@ -439,113 +436,226 @@ mod coverage {
function(_)
return true
end,
tag_names = { "First", "Third", "Schmurd" },
tag_names = { "1", "2", "3" },
},
{
"Pinnacle Window",
loc = { x = 300, y = 0 },
function(op)
return string.match(op.name, "Test") ~= nil
end,
tag_names = { "Test 4", "Test 5" },
},
{
"Output 1",
loc = { "Pinnacle Window", "bottom_align_left" },
}
"Second",
scale = 2.0,
mode = {
pixel_width = 6900,
pixel_height = 420,
refresh_rate_millihz = 69420,
},
},
})
}
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("Output 1", (960, 540).into());
state.new_output("First", (300, 200).into());
state.new_output("Second", (300, 200).into());
state.new_output("Test Third", (300, 200).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = 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 = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let test_third_op = output_for_name(state, "Test Third");
let original_op_geo = state.space.output_geometry(original_op).unwrap();
let output_1_geo = state.space.output_geometry(output_1).unwrap();
let tags_for = |output: &Output| {
output
.with_state(|state| state.tags.iter().map(|t| t.name()).collect::<Vec<_>>())
};
let focused_tags_for = |output: &Output| {
output.with_state(|state| {
state.focused_tags().map(|t| t.name()).collect::<Vec<_>>()
})
};
assert_eq!(tags_for(&original_op), vec!["1", "2", "3"]);
assert_eq!(tags_for(&first_op), vec!["1", "2", "3"]);
assert_eq!(tags_for(&second_op), vec!["1", "2", "3"]);
assert_eq!(
original_op_geo,
Rectangle::from_loc_and_size((300, 0), (1920, 1080))
tags_for(&test_third_op),
vec!["1", "2", "3", "Test 4", "Test 5"]
);
assert_eq!(
output_1_geo,
Rectangle::from_loc_and_size((300, 1080), (960, 540))
);
assert_eq!(focused_tags_for(&original_op), vec!["1"]);
assert_eq!(focused_tags_for(&test_third_op), vec!["1"]);
assert_eq!(
output_1.with_state(|state| state
.tags
.iter()
.map(|tag| tag.name())
.collect::<Vec<_>>()),
vec!["First", "Third", "Schmurd"]
);
assert_eq!(second_op.current_scale().fractional_scale(), 2.0);
state.remove_output(&original_op.clone());
let second_mode = second_op.current_mode().unwrap();
assert_eq!(second_mode.size.w, 6900);
assert_eq!(second_mode.size.h, 420);
assert_eq!(second_mode.refresh, 69420);
});
})
}
#[tokio::main]
#[self::test]
async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> {
test_api(|sender| {
setup_lua! { |Pinnacle|
Pinnacle.output.setup_locs("all", {
{ "Pinnacle Window", loc = { x = 0, y = 0 } },
{ "First", loc = { "Second", "left_align_top" } },
{ "Second", loc = { "First", "right_align_top" } },
})
}
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let output_1 = state
.space
.outputs()
.find(|op| op.name() == "Output 1")
.unwrap();
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let output_1_geo = state.space.output_geometry(output_1).unwrap();
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
assert_eq!(
output_1_geo,
Rectangle::from_loc_and_size((0, 0), (960, 540))
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((1920, 0), (300, 200))
);
state.new_output("Output 2", (300, 500).into());
state.new_output("Second", (500, 500).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let output_1 = state
.space
.outputs()
.find(|op| op.name() == "Output 1")
.unwrap();
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let 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();
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let second_geo = state.space.output_geometry(&second_op).unwrap();
assert_eq!(
output_2_geo,
Rectangle::from_loc_and_size((0, 0), (300, 500))
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
output_1_geo,
Rectangle::from_loc_and_size((300, 0), (960, 540))
first_geo,
Rectangle::from_loc_and_size((1920, 0), (300, 200))
);
assert_eq!(
second_geo,
Rectangle::from_loc_and_size((1920 + 300, 0), (500, 500))
);
});
})
}
#[tokio::main]
#[self::test]
async fn setup_loc_with_relative_locs_with_more_than_one_relative_works() -> anyhow::Result<()>
{
test_api(|sender| {
setup_lua! { |Pinnacle|
Pinnacle.output.setup_locs("all", {
{ "Pinnacle Window", loc = { x = 0, y = 0 } },
{ "First", loc = { "Pinnacle Window", "bottom_align_left" } },
{ "Second", loc = { "First", "bottom_align_left" } },
{
"Third",
loc = {
{ "Second", "bottom_align_left" },
{ "First", "bottom_align_left" },
},
},
})
}
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
state.new_output("Second", (300, 700).into());
state.new_output("Third", (300, 400).into());
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let second_op = output_for_name(state, "Second");
let third_op = output_for_name(state, "Third");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let second_geo = state.space.output_geometry(&second_op).unwrap();
let third_geo = state.space.output_geometry(&third_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((0, 1080), (300, 200))
);
assert_eq!(
second_geo,
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 700))
);
assert_eq!(
third_geo,
Rectangle::from_loc_and_size((0, 1080 + 200 + 700), (300, 400))
);
state.remove_output(&second_op);
});
sleep_secs(1);
with_state(&sender, |state| {
let original_op = output_for_name(state, "Pinnacle Window");
let first_op = output_for_name(state, "First");
let third_op = output_for_name(state, "Third");
let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
let third_geo = state.space.output_geometry(&third_op).unwrap();
assert_eq!(
original_geo,
Rectangle::from_loc_and_size((0, 0), (1920, 1080))
);
assert_eq!(
first_geo,
Rectangle::from_loc_and_size((0, 1080), (300, 200))
);
assert_eq!(
third_geo,
Rectangle::from_loc_and_size((0, 1080 + 200), (300, 400))
);
});
})
}
}

View file

@ -99,7 +99,7 @@ mod output {
#[tokio::main]
#[self::test]
async fn setup_loc_with_cyclic_relative_locs_work() -> anyhow::Result<()> {
async fn setup_loc_with_cyclic_relative_locs_works() -> anyhow::Result<()> {
test_api(|sender| {
setup_rust(|api| {
api.output.setup_locs(