🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
//! `wl_output` — monitory. Model gotowy na wiele monitorów; wykrywanie w [`crate::drm`].
//!
//! Każdy monitor ma własną pozycję w globalnej przestrzeni współrzędnych, dzięki czemu
//! okna i powierzchnie warstwowe można rozkładać i kotwiczyć na konkretnym wyświetlaczu.
use std::sync::{Arc, Mutex};
use libluna_core::Rect;
use wayland_server::protocol::wl_output::{self, WlOutput};
use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
use crate::state::State;
use crate::sync::LockExt;
/// Informacje o monitorze (współdzielone z wątkiem wyjścia, który zna bieżący rozmiar).
#[derive(Debug, Clone)]
pub struct OutputInfo {
pub name: String,
pub width: i32,
pub height: i32,
/// Odświeżanie bieżącego trybu (Hz) — z EDID, nie stałe 60.
pub refresh_hz: i32,
pub scale: i32,
/// Pozycja logiczna (lewy-górny narożnik) w globalnej przestrzeni współrzędnych.
pub x: i32,
pub y: i32,
}
impl Default for OutputInfo {
fn default() -> Self {
Self {
name: "nested-0".to_owned(),
width: 1280,
height: 800,
refresh_hz: 60,
scale: 1,
x: 0,
y: 0,
}
}
}
impl OutputInfo {
/// Prostokąt monitora w globalnej przestrzeni współrzędnych.
pub fn rect(&self) -> Rect {
Rect::from_xywh(
self.x as f64,
self.y as f64,
self.width as f64,
self.height as f64,
)
}
}
/// Współdzielona lista monitorów.
pub type Outputs = Arc<Mutex<Vec<OutputInfo>>>;
/// Układa monitory poziomo, od lewej do prawej (przydatne dla trybu nested i układu lustrzanego).
pub fn layout_horizontal(outs: &mut [OutputInfo]) {
let mut x = 0;
for o in outs.iter_mut() {
o.x = x;
o.y = 0;
x += o.width;
}
}
/// Indeks monitora zawierającego punkt (lub pierwszy, gdy żaden nie pasuje).
pub fn output_at(outs: &[OutputInfo], x: f64, y: f64) -> usize {
outs.iter()
.position(|o| o.rect().contains(libluna_core::Point::new(x, y)))
.unwrap_or(0)
}
/// Wysyła klientowi opis monitora (geometria, tryb, skala, nazwa, gotowe).
pub fn send_info(output: &WlOutput, info: &OutputInfo) {
let _ = output.send_event(wl_output::Event::Geometry {
x: info.x,
y: info.y,
physical_width: info.width * 300 / 1000,
physical_height: info.height * 300 / 1000,
subpixel: wayland_server::WEnum::Value(wl_output::Subpixel::Unknown),
make: "Luna".to_owned(),
model: info.name.clone(),
transform: wayland_server::WEnum::Value(wl_output::Transform::Normal),
});
let _ = output.send_event(wl_output::Event::Mode {
flags: wayland_server::WEnum::Value(wl_output::Mode::Current),
width: info.width,
height: info.height,
refresh: info.refresh_hz.max(1) * 1000,
});
let _ = output.send_event(wl_output::Event::Scale { factor: info.scale });
let _ = output.send_event(wl_output::Event::Name {
name: info.name.clone(),
});
let _ = output.send_event(wl_output::Event::Done);
}
impl GlobalDispatch<WlOutput, usize> for State {
fn bind(
state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: New<WlOutput>,
data: &usize,
data_init: &mut DataInit<'_, Self>,
) {
// Zasób niesie swój indeks monitora (potrzebny m.in. dla `wlr-layer-shell`).
let output = data_init.init(resource, *data);
let info = state
.outputs
.lock_or_recover()
.get(*data)
.cloned()
.unwrap_or_default();
send_info(&output, &info);
// Trzymamy zasób, żeby po faktycznym modesecie DRM wysłać poprawione
// `geometry`/`mode` (wczesny probe i setup mogą chwilowo różnić się trybem).
state.bound_outputs.push((*data, output));
}
}
impl Dispatch<WlOutput, usize> for State {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &WlOutput,
_request: wl_output::Request,
_data: &usize,
_handle: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
}
}
/// Sygnatura listy monitorów — tania do porównania w pętli głównej.
pub fn signature(outs: &[OutputInfo]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for o in outs {
for v in [o.x, o.y, o.width, o.height, o.refresh_hz, o.scale] {
h ^= v as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
for b in o.name.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
}
h
}
/// Wysyła klientom nowe `wl_output` po zmianie modelu (np. faktyczny tryb z DRM).
pub fn refresh_bound(state: &mut State) {
let outs = state.outputs.lock_or_recover().clone();
let sig = signature(&outs);
if sig == state.outputs_sig {
return;
}
state.outputs_sig = sig;
state.bound_outputs.retain(|(_, r)| r.is_alive());
for (idx, res) in &state.bound_outputs {
if let Some(info) = outs.get(*idx) {
send_info(res, info);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn out(name: &str, w: i32, h: i32) -> OutputInfo {
OutputInfo {
name: name.to_owned(),
width: w,
height: h,
refresh_hz: 60,
scale: 1,
x: 0,
y: 0,
}
}
#[test]
fn horizontal_layout_places_monitors_side_by_side() {
let mut outs = vec![out("eDP-1", 1920, 1080), out("HDMI-1", 1280, 720)];
layout_horizontal(&mut outs);
assert_eq!((outs[0].x, outs[0].y), (0, 0));
assert_eq!((outs[1].x, outs[1].y), (1920, 0));
}
#[test]
fn signature_notices_size_and_refresh_changes() {
let a = vec![out("eDP-1", 1920, 1080)];
let mut b = a.clone();
assert_eq!(signature(&a), signature(&b));
b[0].refresh_hz = 75;
assert_ne!(signature(&a), signature(&b));
b[0].refresh_hz = 60;
b[0].width = 1600;
assert_ne!(signature(&a), signature(&b));
}
#[test]
fn output_at_finds_the_monitor_under_the_point() {
let mut outs = vec![out("eDP-1", 1920, 1080), out("HDMI-1", 1280, 720)];
layout_horizontal(&mut outs);
assert_eq!(output_at(&outs, 100.0, 100.0), 0);
assert_eq!(output_at(&outs, 2000.0, 100.0), 1);
// Poza wszystkimi monitorami → pierwszy.
assert_eq!(output_at(&outs, -500.0, -500.0), 0);
}
}