mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
Preserve legacy macOS cursor dimensions with optional Retina artwork
This commit is contained in:
@@ -1118,7 +1118,10 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
? cursor.cache ?? preDefaultCursor.cache
|
||||
: preForbiddenCursor.cache;
|
||||
final peerDpr = cache?.pixelRatio ?? 0;
|
||||
if (!isWeb && isViewScaled() && zoomCursor.isFalse && peerDpr > 0) {
|
||||
if (isViewScaled() && zoomCursor.isFalse && peerDpr > 0 &&
|
||||
(!isWeb || widget.ffi.ffiModel.pi.platform == kPeerPlatformMacOS)) {
|
||||
// Retina export is physical-sized; Web must undo that change too.
|
||||
// Other Web host bitmaps retain their existing sizing policy.
|
||||
// Adaptive/Custom scales the video, but Zoom cursor is off: preserve the
|
||||
// cursor's logical size instead of multiplying it by the canvas scale.
|
||||
// Divide by the source bitmap density to obtain logical cursor pixels.
|
||||
|
||||
@@ -25,8 +25,15 @@ void main() {
|
||||
_thinCursorTests();
|
||||
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||
for (final dpr in [1.0, 2.0]) {
|
||||
testWidgets('ImagePaint Web $style zoom off DPR $dpr keeps source size',
|
||||
(tester) => tester.runAsync(() => _checkPolicy(tester, style, dpr)));
|
||||
for (final (platform, density) in [
|
||||
(kPeerPlatformMacOS, null), (kPeerPlatformMacOS, 0.0),
|
||||
(kPeerPlatformMacOS, 1.0), (kPeerPlatformMacOS, 2.0),
|
||||
(kPeerPlatformLinux, 2.0), (kPeerPlatformWindows, 2.0),
|
||||
]) {
|
||||
testWidgets('Web $style zoom off DPR $dpr $platform density=$density',
|
||||
(tester) => tester.runAsync(() => _checkPolicy(tester, style, dpr,
|
||||
pixelRatio: density, platform: platform)));
|
||||
}
|
||||
}
|
||||
}
|
||||
test('Web cursor aligns CSS hotspots with rounded PNG dimensions', () async {
|
||||
@@ -202,13 +209,18 @@ Map<String, dynamic> _captureCursor() {
|
||||
Future<void> _checkPolicy(WidgetTester tester, String style, double dpr,
|
||||
{bool zoom = false, (int, int) source = (48, 48),
|
||||
(double, double) hotspot = (7, 9), (int, int) expectedSize = (48, 48),
|
||||
(int, int) expectedHotspot = (7, 9)}) async {
|
||||
(int, int) expectedHotspot = (7, 9), double? pixelRatio = 1,
|
||||
String platform = kPeerPlatformMacOS}) async {
|
||||
final registered = _captureCursor();
|
||||
final canvas = _Canvas(style);
|
||||
addTearDown(canvas.dispose);
|
||||
final ffi = _FFI(canvas);
|
||||
final cursor = await _loadCursor(ffi, '$style-$dpr-$zoom-$source',
|
||||
source: source, hotspot: hotspot);
|
||||
ffi.ffiModel.pi.platform = platform;
|
||||
// Retina export changes the bitmap too; varying metadata alone misses this boundary.
|
||||
final density = platform == kPeerPlatformMacOS && pixelRatio == 2 ? 2 : 1;
|
||||
final cursor = await _loadCursor(ffi, '$style-$dpr-$zoom-$source-$platform-$pixelRatio',
|
||||
source: (source.$1 * density, source.$2 * density), pixelRatio: pixelRatio,
|
||||
hotspot: (hotspot.$1 * density, hotspot.$2 * density));
|
||||
await tester.pumpWidget(MediaQuery(
|
||||
data: MediaQueryData(devicePixelRatio: dpr),
|
||||
child: MultiProvider(
|
||||
|
||||
@@ -318,6 +318,8 @@ message CursorData {
|
||||
bytes colors = 6;
|
||||
// Physical bitmap pixels per remote logical pixel; 0 means unknown density.
|
||||
double scale = 7;
|
||||
// Optional physical image. Legacy receivers keep using the logical-sized fields above.
|
||||
CursorData high_resolution = 8;
|
||||
}
|
||||
|
||||
message CursorPosition {
|
||||
|
||||
@@ -642,6 +642,7 @@ impl FlutterHandler {
|
||||
|
||||
impl InvokeUiSession for FlutterHandler {
|
||||
fn set_cursor_data(&self, cd: CursorData) {
|
||||
let cd = cd.high_resolution.as_ref().unwrap_or(&cd);
|
||||
let colors = hbb_common::compress::decompress(&cd.colors);
|
||||
self.push_event(
|
||||
"cursor_data",
|
||||
|
||||
@@ -81,6 +81,16 @@ unsafe fn render(image: id, bitmap: id, size: NSSize) -> ResultType<()> {
|
||||
}
|
||||
|
||||
pub(super) unsafe fn data(cursor: id, id: u64, scale: f64) -> ResultType<CursorData> {
|
||||
// Older receivers ignore density. Keep their image logical-sized while
|
||||
// retaining the complete physical artwork for density-aware controllers.
|
||||
let mut legacy = physical_data(cursor, id, 1.0)?;
|
||||
if scale > 1.0 {
|
||||
legacy.high_resolution = Some(physical_data(cursor, id, scale)?).into();
|
||||
}
|
||||
Ok(legacy)
|
||||
}
|
||||
|
||||
unsafe fn physical_data(cursor: id, id: u64, scale: f64) -> ResultType<CursorData> {
|
||||
let image: id = msg_send![cursor, image];
|
||||
let logical: NSSize = msg_send![image, size];
|
||||
let size = NSSize::new(
|
||||
@@ -142,6 +152,9 @@ fn straight_rgba(pixels: &[u8]) -> Vec<u8> {
|
||||
colors
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod compat_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -184,7 +197,7 @@ mod tests {
|
||||
let cursor = StrongPtr::new(
|
||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(4.0, 9.0)],
|
||||
);
|
||||
let result = data(*cursor, 1, 2.0).unwrap();
|
||||
let result = physical_data(*cursor, 1, 2.0).unwrap();
|
||||
assert_eq!(
|
||||
(result.width, result.height, result.hotx, result.hoty),
|
||||
(18, 36, 8, 18)
|
||||
@@ -206,7 +219,7 @@ mod tests {
|
||||
initWithImage: image hotSpot: NSPoint::new(point.0, point.1)]);
|
||||
let actual: NSPoint = msg_send![*c, hotSpot];
|
||||
assert_eq!((actual.x, actual.y), point);
|
||||
let result = data(*c, 1, 2.0).unwrap();
|
||||
let result = physical_data(*c, 1, 2.0).unwrap();
|
||||
assert_eq!((result.hotx, result.hoty), pixels);
|
||||
assert_eq!(result.colors.as_ref(), expected);
|
||||
}
|
||||
@@ -244,7 +257,7 @@ mod tests {
|
||||
let cursor = StrongPtr::new(
|
||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(1.0, 1.0)],
|
||||
);
|
||||
let result = data(*cursor, 1, SCALE).unwrap();
|
||||
let result = physical_data(*cursor, 1, SCALE).unwrap();
|
||||
// Older Sciter receivers encode the received bytes directly as PNG.
|
||||
let mut png = Vec::new();
|
||||
repng::encode(
|
||||
|
||||
35
src/platform/macos/cursor/compat_tests.rs
Normal file
35
src/platform/macos/cursor/compat_tests.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use super::*;
|
||||
use hbb_common::protobuf::Message;
|
||||
use objc::rc::autoreleasepool;
|
||||
|
||||
#[test]
|
||||
fn retina_export_preserves_legacy_dimensions_and_hotspot() {
|
||||
autoreleasepool(|| unsafe {
|
||||
let logical = NSSize::new(32.0, 32.0);
|
||||
let image: id = msg_send![class!(NSImage), alloc];
|
||||
let image = StrongPtr::new(msg_send![image, initWithSize: logical]);
|
||||
let rep = bitmap(NSSize::new(64.0, 64.0)).unwrap();
|
||||
let buffer: *mut u8 = msg_send![*rep, bitmapData];
|
||||
ptr::write_bytes(buffer, 255, 64 * 64 * CHANNELS);
|
||||
let (): () = msg_send![*rep, setSize: logical];
|
||||
let (): () = msg_send![*image, addRepresentation: *rep];
|
||||
let cursor: id = msg_send![class!(NSCursor), alloc];
|
||||
let cursor = StrongPtr::new(msg_send![cursor,
|
||||
initWithImage: *image hotSpot: NSPoint::new(8.0, 12.0)]);
|
||||
|
||||
let exported = data(*cursor, 123, 2.0).unwrap();
|
||||
let legacy = CursorData::parse_from_bytes(&exported.write_to_bytes().unwrap()).unwrap();
|
||||
assert_eq!((legacy.width, legacy.height), (32, 32));
|
||||
assert_eq!((legacy.hotx, legacy.hoty), (8, 12));
|
||||
assert_eq!(legacy.colors.len(), 32 * 32 * CHANNELS);
|
||||
assert_eq!(legacy.scale, 1.0);
|
||||
assert!(legacy.colors.iter().all(|channel| *channel == 255));
|
||||
let physical = legacy.high_resolution.as_ref().unwrap();
|
||||
assert_eq!((physical.width, physical.height), (64, 64));
|
||||
assert_eq!((physical.hotx, physical.hoty), (16, 24));
|
||||
assert_eq!(physical.colors.len(), 64 * 64 * CHANNELS);
|
||||
assert_eq!((physical.id, physical.scale), (legacy.id, 2.0));
|
||||
assert!(physical.colors.iter().all(|channel| *channel == 255));
|
||||
assert!(data(*cursor, 123, 1.0).unwrap().high_resolution.is_none());
|
||||
});
|
||||
}
|
||||
@@ -422,6 +422,9 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()>
|
||||
#[cfg(not(all(target_os = "linux", feature = "drm")))]
|
||||
let cache_key = hcursor;
|
||||
data.colors = hbb_common::compress::compress(&data.colors[..]).into();
|
||||
if let Some(physical) = data.high_resolution.as_mut() {
|
||||
physical.colors = hbb_common::compress::compress(&physical.colors).into();
|
||||
}
|
||||
let mut tmp = Message::new();
|
||||
tmp.set_cursor_data(data);
|
||||
msg = Arc::new(tmp);
|
||||
|
||||
Reference in New Issue
Block a user