1
0
Fork 0
forked from wry/wry
wry/src/control_center/cc_outputs.rs
2026-03-30 13:58:05 +02:00

1727 lines
57 KiB
Rust

use {
crate::{
backend::{
BackendColorSpace, BackendEotfs, ConnectorId, Mode,
transaction::{
BackendConnectorTransactionError, ConnectorTransaction,
PreparedConnectorTransaction,
},
},
cmm::cmm_luminance::Luminance,
compositor::{MAX_EXTENTS, MAX_SCALE, MIN_SCALE},
control_center::{ControlCenterInner, GridExt, PaneState, grid, grid_label, label, tip},
egui_adapter::{
egui_oklch::Color32Ext,
egui_platform::icons::{ICON_ADD, ICON_REMOVE},
},
ifs::{
head_management::{HeadName, HeadState, ReadOnlyHeadState},
wl_output::BlendSpace,
},
scale::{SCALE_BASE, SCALE_BASEF, Scale},
state::State,
tree::{TearingMode, Transform, VrrMode},
utils::{errorfmt::ErrorFmt, static_text::StaticText},
},
ahash::AHashMap,
egui::{
Align, Button, Checkbox, CollapsingHeader, Color32, ComboBox, DragValue, EventFilter,
FontId, Frame, Grid, Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow,
Stroke, StrokeKind, Style, TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, emath,
pos2, text::LayoutJob, vec2,
},
egui_tiles::{
Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId,
Tiles, Tree, UiResponse,
},
linearize::{Linearize, LinearizeExt},
rand::random,
serde::{Deserialize, Serialize},
std::{
cell::{Cell, Ref},
fmt,
rc::Rc,
},
thiserror::Error,
};
pub struct OutputsPane {
tree: Tree<Pane>,
root_id: TileId,
arrangement_id: Option<TileId>,
inner: OutputsPaneInner,
}
struct OutputsPaneInner {
state: Rc<State>,
in_transaction: Cell<bool>,
heads: AHashMap<HeadName, CompleteHead>,
ui: UiSettings,
settings: Settings,
seed: u64,
}
enum Pane {
Arrangement,
Settings,
}
struct CompleteHead {
id: ConnectorId,
name: HeadName,
pretty_name: Rc<String>,
live_state: ReadOnlyHeadState,
changed_state: Option<HeadState>,
z: u64,
focus: u64,
drag_pos: Option<(f32, f32)>,
}
struct UiSettings {
scale: f32,
origin: Vec2,
origin_drag: Option<Vec2>,
next_z: u64,
focus: u64,
zoom_to_fit: bool,
view: View,
}
struct Settings {
show_guide_lines: bool,
snap_to_neighbor: bool,
show_disconnected: bool,
show_disabled: bool,
show_arrangement: bool,
layout: UiLayout,
}
impl Default for Settings {
fn default() -> Self {
Self {
show_guide_lines: false,
snap_to_neighbor: true,
show_disconnected: false,
show_disabled: true,
show_arrangement: true,
layout: UiLayout::Auto,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Linearize)]
enum UiLayout {
Auto,
Vertical,
Horizontal,
}
#[derive(Copy, Clone)]
pub enum View {
Connectors,
Settings,
}
#[derive(Error, Debug)]
enum HeadTransactionError {
#[error("The connector {} has been removed", .0)]
HeadRemoved(Rc<String>),
#[error("The display connected to connector {} has changed", .0)]
MonitorChanged(Rc<String>),
#[error(transparent)]
Backend(#[from] BackendConnectorTransactionError),
}
macro_rules! effective {
($m:expr, $t:expr) => {
$t.as_ref().unwrap_or($m)
};
}
macro_rules! modify {
($m:expr, $t:expr) => {
$t.get_or_insert_with(|| $m.clone())
};
}
impl ControlCenterInner {
pub fn create_outputs_pane(self: &Rc<Self>) -> OutputsPane {
let seed = random();
let mut tiles = Tiles::default();
let settings_id = tiles.insert_pane(Pane::Settings);
let arrangement_id = tiles.insert_pane(Pane::Arrangement);
let root_id = tiles.insert_container(Linear::new(
LinearDir::Horizontal,
vec![arrangement_id, settings_id],
));
let tree = Tree::new(Id::new(("cc_outputs", seed)), root_id, tiles);
let mut pane = OutputsPane {
root_id,
arrangement_id: Some(arrangement_id),
tree,
inner: OutputsPaneInner {
state: self.state.clone(),
ui: UiSettings {
scale: 0.1,
origin: Default::default(),
origin_drag: None,
next_z: 0,
focus: 0,
zoom_to_fit: true,
view: View::Connectors,
},
settings: Default::default(),
in_transaction: Default::default(),
heads: Default::default(),
seed,
},
};
pane.inner.reset();
pane
}
}
struct B<'a>(&'a mut OutputsPaneInner, &'a mut PaneState);
impl Behavior<Pane> for B<'_> {
fn pane_ui(&mut self, ui: &mut Ui, _tile_id: TileId, pane: &mut Pane) -> UiResponse {
Frame::new().inner_margin(5.0).show(ui, |ui| match pane {
Pane::Arrangement => self.0.show_arrangement(ui),
Pane::Settings => self.0.show_main_area(self.1, ui),
});
UiResponse::None
}
fn tab_title_for_pane(&mut self, _pane: &Pane) -> WidgetText {
"".into()
}
fn gap_width(&self, _style: &Style) -> f32 {
5.0
}
fn simplification_options(&self) -> SimplificationOptions {
SimplificationOptions {
prune_empty_tabs: false,
prune_empty_containers: false,
prune_single_child_tabs: false,
prune_single_child_containers: false,
all_panes_must_have_tabs: false,
join_nested_linear_containers: false,
}
}
fn resize_stroke(&self, style: &Style, resize_state: ResizeState) -> Stroke {
match resize_state {
ResizeState::Idle => style.visuals.widgets.noninteractive.bg_stroke,
ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke,
ResizeState::Dragging => style.visuals.widgets.active.fg_stroke,
}
}
}
impl OutputsPane {
pub fn title(&self, res: &mut String) {
res.push_str("Outputs");
if self.inner.in_transaction.get() {
res.push_str(" (*)");
}
}
pub fn show(&mut self, ps: &mut PaneState, ui: &mut Ui) {
self.inner.add_new_heads();
if let Some(id) = self.arrangement_id {
if !self.inner.settings.show_arrangement {
self.tree.remove_recursively(id);
self.arrangement_id = None;
}
} else {
if self.inner.settings.show_arrangement {
let id = self.tree.tiles.insert_pane(Pane::Arrangement);
self.tree.move_tile_to_container(id, self.root_id, 0, false);
self.arrangement_id = Some(id);
}
}
let show_vertical = match self.inner.settings.layout {
UiLayout::Auto => ui.available_width() < 1024.0,
UiLayout::Vertical => true,
UiLayout::Horizontal => false,
};
if let Some(root) = self.tree.tiles.get_mut(self.root_id)
&& let Tile::Container(root) = root
&& let Container::Linear(root) = root
{
root.dir = match show_vertical {
true => LinearDir::Vertical,
false => LinearDir::Horizontal,
};
}
self.tree.ui(&mut B(&mut self.inner, ps), ui)
}
}
impl OutputsPaneInner {
fn show_main_area(&mut self, ps: &mut PaneState, ui: &mut Ui) {
ui.scope_builder(
UiBuilder::new().id(Id::new(("main_area", self.seed))),
|ui| {
self.show_settings_bar(ps, ui);
ScrollArea::vertical().show(ui, |ui| {
match self.ui.view {
View::Connectors => self.show_connectors(ui),
View::Settings => self.show_settings(ui),
}
ui.allocate_space(ui.available_size());
});
},
);
}
fn show_settings_bar(&mut self, ps: &mut PaneState, ui: &mut Ui) {
ui.horizontal_wrapped(|ui| {
if ui.button("Connectors").clicked() {
self.ui.view = View::Connectors;
ui.request_repaint();
}
if ui.button("Settings").clicked() {
self.ui.view = View::Settings;
ui.request_repaint();
}
if ui
.checkbox(&mut self.ui.zoom_to_fit, "Zoom To Fit")
.changed()
{
ui.request_repaint();
}
{
let mut reset = !self.in_transaction.get();
let reset2 = reset;
let widget = Checkbox::new(&mut reset, "Reset");
if reset2 {
ui.add_enabled(false, widget);
} else {
if widget.ui(ui).changed() {
self.reset();
}
}
}
ui.with_layout(Layout::right_to_left(Align::LEFT), |ui| {
let enabled = self.in_transaction.get();
ui.add_enabled_ui(enabled, |ui| {
if ui.button("Test").clicked() {
if let Err(e) = self.test_transaction() {
ps.errors.push(ErrorFmt(e).to_string());
}
}
});
let enabled = self.in_transaction.get();
ui.add_enabled_ui(enabled, |ui| {
let button = Button::new("Commit").fill(ui.style().visuals.extreme_bg_color);
if ui.add(button).clicked() {
match self.commit_transaction() {
Ok(_) => self.reset(),
Err(e) => {
ps.errors.push(ErrorFmt(e).to_string());
}
}
}
});
ui.add_space(ui.available_width());
});
});
ui.separator();
}
fn show_connectors(&mut self, ui: &mut Ui) {
let mut heads: Vec<_> = self.heads.values_mut().collect();
heads.sort_by(|a, b| {
a.live_state
.borrow()
.name
.cmp(&b.live_state.borrow().name)
.then_with(|| a.name.cmp(&b.name))
});
let mut is_in_transaction = false;
for head in &mut heads {
show_connector(&self.state, &self.settings, head, ui);
if head.changed_state.is_some() {
is_in_transaction = true;
}
}
self.in_transaction.set(is_in_transaction);
}
fn show_settings(&mut self, ui: &mut Ui) {
let mut changed = false;
{
changed |= ui
.checkbox(&mut self.settings.show_guide_lines, "Show guide lines")
.changed();
}
{
ui.horizontal(|ui| {
changed |= ui
.checkbox(&mut self.settings.snap_to_neighbor, "Snap to neighbor")
.changed();
tip(ui, |ui| {
ui.label("Hold Shift to invert this");
});
});
}
{
ui.checkbox(&mut self.settings.show_arrangement, "Show arrangement area");
}
{
let layout_text = |l: UiLayout| match l {
UiLayout::Auto => "Auto",
UiLayout::Vertical => "Vertical",
UiLayout::Horizontal => "Horizontal",
};
changed |= ComboBox::new("layout", "Layout")
.selected_text(layout_text(self.settings.layout))
.show_ui(ui, |ui| {
for l in UiLayout::variants() {
ui.selectable_value(&mut self.settings.layout, l, layout_text(l));
}
})
.response
.changed();
}
{
changed |= ui
.checkbox(
&mut self.settings.show_disconnected,
"Show disconnected heads",
)
.changed();
}
{
changed |= ui
.checkbox(&mut self.settings.show_disabled, "Show disabled heads")
.changed();
}
if changed {
ui.request_repaint();
}
}
fn show_arrangement(&mut self, ui: &mut Ui) {
let clip_rect = ui.available_rect_before_wrap();
let origin = &mut self.ui.origin;
let ox = origin.x.round();
let oy = origin.y.round();
let mut heads = vec![];
let scale = self.ui.scale;
struct PreparedHead<'a> {
name: HeadName,
m: Ref<'a, HeadState>,
changed_state: &'a mut Option<HeadState>,
z: &'a mut u64,
focus: &'a mut u64,
drag_pos: &'a mut Option<(f32, f32)>,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
rect: Rect,
}
for head in self.heads.values_mut() {
let m = head.live_state.borrow();
let e = effective!(&*m, head.changed_state);
if !e.in_compositor_space {
continue;
}
let (x, y) = e.position;
let (w, h) = e.size;
let x1 = (x as f32 * scale).round() - ox + clip_rect.min.x;
let y1 = (y as f32 * scale).round() - oy + clip_rect.min.y;
let x2 = ((x + w) as f32 * scale).round() - ox + clip_rect.min.x;
let y2 = ((y + h) as f32 * scale).round() - oy + clip_rect.min.y;
heads.push(PreparedHead {
name: head.name,
m,
changed_state: &mut head.changed_state,
z: &mut head.z,
focus: &mut head.focus,
drag_pos: &mut head.drag_pos,
x1: x,
y1: y,
x2: x + w,
y2: y + h,
rect: Rect {
min: pos2(x1, y1),
max: pos2(x2, y2),
},
});
}
if self.ui.zoom_to_fit {
let mut x_min = i32::MAX;
let mut x_max = i32::MIN;
let mut y_min = i32::MAX;
let mut y_max = i32::MIN;
for head in &heads {
x_min = x_min.min(head.x1);
x_max = x_max.max(head.x2);
y_min = y_min.min(head.y1);
y_max = y_max.max(head.y2);
}
if x_min > x_max {
x_min = 0;
x_max = 0;
}
if y_min > y_max {
y_min = 0;
y_max = 0;
}
x_min -= 100;
y_min -= 100;
x_max += 100;
y_max += 100;
let dx = x_max - x_min + 1;
let dy = y_max - y_min + 1;
let x_scale = clip_rect.width() / dx as f32;
let y_scale = clip_rect.height() / dy as f32;
let new_scale = x_scale.min(y_scale);
let new_ox = x_min as f32 * new_scale;
let new_oy = y_min as f32 * new_scale;
if new_scale != scale || new_ox != ox || new_oy != oy {
self.ui.scale = new_scale;
origin.x = new_ox;
origin.y = new_oy;
ui.request_repaint();
}
}
heads.sort_by_key(|h| *h.z);
let style = &ui.style().visuals;
let mut bg_color = style.panel_fill.to_oklch();
let mut no_capture_bg_color = bg_color;
let mut disabled_bg_color = bg_color;
if bg_color.l > 0.5 {
bg_color.l -= 0.05;
disabled_bg_color.l -= 0.1;
no_capture_bg_color.l -= 0.075;
} else {
bg_color.l += 0.05;
disabled_bg_color.l += 0.1;
no_capture_bg_color.l += 0.075;
}
let fg_color_base = style.widgets.noninteractive.text_color().to_oklab();
let fg_color_1 = fg_color_base * 1.0 / 3.0 + bg_color.to_oklab() * 2.0 / 3.0;
let fg_color_2 = fg_color_base * 2.0 / 3.0 + bg_color.to_oklab() * 1.0 / 3.0;
let painter = ui.painter_at(clip_rect);
painter.rect(
Rect::EVERYTHING,
0.0,
disabled_bg_color,
Stroke::NONE,
StrokeKind::Inside,
);
{
let x_min = 0.0;
let y_min = 0.0;
let x_max = MAX_EXTENTS as f32;
let y_max = MAX_EXTENTS as f32;
let good = Rect {
min: clip_rect.min + vec2(x_min, y_min) * scale - *origin,
max: clip_rect.min + vec2(x_max, y_max) * scale - *origin,
};
painter.rect(good, 0.0, bg_color, Stroke::NONE, StrokeKind::Inside);
}
painter.hline(
clip_rect.left()..=clip_rect.right(),
clip_rect.min.y - origin.y,
(1.0, fg_color_1),
);
painter.vline(
clip_rect.min.x - origin.x,
clip_rect.top()..=clip_rect.bottom(),
(1.0, fg_color_1),
);
if self.settings.show_guide_lines {
for head in &heads {
let rect = head.rect;
painter.hline(
clip_rect.left()..=clip_rect.right(),
rect.top(),
(1.0, fg_color_2),
);
painter.hline(
clip_rect.left()..=clip_rect.right(),
rect.bottom(),
(1.0, fg_color_2),
);
painter.vline(
rect.left(),
clip_rect.top()..=clip_rect.bottom(),
(1.0, fg_color_2),
);
painter.vline(
rect.right(),
clip_rect.top()..=clip_rect.bottom(),
(1.0, fg_color_2),
);
}
}
for head in &mut heads {
let rect = head.rect;
let mut color = fg_color_2;
if *head.focus == self.ui.focus {
let shape = Shadow {
offset: [0, 0],
blur: (512.0 * scale).sqrt() as u8,
spread: (255.0 * scale).sqrt() as u8,
color: Color32::from_black_alpha(200),
}
.as_shape(rect, 0.0);
painter.add(shape);
color = fg_color_base;
}
painter.hline(rect.left()..=rect.right() + 1.0, rect.top(), (1.0, color));
painter.hline(
rect.left()..=rect.right() + 1.0,
rect.bottom(),
(1.0, color),
);
painter.vline(rect.left(), rect.top()..=rect.bottom() + 1.0, (1.0, color));
painter.vline(rect.right(), rect.top()..=rect.bottom() + 1.0, (1.0, color));
let content_rect = Rect {
min: pos2(rect.min.x + 1.0, rect.min.y + 1.0),
max: pos2(rect.max.x, rect.max.y),
};
let painter = painter.with_clip_rect(content_rect);
painter.rect(
content_rect,
0.0,
no_capture_bg_color,
Stroke::NONE,
StrokeKind::Inside,
);
let galley =
painter.layout_no_wrap(head.m.name.to_string(), FontId::default(), Color32::WHITE);
let rect = Rect::from_min_size(content_rect.min, galley.rect.size() + vec2(2.0, 2.0));
painter.rect(rect, 0.0, Color32::BLUE, Stroke::NONE, StrokeKind::Inside);
painter.galley(rect.min + vec2(1.0, 1.0), galley, Color32::WHITE);
}
ui.allocate_space(ui.available_size());
macro_rules! interacted {
() => {{
self.ui.zoom_to_fit = false;
}};
}
let response = ui.allocate_rect(clip_rect, Sense::all());
if response.has_focus() {
let mut dx = 0;
let mut dy = 0;
ui.input(|i| {
if i.key_pressed(Key::ArrowUp) {
dy -= 1;
}
if i.key_pressed(Key::ArrowDown) {
dy += 1;
}
if i.key_pressed(Key::ArrowLeft) {
dx -= 1;
}
if i.key_pressed(Key::ArrowRight) {
dx += 1;
}
});
if dx != 0 || dy != 0 {
interacted!();
for head in &mut heads {
if *head.focus == self.ui.focus {
let x = (head.x1 + dx).clamp(0, MAX_EXTENTS);
let y = (head.y1 + dy).clamp(0, MAX_EXTENTS);
let pos = (x, y);
if effective!(&*head.m, head.changed_state).position != pos {
modify!(&*head.m, head.changed_state).position = pos;
ui.request_repaint();
}
}
}
}
}
if let Some(pos) = response.hover_pos() {
let scroll = ui.input(|i| i.smooth_scroll_delta);
let mut new = scale;
if scroll.y != 0.0 {
interacted!();
}
if scroll.y < 0.0 {
new /= 1.0 - scroll.y / 1000.0;
} else {
new *= 1.0 + scroll.y / 1000.0;
}
new = new.max(0.01);
if new != scale {
self.ui.scale = new;
ui.request_repaint();
let relative_pos = pos - clip_rect.min;
let real_pos = (relative_pos + *origin) / scale;
*origin = real_pos * new - relative_pos;
}
}
if ui.input(|i| i.pointer.button_pressed(PointerButton::Primary)) {
self.ui.focus += 1;
if let Some(pos) = response.hover_pos() {
interacted!();
response.request_focus();
for head in heads.iter_mut().rev() {
if head.rect.contains(pos) {
*head.z = self.ui.next_z;
self.ui.next_z += 1;
*head.focus = self.ui.focus;
ui.request_repaint();
break;
}
}
}
}
if response.clicked_elsewhere() {
self.ui.focus += 1;
}
if response.drag_started_by(PointerButton::Middle)
|| response.drag_started_by(PointerButton::Secondary)
{
interacted!();
self.ui.origin_drag = Some(self.ui.origin);
} else if response.drag_started_by(PointerButton::Primary)
&& let Some(pos) = response.hover_pos()
{
interacted!();
for head in heads.iter_mut().rev() {
if head.rect.contains(pos) {
*head.drag_pos = Some((head.x1 as f32, head.y1 as f32));
break;
}
}
}
let drag_delta = response.drag_delta();
if drag_delta.x != 0.0 || drag_delta.y != 0.0 {
if let Some(origin_drag) = &mut self.ui.origin_drag {
*origin_drag -= drag_delta;
self.ui.origin = *origin_drag;
ui.request_repaint();
}
let snap = self.settings.snap_to_neighbor ^ ui.input(|i| i.modifiers.shift);
let mut head_positions = vec![];
struct HeadPosition {
name: HeadName,
x1: i32,
y1: i32,
x2: i32,
y2: i32,
}
if snap {
for head in &heads {
let PreparedHead {
name,
x1,
y1,
x2,
y2,
..
} = *head;
head_positions.push(HeadPosition {
name,
x1,
y1,
x2,
y2,
});
}
}
for head in &mut heads {
if let Some((mut x, mut y)) = *head.drag_pos {
x += drag_delta.x / scale;
y += drag_delta.y / scale;
let mut x_int = if x < 0.0 { x.ceil() } else { x.floor() } as i32;
let mut y_int = if y < 0.0 { y.ceil() } else { y.floor() } as i32;
if snap {
for other in &head_positions {
if head.name == other.name {
continue;
}
macro_rules! snap {
($int:ident, $one:ident, $two:ident) => {
if $int.abs() as f32 * scale <= 10.0 {
$int = 0;
} else if ($int - other.$one).abs() as f32 * scale <= 10.0 {
$int = other.$one;
} else if ($int - other.$two).abs() as f32 * scale <= 10.0 {
$int = other.$two;
} else if ($int + head.$two - head.$one - other.$one).abs()
as f32
* scale
<= 10.0
{
$int = other.$one + head.$one - head.$two;
} else if ($int + head.$two - head.$one - other.$two).abs()
as f32
* scale
<= 10.0
{
$int = other.$two + head.$one - head.$two;
}
};
}
snap!(x_int, x1, x2);
snap!(y_int, y1, y2);
}
}
x_int = x_int.clamp(0, MAX_EXTENTS);
y_int = y_int.clamp(0, MAX_EXTENTS);
let pos = (x_int, y_int);
if effective!(&*head.m, head.changed_state).position != pos {
modify!(&*head.m, head.changed_state).position = pos;
ui.request_repaint();
}
*head.drag_pos = Some((x, y));
}
}
}
if response.drag_stopped() {
self.ui.origin_drag = None;
for head in heads.iter_mut().rev() {
*head.drag_pos = None;
}
}
ui.memory_mut(|mem| {
mem.set_focus_lock_filter(
response.id,
EventFilter {
tab: false,
horizontal_arrows: true,
vertical_arrows: true,
escape: false,
},
)
});
}
fn prepare_transaction(&self) -> Result<PreparedConnectorTransaction, HeadTransactionError> {
let mut tran = ConnectorTransaction::new(&self.state);
for head in self.heads.values() {
let Some(desired) = &head.changed_state else {
continue;
};
let Some(connector) = self.state.connectors.get(&head.id) else {
return Err(HeadTransactionError::HeadRemoved(head.pretty_name.clone()));
};
if head.live_state.borrow().monitor_info != desired.monitor_info {
return Err(HeadTransactionError::MonitorChanged(
head.pretty_name.clone(),
));
}
let old = connector.state.borrow().clone();
let mut new = old.clone();
new.enabled = desired.connector_enabled;
new.mode = desired.mode;
new.non_desktop_override = desired.override_non_desktop;
new.format = desired.format;
new.color_space = desired.color_space;
new.eotf = desired.eotf;
if old == new {
continue;
}
tran.add(&connector.connector, new)?;
}
Ok(tran.prepare()?)
}
fn commit_transaction(&self) -> Result<(), HeadTransactionError> {
self.prepare_transaction()?.apply()?.commit();
for head in self.heads.values() {
let Some(desired) = &head.changed_state else {
continue;
};
desired.flush_persistent_state(&self.state);
if let Some(output) = self.state.outputs.get(&head.id)
&& let Some(node) = &output.node
{
node.set_position(desired.position.0, desired.position.1);
node.set_preferred_scale(desired.scale);
node.update_transform(desired.transform);
node.set_vrr_mode(&desired.vrr_mode);
node.set_tearing_mode(&desired.tearing_mode);
node.set_brightness(desired.brightness);
node.set_blend_space(desired.blend_space);
node.set_use_native_gamut(desired.use_native_gamut);
node.schedule
.set_cursor_hz(&self.state, desired.vrr_cursor_hz.unwrap_or(f64::INFINITY));
} else if let Some(mi) = &desired.monitor_info {
let pos = &self.state.persistent_output_states;
let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone();
pos.pos.set(desired.position);
pos.scale.set(desired.scale);
pos.transform.set(desired.transform);
pos.vrr_mode.set(desired.vrr_mode);
pos.tearing_mode.set(desired.tearing_mode);
pos.brightness.set(desired.brightness);
pos.blend_space.set(desired.blend_space);
pos.use_native_gamut.set(desired.use_native_gamut);
pos.vrr_cursor_hz.set(desired.vrr_cursor_hz);
}
}
Ok(())
}
fn test_transaction(&self) -> Result<(), HeadTransactionError> {
self.prepare_transaction()?;
Ok(())
}
fn reset(&mut self) {
self.in_transaction.set(false);
let mut to_remove = vec![];
for head in self.heads.values_mut() {
if self.state.connectors.contains(&head.id) {
head.changed_state = None;
} else {
to_remove.push(head.name);
}
}
for name in to_remove {
self.heads.remove(&name);
}
}
fn add_new_heads(&mut self) {
for connector in self.state.connectors.lock().values() {
let mgrs = &connector.head_managers;
self.heads.entry(mgrs.name).or_insert_with(|| CompleteHead {
id: connector.id,
name: mgrs.name,
pretty_name: connector.name.clone(),
live_state: mgrs.state(),
changed_state: None,
z: 0,
focus: 0,
drag_pos: None,
});
}
}
}
fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, ui: &mut Ui) {
let m = &*head.live_state.borrow();
let t = &mut head.changed_state;
if t.is_none() {
if !m.connector_enabled && !settings.show_disabled {
return;
}
if !m.connected && !settings.show_disconnected {
return;
}
}
let mut layout_job = LayoutJob::default();
layout_job.append(
"Connector",
0.0,
TextFormat {
color: ui.style().visuals.widgets.inactive.text_color(),
..Default::default()
},
);
layout_job.append(
&m.name,
10.0,
TextFormat {
color: ui.style().visuals.widgets.active.text_color(),
..Default::default()
},
);
let mut name = String::new();
if let Some(v) = &m.monitor_info {
name.push_str(&v.output_id.manufacturer);
name.push_str(" - ");
name.push_str(&v.output_id.model);
}
layout_job.append(
&name,
10.0,
TextFormat {
color: ui.style().visuals.widgets.inactive.text_color(),
..Default::default()
},
);
CollapsingHeader::new(layout_job)
.id_salt(("connector", head.name))
.show(ui, |ui| {
grid(ui, ("settings", head.name), |ui| {
let mut diff = false;
show_serial_number(ui, m);
diff |= show_enablement(state, ui, m, t);
diff |= show_position(ui, m, t);
diff |= show_scale(ui, m, t);
diff |= show_mode(ui, m, t);
diff |= show_size(ui, m, t);
diff |= show_transform(ui, m, t);
diff |= show_brightness(ui, m, t);
diff |= show_color_space(ui, m, t);
diff |= show_eotf(ui, m, t);
diff |= show_format(ui, m, t);
diff |= show_tearing(ui, m, t);
diff |= show_vrr(ui, m, t);
diff |= show_non_desktop(state, ui, m, t);
diff |= show_blend_space(ui, m, t);
diff |= show_use_native_gamut(ui, m, t);
show_native_gamut(ui, m);
diff |= show_cursor_hz(ui, m, t);
show_flip_margin(state, ui, m, t, head.id);
if diff {
let ui = &mut *ui.row();
ui.label("");
ui.label("");
ui.label("^ current");
}
});
});
}
fn show_serial_number(ui: &mut Ui, m: &HeadState) {
if let Some(info) = &m.monitor_info {
let ui = &mut *ui.row();
grid_label(ui, "Serial Number");
ui.label(&info.output_id.serial_number);
}
}
fn show_enablement(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
let ui = &mut *ui.row();
grid_label(ui, "Enabled");
let mut v = effective!(m, t).connector_enabled;
let changed = Checkbox::without_text(&mut v).ui(ui).changed();
if changed {
let t = modify!(m, t);
t.connector_enabled = v;
t.update_in_compositor_space(state, m.wl_output);
}
let diff = v != m.connector_enabled;
if diff {
ui.label(match m.connector_enabled {
true => "enabled",
false => "disabled",
});
}
diff
}
fn show_position(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Position");
let (mut x, mut y) = effective!(m, t).position;
ui.horizontal(|ui| {
let value = |ui: &mut Ui, v, min, max| {
let res = DragValue::new(v).range(min..=max).speed(1.0).ui(ui);
res.changed()
};
let mut changed = false;
changed |= value(ui, &mut x, 0, MAX_EXTENTS);
ui.label("x");
changed |= value(ui, &mut y, 0, MAX_EXTENTS);
if changed {
modify!(m, t).position = (x, y);
}
});
let diff = m.position != (x, y);
if diff {
ui.label(format!("{} x {}", m.position.0, m.position.1));
}
diff
}
fn show_scale(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Scale");
let mut v = effective!(m, t).scale;
let old = v;
ui.horizontal(|ui| {
let mut s = v.to_f64();
let res = DragValue::new(&mut s)
.range(MIN_SCALE.to_f64()..=MAX_SCALE.to_f64())
.speed(1.0 / SCALE_BASEF)
.fixed_decimals(5)
.ui(ui);
if res.changed() {
v = Scale::from_f64(s);
}
if ui.button(ICON_REMOVE).clicked() {
v = Scale::from_wl(v.to_wl().saturating_sub(SCALE_BASE)).clamp(MIN_SCALE, MAX_SCALE);
}
if ui.button(ICON_ADD).clicked() {
v = Scale::from_wl(v.to_wl().saturating_add(SCALE_BASE)).clamp(MIN_SCALE, MAX_SCALE);
}
});
if old != v {
let t = modify!(m, t);
t.scale = v;
t.update_size();
}
let diff = m.scale != v;
if diff {
ui.label(format!("{}", m.scale.to_f64()));
}
diff
}
fn show_mode(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
let mut mode = effective!(m, t).mode;
let old = mode;
grid_label(ui, "Mode");
let mode_text = |mode: Mode| {
format!(
"{}x{}@{}",
mode.width,
mode.height,
mode.refresh_rate_millihz as f64 / 1000.0
)
};
if let Some(monitor_info) = &m.monitor_info
&& let Some(modes) = &monitor_info.modes
&& modes.len() > 1
{
ComboBox::from_id_salt("modes")
.selected_text(mode_text(mode))
.show_ui(ui, |ui| {
for v in modes {
ui.selectable_value(&mut mode, *v, mode_text(*v));
}
});
} else if let Some(monitor_info) = &m.monitor_info
&& monitor_info.modes.is_none()
{
ui.horizontal(|ui| {
fn value<T: emath::Numeric>(ui: &mut Ui, v: &mut T, min: T, max: T) -> bool {
let res = DragValue::new(v).range(min..=max).speed(1.0).ui(ui);
res.changed()
}
value(ui, &mut mode.width, 1, u16::MAX as i32);
ui.label("x");
value(ui, &mut mode.height, 1, u16::MAX as i32);
ui.label("@");
let mut hz = mode.refresh_rate_millihz as f64 / 1_000.0;
if value(ui, &mut hz, 0.0, 1_000_000.0) {
mode.refresh_rate_millihz = (hz * 1_000.0).round() as u32;
}
});
} else {
ui.label(mode_text(mode));
}
if old != mode {
let t = modify!(m, t);
t.mode = mode;
t.update_size();
}
let mut diff = false;
if m.mode != mode {
diff = true;
ui.label(mode_text(m.mode));
}
diff
}
fn show_size(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if let Some(info) = &m.monitor_info {
let ui = &mut *ui.row();
grid_label(ui, "Physical Size (mm)");
ui.label(format!("{} x {}", info.width_mm, info.height_mm));
}
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Size");
let (w, h) = effective!(m, t).size;
ui.label(format!("{w} x {h}"));
let diff = m.size != (w, h);
if diff {
ui.label(format!("{} x {}", m.size.0, m.size.1));
}
diff
}
fn show_transform(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Transform");
let mut v = effective!(m, t).transform;
let mut changed = false;
ComboBox::from_id_salt("transform")
.selected_text(v.text())
.show_ui(ui, |ui| {
let transforms = [
Transform::None,
Transform::Rotate90,
Transform::Rotate180,
Transform::Rotate270,
Transform::Flip,
Transform::FlipRotate90,
Transform::FlipRotate180,
Transform::FlipRotate270,
];
for s in transforms {
changed |= ui.selectable_value(&mut v, s, s.text()).changed();
}
});
if changed {
let t = modify!(m, t);
t.transform = v;
t.update_size();
}
let diff = m.transform != v;
if diff {
ui.label(m.transform.text());
}
diff
}
fn show_brightness(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let old_custom_brightness = effective!(m, t).brightness.is_some();
let mut custom_brightness = old_custom_brightness;
let mut changed = false;
grid_label(ui, "Custom Brightness");
Checkbox::without_text(&mut custom_brightness).ui(ui);
changed |= old_custom_brightness != custom_brightness;
let diff1 = m.brightness.is_some() != custom_brightness;
if diff1 {
ui.label(match m.brightness.is_some() {
true => "enabled",
false => "disabled",
});
}
ui.end_row();
if !custom_brightness {
if changed {
modify!(m, t).brightness = None;
}
return diff1;
}
grid_label(ui, "Brightness");
ui.vertical(|ui| {
let effective = effective!(m, t);
let default_brightness = match effective.eotf {
BackendEotfs::Default => effective
.monitor_info
.as_ref()
.and_then(|m| m.luminance.as_ref())
.map(|l| l.max)
.unwrap_or(Luminance::SRGB.white.0),
BackendEotfs::Pq => Luminance::ST2084_PQ.white.0,
};
let mut brightness = effective.brightness.unwrap_or(default_brightness);
changed |= DragValue::new(&mut brightness)
.range(0.0..=1000.0)
.ui(ui)
.changed();
ui.label(format!("reference: {default_brightness})"));
if changed {
modify!(m, t).brightness = Some(brightness);
}
});
let mut diff2 = false;
if let Some(t) = t
&& m.brightness != t.brightness
{
diff2 = true;
ui.label(format!(
"{}",
fmt::from_fn(|f| match m.brightness {
None => f.write_str("disabled"),
Some(v) => write!(f, "{}", v),
})
));
}
ui.end_row();
diff1 || diff2
}
fn show_color_space(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Colorimetry");
let mut v = effective!(m, t).color_space;
ui.horizontal(|ui| {
if let Some(monitor_info) = &effective!(m, t).monitor_info {
if monitor_info.color_spaces.is_empty() {
ui.label(v.name());
} else {
let mut changed = false;
ComboBox::from_id_salt("colorimetry")
.selected_text(v.name())
.show_ui(ui, |ui| {
changed |= ui
.selectable_value(
&mut v,
BackendColorSpace::Default,
BackendColorSpace::Default.name(),
)
.changed();
for &s in &monitor_info.color_spaces {
changed |= ui.selectable_value(&mut v, s, s.name()).changed();
}
});
if changed {
modify!(m, t).color_space = v;
}
}
}
});
let diff = m.color_space != v;
if diff {
ui.label(m.color_space.name());
}
diff
}
fn show_eotf(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "EOTF");
let mut v = effective!(m, t).eotf;
ui.horizontal(|ui| {
if let Some(monitor_info) = &effective!(m, t).monitor_info {
if monitor_info.eotfs.is_empty() {
ui.label(v.name());
} else {
let mut changed = false;
ComboBox::from_id_salt("eotf")
.selected_text(v.name())
.show_ui(ui, |ui| {
changed |= ui
.selectable_value(
&mut v,
BackendEotfs::Default,
BackendEotfs::Default.name(),
)
.changed();
for &s in &monitor_info.eotfs {
changed |= ui.selectable_value(&mut v, s, s.name()).changed();
}
});
if changed {
modify!(m, t).eotf = v;
}
}
}
});
let diff = m.eotf != v;
if diff {
ui.label(m.eotf.name());
}
diff
}
fn show_format(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Format");
let mut v = effective!(m, t).format;
ui.horizontal(|ui| {
if m.supported_formats.len() < 2 {
ui.label(v.name);
} else {
let mut changed = false;
ComboBox::from_id_salt("format")
.selected_text(v.name)
.show_ui(ui, |ui| {
for &s in &*m.supported_formats {
changed |= ui.selectable_value(&mut v, s, s.name).changed();
}
});
if changed {
modify!(m, t).format = v;
}
}
});
let diff = m.format != v;
if diff {
ui.label(m.format.name);
}
diff
}
fn show_tearing(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let render_settings = |ui: &mut Ui, old: TearingMode| {
#[derive(Copy, Clone, PartialEq, Linearize)]
enum Mode {
Never,
Always,
Fullscreen,
}
fn name(mode: Mode) -> &'static str {
match mode {
Mode::Never => "Never",
Mode::Always => "Always",
Mode::Fullscreen => "Fullscreen",
}
}
let mut mode = match old {
TearingMode::Never => Mode::Never,
TearingMode::Always => Mode::Always,
TearingMode::Fullscreen { .. } => Mode::Fullscreen,
};
let old_mode = mode;
let mut surface = None;
ui.vertical(|ui| {
ComboBox::from_id_salt("tearing mode")
.selected_text(name(mode))
.show_ui(ui, |ui| {
for s in Mode::variants() {
ui.selectable_value(&mut mode, s, name(s));
}
});
if mode == Mode::Fullscreen {
if old_mode != mode {
surface = Some(Default::default());
}
if let TearingMode::Fullscreen { surface: s } = old {
surface = s;
}
let mut limit_windows = surface.is_some();
ui.checkbox(&mut limit_windows, "Limit Windows");
if !limit_windows {
surface = None;
} else {
ui.indent("limit windows", |ui| {
let surface = surface.get_or_insert_default();
ui.checkbox(&mut surface.tearing_requested, "Requests Tearing");
});
}
}
});
match mode {
Mode::Never => TearingMode::Never,
Mode::Always => TearingMode::Always,
Mode::Fullscreen => TearingMode::Fullscreen { surface },
}
};
let ui = &mut *ui.row();
grid_label(ui, "Tearing");
let old = effective!(m, t).tearing_mode;
let v = render_settings(ui, old);
if v != old {
modify!(m, t).tearing_mode = v;
}
let diff = v != m.tearing_mode;
if diff {
ui.add_enabled_ui(false, |ui| {
render_settings(ui, m.tearing_mode);
});
}
diff
}
fn show_vrr(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
if let Some(info) = &m.monitor_info
&& !info.vrr_capable
{
return false;
}
{
let ui = &mut *ui.row();
grid_label(ui, "VRR Active");
ui.label(effective!(m, t).vrr.to_string());
}
let render_settings = |ui: &mut Ui, old: VrrMode| {
#[derive(Copy, Clone, PartialEq, Linearize)]
enum Mode {
Never,
Always,
Fullscreen,
}
fn name(mode: Mode) -> &'static str {
match mode {
Mode::Never => "Never",
Mode::Always => "Always",
Mode::Fullscreen => "Fullscreen",
}
}
let mut mode = match old {
VrrMode::Never => Mode::Never,
VrrMode::Always => Mode::Always,
VrrMode::Fullscreen { .. } => Mode::Fullscreen,
};
let mut surface = None;
ui.vertical(|ui| {
ComboBox::from_id_salt("vrr mode")
.selected_text(name(mode))
.show_ui(ui, |ui| {
for s in Mode::variants() {
ui.selectable_value(&mut mode, s, name(s));
}
});
if mode == Mode::Fullscreen {
if let VrrMode::Fullscreen { surface: s } = old {
surface = s;
}
let mut limit_windows = surface.is_some();
ui.checkbox(&mut limit_windows, "Limit Windows");
if !limit_windows {
surface = None;
} else {
ui.indent("limit windows", |ui| {
let surface = surface.get_or_insert_default();
let mut limit_content_type = surface.content_type.is_some();
ui.checkbox(&mut limit_content_type, "Limit Content Types");
if !limit_content_type {
surface.content_type = None;
} else {
ui.indent("limit content type", |ui| {
let limit = surface.content_type.get_or_insert_default();
let fields = [
("Photos", &mut limit.photo),
("Videos", &mut limit.video),
("Games", &mut limit.game),
];
for (name, field) in fields {
ui.checkbox(field, name);
}
});
}
});
}
}
});
match mode {
Mode::Never => VrrMode::Never,
Mode::Always => VrrMode::Always,
Mode::Fullscreen => VrrMode::Fullscreen { surface },
}
};
let ui = &mut *ui.row();
grid_label(ui, "VRR");
let old = effective!(m, t).vrr_mode;
let v = render_settings(ui, old);
if v != old {
modify!(m, t).vrr_mode = v;
}
let diff = v != m.vrr_mode;
if diff {
ui.add_enabled_ui(false, |ui| {
render_settings(ui, m.vrr_mode);
});
}
diff
}
fn show_non_desktop(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
{
let ui = &mut *ui.row();
grid_label(ui, "Non-desktop");
if m.inherent_non_desktop {
ui.label("Yes");
} else {
ui.label("No");
}
}
let ui = &mut *ui.row();
grid_label(ui, "Override");
let mut v = effective!(m, t).override_non_desktop;
let mut changed = false;
let name = |v: Option<bool>| match v {
None => "None",
Some(false) => "Desktop",
Some(true) => "Non-Desktop",
};
ComboBox::from_id_salt("non-desktop-override")
.selected_text(name(v))
.show_ui(ui, |ui| {
for s in [None, Some(false), Some(true)] {
changed |= ui.selectable_value(&mut v, s, name(s)).changed();
}
});
if changed {
let t = modify!(m, t);
t.override_non_desktop = v;
t.update_in_compositor_space(state, m.wl_output);
}
let diff = v != m.override_non_desktop;
if diff {
ui.label(name(m.override_non_desktop));
}
diff
}
fn show_blend_space(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Blend Space");
let mut v = effective!(m, t).blend_space;
ui.horizontal(|ui| {
let mut changed = false;
ComboBox::from_id_salt("blend-space")
.selected_text(v.name())
.show_ui(ui, |ui| {
for s in BlendSpace::variants() {
changed |= ui.selectable_value(&mut v, s, s.name()).changed();
}
});
if changed {
modify!(m, t).blend_space = v;
}
});
let diff = m.blend_space != v;
if diff {
ui.label(m.blend_space.name());
}
diff
}
fn show_use_native_gamut(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let ui = &mut *ui.row();
grid_label(ui, "Use Native Gamut");
let mut use_native_gamut = effective!(m, t).use_native_gamut;
if Checkbox::without_text(&mut use_native_gamut)
.ui(ui)
.changed()
{
modify!(m, t).use_native_gamut = use_native_gamut;
}
let diff = m.use_native_gamut != use_native_gamut;
if diff {
let mut old = m.use_native_gamut;
ui.add_enabled(false, Checkbox::without_text(&mut old));
}
diff
}
fn show_native_gamut(ui: &mut Ui, m: &HeadState) {
let Some(info) = &m.monitor_info else {
return;
};
let p = info.primaries;
let ui = &mut *ui.row();
grid_label(ui, "Native Gamut");
Grid::new("native gamut").show(ui, |ui| {
let fields = [
("red:", p.r),
("green:", p.g),
("blue:", p.b),
("white:", p.wp),
];
for (name, field) in fields {
let ui = &mut *ui.row();
ui.label(name);
ui.label(format!("{:.6}", field.0));
ui.label(format!("{:.6}", field.1));
}
});
}
fn show_cursor_hz(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
if !effective!(m, t).in_compositor_space {
return false;
}
let old_cursor_hz = effective!(m, t).vrr_cursor_hz.is_some();
let mut custom_cursor_hz = old_cursor_hz;
let mut changed = false;
grid_label(ui, "Limit Cursor HZ");
Checkbox::without_text(&mut custom_cursor_hz).ui(ui);
changed |= old_cursor_hz != custom_cursor_hz;
let diff1 = m.vrr_cursor_hz.is_some() != custom_cursor_hz;
if diff1 {
ui.label(match m.vrr_cursor_hz.is_some() {
true => "enabled",
false => "disabled",
});
}
ui.end_row();
if !custom_cursor_hz {
if changed {
modify!(m, t).vrr_cursor_hz = None;
}
return diff1;
}
grid_label(ui, "Cursor HZ");
let mut cursor_hz = effective!(m, t).vrr_cursor_hz.unwrap_or(60.0);
changed |= DragValue::new(&mut cursor_hz)
.range(0.0..=500.0)
.ui(ui)
.changed();
if changed {
modify!(m, t).vrr_cursor_hz = Some(cursor_hz);
}
let mut diff2 = false;
if let Some(t) = t
&& m.vrr_cursor_hz != t.vrr_cursor_hz
{
diff2 = true;
ui.label(format!(
"{}",
fmt::from_fn(|f| match m.vrr_cursor_hz {
None => f.write_str("disabled"),
Some(v) => write!(f, "{}", v),
})
));
}
ui.end_row();
diff1 || diff2
}
fn show_flip_margin(
state: &State,
ui: &mut Ui,
m: &HeadState,
t: &mut Option<HeadState>,
connector_id: ConnectorId,
) {
if !effective!(m, t).in_compositor_space {
return;
}
let Some(node) = state.root.outputs.get(&connector_id) else {
return;
};
let Some(margin) = node.flip_margin_ns.get() else {
return;
};
label(
ui,
"Flip Margin (ms)",
format!("{}", margin as f64 / 1_000_000.0),
);
}