pinnacle/api/rust/src/output.rs

299 lines
9.1 KiB
Rust
Raw Normal View History

//! Output management.
2023-10-20 01:18:34 +02:00
use crate::{
2023-10-20 03:19:00 +02:00
msg::{Args, CallbackId, Msg, Request, RequestResponse},
2023-10-20 02:26:12 +02:00
request, send_msg,
tag::TagHandle,
2023-10-20 02:26:12 +02:00
CALLBACK_VEC,
2023-10-20 01:18:34 +02:00
};
2023-10-20 03:19:00 +02:00
/// A unique identifier for an output.
///
/// An empty string represents an invalid output.
#[derive(Debug, Hash, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub(crate) struct OutputName(pub String);
2023-10-20 01:18:34 +02:00
/// Get an [`OutputHandle`] by its name.
///
/// `name` is the name of the port the output is plugged in to.
/// This is something like `HDMI-1` or `eDP-0`.
pub fn get_by_name(name: &str) -> Option<OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
output_names
.into_iter()
.find(|s| s == name)
.map(|s| OutputHandle(OutputName(s)))
}
2023-10-20 01:18:34 +02:00
/// Get a handle to all connected outputs.
pub fn get_all() -> impl Iterator<Item = OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
2023-10-20 01:18:34 +02:00
output_names
.into_iter()
.map(|name| OutputHandle(OutputName(name)))
}
2023-10-20 01:18:34 +02:00
/// Get the currently focused output.
///
/// This is currently defined as the one with the cursor on it.
pub fn get_focused() -> Option<OutputHandle> {
let RequestResponse::Outputs { output_names } = request(Request::GetOutputs) else {
unreachable!()
};
output_names
.into_iter()
.map(|s| OutputHandle(OutputName(s)))
.find(|op| op.properties().focused == Some(true))
}
2023-10-20 02:26:12 +02:00
/// Connect a function to be run on all current and future outputs.
///
/// When called, `connect_for_all` will run `func` with all currently connected outputs.
/// If a new output is connected, `func` will also be called with it.
///
/// This will *not* be called if it has already been called for a given connector.
/// This means turning your monitor off and on or unplugging and replugging it *to the same port*
/// won't trigger `func`. Plugging it in to a new port *will* trigger `func`.
/// This is intended to prevent duplicate setup.
///
/// Please note: this function will be run *after* Pinnacle processes your entire config.
/// For example, if you define tags in `func` but toggle them directly after `connect_for_all`,
/// nothing will happen as the tags haven't been added yet.
pub fn connect_for_all<F>(mut func: F)
where
F: FnMut(OutputHandle) + Send + 'static,
{
let args_callback = move |args: Option<Args>| {
if let Some(Args::ConnectForAllOutputs { output_name }) = args {
func(OutputHandle(OutputName(output_name)));
}
};
2023-10-20 02:26:12 +02:00
let mut callback_vec = CALLBACK_VEC.lock().unwrap();
let len = callback_vec.len();
callback_vec.push(Box::new(args_callback));
2023-10-20 02:26:12 +02:00
let msg = Msg::ConnectForAllOutputs {
callback_id: CallbackId(len as u32),
};
2023-10-20 02:26:12 +02:00
send_msg(msg).unwrap();
2023-10-20 01:18:34 +02:00
}
/// An output handle.
///
/// This is a handle to one of your monitors.
/// It serves to make it easier to deal with them, defining methods for getting properties and
/// helpers for things like positioning multiple monitors.
2023-10-20 02:26:12 +02:00
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OutputHandle(pub(crate) OutputName);
2023-10-20 01:18:34 +02:00
/// Properties of an output.
pub struct OutputProperties {
/// The make.
2023-10-20 02:26:12 +02:00
pub make: Option<String>,
2023-10-20 01:18:34 +02:00
/// The model.
///
/// This is something like `27GL850` or whatever gibberish monitor manufacturers name their
/// displays.
2023-10-20 02:26:12 +02:00
pub model: Option<String>,
2023-10-20 01:18:34 +02:00
/// The location of the output in the global space.
2023-10-20 02:26:12 +02:00
pub loc: Option<(i32, i32)>,
2023-10-20 01:18:34 +02:00
/// The resolution of the output in pixels, where `res.0` is the width and `res.1` is the
/// height.
2023-10-20 02:26:12 +02:00
pub res: Option<(i32, i32)>,
2023-10-20 01:18:34 +02:00
/// The refresh rate of the output in millihertz.
///
/// For example, 60Hz is returned as 60000.
2023-10-20 02:26:12 +02:00
pub refresh_rate: Option<i32>,
2023-10-20 01:18:34 +02:00
/// The physical size of the output in millimeters.
2023-10-20 02:26:12 +02:00
pub physical_size: Option<(i32, i32)>,
2023-10-20 01:18:34 +02:00
/// Whether or not the output is focused.
2023-10-20 02:26:12 +02:00
pub focused: Option<bool>,
/// The tags on this output.
pub tags: Vec<TagHandle>,
2023-10-20 01:18:34 +02:00
}
impl OutputHandle {
/// Get this output's name.
pub fn name(&self) -> String {
self.0 .0.clone()
}
2023-10-20 01:18:34 +02:00
// TODO: Make OutputProperties an option, make non null fields not options
/// Get all properties of this output.
pub fn properties(&self) -> OutputProperties {
let RequestResponse::OutputProps {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
tag_ids,
} = request(Request::GetOutputProps {
output_name: self.0 .0.clone(),
})
else {
unreachable!()
};
OutputProperties {
make,
model,
loc,
res,
refresh_rate,
physical_size,
focused,
2023-10-20 02:26:12 +02:00
tags: tag_ids
.unwrap_or(vec![])
.into_iter()
.map(TagHandle)
.collect(),
2023-10-20 01:18:34 +02:00
}
}
2023-10-20 02:49:36 +02:00
/// Add tags with the given `names` to this output.
2023-10-20 02:49:36 +02:00
pub fn add_tags(&self, names: &[&str]) {
crate::tag::add(self, names);
2023-10-20 02:49:36 +02:00
}
/// Set this output's location in the global space.
2023-10-20 02:49:36 +02:00
pub fn set_loc(&self, x: Option<i32>, y: Option<i32>) {
let msg = Msg::SetOutputLocation {
output_name: self.0.clone(),
x,
y,
};
send_msg(msg).unwrap();
}
/// Set this output's location to the right of `other`.
///
/// It will be aligned vertically based on the given `alignment`.
2023-10-20 02:49:36 +02:00
pub fn set_loc_right_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
self.set_loc_horizontal(other, LeftOrRight::Right, alignment);
}
/// Set this output's location to the left of `other`.
///
/// It will be aligned vertically based on the given `alignment`.
2023-10-20 02:49:36 +02:00
pub fn set_loc_left_of(&self, other: &OutputHandle, alignment: AlignmentVertical) {
self.set_loc_horizontal(other, LeftOrRight::Left, alignment);
}
/// Set this output's location to the top of `other`.
///
/// It will be aligned horizontally based on the given `alignment`.
2023-10-20 02:49:36 +02:00
pub fn set_loc_top_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
self.set_loc_vertical(other, TopOrBottom::Top, alignment);
}
/// Set this output's location to the bottom of `other`.
///
/// It will be aligned horizontally based on the given `alignment`.
2023-10-20 02:49:36 +02:00
pub fn set_loc_bottom_of(&self, other: &OutputHandle, alignment: AlignmentHorizontal) {
self.set_loc_vertical(other, TopOrBottom::Bottom, alignment);
}
fn set_loc_horizontal(
&self,
other: &OutputHandle,
left_or_right: LeftOrRight,
alignment: AlignmentVertical,
) {
let op1_props = self.properties();
let op2_props = other.properties();
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
else {
return;
};
let x = match left_or_right {
LeftOrRight::Left => other_loc.0 - self_res.0,
LeftOrRight::Right => other_loc.0 + self_res.0,
};
let y = match alignment {
AlignmentVertical::Top => other_loc.1,
AlignmentVertical::Center => other_loc.1 + (other_res.1 - self_res.1) / 2,
AlignmentVertical::Bottom => other_loc.1 + (other_res.1 - self_res.1),
};
self.set_loc(Some(x), Some(y));
}
fn set_loc_vertical(
&self,
other: &OutputHandle,
top_or_bottom: TopOrBottom,
alignment: AlignmentHorizontal,
) {
let op1_props = self.properties();
let op2_props = other.properties();
let (Some(_self_loc), Some(self_res), Some(other_loc), Some(other_res)) =
(op1_props.loc, op1_props.res, op2_props.loc, op2_props.res)
else {
return;
};
let y = match top_or_bottom {
TopOrBottom::Top => other_loc.1 - self_res.1,
TopOrBottom::Bottom => other_loc.1 + other_res.1,
};
let x = match alignment {
AlignmentHorizontal::Left => other_loc.0,
AlignmentHorizontal::Center => other_loc.0 + (other_res.0 - self_res.0) / 2,
AlignmentHorizontal::Right => other_loc.0 + (other_res.0 - self_res.0),
};
self.set_loc(Some(x), Some(y));
}
}
enum TopOrBottom {
Top,
Bottom,
}
enum LeftOrRight {
Left,
Right,
}
2023-10-20 05:35:12 +02:00
/// Horizontal alignment.
2023-10-20 04:44:33 +02:00
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2023-10-20 02:49:36 +02:00
pub enum AlignmentHorizontal {
2023-10-20 05:35:12 +02:00
/// Align the outputs such that the left edges are in line.
2023-10-20 02:49:36 +02:00
Left,
2023-10-20 05:35:12 +02:00
/// Center the outputs horizontally.
2023-10-20 02:49:36 +02:00
Center,
2023-10-20 05:35:12 +02:00
/// Align the outputs such that the right edges are in line.
2023-10-20 02:49:36 +02:00
Right,
}
2023-10-20 05:35:12 +02:00
/// Vertical alignment.
2023-10-20 04:44:33 +02:00
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2023-10-20 02:49:36 +02:00
pub enum AlignmentVertical {
2023-10-20 05:35:12 +02:00
/// Align the outputs such that the top edges are in line.
2023-10-20 02:49:36 +02:00
Top,
2023-10-20 05:35:12 +02:00
/// Center the outputs vertically.
2023-10-20 02:49:36 +02:00
Center,
2023-10-20 05:35:12 +02:00
/// Align the outputs such that the bottom edges are in line.
2023-10-20 02:49:36 +02:00
Bottom,
2023-10-20 01:18:34 +02:00
}