Add Output::setup

Still needs polishing and more importantly testing, also the Lua impl isn't finished yet
This commit is contained in:
Ottatop 2024-04-15 15:50:09 -05:00
parent 023ebe8a2d
commit ce8b56eee8
14 changed files with 851 additions and 34 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -63,6 +63,7 @@ pub mod pinnacle {
impl_signal_request!(
OutputConnectRequest,
OutputDisconnectRequest,
OutputResizeRequest,
OutputMoveRequest,
WindowPointerEnterRequest,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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