mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
fix(cursor): report X11 and Wayland cursor density
This commit is contained in:
@@ -25,7 +25,7 @@ inline = []
|
||||
use_samplerate = ["samplerate"]
|
||||
use_rubato = ["rubato"]
|
||||
use_dasp = ["dasp"]
|
||||
flutter = ["flutter_rust_bridge", "scrap/cursor"]
|
||||
flutter = ["flutter_rust_bridge", "scrap/cursor", "dep:x11rb"]
|
||||
default = ["use_dasp"]
|
||||
hwcodec = ["scrap/hwcodec"]
|
||||
vram = ["scrap/vram"]
|
||||
|
||||
@@ -35,6 +35,9 @@ use std::{
|
||||
use terminfo::{capability as cap, Database};
|
||||
use wallpaper;
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
mod cursor;
|
||||
|
||||
pub const PA_SAMPLE_RATE: u32 = 48000;
|
||||
static mut UNMODIFIED: bool = true;
|
||||
|
||||
@@ -570,7 +573,12 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
// polled there is a live session, which is the case the latch reads correctly.
|
||||
#[cfg(feature = "drm")]
|
||||
if !is_x11() {
|
||||
if let Some(id) = crate::server::drm_capturer::drm_cursor_id() {
|
||||
#[cfg(feature = "flutter")]
|
||||
let cursor = cursor::drm_snapshot(|c| c.id)?
|
||||
.map(|(id, scale)| cursor::cache_id(id, scale));
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let cursor = crate::server::drm_capturer::drm_cursor_id();
|
||||
if let Some(id) = cursor {
|
||||
// In a mixed DRM + PipeWire session the DRM streams only cover the DRM-backed displays;
|
||||
// when the pointer sits on a PipeWire-served display every DRM stream reports the hidden
|
||||
// sentinel. Returning that sentinel here would hide the cursor globally, including on the
|
||||
@@ -598,6 +606,10 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
}
|
||||
}
|
||||
});
|
||||
#[cfg(feature = "flutter")]
|
||||
let res = res
|
||||
.map(|id| cursor::x11_scale().map(|scale| cursor::cache_id(id, scale)))
|
||||
.transpose()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -610,7 +622,13 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
// agree anyway, since a caller that took the DRM branch there has to take it here.
|
||||
#[cfg(feature = "drm")]
|
||||
if !is_x11() {
|
||||
if let Some(c) = crate::server::drm_capturer::drm_cursor() {
|
||||
#[cfg(feature = "flutter")]
|
||||
let cursor = cursor::drm_snapshot(Clone::clone)?;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let cursor = crate::server::drm_capturer::drm_cursor();
|
||||
if let Some(c) = cursor {
|
||||
#[cfg(feature = "flutter")]
|
||||
let (c, scale) = c;
|
||||
// See get_cursor(): a hidden DRM sentinel is authoritative only in a pure-DRM session. In
|
||||
// a mixed DRM + PipeWire session fall through so the PipeWire display's cursor is served
|
||||
// by the normal path instead of being hidden everywhere.
|
||||
@@ -624,24 +642,39 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
cd.hotx = c.hotx;
|
||||
cd.hoty = c.hoty;
|
||||
cd.colors = c.colors.into();
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
cd.id = cursor::cache_id(cd.id, scale);
|
||||
cd.scale = scale;
|
||||
}
|
||||
return Ok(cd);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
let scale = cursor::x11_scale()?;
|
||||
#[cfg(feature = "flutter")]
|
||||
let matches = |id| cursor::cache_id(id, scale) == hcursor;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
let matches = |id| id == hcursor;
|
||||
let mut res = None;
|
||||
DISPLAY.with(|conn| {
|
||||
if let Ok(ref mut d) = conn.try_borrow_mut() {
|
||||
if !d.is_null() {
|
||||
unsafe {
|
||||
let img = XFixesGetCursorImage(**d);
|
||||
if !img.is_null() && hcursor == (*img).cursor_serial as u64 {
|
||||
if !img.is_null() && matches((*img).cursor_serial as u64) {
|
||||
let mut cd: CursorData = Default::default();
|
||||
cd.hotx = (*img).xhot as _;
|
||||
cd.hoty = (*img).yhot as _;
|
||||
cd.width = (*img).width as _;
|
||||
cd.height = (*img).height as _;
|
||||
// to-do: how about if it is 0
|
||||
cd.id = (*img).cursor_serial as _;
|
||||
cd.id = hcursor;
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
cd.scale = scale;
|
||||
}
|
||||
let pixels =
|
||||
std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _);
|
||||
// cd.colors.resize(pixels.len() * 4, 0);
|
||||
|
||||
124
src/platform/linux/cursor.rs
Normal file
124
src/platform/linux/cursor.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use hbb_common::{anyhow::Context, bail, ResultType};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use x11rb::{protocol::xproto::ConnectionExt, rust_connection::RustConnection, NONE};
|
||||
|
||||
mod xsettings;
|
||||
|
||||
thread_local! {
|
||||
static SETTINGS: RefCell<Option<(RustConnection, usize)>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub(super) fn cache_id(id: u64, scale: f64) -> u64 {
|
||||
if scale == 0.0 {
|
||||
return id;
|
||||
}
|
||||
let mut hash = DefaultHasher::new();
|
||||
(id, scale.to_bits()).hash(&mut hash);
|
||||
hash.finish()
|
||||
}
|
||||
|
||||
pub(super) fn x11_scale() -> ResultType<f64> {
|
||||
if !super::is_x11() {
|
||||
return Ok(0.0);
|
||||
}
|
||||
SETTINGS.with(|settings| {
|
||||
let mut state = settings.try_borrow_mut()?;
|
||||
if state.is_none() {
|
||||
*state = Some(x11rb::connect(None)?);
|
||||
}
|
||||
let (connection, screen) = state.as_ref().context("Missing XSETTINGS connection")?;
|
||||
let result = read_settings(connection, *screen);
|
||||
if result.is_err() {
|
||||
*state = None;
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn read_settings(connection: &RustConnection, screen: usize) -> ResultType<f64> {
|
||||
let selection = connection
|
||||
.intern_atom(true, format!("_XSETTINGS_S{screen}").as_bytes())?
|
||||
.reply()?
|
||||
.atom;
|
||||
if selection == NONE {
|
||||
return Ok(0.0);
|
||||
}
|
||||
let owner = connection.get_selection_owner(selection)?.reply()?.owner;
|
||||
if owner == NONE {
|
||||
return Ok(0.0);
|
||||
}
|
||||
let property = connection
|
||||
.intern_atom(true, b"_XSETTINGS_SETTINGS")?
|
||||
.reply()?
|
||||
.atom;
|
||||
let reply = connection
|
||||
.get_property(false, owner, property, property, 0, u32::MAX)?
|
||||
.reply()?;
|
||||
if reply.format != 8 || reply.bytes_after != 0 {
|
||||
bail!("Incomplete XSETTINGS property");
|
||||
}
|
||||
// Xft/DPI includes text scaling; it is not the cursor's pixel density.
|
||||
// Zero explicitly keeps the existing policy on desktops without a window scale.
|
||||
Ok(xsettings::scale(&reply.value)?.unwrap_or(0.0))
|
||||
}
|
||||
|
||||
#[cfg(feature = "drm")]
|
||||
pub(super) fn drm_snapshot<T>(
|
||||
f: impl Fn(&crate::server::drm_capturer::DrmCursorData) -> T,
|
||||
) -> ResultType<Option<(T, f64)>> {
|
||||
crate::server::drm_capturer::drm_cursor_snapshot(f)
|
||||
.map(|(cursor, display)| {
|
||||
// A hidden cursor or an unavailable display probe has no density metadata.
|
||||
let scale = display
|
||||
.as_ref()
|
||||
.map(wayland_scale)
|
||||
.transpose()?
|
||||
.unwrap_or(0.0);
|
||||
Ok((cursor, scale))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[cfg(feature = "drm")]
|
||||
fn wayland_scale(display: &base::platform::linux::WaylandDisplayInfo) -> ResultType<f64> {
|
||||
// Missing logical geometry means unknown density, as with older senders.
|
||||
let Some((logical_width, logical_height)) = display.logical_size else {
|
||||
return Ok(0.0);
|
||||
};
|
||||
if logical_width <= 0 || logical_height <= 0 || display.width <= 0 || display.height <= 0 {
|
||||
bail!("Invalid Wayland cursor display dimensions");
|
||||
}
|
||||
// Logical geometry is already rotated; the physical mode dimensions are not.
|
||||
let width = if matches!(display.transform, 90 | 270) {
|
||||
display.height
|
||||
} else {
|
||||
display.width
|
||||
};
|
||||
Ok(f64::from(width) / f64::from(logical_width))
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "drm"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cursor_density_tracks_fractional_rotation_and_cache_identity() {
|
||||
let display = base::platform::linux::WaylandDisplayInfo {
|
||||
name: "test".into(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
logical_size: Some((600, 960)),
|
||||
refresh_rate: 60000,
|
||||
transform: 90,
|
||||
};
|
||||
assert_eq!(wayland_scale(&display).unwrap(), 4.0 / 3.0);
|
||||
assert_ne!(cache_id(1, 1.0), cache_id(1, 2.0));
|
||||
assert_eq!(cache_id(1, 0.0), 1);
|
||||
}
|
||||
}
|
||||
135
src/platform/linux/cursor/xsettings.rs
Normal file
135
src/platform/linux/cursor/xsettings.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::{convert::TryInto, io};
|
||||
|
||||
const HEADER_SIZE: usize = 12;
|
||||
const ALIGNMENT: usize = 4;
|
||||
const INTEGER: u8 = 0;
|
||||
const STRING: u8 = 1;
|
||||
const COLOR: u8 = 2;
|
||||
const WINDOW_SCALE: &[u8] = b"Gdk/WindowScalingFactor";
|
||||
|
||||
struct Reader<'a> {
|
||||
bytes: &'a [u8],
|
||||
offset: usize,
|
||||
little_endian: bool,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
fn take(&mut self, length: usize) -> io::Result<&'a [u8]> {
|
||||
let end = self.offset.checked_add(length).ok_or_else(invalid)?;
|
||||
let bytes = self.bytes.get(self.offset..end).ok_or_else(invalid)?;
|
||||
self.offset = end;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn number(&mut self, length: usize) -> io::Result<u32> {
|
||||
let bytes = self.take(length)?;
|
||||
Ok(if self.little_endian {
|
||||
bytes
|
||||
.iter()
|
||||
.rev()
|
||||
.fold(0, |n, byte| (n << 8) | *byte as u32)
|
||||
} else {
|
||||
bytes.iter().fold(0, |n, byte| (n << 8) | *byte as u32)
|
||||
})
|
||||
}
|
||||
|
||||
fn string(&mut self, length: usize) -> io::Result<&'a [u8]> {
|
||||
let bytes = self.take(length)?;
|
||||
self.take((ALIGNMENT - length % ALIGNMENT) % ALIGNMENT)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid() -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid XSETTINGS cursor density",
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn scale(bytes: &[u8]) -> io::Result<Option<f64>> {
|
||||
let little_endian = match bytes.first() {
|
||||
Some(0) => true,
|
||||
Some(1) => false,
|
||||
_ => return Err(invalid()),
|
||||
};
|
||||
let mut reader = Reader {
|
||||
bytes,
|
||||
offset: 0,
|
||||
little_endian,
|
||||
};
|
||||
reader.take(HEADER_SIZE - ALIGNMENT)?;
|
||||
let count = reader.number(ALIGNMENT)?;
|
||||
let mut scale = None;
|
||||
for _ in 0..count {
|
||||
let kind = reader.number(1)? as u8;
|
||||
reader.take(1)?;
|
||||
let length = reader.number(2)? as usize;
|
||||
let name = reader.string(length)?;
|
||||
reader.take(ALIGNMENT)?; // Last-change serial.
|
||||
match kind {
|
||||
INTEGER => {
|
||||
let value = reader.number(ALIGNMENT)? as i32;
|
||||
if name == WINDOW_SCALE {
|
||||
if value <= 0 {
|
||||
return Err(invalid());
|
||||
}
|
||||
scale = Some(f64::from(value));
|
||||
}
|
||||
}
|
||||
STRING => {
|
||||
let length = reader
|
||||
.number(ALIGNMENT)?
|
||||
.try_into()
|
||||
.map_err(|_| invalid())?;
|
||||
reader.string(length)?;
|
||||
}
|
||||
COLOR => {
|
||||
reader.take(ALIGNMENT * 2)?;
|
||||
}
|
||||
_ => return Err(invalid()),
|
||||
}
|
||||
}
|
||||
Ok(scale)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn window_scale_ignores_text_dpi_and_checks_the_entire_property() {
|
||||
for little in [false, true] {
|
||||
let mut bytes = vec![u8::from(!little), 0, 0, 0];
|
||||
let number = |n: u32| {
|
||||
if little {
|
||||
n.to_le_bytes()
|
||||
} else {
|
||||
n.to_be_bytes()
|
||||
}
|
||||
};
|
||||
bytes.extend(number(1));
|
||||
bytes.extend(number(2));
|
||||
for (name, value) in [(b"Xft/DPI".as_slice(), 196608), (WINDOW_SCALE, 2)] {
|
||||
bytes.extend([INTEGER, 0]);
|
||||
let length = name.len() as u16;
|
||||
bytes.extend(if little {
|
||||
length.to_le_bytes()
|
||||
} else {
|
||||
length.to_be_bytes()
|
||||
});
|
||||
bytes.extend(name);
|
||||
bytes.resize(bytes.len().div_ceil(ALIGNMENT) * ALIGNMENT, 0);
|
||||
bytes.extend(number(1));
|
||||
bytes.extend(number(value));
|
||||
}
|
||||
assert_eq!(scale(&bytes).unwrap(), Some(2.0));
|
||||
for length in 0..bytes.len() {
|
||||
assert!(scale(&bytes[..length]).is_err());
|
||||
}
|
||||
let end = bytes.len();
|
||||
bytes[end - ALIGNMENT..].copy_from_slice(&number(0));
|
||||
assert!(scale(&bytes).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,6 +974,38 @@ pub fn drm_cursor() -> Option<DrmCursorData> {
|
||||
with_drm_cursor(|c| c.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn drm_cursor_snapshot<T>(
|
||||
f: impl Fn(&DrmCursorData) -> T,
|
||||
) -> Option<(T, Option<base::platform::linux::WaylandDisplayInfo>)> {
|
||||
// Keep cursor identity and output together, then release the map before DRM_STATE.
|
||||
let (value, display, hidden) = {
|
||||
let map = DRM_CURSOR.lock().unwrap();
|
||||
let (display, (_, cursor)) = map
|
||||
.iter()
|
||||
.find(|(_, (_, cursor))| cursor.id != scrap::drm_reader::HIDDEN_CURSOR_ID)
|
||||
.or_else(|| map.iter().next())?;
|
||||
(
|
||||
f(cursor),
|
||||
*display,
|
||||
cursor.id == scrap::drm_reader::HIDDEN_CURSOR_ID,
|
||||
)
|
||||
};
|
||||
let monitor = if hidden {
|
||||
None
|
||||
} else {
|
||||
display_info_of(display).and_then(|display| {
|
||||
let wayland = scrap::wayland::display::get_displays();
|
||||
let index = identity_matches(&[display], &wayland.displays)
|
||||
.into_iter()
|
||||
.next()
|
||||
.flatten()?;
|
||||
wayland.displays.get(index).cloned()
|
||||
})
|
||||
};
|
||||
Some((value, monitor))
|
||||
}
|
||||
|
||||
enum ProbeState {
|
||||
Unknown,
|
||||
Unavailable(Instant),
|
||||
|
||||
Reference in New Issue
Block a user