Split off location setting from setup

This commit is contained in:
Ottatop 2024-04-16 14:29:53 -05:00
parent 43809d4210
commit ce4352a9fb
3 changed files with 282 additions and 173 deletions

View file

@ -199,6 +199,9 @@ impl Output {
setup.apply(output, &tag_mod); setup.apply(output, &tag_mod);
} }
} }
if let Some(tag) = output.tags().first() {
tag.set_active(true);
}
}; };
let outputs = self.get_all(); let outputs = self.get_all();
@ -211,10 +214,18 @@ impl Output {
}))); })));
} }
pub fn setup_locs(&self, setup: OutputLocSetup) { pub fn setup_locs(
&self,
update_locs_on: UpdateLocsOn,
setup: impl IntoIterator<Item = (impl ToString, OutputLoc)>,
) {
let setup: HashMap<_, _> = setup
.into_iter()
.map(|(name, align)| (name.to_string(), align))
.collect();
let api = self.api.get().unwrap().clone(); let api = self.api.get().unwrap().clone();
let layout_outputs = move || { let layout_outputs = move || {
let setups = setups_clone.clone().into_iter().collect::<Vec<_>>();
let outputs = api.output.get_all(); let outputs = api.output.get_all();
let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None; let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None;
@ -225,23 +236,19 @@ impl Output {
// Place outputs with OutputSetupLoc::Point // Place outputs with OutputSetupLoc::Point
for output in outputs.iter() { for output in outputs.iter() {
for setup in setups.iter() { if let Some(&OutputLoc::Point(x, y)) = setup.get(output.name()) {
if setup.output.matches(output) { output.set_location(x, y);
if let Some(OutputLoc::Point(x, y)) = setup.loc {
output.set_location(x, y);
placed_outputs.insert(output.clone()); placed_outputs.insert(output.clone());
let props = output.props(); let props = output.props();
let x = props.x.unwrap(); let x = props.x.unwrap();
let width = props.logical_width.unwrap() as i32; let width = props.logical_width.unwrap() as i32;
if rightmost_output_and_x.is_none() if rightmost_output_and_x.is_none()
|| rightmost_output_and_x || rightmost_output_and_x
.as_ref() .as_ref()
.is_some_and(|(_, rm_x)| x + width > *rm_x) .is_some_and(|(_, rm_x)| x + width > *rm_x)
{ {
rightmost_output_and_x = Some((output.clone(), x + width)); rightmost_output_and_x = Some((output.clone(), x + width));
}
}
} }
} }
} }
@ -252,48 +259,48 @@ impl Output {
.filter(|op| !placed_outputs.contains(op)) .filter(|op| !placed_outputs.contains(op))
.collect::<Vec<_>>() .collect::<Vec<_>>()
{ {
for setup in setups.iter() { if setup.get(output.name()).is_none() {
if setup.output.matches(output) && setup.loc.is_none() { if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() {
if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() { output.set_loc_adj_to(rm_op, Alignment::RightAlignTop);
output.set_loc_adj_to(rm_op, Alignment::RightAlignTop); } else {
} else { output.set_location(0, 0);
output.set_location(0, 0); }
println!("SET LOC FOR {} TO (0, 0)", output.name());
}
placed_outputs.insert(output.clone()); placed_outputs.insert(output.clone());
let props = output.props(); let props = output.props();
let x = props.x.unwrap(); let x = props.x.unwrap();
let width = props.logical_width.unwrap() as i32; let width = props.logical_width.unwrap() as i32;
if rightmost_output_and_x.is_none() if rightmost_output_and_x.is_none()
|| rightmost_output_and_x || rightmost_output_and_x
.as_ref() .as_ref()
.is_some_and(|(_, rm_x)| x + width > *rm_x) .is_some_and(|(_, rm_x)| x + width > *rm_x)
{ {
rightmost_output_and_x = Some((output.clone(), x + width)); rightmost_output_and_x = Some((output.clone(), x + width));
}
} }
} }
} }
// Attempt to place relative outputs // Attempt to place relative outputs
while let Some((output, relative_to, alignment)) = setups.iter().find_map(|setup| { while let Some((output, relative_to, alignment)) =
outputs.iter().find_map(|op| { setup.iter().find_map(|(setup_op_name, loc)| {
if !placed_outputs.contains(op) && setup.output.matches(op) { outputs
match &setup.loc { .iter()
Some(OutputLoc::RelativeTo(matcher, alignment)) => { .find(|setup_op| {
let first_matched_op = outputs !placed_outputs.contains(setup_op) && setup_op.name() == setup_op_name
.iter() })
.find(|o| matcher.matches(o) && placed_outputs.contains(o))?; .and_then(|setup_op| match loc {
Some((op, first_matched_op, alignment)) OutputLoc::RelativeTo(relative_tos) => {
relative_tos.iter().find_map(|(rel_name, align)| {
placed_outputs.iter().find_map(|pl_op| {
(pl_op.name() == rel_name)
.then_some((setup_op, pl_op, align))
})
})
} }
_ => None, _ => None,
} })
} else {
None
}
}) })
}) { {
output.set_loc_adj_to(relative_to, *alignment); output.set_loc_adj_to(relative_to, *alignment);
placed_outputs.insert(output.clone()); placed_outputs.insert(output.clone());
@ -349,17 +356,23 @@ impl Output {
let layout_outputs_clone1 = layout_outputs.clone(); let layout_outputs_clone1 = layout_outputs.clone();
let layout_outputs_clone2 = layout_outputs.clone(); let layout_outputs_clone2 = layout_outputs.clone();
self.connect_signal(OutputSignal::Connect(Box::new(move |output| { if update_locs_on.contains(UpdateLocsOn::CONNECT) {
layout_outputs_clone2(); self.connect_signal(OutputSignal::Connect(Box::new(move |_| {
}))); layout_outputs_clone2();
})));
}
self.connect_signal(OutputSignal::Disconnect(Box::new(move |_| { if update_locs_on.contains(UpdateLocsOn::DISCONNECT) {
layout_outputs_clone1(); self.connect_signal(OutputSignal::Disconnect(Box::new(move |_| {
}))); layout_outputs_clone1();
})));
}
self.connect_signal(OutputSignal::Resize(Box::new(move |_, _, _| { if update_locs_on.contains(UpdateLocsOn::RESIZE) {
layout_outputs(); self.connect_signal(OutputSignal::Resize(Box::new(move |_, _, _| {
}))); layout_outputs();
})));
}
} }
} }
@ -481,45 +494,36 @@ impl OutputSetup {
output.set_scale(scale); output.set_scale(scale);
} }
if let Some(tag_names) = &self.tag_names { if let Some(tag_names) = &self.tag_names {
let tags = tag.add(output, tag_names); tag.add(output, tag_names);
if let Some(tag) = tags.first() {
tag.set_active(true);
}
} }
} }
} }
/// A location for an output.
#[derive(Clone, Debug)]
pub enum OutputLoc { pub enum OutputLoc {
/// A specific point in the global space.
Point(i32, i32), Point(i32, i32),
/// A location relative to another output.
///
/// This holds a `Vec` of output names to alignments.
/// The output that is relative to will be chosen from the first
/// connected and placed output in this `Vec`.
RelativeTo(Vec<(String, Alignment)>), RelativeTo(Vec<(String, Alignment)>),
} }
pub struct OutputLocSetup {
update_locs_on: UpdateLocsOn,
setup: HashMap<String, OutputLoc>,
}
impl OutputLocSetup {
pub fn new(
update_locs_on: UpdateLocsOn,
setup: impl IntoIterator<Item = (impl ToString, OutputLoc)>,
) -> Self {
Self {
update_locs_on,
setup: setup
.into_iter()
.map(|(s, loc)| (s.to_string(), loc))
.collect(),
}
}
}
impl OutputLoc { impl OutputLoc {
pub fn from_point(x: i32, y: i32) -> Self { /// Creates an `OutputLoc` that will place outputs relative to
Self::Point(x, y) /// the output with the given name using the given alignment.
pub fn relative_to(name: impl ToString, alignment: Alignment) -> Self {
Self::RelativeTo(vec![(name.to_string(), alignment)])
} }
pub fn from_relatives(relatives: impl IntoIterator<Item = (impl ToString, Alignment)>) -> Self { /// Creates an `OutputLoc` from multiple (output_name, alignment) pairs
/// that serve as fallbacks.
pub fn relative_to_with_fallbacks(
relatives: impl IntoIterator<Item = (impl ToString, Alignment)>,
) -> Self {
Self::RelativeTo( Self::RelativeTo(
relatives relatives
.into_iter() .into_iter()

View file

@ -1,9 +1,12 @@
use std::{panic::UnwindSafe, time::Duration}; use std::{panic::UnwindSafe, time::Duration};
use pinnacle::{backend::dummy::setup_dummy, state::State}; use pinnacle::{backend::dummy::setup_dummy, state::State};
use smithay::reexports::calloop::{ use smithay::{
self, output::Output,
channel::{Event, Sender}, reexports::calloop::{
self,
channel::{Event, Sender},
},
}; };
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@ -74,3 +77,12 @@ pub fn test_api(
Ok(()) Ok(())
} }
pub fn output_for_name(state: &State, name: &str) -> Output {
state
.space
.outputs()
.find(|op| op.name() == name)
.unwrap()
.clone()
}

View file

@ -5,6 +5,7 @@ use std::thread::JoinHandle;
use pinnacle_api::ApiModules; use pinnacle_api::ApiModules;
use test_log::test; use test_log::test;
use crate::common::output_for_name;
use crate::common::{sleep_secs, test_api, with_state}; use crate::common::{sleep_secs, test_api, with_state};
#[tokio::main] #[tokio::main]
@ -23,42 +24,27 @@ fn setup_rust(run: impl FnOnce(ApiModules) + Send + 'static) -> JoinHandle<()> {
} }
mod output { mod output {
use pinnacle_api::output::{Alignment, OutputMatcher, OutputSetup}; use pinnacle::state::WithState;
use smithay::utils::Rectangle; use pinnacle_api::output::{Alignment, OutputLoc, OutputSetup, UpdateLocsOn};
use smithay::{output::Output, utils::Rectangle};
use super::*; use super::*;
#[tokio::main] #[tokio::main]
#[self::test] #[self::test]
async fn setup() -> anyhow::Result<()> { async fn setup() -> anyhow::Result<()> {
test_api(|sender| {
setup_rust(|api| {
api.output
.setup([OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"])]);
});
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).into());
});
})
}
#[tokio::main]
#[self::test]
async fn setup_with_cyclic_relative_locs_work() -> anyhow::Result<()> {
test_api(|sender| { test_api(|sender| {
setup_rust(|api| { setup_rust(|api| {
api.output.setup([ api.output.setup([
OutputSetup::new("Pinnacle Window"), OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"]),
OutputSetup::new("First").with_relative_loc( OutputSetup::new_with_matcher(|op| op.name().contains("Test"))
OutputMatcher::Name("Second".into()), .with_tags(["Test 4", "Test 5"]),
Alignment::RightAlignTop, OutputSetup::new("Second").with_scale(2.0).with_mode(
), pinnacle_api::output::Mode {
OutputSetup::new("Second").with_relative_loc( pixel_width: 6900,
OutputMatcher::Name("First".into()), pixel_height: 420,
Alignment::LeftAlignTop, refresh_rate_millihertz: 69420,
},
), ),
]); ]);
}); });
@ -67,24 +53,91 @@ mod output {
with_state(&sender, |state| { with_state(&sender, |state| {
state.new_output("First", (300, 200).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); sleep_secs(1);
with_state(&sender, |state| { with_state(&sender, |state| {
let original_op = state let original_op = output_for_name(state, "Pinnacle Window");
.space let first_op = output_for_name(state, "First");
.outputs() let second_op = output_for_name(state, "Second");
.find(|op| op.name() == "Pinnacle Window") let test_third_op = output_for_name(state, "Test Third");
.unwrap();
let first_op = state
.space
.outputs()
.find(|op| op.name() == "First")
.unwrap();
let original_geo = state.space.output_geometry(original_op).unwrap(); let tags_for = |output: &Output| {
let first_geo = state.space.output_geometry(first_op).unwrap(); 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!(
tags_for(&test_third_op),
vec!["1", "2", "3", "Test 4", "Test 5"]
);
assert_eq!(focused_tags_for(&original_op), vec!["1"]);
assert_eq!(focused_tags_for(&test_third_op), vec!["1"]);
assert_eq!(second_op.current_scale().fractional_scale(), 2.0);
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_work() -> anyhow::Result<()> {
test_api(|sender| {
setup_rust(|api| {
api.output.setup_locs(
UpdateLocsOn::all(),
[
("Pinnacle Window", OutputLoc::Point(0, 0)),
(
"First",
OutputLoc::relative_to_with_fallbacks([(
"Second",
Alignment::LeftAlignTop,
)]),
),
(
"Second",
OutputLoc::relative_to_with_fallbacks([(
"First",
Alignment::RightAlignTop,
)]),
),
],
);
});
sleep_secs(1);
with_state(&sender, |state| {
state.new_output("First", (300, 200).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 original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(&first_op).unwrap();
assert_eq!( assert_eq!(
original_geo, original_geo,
@ -101,25 +154,13 @@ mod output {
sleep_secs(1); sleep_secs(1);
with_state(&sender, |state| { with_state(&sender, |state| {
let original_op = state let original_op = output_for_name(state, "Pinnacle Window");
.space let first_op = output_for_name(state, "First");
.outputs() let second_op = output_for_name(state, "Second");
.find(|op| op.name() == "Pinnacle Window")
.unwrap();
let first_op = state
.space
.outputs()
.find(|op| op.name() == "First")
.unwrap();
let second_op = state
.space
.outputs()
.find(|op| op.name() == "Second")
.unwrap();
let original_geo = state.space.output_geometry(original_op).unwrap(); let original_geo = state.space.output_geometry(&original_op).unwrap();
let first_geo = state.space.output_geometry(first_op).unwrap(); let first_geo = state.space.output_geometry(&first_op).unwrap();
let second_geo = state.space.output_geometry(second_op).unwrap(); let second_geo = state.space.output_geometry(&second_op).unwrap();
assert_eq!( assert_eq!(
original_geo, original_geo,
@ -139,45 +180,97 @@ mod output {
#[tokio::main] #[tokio::main]
#[self::test] #[self::test]
async fn setup_with_relative_output_matcher_works() -> anyhow::Result<()> { async fn setup_loc_with_relative_locs_with_more_than_one_relative_works() -> anyhow::Result<()>
{
test_api(|sender| { test_api(|sender| {
setup_rust(|api| { setup_rust(|api| {
api.output.setup([ api.output.setup_locs(
OutputSetup::new("Pinnacle Window"), UpdateLocsOn::all(),
OutputSetup::new_with_matcher(|_| true) [
.with_relative_loc(|_: &_| true, Alignment::BottomAlignLeft), ("Pinnacle Window", OutputLoc::Point(0, 0)),
]); (
"First",
OutputLoc::relative_to("Pinnacle Window", Alignment::BottomAlignLeft),
),
(
"Second",
OutputLoc::relative_to("First", Alignment::BottomAlignLeft),
),
(
"Third",
OutputLoc::relative_to_with_fallbacks([
("Second", Alignment::BottomAlignLeft),
("First", Alignment::BottomAlignLeft),
]),
),
],
);
}); });
sleep_secs(1); sleep_secs(1);
with_state(&sender, |state| { with_state(&sender, |state| {
state.new_output("First", (300, 200).into()); state.new_output("First", (300, 200).into());
state.new_output("Second", (400, 600).into()); state.new_output("Second", (300, 700).into());
state.new_output("Third", (400, 300).into()); state.new_output("Third", (300, 400).into());
}); });
sleep_secs(1); sleep_secs(1);
with_state(&sender, |state| { with_state(&sender, |state| {
for output in state.space.outputs() { let original_op = output_for_name(state, "Pinnacle Window");
let geo = state.space.output_geometry(output).unwrap(); let first_op = output_for_name(state, "First");
match output.name().as_str() { let second_op = output_for_name(state, "Second");
"Pinnacle Window" => { let third_op = output_for_name(state, "Third");
assert_eq!(geo, Rectangle::from_loc_and_size((0, 0), (1920, 1080)));
} let original_geo = state.space.output_geometry(&original_op).unwrap();
"First" => { let first_geo = state.space.output_geometry(&first_op).unwrap();
assert_eq!(geo, Rectangle::from_loc_and_size((0, 1080), (300, 200))); let second_geo = state.space.output_geometry(&second_op).unwrap();
} let third_geo = state.space.output_geometry(&third_op).unwrap();
"Second" => {
assert_eq!(geo, Rectangle::from_loc_and_size((0, 1080), (400, 600))); assert_eq!(
} original_geo,
"Third" => { Rectangle::from_loc_and_size((0, 0), (1920, 1080))
assert_eq!(geo, Rectangle::from_loc_and_size((0, 1080), (400, 300))); );
} assert_eq!(
_ => unreachable!(), 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))
);
}); });
}) })
} }