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" }
|
||||
|
||||
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.
|
||||
-- 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)
|
||||
-- Output.connect_for_all(function(op)
|
||||
-- local tags = Tag.add(op, tag_names)
|
||||
-- tags[1]:set_active(true)
|
||||
-- end)
|
||||
|
||||
-- Tag keybinds
|
||||
for _, tag_name in ipairs(tag_names) do
|
||||
|
|
|
@ -166,15 +166,231 @@ function output.connect_for_all(callback)
|
|||
})
|
||||
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>
|
||||
local signal_name_to_SignalName = {
|
||||
connect = "OutputConnect",
|
||||
disconnect = "OutputDisconnect",
|
||||
resize = "OutputResize",
|
||||
move = "OutputMove",
|
||||
}
|
||||
|
||||
---@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 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 move fun(output: OutputHandle, x: integer, y: integer)? An output moved.
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@ local rpc_types = {
|
|||
OutputConnect = {
|
||||
response_type = "OutputConnectResponse",
|
||||
},
|
||||
OutputDisconnect = {
|
||||
response_type = "OutputDisconnectResponse",
|
||||
},
|
||||
OutputResize = {
|
||||
response_type = "OutputResizeResponse",
|
||||
},
|
||||
|
@ -68,6 +71,17 @@ local signals = {
|
|||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
OutputDisconnect = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
sender = nil,
|
||||
---@nodoc
|
||||
---@type (fun(output: OutputHandle))[]
|
||||
callbacks = {},
|
||||
---@nodoc
|
||||
---@type fun(response: table)
|
||||
on_response = nil,
|
||||
},
|
||||
OutputResize = {
|
||||
---@nodoc
|
||||
---@type H2Stream?
|
||||
|
@ -122,6 +136,14 @@ signals.OutputConnect.on_response = function(response)
|
|||
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)
|
||||
---@diagnostic disable-next-line: invisible
|
||||
local handle = require("pinnacle.output").handle.new(response.output_name)
|
||||
|
|
|
@ -16,6 +16,12 @@ message OutputConnectRequest {
|
|||
message OutputConnectResponse {
|
||||
optional string output_name = 1;
|
||||
}
|
||||
message OutputDisconnectRequest {
|
||||
optional StreamControl control = 1;
|
||||
}
|
||||
message OutputDisconnectResponse {
|
||||
optional string output_name = 1;
|
||||
}
|
||||
|
||||
message OutputResizeRequest {
|
||||
optional StreamControl control = 1;
|
||||
|
@ -55,6 +61,7 @@ message WindowPointerLeaveResponse {
|
|||
|
||||
service SignalService {
|
||||
rpc OutputConnect(stream OutputConnectRequest) returns (stream OutputConnectResponse);
|
||||
rpc OutputDisconnect(stream OutputDisconnectRequest) returns (stream OutputDisconnectResponse);
|
||||
rpc OutputResize(stream OutputResizeRequest) returns (stream OutputResizeResponse);
|
||||
rpc OutputMove(stream OutputMoveRequest) returns (stream OutputMoveResponse);
|
||||
rpc WindowPointerEnter(stream WindowPointerEnterRequest) returns (stream WindowPointerEnterResponse);
|
||||
|
|
|
@ -2,6 +2,7 @@ use pinnacle_api::layout::{
|
|||
CornerLayout, CornerLocation, CyclingLayoutManager, DwindleLayout, FairLayout, MasterSide,
|
||||
MasterStackLayout, SpiralLayout,
|
||||
};
|
||||
use pinnacle_api::output::OutputSetup;
|
||||
use pinnacle_api::signal::WindowSignal;
|
||||
use pinnacle_api::util::{Axis, Batch};
|
||||
use pinnacle_api::xkbcommon::xkb::Keysym;
|
||||
|
@ -206,12 +207,7 @@ async fn main() {
|
|||
let tag_names = ["1", "2", "3", "4", "5"];
|
||||
|
||||
// Setup all monitors with tags "1" through "5"
|
||||
output.connect_for_all(move |op| {
|
||||
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);
|
||||
});
|
||||
output.setup([OutputSetup::new_with_matcher(|_| true).with_tags(tag_names)]);
|
||||
|
||||
for tag_name in tag_names {
|
||||
// `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
|
||||
//! connected monitors and set them up.
|
||||
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use futures::FutureExt;
|
||||
use pinnacle_api_defs::pinnacle::output::{
|
||||
self,
|
||||
|
@ -24,7 +26,7 @@ use crate::{
|
|||
signal::{OutputSignal, SignalHandle},
|
||||
tag::TagHandle,
|
||||
util::Batch,
|
||||
SIGNAL, TAG,
|
||||
OUTPUT, SIGNAL, TAG,
|
||||
};
|
||||
|
||||
/// A struct that allows you to get handles to connected outputs and set them up.
|
||||
|
@ -159,10 +161,298 @@ impl Output {
|
|||
|
||||
match signal {
|
||||
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::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.
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
/// 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) output_connect: SignalData<OutputConnect>,
|
||||
pub(crate) output_disconnect: SignalData<OutputDisconnect>,
|
||||
pub(crate) output_resize: SignalData<OutputResize>,
|
||||
pub(crate) output_move: SignalData<OutputMove>,
|
||||
pub(crate) window_pointer_enter: SignalData<WindowPointerEnter>,
|
||||
|
@ -237,6 +256,7 @@ impl SignalState {
|
|||
let client = SignalServiceClient::new(channel);
|
||||
Self {
|
||||
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_move: 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!(
|
||||
OutputConnectRequest,
|
||||
OutputDisconnectRequest,
|
||||
OutputResizeRequest,
|
||||
OutputMoveRequest,
|
||||
WindowPointerEnterRequest,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputMoveRequest,
|
||||
OutputMoveResponse, OutputResizeRequest, OutputResizeResponse, SignalRequest, StreamControl,
|
||||
WindowPointerEnterRequest, WindowPointerEnterResponse, WindowPointerLeaveRequest,
|
||||
WindowPointerLeaveResponse,
|
||||
signal_service_server, OutputConnectRequest, OutputConnectResponse, OutputDisconnectRequest,
|
||||
OutputDisconnectResponse, OutputMoveRequest, OutputMoveResponse, OutputResizeRequest,
|
||||
OutputResizeResponse, SignalRequest, StreamControl, WindowPointerEnterRequest,
|
||||
WindowPointerEnterResponse, WindowPointerLeaveRequest, WindowPointerLeaveResponse,
|
||||
};
|
||||
use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
@ -18,6 +18,7 @@ use super::{run_bidirectional_streaming, ResponseStream, StateFnSender};
|
|||
pub struct SignalState {
|
||||
// Output
|
||||
pub output_connect: SignalData<OutputConnectResponse, VecDeque<OutputConnectResponse>>,
|
||||
pub output_disconnect: SignalData<OutputDisconnectResponse, VecDeque<OutputDisconnectResponse>>,
|
||||
pub output_resize: SignalData<OutputResizeResponse, VecDeque<OutputResizeResponse>>,
|
||||
pub output_move: SignalData<OutputMoveResponse, VecDeque<OutputMoveResponse>>,
|
||||
|
||||
|
@ -31,6 +32,9 @@ pub struct SignalState {
|
|||
impl SignalState {
|
||||
pub fn clear(&mut self) {
|
||||
self.output_connect.disconnect();
|
||||
self.output_disconnect.disconnect();
|
||||
self.output_resize.disconnect();
|
||||
self.output_move.disconnect();
|
||||
self.window_pointer_enter.disconnect();
|
||||
self.window_pointer_leave.disconnect();
|
||||
}
|
||||
|
@ -177,6 +181,7 @@ impl SignalService {
|
|||
#[tonic::async_trait]
|
||||
impl signal_service_server::SignalService for SignalService {
|
||||
type OutputConnectStream = ResponseStream<OutputConnectResponse>;
|
||||
type OutputDisconnectStream = ResponseStream<OutputDisconnectResponse>;
|
||||
type OutputResizeStream = ResponseStream<OutputResizeResponse>;
|
||||
type OutputMoveStream = ResponseStream<OutputMoveResponse>;
|
||||
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(
|
||||
&self,
|
||||
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::ImportMemWl;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Physical, Size};
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -116,3 +120,45 @@ pub fn setup_dummy(
|
|||
|
||||
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 pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse;
|
||||
use pinnacle_api_defs::pinnacle::signal::v0alpha1::{
|
||||
OutputConnectResponse, OutputDisconnectResponse,
|
||||
};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
|
@ -1184,6 +1186,12 @@ impl State {
|
|||
);
|
||||
self.space.unmap_output(&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(),
|
||||
};
|
||||
|
||||
let output = Output::new("Pinnacle window".to_string(), physical_properties);
|
||||
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
|
||||
|
||||
output.change_current_state(
|
||||
Some(mode),
|
||||
|
|
|
@ -7,6 +7,7 @@ use smithay::{
|
|||
output::{Mode, Output, Scale},
|
||||
utils::{Logical, Point, Transform},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
focus::WindowKeyboardFocusStack,
|
||||
|
@ -86,6 +87,7 @@ impl State {
|
|||
) {
|
||||
output.change_current_state(mode, transform, scale, location);
|
||||
if let Some(location) = location {
|
||||
info!(?location);
|
||||
self.space.map_output(output, location);
|
||||
self.signal_state.output_move.signal(|buf| {
|
||||
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)]
|
||||
fn assert(
|
||||
fn with_state(
|
||||
sender: &Sender<Box<dyn FnOnce(&mut State) + Send>>,
|
||||
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(
|
||||
test: impl FnOnce(Sender<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
|
||||
) -> anyhow::Result<()> {
|
||||
|
@ -149,7 +200,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.windows.len(), 1);
|
||||
assert_eq!(state.windows[0].class(), Some("foot".to_string()));
|
||||
});
|
||||
|
@ -166,7 +217,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |_state| {
|
||||
with_state(&sender, |_state| {
|
||||
assert_eq!(
|
||||
std::env::var("PROCESS_SET_ENV"),
|
||||
Ok("env value".to_string())
|
||||
|
@ -234,7 +285,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.config.window_rules.len(), 1);
|
||||
assert_eq!(
|
||||
state.config.window_rules[0],
|
||||
|
@ -271,7 +322,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.config.window_rules.len(), 2);
|
||||
assert_eq!(
|
||||
state.config.window_rules[1],
|
||||
|
@ -312,7 +363,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.windows.len(), 1);
|
||||
});
|
||||
|
||||
|
@ -322,7 +373,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.windows.len(), 0);
|
||||
});
|
||||
})
|
||||
|
@ -341,7 +392,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(
|
||||
state.windows[0].with_state(|st| st
|
||||
.tags
|
||||
|
@ -359,7 +410,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(
|
||||
state.windows[0].with_state(|st| st
|
||||
.tags
|
||||
|
@ -377,7 +428,7 @@ mod coverage {
|
|||
|
||||
sleep_secs(1);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(
|
||||
state.windows[0].with_state(|st| st
|
||||
.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]
|
||||
|
@ -459,7 +634,7 @@ async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
|||
|
||||
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|
|
||||
for i = 1, 20 do
|
||||
|
@ -469,7 +644,7 @@ async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
|||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(
|
||||
state
|
||||
.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);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
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);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.windows.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);
|
||||
|
||||
assert(&sender, |state| {
|
||||
with_state(&sender, |state| {
|
||||
assert_eq!(state.windows.len(), 2);
|
||||
assert_eq!(state.windows[1].with_state(|st| st.tags.len()), 2);
|
||||
assert_eq!(
|
||||
|
|
Loading…
Reference in a new issue