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
|
? cursor.cache ?? preDefaultCursor.cache
|
||||||
: preForbiddenCursor.cache;
|
: preForbiddenCursor.cache;
|
||||||
final peerDpr = cache?.pixelRatio ?? 0;
|
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
|
// Adaptive/Custom scales the video, but Zoom cursor is off: preserve the
|
||||||
// cursor's logical size instead of multiplying it by the canvas scale.
|
// cursor's logical size instead of multiplying it by the canvas scale.
|
||||||
// Divide by the source bitmap density to obtain logical cursor pixels.
|
// Divide by the source bitmap density to obtain logical cursor pixels.
|
||||||
|
|||||||
@@ -25,8 +25,15 @@ void main() {
|
|||||||
_thinCursorTests();
|
_thinCursorTests();
|
||||||
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||||
for (final dpr in [1.0, 2.0]) {
|
for (final dpr in [1.0, 2.0]) {
|
||||||
testWidgets('ImagePaint Web $style zoom off DPR $dpr keeps source size',
|
for (final (platform, density) in [
|
||||||
(tester) => tester.runAsync(() => _checkPolicy(tester, style, dpr)));
|
(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 {
|
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,
|
Future<void> _checkPolicy(WidgetTester tester, String style, double dpr,
|
||||||
{bool zoom = false, (int, int) source = (48, 48),
|
{bool zoom = false, (int, int) source = (48, 48),
|
||||||
(double, double) hotspot = (7, 9), (int, int) expectedSize = (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 registered = _captureCursor();
|
||||||
final canvas = _Canvas(style);
|
final canvas = _Canvas(style);
|
||||||
addTearDown(canvas.dispose);
|
addTearDown(canvas.dispose);
|
||||||
final ffi = _FFI(canvas);
|
final ffi = _FFI(canvas);
|
||||||
final cursor = await _loadCursor(ffi, '$style-$dpr-$zoom-$source',
|
ffi.ffiModel.pi.platform = platform;
|
||||||
source: source, hotspot: hotspot);
|
// 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(
|
await tester.pumpWidget(MediaQuery(
|
||||||
data: MediaQueryData(devicePixelRatio: dpr),
|
data: MediaQueryData(devicePixelRatio: dpr),
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
|
|||||||
@@ -318,6 +318,8 @@ message CursorData {
|
|||||||
bytes colors = 6;
|
bytes colors = 6;
|
||||||
// Physical bitmap pixels per remote logical pixel; 0 means unknown density.
|
// Physical bitmap pixels per remote logical pixel; 0 means unknown density.
|
||||||
double scale = 7;
|
double scale = 7;
|
||||||
|
// Optional physical image. Legacy receivers keep using the logical-sized fields above.
|
||||||
|
CursorData high_resolution = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CursorPosition {
|
message CursorPosition {
|
||||||
|
|||||||
@@ -642,6 +642,7 @@ impl FlutterHandler {
|
|||||||
|
|
||||||
impl InvokeUiSession for FlutterHandler {
|
impl InvokeUiSession for FlutterHandler {
|
||||||
fn set_cursor_data(&self, cd: CursorData) {
|
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);
|
let colors = hbb_common::compress::decompress(&cd.colors);
|
||||||
self.push_event(
|
self.push_event(
|
||||||
"cursor_data",
|
"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> {
|
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 image: id = msg_send![cursor, image];
|
||||||
let logical: NSSize = msg_send![image, size];
|
let logical: NSSize = msg_send![image, size];
|
||||||
let size = NSSize::new(
|
let size = NSSize::new(
|
||||||
@@ -142,6 +152,9 @@ fn straight_rgba(pixels: &[u8]) -> Vec<u8> {
|
|||||||
colors
|
colors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod compat_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -184,7 +197,7 @@ mod tests {
|
|||||||
let cursor = StrongPtr::new(
|
let cursor = StrongPtr::new(
|
||||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(4.0, 9.0)],
|
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!(
|
assert_eq!(
|
||||||
(result.width, result.height, result.hotx, result.hoty),
|
(result.width, result.height, result.hotx, result.hoty),
|
||||||
(18, 36, 8, 18)
|
(18, 36, 8, 18)
|
||||||
@@ -206,7 +219,7 @@ mod tests {
|
|||||||
initWithImage: image hotSpot: NSPoint::new(point.0, point.1)]);
|
initWithImage: image hotSpot: NSPoint::new(point.0, point.1)]);
|
||||||
let actual: NSPoint = msg_send![*c, hotSpot];
|
let actual: NSPoint = msg_send![*c, hotSpot];
|
||||||
assert_eq!((actual.x, actual.y), point);
|
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.hotx, result.hoty), pixels);
|
||||||
assert_eq!(result.colors.as_ref(), expected);
|
assert_eq!(result.colors.as_ref(), expected);
|
||||||
}
|
}
|
||||||
@@ -244,7 +257,7 @@ mod tests {
|
|||||||
let cursor = StrongPtr::new(
|
let cursor = StrongPtr::new(
|
||||||
msg_send![cursor, initWithImage: *image hotSpot: NSPoint::new(1.0, 1.0)],
|
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.
|
// Older Sciter receivers encode the received bytes directly as PNG.
|
||||||
let mut png = Vec::new();
|
let mut png = Vec::new();
|
||||||
repng::encode(
|
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")))]
|
#[cfg(not(all(target_os = "linux", feature = "drm")))]
|
||||||
let cache_key = hcursor;
|
let cache_key = hcursor;
|
||||||
data.colors = hbb_common::compress::compress(&data.colors[..]).into();
|
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();
|
let mut tmp = Message::new();
|
||||||
tmp.set_cursor_data(data);
|
tmp.set_cursor_data(data);
|
||||||
msg = Arc::new(tmp);
|
msg = Arc::new(tmp);
|
||||||
|
|||||||
Reference in New Issue
Block a user