From aa6350d9f42d4801156c0bf0d622e0af74d12ede Mon Sep 17 00:00:00 2001 From: Galygious Date: Sun, 8 Mar 2026 07:08:18 -0500 Subject: [PATCH 01/14] fix(flutter): add configurable remote canvas margin Signed-off-by: Galygious Made-with: Cursor --- flutter/lib/consts.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 23 +++++ flutter/lib/desktop/pages/remote_page.dart | 18 ++-- .../lib/desktop/widgets/remote_toolbar.dart | 53 +++++++++-- flutter/lib/models/input_model.dart | 39 +++++++- flutter/lib/models/model.dart | 88 +++++++++++++++---- 6 files changed, 186 insertions(+), 36 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3b9940c9c..9ace9da01 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -81,6 +81,7 @@ const String kWindowEventOpenMonitorSession = "open_monitor_session"; const String kOptionViewStyle = "view_style"; const String kOptionScrollStyle = "scroll_style"; const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness"; +const String kOptionRemoteCanvasMargin = "remote-canvas-margin"; const String kOptionImageQuality = "image_quality"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionTextureRender = "use-texture-render"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 82212d191..4dee773f7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1733,6 +1733,7 @@ class _DisplayState extends State<_Display> { return ListView(controller: scrollController, children: [ viewStyle(context), scrollStyle(context), + remoteCanvasMargin(context), imageQuality(context), codec(context), if (isDesktop) trackpadSpeed(context), @@ -1810,6 +1811,28 @@ class _DisplayState extends State<_Display> { ]); } + Widget remoteCanvasMargin(BuildContext context) { + onChanged(double value) async { + await bind.mainSetUserDefaultOption( + key: kOptionRemoteCanvasMargin, value: value.round().toString()); + setState(() {}); + } + + final currentValue = double.tryParse( + bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin)) ?? + 0; + + return _Card(title: 'Remote canvas margin', children: [ + EdgeThicknessControl( + value: currentValue, + min: 0, + max: 400, + onChanged: + isOptionFixed(kOptionRemoteCanvasMargin) ? null : onChanged, + ), + ]); + } + Widget imageQuality(BuildContext context) { onChanged(String value) async { await bind.mainSetUserDefaultOption( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 29e710bbc..4ec2724bd 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -784,7 +784,7 @@ class _ImagePaintState extends State { m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender ? _BuildPaintTextureRender( c, s, Offset.zero, paintSize, isViewOriginal()) - : _buildScrollbarNonTextureRender(m, paintSize, s); + : _buildScrollbarNonTextureRender(m, c, paintSize, s); return NotificationListener( onNotification: (notification) { c.updateScrollPercent(); @@ -822,10 +822,14 @@ class _ImagePaintState extends State { } Widget _buildScrollbarNonTextureRender( - ImageModel m, Size imageSize, double s) { + ImageModel m, CanvasModel c, Size imageSize, double s) { return CustomPaint( size: imageSize, - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + painter: ImagePainter( + image: m.image, + x: c.displayPaddingX, + y: c.displayPaddingY, + scale: s), ); } @@ -842,8 +846,8 @@ class _ImagePaintState extends State { size: Size(c.size.width, c.size.height), painter: ImagePainter( image: m.image, - x: c.x / sizeScale, - y: c.y / sizeScale, + x: c.x / sizeScale + c.displayPaddingX, + y: c.y / sizeScale + c.displayPaddingY, scale: sizeScale), ); } @@ -853,7 +857,7 @@ class _ImagePaintState extends State { final ffiModel = c.parent.target!.ffiModel; final displays = ffiModel.pi.getCurDisplays(); final children = []; - final rect = ffiModel.rect; + final rect = c.paddedRect; if (rect == null) { return Container(); } @@ -1016,7 +1020,7 @@ class CursorPaint extends StatelessWidget { double cy = c.y; if (c.viewStyle.style == kRemoteViewStyleOriginal && c.scrollStyle == ScrollStyle.scrollbar) { - final rect = c.parent.target!.ffiModel.rect; + final rect = c.paddedRect; if (rect == null) { // unreachable! debugPrint('unreachable! The displays rect is null.'); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index ec05c987f..52f8b7126 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1110,16 +1110,22 @@ class _DisplayMenuState extends State<_DisplayMenu> { await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? ''; final edgeScrollEdgeThickness = await bind .sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId); + final remoteCanvasMargin = + double.tryParse(bind.mainGetUserDefaultOption( + key: kOptionRemoteCanvasMargin)) ?? + 0; return { 'visible': visible, 'scrollStyle': scrollStyle, 'edgeScrollEdgeThickness': edgeScrollEdgeThickness, + 'remoteCanvasMargin': remoteCanvasMargin, }; }(), hasData: (data) { final visible = data['visible'] as bool; if (!visible) return Offstage(); final groupValue = data['scrollStyle'] as String; final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int; + final remoteCanvasMargin = data['remoteCanvasMargin'] as double; onChangeScrollStyle(String? value) async { if (value == null) return; @@ -1138,6 +1144,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { state.setState(() {}); } + onChangeRemoteCanvasMargin(double? value) async { + if (value == null) return; + await bind.mainSetUserDefaultOption( + key: kOptionRemoteCanvasMargin, value: value.round().toString()); + await widget.ffi.canvasModel.updateViewStyle(); + state.setState(() {}); + } + return Obx(() => Column(children: [ RdoMenuButton( child: Text(translate('ScrollAuto')), @@ -1178,6 +1192,24 @@ class _DisplayMenuState extends State<_DisplayMenu> { colorScheme: colorScheme, )), ], + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded(child: Text('Canvas Margin')), + SizedBox( + width: 160, + child: EdgeThicknessControl( + value: remoteCanvasMargin, + min: 0, + max: 400, + onChanged: onChangeRemoteCanvasMargin, + colorScheme: colorScheme, + ), + ), + ], + ), + ), Divider(), ])); }); @@ -2751,12 +2783,18 @@ class EdgeThicknessControl extends StatelessWidget { final double value; final ValueChanged? onChanged; final ColorScheme? colorScheme; + final double min; + final double max; + final String unit; const EdgeThicknessControl({ Key? key, required this.value, this.onChanged, this.colorScheme, + this.min = kMin, + this.max = kMax, + this.unit = 'px', }) : super(key: key); static const double kMin = 20; @@ -2773,24 +2811,23 @@ class EdgeThicknessControl extends StatelessWidget { overlayColor: colorScheme.primary.withOpacity(0.1), showValueIndicator: ShowValueIndicator.never, thumbShape: _RectValueThumbShape( - min: EdgeThicknessControl.kMin, - max: EdgeThicknessControl.kMax, + min: min, + max: max, width: 52, height: 24, radius: 4, - unit: 'px', + unit: unit, ), ), child: Semantics( value: value.toInt().toString(), child: Slider( value: value, - min: EdgeThicknessControl.kMin, - max: EdgeThicknessControl.kMax, - divisions: - (EdgeThicknessControl.kMax - EdgeThicknessControl.kMin).round(), + min: min, + max: max, + divisions: (max - min).round(), semanticFormatterCallback: (double newValue) => - "${newValue.round()}px", + "${newValue.round()}$unit", onChanged: onChanged, ), ), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 675a95e42..76031559a 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -32,6 +32,10 @@ class CanvasCoords { double scale = 1.0; double scrollX = 0; double scrollY = 0; + double displayWidth = 0; + double displayHeight = 0; + double paddingX = 0; + double paddingY = 0; ScrollStyle scrollStyle = ScrollStyle.scrollauto; Size size = Size.zero; @@ -44,6 +48,10 @@ class CanvasCoords { 'scale': scale, 'scrollX': scrollX, 'scrollY': scrollY, + 'displayWidth': displayWidth, + 'displayHeight': displayHeight, + 'paddingX': paddingX, + 'paddingY': paddingY, 'scrollStyle': scrollStyle.toJson(), 'size': { 'w': size.width, @@ -59,6 +67,10 @@ class CanvasCoords { model.scale = json['scale']; model.scrollX = json['scrollX']; model.scrollY = json['scrollY']; + model.displayWidth = (json['displayWidth'] ?? 0).toDouble(); + model.displayHeight = (json['displayHeight'] ?? 0).toDouble(); + model.paddingX = (json['paddingX'] ?? 0).toDouble(); + model.paddingY = (json['paddingY'] ?? 0).toDouble(); model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); model.size = Size(json['size']['w'], json['size']['h']); @@ -72,6 +84,10 @@ class CanvasCoords { coords.scale = model.scale; coords.scrollX = model.scrollX; coords.scrollY = model.scrollY; + coords.displayWidth = model.getDisplayWidth().toDouble(); + coords.displayHeight = model.getDisplayHeight().toDouble(); + coords.paddingX = model.displayPaddingX; + coords.paddingY = model.displayPaddingY; coords.scrollStyle = model.scrollStyle; coords.size = model.size; return coords; @@ -1792,8 +1808,12 @@ class InputModel { final nearThr = 3; var nearRight = (canvas.size.width - x) < nearThr; var nearBottom = (canvas.size.height - y) < nearThr; - final imageWidth = rect.width * canvas.scale; - final imageHeight = rect.height * canvas.scale; + final displayWidth = + canvas.displayWidth > 0 ? canvas.displayWidth : rect.width; + final displayHeight = + canvas.displayHeight > 0 ? canvas.displayHeight : rect.height; + final imageWidth = displayWidth * canvas.scale; + final imageHeight = displayHeight * canvas.scale; if (canvas.scrollStyle != ScrollStyle.scrollauto) { x += imageWidth * canvas.scrollX; y += imageHeight * canvas.scrollY; @@ -1821,8 +1841,19 @@ class InputModel { y += step; } } - x += rect.left; - y += rect.top; + final paddedX = x; + final paddedY = y; + x = paddedX - canvas.paddingX + rect.left; + y = paddedY - canvas.paddingY + rect.top; + + final insidePaddedRect = paddedX >= 0 && + paddedY >= 0 && + paddedX <= displayWidth && + paddedY <= displayHeight; + if (insidePaddedRect) { + x = x.clamp(rect.left, rect.right).toDouble(); + y = y.clamp(rect.top, rect.bottom).toDouble(); + } if (onExit) { final pos = setNearestEdge(x, y, rect); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ff298c380..ee5f0ec19 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2176,6 +2176,52 @@ class CanvasModel with ChangeNotifier { ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; RxBool get imageOverflow => _imageOverflow; + Rect? get realRect => parent.target?.ffiModel.rect; + + double get remoteCanvasMargin { + if (!(isDesktop || isWebDesktop)) { + return 0; + } + final value = + double.tryParse(bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin)) ?? + 0; + return max(0, value); + } + + Rect? get paddedRect { + final rect = realRect; + if (rect == null) { + return null; + } + final margin = remoteCanvasMargin; + if (margin <= 0) { + return rect; + } + return Rect.fromLTRB( + rect.left - margin, + rect.top - margin, + rect.right + margin, + rect.bottom + margin, + ); + } + + double get displayPaddingX { + final padded = paddedRect; + final rect = realRect; + if (padded == null || rect == null) { + return 0; + } + return rect.left - padded.left; + } + + double get displayPaddingY { + final padded = paddedRect; + final rect = realRect; + if (padded == null || rect == null) { + return 0; + } + return rect.top - padded.top; + } _resetScroll() => setScrollPercent(0.0, 0.0); @@ -2381,14 +2427,14 @@ class CanvasModel with ChangeNotifier { final defaultWidth = (isDesktop || isWebDesktop) ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; - return parent.target?.ffiModel.rect?.width.toInt() ?? defaultWidth; + return paddedRect?.width.toInt() ?? defaultWidth; } int getDisplayHeight() { final defaultHeight = (isDesktop || isWebDesktop) ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; - return parent.target?.ffiModel.rect?.height.toInt() ?? defaultHeight; + return paddedRect?.height.toInt() ?? defaultHeight; } static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; @@ -2938,8 +2984,10 @@ class CursorModel with ChangeNotifier { ui.Image? get image => _image; CursorData? get cache => _cache; - double get x => _x - _displayOriginX; - double get y => _y - _displayOriginY; + double get x => + _x - _displayOriginX + (parent.target?.canvasModel.displayPaddingX ?? 0); + double get y => + _y - _displayOriginY + (parent.target?.canvasModel.displayPaddingY ?? 0); double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; @@ -2976,8 +3024,12 @@ class CursorModel with ChangeNotifier { final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; final scale = parent.target?.canvasModel.scale ?? 1; - final x0 = _displayOriginX - xoffset / scale; - final y0 = _displayOriginY - yoffset / scale; + final x0 = _displayOriginX - + (parent.target?.canvasModel.displayPaddingX ?? 0) - + xoffset / scale; + final y0 = _displayOriginY - + (parent.target?.canvasModel.displayPaddingY ?? 0) - + yoffset / scale; return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); } @@ -2988,10 +3040,14 @@ class CursorModel with ChangeNotifier { // See `getVisibleRect()` // _x = _displayOriginX - xoffset / scale + size.width / scale * 0.5; // _y = _displayOriginY - yoffset / scale + size.height / scale * 0.5; + final displayOriginX = + _displayOriginX - (parent.target?.canvasModel.displayPaddingX ?? 0); + final displayOriginY = + _displayOriginY - (parent.target?.canvasModel.displayPaddingY ?? 0); final size = parent.target?.canvasModel.getSize() ?? MediaQueryData.fromView(ui.window).size; - final xoffset = (_displayOriginX - _x) * scale + size.width * 0.5; - final yoffset = (_displayOriginY - _y) * scale + size.height * 0.5; + final xoffset = (displayOriginX - _x) * scale + size.width * 0.5; + final yoffset = (displayOriginY - _y) * scale + size.height * 0.5; return Offset(xoffset, yoffset); } @@ -3107,11 +3163,10 @@ class CursorModel with ChangeNotifier { var cx = r.center.dx; var cy = r.center.dy; var tryMoveCanvasX = false; - final displayRect = parent.target?.ffiModel.rect; + final displayRect = parent.target?.canvasModel.paddedRect; if (dx > 0) { - final maxCanvasCanMove = _displayOriginX + - (displayRect?.width ?? 1280) - - r.right.roundToDouble(); + final maxCanvasCanMove = + (displayRect?.right ?? 1280) - r.right.roundToDouble(); tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0; if (tryMoveCanvasX) { dx = min(dx, maxCanvasCanMove); @@ -3120,7 +3175,7 @@ class CursorModel with ChangeNotifier { dx = min(dx, maxCursorCanMove); } } else if (dx < 0) { - final maxCanvasCanMove = _displayOriginX - r.left.roundToDouble(); + final maxCanvasCanMove = (displayRect?.left ?? 0) - r.left.roundToDouble(); tryMoveCanvasX = _x + dx < cx && maxCanvasCanMove < 0; if (tryMoveCanvasX) { dx = max(dx, maxCanvasCanMove); @@ -3131,9 +3186,8 @@ class CursorModel with ChangeNotifier { } var tryMoveCanvasY = false; if (dy > 0) { - final mayCanvasCanMove = _displayOriginY + - (displayRect?.height ?? 720) - - r.bottom.roundToDouble(); + final mayCanvasCanMove = + (displayRect?.bottom ?? 720) - r.bottom.roundToDouble(); tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0; if (tryMoveCanvasY) { dy = min(dy, mayCanvasCanMove); @@ -3142,7 +3196,7 @@ class CursorModel with ChangeNotifier { dy = min(dy, mayCursorCanMove); } } else if (dy < 0) { - final mayCanvasCanMove = _displayOriginY - r.top.roundToDouble(); + final mayCanvasCanMove = (displayRect?.top ?? 0) - r.top.roundToDouble(); tryMoveCanvasY = _y + dy < cy && mayCanvasCanMove < 0; if (tryMoveCanvasY) { dy = max(dy, mayCanvasCanMove); From c4ad53c538ffc9a7454690780c100947261d0105 Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 04:28:26 -0500 Subject: [PATCH 02/14] fix(flutter): localize canvas margin label --- flutter/lib/desktop/widgets/remote_toolbar.dart | 2 +- src/lang/cn.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/en.rs | 1 + src/lang/es.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/template.rs | 1 + src/lang/tw.rs | 1 + 18 files changed, 18 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 52f8b7126..2474aab38 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1196,7 +1196,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { padding: EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ - Expanded(child: Text('Canvas Margin')), + Expanded(child: Text(translate('canvas_margin'))), SizedBox( width: 160, child: EdgeThicknessControl( diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0cc6aacd1..0f15b6f35 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "阻止用户输入"), ("Unblock user input", "取消阻止用户输入"), ("Adjust Window", "调节窗口"), + ("canvas_margin", "画布边距"), ("Original", "原始比例"), ("Shrink", "收缩"), ("Stretch", "伸展"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 8140fcaec..5ecca3978 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloker brugerinput"), ("Unblock user input", "Fjern blokering af brugerinput"), ("Adjust Window", "Juster vinduet"), + ("canvas_margin", "Lærredsmargen"), ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Stræk ud"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 03e501848..5c1687c16 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Benutzereingaben blockieren"), ("Unblock user input", "Benutzereingaben freigeben"), ("Adjust Window", "Fenster anpassen"), + ("canvas_margin", "Leinwandrand"), ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 511ddff4a..3d829da03 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -99,6 +99,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Control Actions", "Control actions"), ("Display Settings", "Display settings"), ("Image Quality", "Image quality"), + ("canvas_margin", "Canvas margin"), ("Scroll Style", "Scroll style"), ("Show Toolbar", "Show toolbar"), ("Hide Toolbar", "Hide toolbar"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 8ad0c4cab..84db84b68 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloquear entrada de usuario"), ("Unblock user input", "Desbloquear entrada de usuario"), ("Adjust Window", "Ajustar ventana"), + ("canvas_margin", "Margen del lienzo"), ("Original", "Original"), ("Shrink", "Encoger"), ("Stretch", "Estirar"), diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 0d9b42ddd..f842a8c2f 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Estä käyttäjän toiminta"), ("Unblock user input", "Salli käyttäjän toiminta"), ("Adjust Window", "Sovita ikkuna"), + ("canvas_margin", "Piirtoalueen marginaali"), ("Original", "Alkuperäinen"), ("Shrink", "Pienennä"), ("Stretch", "Venytä"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index fed35727e..945e758f0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloquer la saisie de l’utilisateur"), ("Unblock user input", "Débloquer la saisie de l’utilisateur"), ("Adjust Window", "Ajuster la fenêtre"), + ("canvas_margin", "Marge du canevas"), ("Original", "Ratio d'origine"), ("Shrink", "Rétrécir"), ("Stretch", "Étirer"), diff --git a/src/lang/it.rs b/src/lang/it.rs index aac87109d..c4e7dbb4d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blocca input utente"), ("Unblock user input", "Sblocca input utente"), ("Adjust Window", "Adatta finestra"), + ("canvas_margin", "Margine della tela"), ("Original", "Originale"), ("Shrink", "Restringi"), ("Stretch", "Allarga"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e033de3b3..eb99a2755 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "ユーザーの入力をブロック"), ("Unblock user input", "ユーザーの入力を許可"), ("Adjust Window", "ウィンドウを調整"), + ("canvas_margin", "キャンバス余白"), ("Original", "オリジナル"), ("Shrink", "縮小"), ("Stretch", "伸縮"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7230d1a1f..5cea531fa 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "사용자 입력 차단"), ("Unblock user input", "사용자 입력 차단 해제"), ("Adjust Window", "창 크기 조정"), + ("canvas_margin", "캔버스 여백"), ("Original", "원본"), ("Shrink", "축소"), ("Stretch", "늘이기"), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 9c38fcbb8..5886ea0bd 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blokker brukerinput"), ("Unblock user input", "Fjern blokkering av brukerinput"), ("Adjust Window", "Juster vinduet"), + ("canvas_margin", "Lerretsmarg"), ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Strekk ut"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 99b859248..acf41f8f4 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Gebruikersinvoer blokkeren"), ("Unblock user input", "Gebruikersinvoer deblokkeren"), ("Adjust Window", "Venster Aanpassen"), + ("canvas_margin", "Canvasmarge"), ("Original", "Origineel"), ("Shrink", "Verkleinen"), ("Stretch", "Uitrekken"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 000c05921..f98f969b8 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blokuj peryferia użytkownika"), ("Unblock user input", "Odblokuj peryferia użytkownika"), ("Adjust Window", "Dostosuj okno"), + ("canvas_margin", "Margines płótna"), ("Original", "Oryginalny"), ("Shrink", "Zmniejsz"), ("Stretch", "Rozciągnij"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ccbdd574e..d93cc98cd 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloquear entrada de utilizador"), ("Unblock user input", "Desbloquear entrada de utilizador"), ("Adjust Window", "Ajustar Janela"), + ("canvas_margin", "Margem da tela"), ("Original", "Original"), ("Shrink", "Reduzir"), ("Stretch", "Aumentar"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a7a2f7db6..c87510cf9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloquear entrada de usuário"), ("Unblock user input", "Desbloquear entrada de usuário"), ("Adjust Window", "Ajustar Janela"), + ("canvas_margin", "Margem da tela"), ("Original", "Original"), ("Shrink", "Reduzir"), ("Stretch", "Aumentar"), diff --git a/src/lang/template.rs b/src/lang/template.rs index b21f64f14..0feb737cb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", ""), ("Unblock user input", ""), ("Adjust Window", ""), + ("canvas_margin", ""), ("Original", ""), ("Shrink", ""), ("Stretch", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0e01fcde5..e0476fbf8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -117,6 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "封鎖使用者輸入"), ("Unblock user input", "取消封鎖使用者輸入"), ("Adjust Window", "調整視窗"), + ("canvas_margin", "畫布邊距"), ("Original", "原始"), ("Shrink", "縮減"), ("Stretch", "延展"), From 43f70329cbf568552afb2171f4011b558e0fbddc Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 06:31:44 -0500 Subject: [PATCH 03/14] fix(flutter): respect fixed remote canvas margin --- flutter/lib/desktop/widgets/remote_toolbar.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 2474aab38..5e4dc677b 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1126,6 +1126,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { final groupValue = data['scrollStyle'] as String; final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int; final remoteCanvasMargin = data['remoteCanvasMargin'] as double; + final isRemoteCanvasMarginFixed = + isOptionFixed(kOptionRemoteCanvasMargin); onChangeScrollStyle(String? value) async { if (value == null) return; @@ -1145,7 +1147,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { } onChangeRemoteCanvasMargin(double? value) async { - if (value == null) return; + if (value == null || isRemoteCanvasMarginFixed) return; await bind.mainSetUserDefaultOption( key: kOptionRemoteCanvasMargin, value: value.round().toString()); await widget.ffi.canvasModel.updateViewStyle(); @@ -1203,7 +1205,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { value: remoteCanvasMargin, min: 0, max: 400, - onChanged: onChangeRemoteCanvasMargin, + onChanged: isRemoteCanvasMarginFixed + ? null + : onChangeRemoteCanvasMargin, colorScheme: colorScheme, ), ), From 5f2daf9d068bb41d13d266dae1a8c2d40cf82a7e Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 06:46:29 -0500 Subject: [PATCH 04/14] fix(flutter): recompute canvas geometry on margin changes --- flutter/lib/desktop/pages/desktop_setting_page.dart | 5 +++-- flutter/lib/desktop/widgets/remote_toolbar.dart | 4 +--- flutter/lib/models/model.dart | 7 +++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4dee773f7..37648a163 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -1812,9 +1813,9 @@ class _DisplayState extends State<_Display> { } Widget remoteCanvasMargin(BuildContext context) { + final canvasModel = Provider.of(context, listen: false); onChanged(double value) async { - await bind.mainSetUserDefaultOption( - key: kOptionRemoteCanvasMargin, value: value.round().toString()); + await canvasModel.setRemoteCanvasMargin(value); setState(() {}); } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 5e4dc677b..0133efb8e 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1148,9 +1148,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChangeRemoteCanvasMargin(double? value) async { if (value == null || isRemoteCanvasMarginFixed) return; - await bind.mainSetUserDefaultOption( - key: kOptionRemoteCanvasMargin, value: value.round().toString()); - await widget.ffi.canvasModel.updateViewStyle(); + await widget.ffi.canvasModel.setRemoteCanvasMargin(value); state.setState(() {}); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3f2de5ed8..dfc72e1af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2203,6 +2203,13 @@ class CanvasModel with ChangeNotifier { return max(0, value); } + Future setRemoteCanvasMargin(double value) async { + final normalizedValue = max(0, value.round()); + await bind.mainSetUserDefaultOption( + key: kOptionRemoteCanvasMargin, value: normalizedValue.toString()); + await updateViewStyle(); + } + Rect? get paddedRect { final rect = realRect; if (rect == null) { From aba8c1e85ce919e8b03a1f23a1683d793ad71b78 Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 06:51:22 -0500 Subject: [PATCH 05/14] fix(flutter): clamp remote canvas margin getter --- flutter/lib/models/model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dfc72e1af..c4b079e06 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2200,7 +2200,7 @@ class CanvasModel with ChangeNotifier { final value = double.tryParse(bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin)) ?? 0; - return max(0, value); + return min(400, max(0, value)); } Future setRemoteCanvasMargin(double value) async { From 65d2fb4468e62de566fec05f2f359ae53bf4a96b Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 06:54:44 -0500 Subject: [PATCH 06/14] fix(flutter): clamp remote canvas margin persistence --- flutter/lib/desktop/widgets/remote_toolbar.dart | 8 +++++--- flutter/lib/models/model.dart | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 0133efb8e..8ad6238bc 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1111,9 +1111,11 @@ class _DisplayMenuState extends State<_DisplayMenu> { final edgeScrollEdgeThickness = await bind .sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId); final remoteCanvasMargin = - double.tryParse(bind.mainGetUserDefaultOption( - key: kOptionRemoteCanvasMargin)) ?? - 0; + (double.tryParse(bind.mainGetUserDefaultOption( + key: kOptionRemoteCanvasMargin)) ?? + 0) + .clamp(0, 400) + .toDouble(); return { 'visible': visible, 'scrollStyle': scrollStyle, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c4b079e06..617d7975e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2204,7 +2204,7 @@ class CanvasModel with ChangeNotifier { } Future setRemoteCanvasMargin(double value) async { - final normalizedValue = max(0, value.round()); + final normalizedValue = value.clamp(0, 400).round(); await bind.mainSetUserDefaultOption( key: kOptionRemoteCanvasMargin, value: normalizedValue.toString()); await updateViewStyle(); From 3b375de188b3982eb9c0e079070c0bb3ef571877 Mon Sep 17 00:00:00 2001 From: Galygious Date: Mon, 16 Mar 2026 06:59:34 -0500 Subject: [PATCH 07/14] fix(flutter): normalize settings canvas margin slider --- flutter/lib/desktop/pages/desktop_setting_page.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 37648a163..3c1c0af16 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1819,9 +1819,7 @@ class _DisplayState extends State<_Display> { setState(() {}); } - final currentValue = double.tryParse( - bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin)) ?? - 0; + final currentValue = canvasModel.remoteCanvasMargin; return _Card(title: 'Remote canvas margin', children: [ EdgeThicknessControl( From a5a9c26e17aeb1592a209c00c67a78cc02026901 Mon Sep 17 00:00:00 2001 From: Galygious Date: Tue, 14 Apr 2026 04:28:24 -0500 Subject: [PATCH 08/14] test(flutter): add unit tests for remote canvas margin 49 tests covering margin clamping, paddedRect computation, display padding, ViewStyle.scale, canvas offset centering, CanvasCoords serialization, and pointer coordinate transforms across different remote/local display scale ratios, view styles, scroll styles, and margin values. Co-Authored-By: Claude Opus 4.6 --- flutter/pubspec.lock | 83 ++- flutter/pubspec.yaml | 4 +- flutter/test/canvas_margin_test.dart | 922 +++++++++++++++++++++++++++ 3 files changed, 988 insertions(+), 21 deletions(-) create mode 100644 flutter/test/canvas_margin_test.dart diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c6f8aa1c2..24662b06a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.11.0" auto_size_text: dependency: "direct main" description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" bot_toast: dependency: "direct main" description: @@ -417,6 +417,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" ffi: dependency: "direct main" description: @@ -644,6 +652,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -849,6 +862,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: @@ -877,10 +914,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1218,10 +1255,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.0" sqflite: dependency: "direct main" description: @@ -1242,18 +1279,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1266,10 +1303,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.2.0" synchronized: dependency: transitive description: @@ -1282,18 +1319,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.2" texture_rgba_renderer: dependency: "direct main" description: @@ -1473,7 +1510,7 @@ packages: source: hosted version: "1.1.16" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" @@ -1528,6 +1565,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" wakelock_plus: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index eb6d76161..eddf5a19d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -113,8 +113,8 @@ dependencies: dev_dependencies: icons_launcher: ^2.0.4 - #flutter_test: - #sdk: flutter + flutter_test: + sdk: flutter build_runner: ^2.4.6 freezed: ^2.4.2 flutter_lints: ^2.0.2 diff --git a/flutter/test/canvas_margin_test.dart b/flutter/test/canvas_margin_test.dart new file mode 100644 index 000000000..aef8a4040 --- /dev/null +++ b/flutter/test/canvas_margin_test.dart @@ -0,0 +1,922 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; + +// ============================================================================= +// Standalone replicas of production types and logic for testability. +// +// These mirror the pure math from CanvasModel, ViewStyle, CanvasCoords, +// and InputModel._handlePointerDevicePos without pulling in the full app +// dependency tree (which includes FFI bindings that can't run in test). +// ============================================================================= + +// --- Constants (from consts.dart) --- +const int kDesktopDefaultDisplayWidth = 1080; +const int kDesktopDefaultDisplayHeight = 720; +const int kMobileDefaultDisplayWidth = 720; +const int kMobileDefaultDisplayHeight = 1280; +const kRemoteViewStyleOriginal = 'original'; +const kRemoteViewStyleAdaptive = 'adaptive'; +const kRemoteViewStyleCustom = 'custom'; + +// --- ScrollStyle (from model.dart) --- +enum ScrollStyle { scrollbar, scrollauto, scrolledge } + +// --- ViewStyle (from model.dart) --- +class ViewStyle { + final String style; + final double width; + final double height; + final int displayWidth; + final int displayHeight; + + ViewStyle({ + required this.style, + required this.width, + required this.height, + required this.displayWidth, + required this.displayHeight, + }); + + static int _double2Int(double v) => (v * 100).round().toInt(); + + @override + bool operator ==(Object other) => + other is ViewStyle && + other.runtimeType == runtimeType && + _innerEqual(other); + + bool _innerEqual(ViewStyle other) { + return style == other.style && + ViewStyle._double2Int(other.width) == ViewStyle._double2Int(width) && + ViewStyle._double2Int(other.height) == ViewStyle._double2Int(height) && + other.displayWidth == displayWidth && + other.displayHeight == displayHeight; + } + + @override + int get hashCode => Object.hash( + style, + ViewStyle._double2Int(width), + ViewStyle._double2Int(height), + displayWidth, + displayHeight, + ).hashCode; + + double get scale { + double s = 1.0; + if (style == kRemoteViewStyleAdaptive) { + if (width != 0 && + height != 0 && + displayWidth != 0 && + displayHeight != 0) { + final s1 = width / displayWidth; + final s2 = height / displayHeight; + s = s1 < s2 ? s1 : s2; + } + } + return s; + } +} + +// --- CanvasCoords (from input_model.dart) --- +class CanvasCoords { + double x = 0; + double y = 0; + double scale = 1.0; + double scrollX = 0; + double scrollY = 0; + double displayWidth = 0; + double displayHeight = 0; + double paddingX = 0; + double paddingY = 0; + ScrollStyle scrollStyle = ScrollStyle.scrollauto; + Size size = Size.zero; + + CanvasCoords(); + + Map toJson() { + return { + 'x': x, + 'y': y, + 'scale': scale, + 'scrollX': scrollX, + 'scrollY': scrollY, + 'displayWidth': displayWidth, + 'displayHeight': displayHeight, + 'paddingX': paddingX, + 'paddingY': paddingY, + 'scrollStyle': scrollStyle.name, + 'size': { + 'w': size.width, + 'h': size.height, + } + }; + } + + static CanvasCoords fromJson(Map json) { + final model = CanvasCoords(); + model.x = json['x']; + model.y = json['y']; + model.scale = json['scale']; + model.scrollX = json['scrollX']; + model.scrollY = json['scrollY']; + model.displayWidth = (json['displayWidth'] ?? 0).toDouble(); + model.displayHeight = (json['displayHeight'] ?? 0).toDouble(); + model.paddingX = (json['paddingX'] ?? 0).toDouble(); + model.paddingY = (json['paddingY'] ?? 0).toDouble(); + model.scrollStyle = ScrollStyle.values.firstWhere( + (e) => e.name == json['scrollStyle'], + orElse: () => ScrollStyle.scrollauto, + ); + model.size = Size(json['size']['w'], json['size']['h']); + return model; + } +} + +// ============================================================================= +// Helper functions replicating CanvasModel pure math +// ============================================================================= + +/// Replicates CanvasModel.remoteCanvasMargin clamping logic. +double clampMargin(double value) { + return min(400, max(0, value)); +} + +/// Replicates CanvasModel.setRemoteCanvasMargin normalization logic. +int normalizeMarginForStorage(double value) { + return value.clamp(0, 400).round(); +} + +/// Replicates CanvasModel.paddedRect computation. +Rect? computePaddedRect(Rect? realRect, double margin) { + if (realRect == null) return null; + if (margin <= 0) return realRect; + return Rect.fromLTRB( + realRect.left - margin, + realRect.top - margin, + realRect.right + margin, + realRect.bottom + margin, + ); +} + +/// Replicates CanvasModel.displayPaddingX computation. +double computeDisplayPaddingX(Rect? paddedRect, Rect? realRect) { + if (paddedRect == null || realRect == null) return 0; + return realRect.left - paddedRect.left; +} + +/// Replicates CanvasModel.displayPaddingY computation. +double computeDisplayPaddingY(Rect? paddedRect, Rect? realRect) { + if (paddedRect == null || realRect == null) return 0; + return realRect.top - paddedRect.top; +} + +/// Replicates CanvasModel.getDisplayWidth using paddedRect. +int computeDisplayWidth(Rect? paddedRect, {bool isDesktop = true}) { + final defaultWidth = + isDesktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; + return paddedRect?.width.toInt() ?? defaultWidth; +} + +/// Replicates CanvasModel.getDisplayHeight using paddedRect. +int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { + final defaultHeight = + isDesktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; + return paddedRect?.height.toInt() ?? defaultHeight; +} + +/// Replicates CanvasModel._resetCanvasOffset centering computation. +(double, double) computeCanvasOffset( + Size viewSize, int displayWidth, int displayHeight, double scale) { + final x = (viewSize.width - displayWidth * scale) / 2; + final y = (viewSize.height - displayHeight * scale) / 2; + return (x, y); +} + +/// Replicates the core coordinate transform math from +/// InputModel._handlePointerDevicePos. +(double, double)? computePointerPosition({ + required double pointerX, + required double pointerY, + required CanvasCoords canvas, + required Rect remoteRect, +}) { + double x = pointerX; + double y = pointerY; + + final displayWidth = + canvas.displayWidth > 0 ? canvas.displayWidth : remoteRect.width; + final displayHeight = + canvas.displayHeight > 0 ? canvas.displayHeight : remoteRect.height; + final imageWidth = displayWidth * canvas.scale; + final imageHeight = displayHeight * canvas.scale; + + if (canvas.scrollStyle != ScrollStyle.scrollauto) { + x += imageWidth * canvas.scrollX; + y += imageHeight * canvas.scrollY; + + if (canvas.size.width > imageWidth) { + x -= ((canvas.size.width - imageWidth) / 2); + } + if (canvas.size.height > imageHeight) { + y -= ((canvas.size.height - imageHeight) / 2); + } + } else { + x -= canvas.x; + y -= canvas.y; + } + + x /= canvas.scale; + y /= canvas.scale; + + final paddedX = x; + final paddedY = y; + x = paddedX - canvas.paddingX + remoteRect.left; + y = paddedY - canvas.paddingY + remoteRect.top; + + final insidePaddedRect = paddedX >= 0 && + paddedY >= 0 && + paddedX <= displayWidth && + paddedY <= displayHeight; + if (insidePaddedRect) { + x = x.clamp(remoteRect.left, remoteRect.right).toDouble(); + y = y.clamp(remoteRect.top, remoteRect.bottom).toDouble(); + } + + return (x, y); +} + +// ============================================================================= +// Tests +// ============================================================================= + +void main() { + // =========================================================================== + // Margin clamping + // =========================================================================== + group('Margin clamping', () { + test('clamps negative values to 0', () { + expect(clampMargin(-10), 0); + expect(clampMargin(-0.5), 0); + }); + + test('clamps values above 400 to 400', () { + expect(clampMargin(500), 400); + expect(clampMargin(999), 400); + }); + + test('passes through valid values', () { + expect(clampMargin(0), 0); + expect(clampMargin(50), 50); + expect(clampMargin(200), 200); + expect(clampMargin(400), 400); + }); + + test('normalizeMarginForStorage rounds to int', () { + expect(normalizeMarginForStorage(50.7), 51); + expect(normalizeMarginForStorage(50.3), 50); + expect(normalizeMarginForStorage(-5), 0); + expect(normalizeMarginForStorage(999), 400); + }); + }); + + // =========================================================================== + // paddedRect computation + // =========================================================================== + group('paddedRect computation', () { + test('returns null when realRect is null', () { + expect(computePaddedRect(null, 50), isNull); + }); + + test('returns realRect unchanged when margin is 0', () { + final rect = Rect.fromLTWH(0, 0, 1920, 1080); + expect(computePaddedRect(rect, 0), rect); + }); + + test('returns realRect unchanged when margin is negative', () { + final rect = Rect.fromLTWH(0, 0, 1920, 1080); + expect(computePaddedRect(rect, -10), rect); + }); + + test('expands rect by margin on all sides', () { + final rect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(rect, 50)!; + expect(padded.left, -50); + expect(padded.top, -50); + expect(padded.right, 1970); + expect(padded.bottom, 1130); + expect(padded.width, 2020); // 1920 + 2*50 + expect(padded.height, 1180); // 1080 + 2*50 + }); + + test('works with non-zero origin rect', () { + final rect = Rect.fromLTRB(100, 200, 1920, 1080); + final padded = computePaddedRect(rect, 100)!; + expect(padded.left, 0); + expect(padded.top, 100); + expect(padded.right, 2020); + expect(padded.bottom, 1180); + }); + + test('handles max margin (400)', () { + final rect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(rect, 400)!; + expect(padded.width, 2720); // 1920 + 2*400 + expect(padded.height, 1880); // 1080 + 2*400 + }); + }); + + // =========================================================================== + // displayPadding computation + // =========================================================================== + group('displayPadding computation', () { + test('returns 0 when either rect is null', () { + final rect = Rect.fromLTWH(0, 0, 100, 100); + expect(computeDisplayPaddingX(null, rect), 0); + expect(computeDisplayPaddingX(rect, null), 0); + expect(computeDisplayPaddingY(null, rect), 0); + expect(computeDisplayPaddingY(rect, null), 0); + }); + + test('padding equals the margin value', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final paddedRect = computePaddedRect(realRect, 75)!; + expect(computeDisplayPaddingX(paddedRect, realRect), 75); + expect(computeDisplayPaddingY(paddedRect, realRect), 75); + }); + + test('padding is 0 when margin is 0', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final paddedRect = computePaddedRect(realRect, 0)!; + expect(computeDisplayPaddingX(paddedRect, realRect), 0); + expect(computeDisplayPaddingY(paddedRect, realRect), 0); + }); + + test('padding equals margin for non-zero origin rect', () { + final realRect = Rect.fromLTRB(100, 200, 1920, 1080); + final paddedRect = computePaddedRect(realRect, 150)!; + expect(computeDisplayPaddingX(paddedRect, realRect), 150); + expect(computeDisplayPaddingY(paddedRect, realRect), 150); + }); + }); + + // =========================================================================== + // Display dimensions with margin + // =========================================================================== + group('Display dimensions with margin', () { + test('includes margin in display width and height', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(realRect, 50)!; + expect(computeDisplayWidth(padded), 2020); + expect(computeDisplayHeight(padded), 1180); + }); + + test('returns default dimensions when paddedRect is null', () { + expect(computeDisplayWidth(null, isDesktop: true), + kDesktopDefaultDisplayWidth); + expect(computeDisplayHeight(null, isDesktop: true), + kDesktopDefaultDisplayHeight); + }); + + test('no margin means display dimensions equal real rect', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(realRect, 0)!; + expect(computeDisplayWidth(padded), 1920); + expect(computeDisplayHeight(padded), 1080); + }); + + test('max margin (400) display dimensions', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(realRect, 400)!; + expect(computeDisplayWidth(padded), 2720); + expect(computeDisplayHeight(padded), 1880); + }); + }); + + // =========================================================================== + // ViewStyle.scale with different display ratios and margins + // =========================================================================== + group('ViewStyle.scale computation', () { + test('original style always returns scale 1.0', () { + final vs = ViewStyle( + style: kRemoteViewStyleOriginal, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080, + ); + expect(vs.scale, 1.0); + }); + + test('adaptive — same local and remote dimensions', () { + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080, + ); + expect(vs.scale, 1.0); + }); + + test('adaptive — remote 2x larger (3840x2160 into 1920x1080)', () { + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 3840, + displayHeight: 2160, + ); + expect(vs.scale, 0.5); + }); + + test('adaptive — picks smaller ratio when aspect ratios differ', () { + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 3840, + displayHeight: 1080, + ); + // s1 = 1920/3840 = 0.5, s2 = 1080/1080 = 1.0 → 0.5 + expect(vs.scale, 0.5); + }); + + test('adaptive — margin-expanded display (margin=100 on 1920x1080)', () { + // Padded: 2120x1280 + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 2120, + displayHeight: 1280, + ); + // s1 = 1920/2120 ≈ 0.9057, s2 = 1080/1280 = 0.84375 → 0.84375 + expect(vs.scale, closeTo(0.84375, 0.0001)); + }); + + test('adaptive — 2x remote scale, 1x local, margin=100', () { + // Remote: 3840x2160, margin=100 → padded: 4040x2360 + // Local: 1920x1080 + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 4040, + displayHeight: 2360, + ); + expect(vs.scale, closeTo(1080.0 / 2360, 0.0001)); + }); + + test('adaptive — 1x remote, 2x local (HiDPI), margin=100', () { + // Remote: 1920x1080, margin=100 → padded: 2120x1280 + // Local: 3840x2160 + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 3840, + height: 2160, + displayWidth: 2120, + displayHeight: 1280, + ); + expect(vs.scale, closeTo(2160.0 / 1280, 0.0001)); + }); + + test('custom style returns 1.0 (actual scale applied externally)', () { + final vs = ViewStyle( + style: kRemoteViewStyleCustom, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080, + ); + expect(vs.scale, 1.0); + }); + }); + + // =========================================================================== + // ViewStyle equality — margin changes display dimensions + // =========================================================================== + group('ViewStyle equality', () { + test('equal when all fields match', () { + final a = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080); + final b = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080); + expect(a, equals(b)); + }); + + test('not equal when margin changes display dimensions', () { + final noMargin = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 1920, + displayHeight: 1080); + final withMargin = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 2120, + displayHeight: 1280); + expect(noMargin, isNot(equals(withMargin))); + }); + }); + + // =========================================================================== + // Canvas offset centering (_resetCanvasOffset) + // =========================================================================== + group('Canvas offset centering', () { + test('same size, no margin — offset is 0', () { + final (x, y) = computeCanvasOffset(Size(1920, 1080), 1920, 1080, 1.0); + expect(x, 0); + expect(y, 0); + }); + + test('display smaller than view — positive offset centers it', () { + final (x, y) = computeCanvasOffset(Size(1920, 1080), 1280, 720, 1.0); + expect(x, 320); // (1920 - 1280) / 2 + expect(y, 180); // (1080 - 720) / 2 + }); + + test('with margin — padded display affects centering', () { + // View: 1920x1080, padded display: 2120x1280, adaptive scale + final scale = 1080.0 / 1280; // 0.84375 + final (x, y) = computeCanvasOffset(Size(1920, 1080), 2120, 1280, scale); + expect(x, closeTo((1920 - 2120 * scale) / 2, 0.01)); + expect(y, closeTo((1080 - 1280 * scale) / 2, 0.01)); + }); + + test('2x remote display with margin — scale < 1', () { + // View: 1920x1080, padded: 4040x2360 + final scale = 1080.0 / 2360; + final (x, y) = computeCanvasOffset(Size(1920, 1080), 4040, 2360, scale); + expect(x, closeTo((1920 - 4040 * scale) / 2, 0.01)); + expect(y, closeTo((1080 - 2360 * scale) / 2, 0.01)); + }); + + test('small remote, large local (HiDPI) with margin — scale > 1', () { + // View: 3840x2160, padded: 2120x1280 + final scale = 2160.0 / 1280; + final (x, y) = computeCanvasOffset(Size(3840, 2160), 2120, 1280, scale); + expect(x, closeTo((3840 - 2120 * scale) / 2, 0.01)); + expect(y, closeTo((2160 - 1280 * scale) / 2, 0.01)); + }); + }); + + // =========================================================================== + // CanvasCoords serialization + // =========================================================================== + group('CanvasCoords serialization', () { + test('toJson includes padding and display fields', () { + final coords = CanvasCoords(); + coords.paddingX = 100; + coords.paddingY = 100; + coords.displayWidth = 2120; + coords.displayHeight = 1280; + + final json = coords.toJson(); + expect(json['paddingX'], 100); + expect(json['paddingY'], 100); + expect(json['displayWidth'], 2120); + expect(json['displayHeight'], 1280); + }); + + test('fromJson roundtrip preserves all fields', () { + final original = CanvasCoords(); + original.x = 10; + original.y = 20; + original.scale = 0.75; + original.scrollX = 0.1; + original.scrollY = 0.2; + original.displayWidth = 2120; + original.displayHeight = 1280; + original.paddingX = 100; + original.paddingY = 100; + original.scrollStyle = ScrollStyle.scrollbar; + original.size = Size(1920, 1080); + + final json = original.toJson(); + final restored = CanvasCoords.fromJson(json); + + expect(restored.x, original.x); + expect(restored.y, original.y); + expect(restored.scale, original.scale); + expect(restored.scrollX, original.scrollX); + expect(restored.scrollY, original.scrollY); + expect(restored.displayWidth, original.displayWidth); + expect(restored.displayHeight, original.displayHeight); + expect(restored.paddingX, original.paddingX); + expect(restored.paddingY, original.paddingY); + expect(restored.scrollStyle, original.scrollStyle); + expect(restored.size, original.size); + }); + + test('fromJson defaults padding/display to 0 when fields missing', () { + final json = { + 'x': 0.0, + 'y': 0.0, + 'scale': 1.0, + 'scrollX': 0.0, + 'scrollY': 0.0, + 'scrollStyle': 'scrollauto', + 'size': {'w': 1920.0, 'h': 1080.0}, + }; + final coords = CanvasCoords.fromJson(json); + expect(coords.displayWidth, 0); + expect(coords.displayHeight, 0); + expect(coords.paddingX, 0); + expect(coords.paddingY, 0); + }); + }); + + // =========================================================================== + // Pointer coordinate transforms with margin + // =========================================================================== + group('Pointer coordinate transforms', () { + test('no margin — pointer maps directly (scrollauto)', () { + final canvas = CanvasCoords(); + canvas.x = 0; + canvas.y = 0; + canvas.scale = 1.0; + canvas.displayWidth = 1920; + canvas.displayHeight = 1080; + canvas.paddingX = 0; + canvas.paddingY = 0; + canvas.scrollStyle = ScrollStyle.scrollauto; + canvas.size = Size(1920, 1080); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + final result = computePointerPosition( + pointerX: 960, pointerY: 540, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(960, 0.01)); + expect(result.$2, closeTo(540, 0.01)); + }); + + test('with margin — pointer offset by padding (scrollauto)', () { + final canvas = CanvasCoords(); + canvas.displayWidth = 2120; // 1920 + 2*100 + canvas.displayHeight = 1280; // 1080 + 2*100 + canvas.paddingX = 100; + canvas.paddingY = 100; + canvas.scale = 1.0; + canvas.x = 0; + canvas.y = 0; + canvas.scrollStyle = ScrollStyle.scrollauto; + canvas.size = Size(2120, 1280); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + + // Pointer at (100, 100) → subtract padding → remote (0, 0) + final result = computePointerPosition( + pointerX: 100, pointerY: 100, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(0, 0.01)); + expect(result.$2, closeTo(0, 0.01)); + }); + + test('with margin — center of padded view maps to center of remote', () { + final canvas = CanvasCoords(); + canvas.displayWidth = 2120; + canvas.displayHeight = 1280; + canvas.paddingX = 100; + canvas.paddingY = 100; + canvas.scale = 1.0; + canvas.x = 0; + canvas.y = 0; + canvas.scrollStyle = ScrollStyle.scrollauto; + canvas.size = Size(2120, 1280); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + + // Center of padded display: (1060, 640) → minus padding → (960, 540) + final result = computePointerPosition( + pointerX: 1060, + pointerY: 640, + canvas: canvas, + remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(960, 0.01)); + expect(result.$2, closeTo(540, 0.01)); + }); + + test('with margin and adaptive scale — 2x remote display', () { + // Remote: 3840x2160, margin: 100, padded: 4040x2360 + // Local view: 1920x1080, adaptive scale: 1080/2360 + final scale = 1080.0 / 2360; + final displayWidth = 4040.0; + final displayHeight = 2360.0; + + final canvas = CanvasCoords(); + canvas.displayWidth = displayWidth; + canvas.displayHeight = displayHeight; + canvas.paddingX = 100; + canvas.paddingY = 100; + canvas.scale = scale; + canvas.x = (1920 - displayWidth * scale) / 2; + canvas.y = (1080 - displayHeight * scale) / 2; + canvas.scrollStyle = ScrollStyle.scrollauto; + canvas.size = Size(1920, 1080); + + final remoteRect = Rect.fromLTWH(0, 0, 3840, 2160); + + // Pointer at center of view → should map to center of remote + final result = computePointerPosition( + pointerX: 960, pointerY: 540, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(1920, 1)); + expect(result.$2, closeTo(1080, 1)); + }); + + test('with margin — scrollbar style, no scroll offset', () { + final canvas = CanvasCoords(); + canvas.displayWidth = 2020; // 1920 + 2*50 + canvas.displayHeight = 1180; // 1080 + 2*50 + canvas.paddingX = 50; + canvas.paddingY = 50; + canvas.scale = 1.0; + canvas.scrollX = 0; + canvas.scrollY = 0; + canvas.scrollStyle = ScrollStyle.scrollbar; + canvas.size = Size(1920, 1080); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + + // Image (2020x1180) > view (1920x1080), no centering. + // Pointer at (50, 50), scrollX=0 → x=50/1.0=50, paddedX=50 + // x = 50 - 50(padding) + 0(rect.left) = 0 + final result = computePointerPosition( + pointerX: 50, pointerY: 50, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(0, 0.01)); + expect(result.$2, closeTo(0, 0.01)); + }); + + test('with margin — scrollbar style, 50% scroll offset', () { + final canvas = CanvasCoords(); + canvas.displayWidth = 2020; + canvas.displayHeight = 1180; + canvas.paddingX = 50; + canvas.paddingY = 50; + canvas.scale = 1.0; + canvas.scrollX = 0.5; + canvas.scrollY = 0.5; + canvas.scrollStyle = ScrollStyle.scrollbar; + canvas.size = Size(1920, 1080); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + + // With 50% scroll: x = 0 + 2020*0.5 = 1010 + // Image > view → no centering subtraction + // /scale(1.0) → 1010, minus padding(50) = 960 + final result = computePointerPosition( + pointerX: 0, pointerY: 0, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(960, 0.01)); + expect(result.$2, closeTo(540, 0.01)); + }); + + test('pointer in margin area clamps to remote rect boundary', () { + final canvas = CanvasCoords(); + canvas.displayWidth = 2120; + canvas.displayHeight = 1280; + canvas.paddingX = 100; + canvas.paddingY = 100; + canvas.scale = 1.0; + canvas.x = 0; + canvas.y = 0; + canvas.scrollStyle = ScrollStyle.scrollauto; + canvas.size = Size(2120, 1280); + + final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + + // Pointer at (50, 50): inside padded rect, but maps to (-50, -50) + // in remote coords → clamped to (0, 0) + final result = computePointerPosition( + pointerX: 50, pointerY: 50, canvas: canvas, remoteRect: remoteRect); + + expect(result, isNotNull); + expect(result!.$1, closeTo(0, 0.01)); + expect(result.$2, closeTo(0, 0.01)); + }); + }); + + // =========================================================================== + // End-to-end: different scale ratios with margin + // =========================================================================== + group('Scale ratio scenarios with margin', () { + test('1:1 ratio, no margin', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(realRect, 0)!; + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: padded.width.toInt(), + displayHeight: padded.height.toInt(), + ); + expect(vs.scale, 1.0); + expect(computeDisplayWidth(padded), 1920); + expect(computeDisplayHeight(padded), 1080); + }); + + test('2x remote, 1x local, margin=100', () { + final realRect = Rect.fromLTWH(0, 0, 3840, 2160); + final padded = computePaddedRect(realRect, 100)!; + expect(padded.width, 4040); + expect(padded.height, 2360); + + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 4040, + displayHeight: 2360, + ); + expect(vs.scale, closeTo(1080.0 / 2360, 0.0001)); + }); + + test('1x remote, 2x local (HiDPI), margin=200', () { + final realRect = Rect.fromLTWH(0, 0, 1920, 1080); + final padded = computePaddedRect(realRect, 200)!; + expect(padded.width, 2320); + expect(padded.height, 1480); + + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 3840, + height: 2160, + displayWidth: 2320, + displayHeight: 1480, + ); + expect(vs.scale, closeTo(2160.0 / 1480, 0.0001)); + }); + + test('1.5x remote, 1x local, margin=50, original view style', () { + final realRect = Rect.fromLTWH(0, 0, 2880, 1620); + final padded = computePaddedRect(realRect, 50)!; + + final vs = ViewStyle( + style: kRemoteViewStyleOriginal, + width: 1920, + height: 1080, + displayWidth: padded.width.toInt(), + displayHeight: padded.height.toInt(), + ); + // Original always 1.0 + expect(vs.scale, 1.0); + }); + + test('portrait remote, landscape local, margin=100', () { + final realRect = Rect.fromLTWH(0, 0, 1080, 1920); + final padded = computePaddedRect(realRect, 100)!; + expect(padded.width, 1280); + expect(padded.height, 2120); + + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 1280, + displayHeight: 2120, + ); + // s1 = 1920/1280 = 1.5, s2 = 1080/2120 ≈ 0.5094 → 0.5094 + expect(vs.scale, closeTo(1080.0 / 2120, 0.0001)); + }); + + test('multi-monitor: wide remote, standard local, margin=100', () { + // Dual-monitor remote: 3840x1080, margin=100 → padded: 4040x1280 + // Local: 1920x1080 + final realRect = Rect.fromLTWH(0, 0, 3840, 1080); + final padded = computePaddedRect(realRect, 100)!; + expect(padded.width, 4040); + expect(padded.height, 1280); + + final vs = ViewStyle( + style: kRemoteViewStyleAdaptive, + width: 1920, + height: 1080, + displayWidth: 4040, + displayHeight: 1280, + ); + // s1 = 1920/4040 ≈ 0.4752, s2 = 1080/1280 = 0.84375 → 0.4752 + expect(vs.scale, closeTo(1920.0 / 4040, 0.0001)); + }); + }); +} From 4f301fe4275fad02f67a219126cc3501c70693e7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 5 May 2026 23:35:53 +0800 Subject: [PATCH 09/14] feat(canvas): canvas margin options Signed-off-by: fufesou --- .../desktop/pages/desktop_setting_page.dart | 17 ++- .../lib/desktop/widgets/remote_toolbar.dart | 130 ++++++++---------- flutter/lib/models/model.dart | 36 ++++- 3 files changed, 100 insertions(+), 83 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 3c1c0af16..6df1ce531 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -13,7 +13,6 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -1813,21 +1812,25 @@ class _DisplayState extends State<_Display> { } Widget remoteCanvasMargin(BuildContext context) { - final canvasModel = Provider.of(context, listen: false); onChanged(double value) async { - await canvasModel.setRemoteCanvasMargin(value); + final normalizedValue = value.clamp(0, 400).round(); + await bind.mainSetUserDefaultOption( + key: kOptionRemoteCanvasMargin, value: normalizedValue.toString()); setState(() {}); } - final currentValue = canvasModel.remoteCanvasMargin; + final currentValue = (double.tryParse(bind.mainGetUserDefaultOption( + key: kOptionRemoteCanvasMargin)) ?? + 0) + .clamp(0, 400) + .toDouble(); - return _Card(title: 'Remote canvas margin', children: [ + return _Card(title: 'canvas_margin', children: [ EdgeThicknessControl( value: currentValue, min: 0, max: 400, - onChanged: - isOptionFixed(kOptionRemoteCanvasMargin) ? null : onChanged, + onChanged: onChanged, ), ]); } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 8ad6238bc..820b78730 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1104,32 +1104,24 @@ class _DisplayMenuState extends State<_DisplayMenu> { return futureBuilder(future: () async { final viewStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? ''; - final visible = viewStyle == kRemoteViewStyleOriginal || + final scrollVisible = viewStyle == kRemoteViewStyleOriginal || viewStyle == kRemoteViewStyleCustom; final scrollStyle = await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? ''; final edgeScrollEdgeThickness = await bind .sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId); - final remoteCanvasMargin = - (double.tryParse(bind.mainGetUserDefaultOption( - key: kOptionRemoteCanvasMargin)) ?? - 0) - .clamp(0, 400) - .toDouble(); + await widget.ffi.canvasModel.initializeRemoteCanvasMargin(); return { - 'visible': visible, + 'scrollVisible': scrollVisible, 'scrollStyle': scrollStyle, 'edgeScrollEdgeThickness': edgeScrollEdgeThickness, - 'remoteCanvasMargin': remoteCanvasMargin, + 'remoteCanvasMargin': widget.ffi.canvasModel.remoteCanvasMargin, }; }(), hasData: (data) { - final visible = data['visible'] as bool; - if (!visible) return Offstage(); + final scrollVisible = data['scrollVisible'] as bool; final groupValue = data['scrollStyle'] as String; final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int; final remoteCanvasMargin = data['remoteCanvasMargin'] as double; - final isRemoteCanvasMarginFixed = - isOptionFixed(kOptionRemoteCanvasMargin); onChangeScrollStyle(String? value) async { if (value == null) return; @@ -1149,73 +1141,73 @@ class _DisplayMenuState extends State<_DisplayMenu> { } onChangeRemoteCanvasMargin(double? value) async { - if (value == null || isRemoteCanvasMarginFixed) return; + if (value == null) return; await widget.ffi.canvasModel.setRemoteCanvasMargin(value); state.setState(() {}); } - return Obx(() => Column(children: [ - RdoMenuButton( - child: Text(translate('ScrollAuto')), - value: kRemoteScrollStyleAuto, - groupValue: groupValue, - onChanged: widget.ffi.canvasModel.imageOverflow.value - ? (value) => onChangeScrollStyle(value) - : null, - closeOnActivate: groupValue != kRemoteScrollStyleEdge, - ffi: widget.ffi, - ), - RdoMenuButton( - child: Text(translate('Scrollbar')), - value: kRemoteScrollStyleBar, - groupValue: groupValue, - onChanged: widget.ffi.canvasModel.imageOverflow.value - ? (value) => onChangeScrollStyle(value) - : null, - closeOnActivate: groupValue != kRemoteScrollStyleEdge, - ffi: widget.ffi, - ), - if (!isWeb) ...[ - RdoMenuButton( - child: Text(translate('ScrollEdge')), - value: kRemoteScrollStyleEdge, + return Column(children: [ + if (scrollVisible) ...[ + Obx(() => RdoMenuButton( + child: Text(translate('ScrollAuto')), + value: kRemoteScrollStyleAuto, groupValue: groupValue, - closeOnActivate: false, onChanged: widget.ffi.canvasModel.imageOverflow.value ? (value) => onChangeScrollStyle(value) : null, + closeOnActivate: groupValue != kRemoteScrollStyleEdge, ffi: widget.ffi, + )), + Obx(() => RdoMenuButton( + child: Text(translate('Scrollbar')), + value: kRemoteScrollStyleBar, + groupValue: groupValue, + onChanged: widget.ffi.canvasModel.imageOverflow.value + ? (value) => onChangeScrollStyle(value) + : null, + closeOnActivate: groupValue != kRemoteScrollStyleEdge, + ffi: widget.ffi, + )), + if (!isWeb) ...[ + Obx(() => RdoMenuButton( + child: Text(translate('ScrollEdge')), + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + closeOnActivate: false, + onChanged: widget.ffi.canvasModel.imageOverflow.value + ? (value) => onChangeScrollStyle(value) + : null, + ffi: widget.ffi, + )), + Offstage( + offstage: groupValue != kRemoteScrollStyleEdge, + child: EdgeThicknessControl( + value: edgeScrollEdgeThickness.toDouble(), + onChanged: onChangeEdgeScrollEdgeThickness, + colorScheme: colorScheme, + )), + ], + ], + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded(child: Text(translate('canvas_margin'))), + SizedBox( + width: 160, + child: EdgeThicknessControl( + value: remoteCanvasMargin, + min: 0, + max: 400, + onChanged: onChangeRemoteCanvasMargin, + colorScheme: colorScheme, + ), ), - Offstage( - offstage: groupValue != kRemoteScrollStyleEdge, - child: EdgeThicknessControl( - value: edgeScrollEdgeThickness.toDouble(), - onChanged: onChangeEdgeScrollEdgeThickness, - colorScheme: colorScheme, - )), ], - Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Expanded(child: Text(translate('canvas_margin'))), - SizedBox( - width: 160, - child: EdgeThicknessControl( - value: remoteCanvasMargin, - min: 0, - max: 400, - onChanged: isRemoteCanvasMarginFixed - ? null - : onChangeRemoteCanvasMargin, - colorScheme: colorScheme, - ), - ), - ], - ), - ), - Divider(), - ])); + ), + ), + Divider(), + ]); }); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 617d7975e..7ff88609c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2154,6 +2154,8 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; // edge scroll mode: trigger scrolling when the cursor is close to the edge of the view int _edgeScrollEdgeThickness = 100; + double _remoteCanvasMargin = 0; + bool _remoteCanvasMarginInitialized = false; // tracks whether edge scroll should be active, prevents spurious // scrolling when the cursor enters the view from outside EdgeScrollState _edgeScrollState = EdgeScrollState.inactive; @@ -2197,19 +2199,35 @@ class CanvasModel with ChangeNotifier { if (!(isDesktop || isWebDesktop)) { return 0; } - final value = - double.tryParse(bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin)) ?? - 0; - return min(400, max(0, value)); + return _remoteCanvasMargin; } Future setRemoteCanvasMargin(double value) async { final normalizedValue = value.clamp(0, 400).round(); - await bind.mainSetUserDefaultOption( - key: kOptionRemoteCanvasMargin, value: normalizedValue.toString()); + await bind.sessionSetFlutterOption( + sessionId: sessionId, + k: kOptionRemoteCanvasMargin, + v: normalizedValue.toString()); + _remoteCanvasMargin = normalizedValue.toDouble(); + _remoteCanvasMarginInitialized = true; await updateViewStyle(); } + Future initializeRemoteCanvasMargin() async { + if (_remoteCanvasMarginInitialized || !(isDesktop || isWebDesktop)) { + return; + } + final sessionValue = await bind.sessionGetFlutterOption( + sessionId: sessionId, k: kOptionRemoteCanvasMargin); + final defaultValue = + bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin); + final value = + sessionValue?.isNotEmpty == true ? sessionValue : defaultValue; + _remoteCanvasMargin = + (double.tryParse(value ?? '') ?? 0).clamp(0, 400).toDouble(); + _remoteCanvasMarginInitialized = true; + } + Rect? get paddedRect { final rect = realRect; if (rect == null) { @@ -2328,6 +2346,7 @@ class CanvasModel with ChangeNotifier { return; } + await initializeRemoteCanvasMargin(); updateSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); @@ -2706,6 +2725,8 @@ class CanvasModel with ChangeNotifier { _y = 0; _scale = 1.0; _lastViewStyle = ViewStyle.defaultViewStyle(); + _remoteCanvasMargin = 0; + _remoteCanvasMarginInitialized = false; _timerMobileFocusCanvasCursor?.cancel(); _timerMobileRestoreCanvasOffset?.cancel(); _offsetBeforeMobileSoftKeyboard = null; @@ -3230,7 +3251,8 @@ class CursorModel with ChangeNotifier { dx = min(dx, maxCursorCanMove); } } else if (dx < 0) { - final maxCanvasCanMove = (displayRect?.left ?? 0) - r.left.roundToDouble(); + final maxCanvasCanMove = + (displayRect?.left ?? 0) - r.left.roundToDouble(); tryMoveCanvasX = _x + dx < cx && maxCanvasCanMove < 0; if (tryMoveCanvasX) { dx = max(dx, maxCanvasCanMove); From a302703fe3b14dff5cd679008e2f6dade2f49aa0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 6 May 2026 11:01:36 +0800 Subject: [PATCH 10/14] feat(cavas): margin, refactor Limit remote canvas margin to remote desktop sessions, keep the toolbar option available across view styles, and fix panning into the added margin. Also update pt-PT wording and trim duplicated margin tests. Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 47 +- flutter/lib/models/model.dart | 27 +- flutter/test/canvas_margin_test.dart | 851 ++---------------- src/lang/pt_PT.rs | 2 +- 4 files changed, 150 insertions(+), 777 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 820b78730..e491d764b 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1108,19 +1108,26 @@ class _DisplayMenuState extends State<_DisplayMenu> { viewStyle == kRemoteViewStyleCustom; final scrollStyle = await bind.sessionGetScrollStyle(sessionId: ffi.sessionId) ?? ''; - final edgeScrollEdgeThickness = await bind - .sessionGetEdgeScrollEdgeThickness(sessionId: ffi.sessionId); + final edgeScrollEdgeThickness = scrollVisible + ? await bind.sessionGetEdgeScrollEdgeThickness( + sessionId: ffi.sessionId) + : null; await widget.ffi.canvasModel.initializeRemoteCanvasMargin(); return { 'scrollVisible': scrollVisible, 'scrollStyle': scrollStyle, 'edgeScrollEdgeThickness': edgeScrollEdgeThickness, + 'supportsRemoteCanvasMargin': + widget.ffi.canvasModel.supportsRemoteCanvasMargin, 'remoteCanvasMargin': widget.ffi.canvasModel.remoteCanvasMargin, }; }(), hasData: (data) { final scrollVisible = data['scrollVisible'] as bool; final groupValue = data['scrollStyle'] as String; - final edgeScrollEdgeThickness = data['edgeScrollEdgeThickness'] as int; + final edgeScrollEdgeThickness = + (data['edgeScrollEdgeThickness'] as int?) ?? 0; + final supportsRemoteCanvasMargin = + data['supportsRemoteCanvasMargin'] as bool; final remoteCanvasMargin = data['remoteCanvasMargin'] as double; onChangeScrollStyle(String? value) async { @@ -1188,24 +1195,26 @@ class _DisplayMenuState extends State<_DisplayMenu> { )), ], ], - Padding( - padding: EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Expanded(child: Text(translate('canvas_margin'))), - SizedBox( - width: 160, - child: EdgeThicknessControl( - value: remoteCanvasMargin, - min: 0, - max: 400, - onChanged: onChangeRemoteCanvasMargin, - colorScheme: colorScheme, + if (supportsRemoteCanvasMargin) ...[ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded(child: Text(translate('canvas_margin'))), + SizedBox( + width: 160, + child: EdgeThicknessControl( + value: remoteCanvasMargin, + min: 0, + max: 400, + onChanged: onChangeRemoteCanvasMargin, + colorScheme: colorScheme, + ), ), - ), - ], + ], + ), ), - ), + ], Divider(), ]); }); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7ff88609c..37e669513 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2196,13 +2196,20 @@ class CanvasModel with ChangeNotifier { Rect? get realRect => parent.target?.ffiModel.rect; double get remoteCanvasMargin { - if (!(isDesktop || isWebDesktop)) { + if (!supportsRemoteCanvasMargin) { return 0; } return _remoteCanvasMargin; } + bool get supportsRemoteCanvasMargin => + (isDesktop || isWebDesktop) && + parent.target?.connType == ConnType.defaultConn; + Future setRemoteCanvasMargin(double value) async { + if (!supportsRemoteCanvasMargin) { + return; + } final normalizedValue = value.clamp(0, 400).round(); await bind.sessionSetFlutterOption( sessionId: sessionId, @@ -2214,7 +2221,7 @@ class CanvasModel with ChangeNotifier { } Future initializeRemoteCanvasMargin() async { - if (_remoteCanvasMarginInitialized || !(isDesktop || isWebDesktop)) { + if (_remoteCanvasMarginInitialized || !supportsRemoteCanvasMargin) { return; } final sessionValue = await bind.sessionGetFlutterOption( @@ -3284,6 +3291,8 @@ class CursorModel with ChangeNotifier { } if (dx == 0 && dy == 0) return; + final canvasDx = dx; + final canvasDy = dy; Point? newPos; final rect = parent.target?.ffiModel.rect; @@ -3301,17 +3310,23 @@ class CursorModel with ChangeNotifier { rect, buttons: kPrimaryButton); if (newPos == null) { + if (tryMoveCanvasX && canvasDx != 0) { + parent.target?.canvasModel.panX(-canvasDx * scale); + } + if (tryMoveCanvasY && canvasDy != 0) { + parent.target?.canvasModel.panY(-canvasDy * scale); + } return; } dx = newPos.x - _x; dy = newPos.y - _y; _x = newPos.x; _y = newPos.y; - if (tryMoveCanvasX && dx != 0) { - parent.target?.canvasModel.panX(-dx * scale); + if (tryMoveCanvasX && canvasDx != 0) { + parent.target?.canvasModel.panX(-canvasDx * scale); } - if (tryMoveCanvasY && dy != 0) { - parent.target?.canvasModel.panY(-dy * scale); + if (tryMoveCanvasY && canvasDy != 0) { + parent.target?.canvasModel.panY(-canvasDy * scale); } parent.target?.inputModel.moveMouse(_x, _y); diff --git a/flutter/test/canvas_margin_test.dart b/flutter/test/canvas_margin_test.dart index aef8a4040..f55a34dfa 100644 --- a/flutter/test/canvas_margin_test.dart +++ b/flutter/test/canvas_margin_test.dart @@ -3,84 +3,13 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; -// ============================================================================= -// Standalone replicas of production types and logic for testability. -// -// These mirror the pure math from CanvasModel, ViewStyle, CanvasCoords, -// and InputModel._handlePointerDevicePos without pulling in the full app -// dependency tree (which includes FFI bindings that can't run in test). -// ============================================================================= +const _maxRemoteCanvasMargin = 400.0; +const _nearEdgeThreshold = 3.0; -// --- Constants (from consts.dart) --- -const int kDesktopDefaultDisplayWidth = 1080; -const int kDesktopDefaultDisplayHeight = 720; -const int kMobileDefaultDisplayWidth = 720; -const int kMobileDefaultDisplayHeight = 1280; -const kRemoteViewStyleOriginal = 'original'; -const kRemoteViewStyleAdaptive = 'adaptive'; -const kRemoteViewStyleCustom = 'custom'; +// Minimal test fixtures for the pure canvas-margin math. These intentionally +// cover only the fields used by pointer mapping. +enum ScrollStyle { scrollbar, scrollauto } -// --- ScrollStyle (from model.dart) --- -enum ScrollStyle { scrollbar, scrollauto, scrolledge } - -// --- ViewStyle (from model.dart) --- -class ViewStyle { - final String style; - final double width; - final double height; - final int displayWidth; - final int displayHeight; - - ViewStyle({ - required this.style, - required this.width, - required this.height, - required this.displayWidth, - required this.displayHeight, - }); - - static int _double2Int(double v) => (v * 100).round().toInt(); - - @override - bool operator ==(Object other) => - other is ViewStyle && - other.runtimeType == runtimeType && - _innerEqual(other); - - bool _innerEqual(ViewStyle other) { - return style == other.style && - ViewStyle._double2Int(other.width) == ViewStyle._double2Int(width) && - ViewStyle._double2Int(other.height) == ViewStyle._double2Int(height) && - other.displayWidth == displayWidth && - other.displayHeight == displayHeight; - } - - @override - int get hashCode => Object.hash( - style, - ViewStyle._double2Int(width), - ViewStyle._double2Int(height), - displayWidth, - displayHeight, - ).hashCode; - - double get scale { - double s = 1.0; - if (style == kRemoteViewStyleAdaptive) { - if (width != 0 && - height != 0 && - displayWidth != 0 && - displayHeight != 0) { - final s1 = width / displayWidth; - final s2 = height / displayHeight; - s = s1 < s2 ? s1 : s2; - } - } - return s; - } -} - -// --- CanvasCoords (from input_model.dart) --- class CanvasCoords { double x = 0; double y = 0; @@ -93,63 +22,16 @@ class CanvasCoords { double paddingY = 0; ScrollStyle scrollStyle = ScrollStyle.scrollauto; Size size = Size.zero; - - CanvasCoords(); - - Map toJson() { - return { - 'x': x, - 'y': y, - 'scale': scale, - 'scrollX': scrollX, - 'scrollY': scrollY, - 'displayWidth': displayWidth, - 'displayHeight': displayHeight, - 'paddingX': paddingX, - 'paddingY': paddingY, - 'scrollStyle': scrollStyle.name, - 'size': { - 'w': size.width, - 'h': size.height, - } - }; - } - - static CanvasCoords fromJson(Map json) { - final model = CanvasCoords(); - model.x = json['x']; - model.y = json['y']; - model.scale = json['scale']; - model.scrollX = json['scrollX']; - model.scrollY = json['scrollY']; - model.displayWidth = (json['displayWidth'] ?? 0).toDouble(); - model.displayHeight = (json['displayHeight'] ?? 0).toDouble(); - model.paddingX = (json['paddingX'] ?? 0).toDouble(); - model.paddingY = (json['paddingY'] ?? 0).toDouble(); - model.scrollStyle = ScrollStyle.values.firstWhere( - (e) => e.name == json['scrollStyle'], - orElse: () => ScrollStyle.scrollauto, - ); - model.size = Size(json['size']['w'], json['size']['h']); - return model; - } } -// ============================================================================= -// Helper functions replicating CanvasModel pure math -// ============================================================================= - -/// Replicates CanvasModel.remoteCanvasMargin clamping logic. double clampMargin(double value) { - return min(400, max(0, value)); + return min(_maxRemoteCanvasMargin, max(0, value)); } -/// Replicates CanvasModel.setRemoteCanvasMargin normalization logic. int normalizeMarginForStorage(double value) { - return value.clamp(0, 400).round(); + return value.clamp(0, _maxRemoteCanvasMargin).round(); } -/// Replicates CanvasModel.paddedRect computation. Rect? computePaddedRect(Rect? realRect, double margin) { if (realRect == null) return null; if (margin <= 0) return realRect; @@ -161,33 +43,31 @@ Rect? computePaddedRect(Rect? realRect, double margin) { ); } -/// Replicates CanvasModel.displayPaddingX computation. double computeDisplayPaddingX(Rect? paddedRect, Rect? realRect) { if (paddedRect == null || realRect == null) return 0; return realRect.left - paddedRect.left; } -/// Replicates CanvasModel.displayPaddingY computation. double computeDisplayPaddingY(Rect? paddedRect, Rect? realRect) { if (paddedRect == null || realRect == null) return 0; return realRect.top - paddedRect.top; } -/// Replicates CanvasModel.getDisplayWidth using paddedRect. -int computeDisplayWidth(Rect? paddedRect, {bool isDesktop = true}) { - final defaultWidth = - isDesktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; - return paddedRect?.width.toInt() ?? defaultWidth; +double computeAdaptiveScale({ + required double viewWidth, + required double viewHeight, + required int displayWidth, + required int displayHeight, +}) { + if (viewWidth == 0 || + viewHeight == 0 || + displayWidth == 0 || + displayHeight == 0) { + return 1.0; + } + return min(viewWidth / displayWidth, viewHeight / displayHeight); } -/// Replicates CanvasModel.getDisplayHeight using paddedRect. -int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { - final defaultHeight = - isDesktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; - return paddedRect?.height.toInt() ?? defaultHeight; -} - -/// Replicates CanvasModel._resetCanvasOffset centering computation. (double, double) computeCanvasOffset( Size viewSize, int displayWidth, int displayHeight, double scale) { final x = (viewSize.width - displayWidth * scale) / 2; @@ -195,8 +75,6 @@ int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { return (x, y); } -/// Replicates the core coordinate transform math from -/// InputModel._handlePointerDevicePos. (double, double)? computePointerPosition({ required double pointerX, required double pointerY, @@ -206,6 +84,8 @@ int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { double x = pointerX; double y = pointerY; + final nearRight = (canvas.size.width - x) < _nearEdgeThreshold; + final nearBottom = (canvas.size.height - y) < _nearEdgeThreshold; final displayWidth = canvas.displayWidth > 0 ? canvas.displayWidth : remoteRect.width; final displayHeight = @@ -230,6 +110,15 @@ int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { x /= canvas.scale; y /= canvas.scale; + if (canvas.scale > 0 && canvas.scale < 1) { + final step = 1.0 / canvas.scale - 1; + if (nearRight) { + x += step; + } + if (nearBottom) { + y += step; + } + } final paddedX = x; final paddedY = y; @@ -248,414 +137,56 @@ int computeDisplayHeight(Rect? paddedRect, {bool isDesktop = true}) { return (x, y); } -// ============================================================================= -// Tests -// ============================================================================= - void main() { - // =========================================================================== - // Margin clamping - // =========================================================================== - group('Margin clamping', () { - test('clamps negative values to 0', () { + group('Remote canvas margin math', () { + test('clamps and normalizes margin values', () { expect(clampMargin(-10), 0); - expect(clampMargin(-0.5), 0); - }); - - test('clamps values above 400 to 400', () { - expect(clampMargin(500), 400); - expect(clampMargin(999), 400); - }); - - test('passes through valid values', () { - expect(clampMargin(0), 0); expect(clampMargin(50), 50); - expect(clampMargin(200), 200); - expect(clampMargin(400), 400); - }); + expect(clampMargin(999), _maxRemoteCanvasMargin); - test('normalizeMarginForStorage rounds to int', () { expect(normalizeMarginForStorage(50.7), 51); - expect(normalizeMarginForStorage(50.3), 50); expect(normalizeMarginForStorage(-5), 0); - expect(normalizeMarginForStorage(999), 400); - }); - }); - - // =========================================================================== - // paddedRect computation - // =========================================================================== - group('paddedRect computation', () { - test('returns null when realRect is null', () { - expect(computePaddedRect(null, 50), isNull); + expect(normalizeMarginForStorage(999), _maxRemoteCanvasMargin.toInt()); }); - test('returns realRect unchanged when margin is 0', () { - final rect = Rect.fromLTWH(0, 0, 1920, 1080); - expect(computePaddedRect(rect, 0), rect); - }); - - test('returns realRect unchanged when margin is negative', () { - final rect = Rect.fromLTWH(0, 0, 1920, 1080); - expect(computePaddedRect(rect, -10), rect); - }); - - test('expands rect by margin on all sides', () { - final rect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(rect, 50)!; - expect(padded.left, -50); - expect(padded.top, -50); - expect(padded.right, 1970); - expect(padded.bottom, 1130); - expect(padded.width, 2020); // 1920 + 2*50 - expect(padded.height, 1180); // 1080 + 2*50 - }); - - test('works with non-zero origin rect', () { - final rect = Rect.fromLTRB(100, 200, 1920, 1080); - final padded = computePaddedRect(rect, 100)!; - expect(padded.left, 0); - expect(padded.top, 100); - expect(padded.right, 2020); - expect(padded.bottom, 1180); - }); - - test('handles max margin (400)', () { - final rect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(rect, 400)!; - expect(padded.width, 2720); // 1920 + 2*400 - expect(padded.height, 1880); // 1080 + 2*400 - }); - }); - - // =========================================================================== - // displayPadding computation - // =========================================================================== - group('displayPadding computation', () { - test('returns 0 when either rect is null', () { - final rect = Rect.fromLTWH(0, 0, 100, 100); - expect(computeDisplayPaddingX(null, rect), 0); - expect(computeDisplayPaddingX(rect, null), 0); - expect(computeDisplayPaddingY(null, rect), 0); - expect(computeDisplayPaddingY(rect, null), 0); - }); - - test('padding equals the margin value', () { + test('expands remote rect and derives display padding', () { final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final paddedRect = computePaddedRect(realRect, 75)!; - expect(computeDisplayPaddingX(paddedRect, realRect), 75); - expect(computeDisplayPaddingY(paddedRect, realRect), 75); + final paddedRect = computePaddedRect(realRect, 100)!; + + expect(paddedRect, Rect.fromLTRB(-100, -100, 2020, 1180)); + expect(computeDisplayPaddingX(paddedRect, realRect), 100); + expect(computeDisplayPaddingY(paddedRect, realRect), 100); }); - test('padding is 0 when margin is 0', () { + test('margin-expanded display affects adaptive scale and centering', () { final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final paddedRect = computePaddedRect(realRect, 0)!; - expect(computeDisplayPaddingX(paddedRect, realRect), 0); - expect(computeDisplayPaddingY(paddedRect, realRect), 0); - }); - - test('padding equals margin for non-zero origin rect', () { - final realRect = Rect.fromLTRB(100, 200, 1920, 1080); - final paddedRect = computePaddedRect(realRect, 150)!; - expect(computeDisplayPaddingX(paddedRect, realRect), 150); - expect(computeDisplayPaddingY(paddedRect, realRect), 150); - }); - }); - - // =========================================================================== - // Display dimensions with margin - // =========================================================================== - group('Display dimensions with margin', () { - test('includes margin in display width and height', () { - final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(realRect, 50)!; - expect(computeDisplayWidth(padded), 2020); - expect(computeDisplayHeight(padded), 1180); - }); - - test('returns default dimensions when paddedRect is null', () { - expect(computeDisplayWidth(null, isDesktop: true), - kDesktopDefaultDisplayWidth); - expect(computeDisplayHeight(null, isDesktop: true), - kDesktopDefaultDisplayHeight); - }); - - test('no margin means display dimensions equal real rect', () { - final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(realRect, 0)!; - expect(computeDisplayWidth(padded), 1920); - expect(computeDisplayHeight(padded), 1080); - }); - - test('max margin (400) display dimensions', () { - final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(realRect, 400)!; - expect(computeDisplayWidth(padded), 2720); - expect(computeDisplayHeight(padded), 1880); - }); - }); - - // =========================================================================== - // ViewStyle.scale with different display ratios and margins - // =========================================================================== - group('ViewStyle.scale computation', () { - test('original style always returns scale 1.0', () { - final vs = ViewStyle( - style: kRemoteViewStyleOriginal, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080, + final paddedRect = computePaddedRect(realRect, 100)!; + final displayWidth = paddedRect.width.toInt(); + final displayHeight = paddedRect.height.toInt(); + final scale = computeAdaptiveScale( + viewWidth: 1920, + viewHeight: 1080, + displayWidth: displayWidth, + displayHeight: displayHeight, ); - expect(vs.scale, 1.0); - }); + final (x, y) = computeCanvasOffset( + Size(1920, 1080), displayWidth, displayHeight, scale); - test('adaptive — same local and remote dimensions', () { - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080, - ); - expect(vs.scale, 1.0); - }); - - test('adaptive — remote 2x larger (3840x2160 into 1920x1080)', () { - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 3840, - displayHeight: 2160, - ); - expect(vs.scale, 0.5); - }); - - test('adaptive — picks smaller ratio when aspect ratios differ', () { - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 3840, - displayHeight: 1080, - ); - // s1 = 1920/3840 = 0.5, s2 = 1080/1080 = 1.0 → 0.5 - expect(vs.scale, 0.5); - }); - - test('adaptive — margin-expanded display (margin=100 on 1920x1080)', () { - // Padded: 2120x1280 - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 2120, - displayHeight: 1280, - ); - // s1 = 1920/2120 ≈ 0.9057, s2 = 1080/1280 = 0.84375 → 0.84375 - expect(vs.scale, closeTo(0.84375, 0.0001)); - }); - - test('adaptive — 2x remote scale, 1x local, margin=100', () { - // Remote: 3840x2160, margin=100 → padded: 4040x2360 - // Local: 1920x1080 - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 4040, - displayHeight: 2360, - ); - expect(vs.scale, closeTo(1080.0 / 2360, 0.0001)); - }); - - test('adaptive — 1x remote, 2x local (HiDPI), margin=100', () { - // Remote: 1920x1080, margin=100 → padded: 2120x1280 - // Local: 3840x2160 - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 3840, - height: 2160, - displayWidth: 2120, - displayHeight: 1280, - ); - expect(vs.scale, closeTo(2160.0 / 1280, 0.0001)); - }); - - test('custom style returns 1.0 (actual scale applied externally)', () { - final vs = ViewStyle( - style: kRemoteViewStyleCustom, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080, - ); - expect(vs.scale, 1.0); - }); - }); - - // =========================================================================== - // ViewStyle equality — margin changes display dimensions - // =========================================================================== - group('ViewStyle equality', () { - test('equal when all fields match', () { - final a = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080); - final b = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080); - expect(a, equals(b)); - }); - - test('not equal when margin changes display dimensions', () { - final noMargin = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 1920, - displayHeight: 1080); - final withMargin = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 2120, - displayHeight: 1280); - expect(noMargin, isNot(equals(withMargin))); - }); - }); - - // =========================================================================== - // Canvas offset centering (_resetCanvasOffset) - // =========================================================================== - group('Canvas offset centering', () { - test('same size, no margin — offset is 0', () { - final (x, y) = computeCanvasOffset(Size(1920, 1080), 1920, 1080, 1.0); - expect(x, 0); - expect(y, 0); - }); - - test('display smaller than view — positive offset centers it', () { - final (x, y) = computeCanvasOffset(Size(1920, 1080), 1280, 720, 1.0); - expect(x, 320); // (1920 - 1280) / 2 - expect(y, 180); // (1080 - 720) / 2 - }); - - test('with margin — padded display affects centering', () { - // View: 1920x1080, padded display: 2120x1280, adaptive scale - final scale = 1080.0 / 1280; // 0.84375 - final (x, y) = computeCanvasOffset(Size(1920, 1080), 2120, 1280, scale); + expect(scale, closeTo(1080 / 1280, 0.0001)); expect(x, closeTo((1920 - 2120 * scale) / 2, 0.01)); - expect(y, closeTo((1080 - 1280 * scale) / 2, 0.01)); - }); - - test('2x remote display with margin — scale < 1', () { - // View: 1920x1080, padded: 4040x2360 - final scale = 1080.0 / 2360; - final (x, y) = computeCanvasOffset(Size(1920, 1080), 4040, 2360, scale); - expect(x, closeTo((1920 - 4040 * scale) / 2, 0.01)); - expect(y, closeTo((1080 - 2360 * scale) / 2, 0.01)); - }); - - test('small remote, large local (HiDPI) with margin — scale > 1', () { - // View: 3840x2160, padded: 2120x1280 - final scale = 2160.0 / 1280; - final (x, y) = computeCanvasOffset(Size(3840, 2160), 2120, 1280, scale); - expect(x, closeTo((3840 - 2120 * scale) / 2, 0.01)); - expect(y, closeTo((2160 - 1280 * scale) / 2, 0.01)); + expect(y, closeTo(0, 0.01)); }); }); - // =========================================================================== - // CanvasCoords serialization - // =========================================================================== - group('CanvasCoords serialization', () { - test('toJson includes padding and display fields', () { - final coords = CanvasCoords(); - coords.paddingX = 100; - coords.paddingY = 100; - coords.displayWidth = 2120; - coords.displayHeight = 1280; - - final json = coords.toJson(); - expect(json['paddingX'], 100); - expect(json['paddingY'], 100); - expect(json['displayWidth'], 2120); - expect(json['displayHeight'], 1280); - }); - - test('fromJson roundtrip preserves all fields', () { - final original = CanvasCoords(); - original.x = 10; - original.y = 20; - original.scale = 0.75; - original.scrollX = 0.1; - original.scrollY = 0.2; - original.displayWidth = 2120; - original.displayHeight = 1280; - original.paddingX = 100; - original.paddingY = 100; - original.scrollStyle = ScrollStyle.scrollbar; - original.size = Size(1920, 1080); - - final json = original.toJson(); - final restored = CanvasCoords.fromJson(json); - - expect(restored.x, original.x); - expect(restored.y, original.y); - expect(restored.scale, original.scale); - expect(restored.scrollX, original.scrollX); - expect(restored.scrollY, original.scrollY); - expect(restored.displayWidth, original.displayWidth); - expect(restored.displayHeight, original.displayHeight); - expect(restored.paddingX, original.paddingX); - expect(restored.paddingY, original.paddingY); - expect(restored.scrollStyle, original.scrollStyle); - expect(restored.size, original.size); - }); - - test('fromJson defaults padding/display to 0 when fields missing', () { - final json = { - 'x': 0.0, - 'y': 0.0, - 'scale': 1.0, - 'scrollX': 0.0, - 'scrollY': 0.0, - 'scrollStyle': 'scrollauto', - 'size': {'w': 1920.0, 'h': 1080.0}, - }; - final coords = CanvasCoords.fromJson(json); - expect(coords.displayWidth, 0); - expect(coords.displayHeight, 0); - expect(coords.paddingX, 0); - expect(coords.paddingY, 0); - }); - }); - - // =========================================================================== - // Pointer coordinate transforms with margin - // =========================================================================== group('Pointer coordinate transforms', () { - test('no margin — pointer maps directly (scrollauto)', () { - final canvas = CanvasCoords(); - canvas.x = 0; - canvas.y = 0; - canvas.scale = 1.0; - canvas.displayWidth = 1920; - canvas.displayHeight = 1080; - canvas.paddingX = 0; - canvas.paddingY = 0; - canvas.scrollStyle = ScrollStyle.scrollauto; - canvas.size = Size(1920, 1080); - + test('no margin maps pointer directly in scrollauto mode', () { + final canvas = CanvasCoords() + ..scale = 1.0 + ..displayWidth = 1920 + ..displayHeight = 1080 + ..size = Size(1920, 1080); final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); + final result = computePointerPosition( pointerX: 960, pointerY: 540, canvas: canvas, remoteRect: remoteRect); @@ -664,21 +195,16 @@ void main() { expect(result.$2, closeTo(540, 0.01)); }); - test('with margin — pointer offset by padding (scrollauto)', () { - final canvas = CanvasCoords(); - canvas.displayWidth = 2120; // 1920 + 2*100 - canvas.displayHeight = 1280; // 1080 + 2*100 - canvas.paddingX = 100; - canvas.paddingY = 100; - canvas.scale = 1.0; - canvas.x = 0; - canvas.y = 0; - canvas.scrollStyle = ScrollStyle.scrollauto; - canvas.size = Size(2120, 1280); - + test('margin padding offsets pointer coordinates', () { + final canvas = CanvasCoords() + ..displayWidth = 2120 + ..displayHeight = 1280 + ..paddingX = 100 + ..paddingY = 100 + ..scale = 1.0 + ..size = Size(2120, 1280); final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); - // Pointer at (100, 100) → subtract padding → remote (0, 0) final result = computePointerPosition( pointerX: 100, pointerY: 100, canvas: canvas, remoteRect: remoteRect); @@ -687,53 +213,37 @@ void main() { expect(result.$2, closeTo(0, 0.01)); }); - test('with margin — center of padded view maps to center of remote', () { - final canvas = CanvasCoords(); - canvas.displayWidth = 2120; - canvas.displayHeight = 1280; - canvas.paddingX = 100; - canvas.paddingY = 100; - canvas.scale = 1.0; - canvas.x = 0; - canvas.y = 0; - canvas.scrollStyle = ScrollStyle.scrollauto; - canvas.size = Size(2120, 1280); - + test('pointer in margin area clamps to remote rect boundary', () { + final canvas = CanvasCoords() + ..displayWidth = 2120 + ..displayHeight = 1280 + ..paddingX = 100 + ..paddingY = 100 + ..scale = 1.0 + ..size = Size(2120, 1280); final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); - // Center of padded display: (1060, 640) → minus padding → (960, 540) final result = computePointerPosition( - pointerX: 1060, - pointerY: 640, - canvas: canvas, - remoteRect: remoteRect); + pointerX: 50, pointerY: 50, canvas: canvas, remoteRect: remoteRect); expect(result, isNotNull); - expect(result!.$1, closeTo(960, 0.01)); - expect(result.$2, closeTo(540, 0.01)); + expect(result!.$1, closeTo(0, 0.01)); + expect(result.$2, closeTo(0, 0.01)); }); - test('with margin and adaptive scale — 2x remote display', () { - // Remote: 3840x2160, margin: 100, padded: 4040x2360 - // Local view: 1920x1080, adaptive scale: 1080/2360 + test('margin and adaptive scale map view center to remote center', () { final scale = 1080.0 / 2360; - final displayWidth = 4040.0; - final displayHeight = 2360.0; - - final canvas = CanvasCoords(); - canvas.displayWidth = displayWidth; - canvas.displayHeight = displayHeight; - canvas.paddingX = 100; - canvas.paddingY = 100; - canvas.scale = scale; - canvas.x = (1920 - displayWidth * scale) / 2; - canvas.y = (1080 - displayHeight * scale) / 2; - canvas.scrollStyle = ScrollStyle.scrollauto; - canvas.size = Size(1920, 1080); - + final canvas = CanvasCoords() + ..displayWidth = 4040 + ..displayHeight = 2360 + ..paddingX = 100 + ..paddingY = 100 + ..scale = scale + ..x = (1920 - 4040 * scale) / 2 + ..y = (1080 - 2360 * scale) / 2 + ..size = Size(1920, 1080); final remoteRect = Rect.fromLTWH(0, 0, 3840, 2160); - // Pointer at center of view → should map to center of remote final result = computePointerPosition( pointerX: 960, pointerY: 540, canvas: canvas, remoteRect: remoteRect); @@ -742,181 +252,20 @@ void main() { expect(result.$2, closeTo(1080, 1)); }); - test('with margin — scrollbar style, no scroll offset', () { - final canvas = CanvasCoords(); - canvas.displayWidth = 2020; // 1920 + 2*50 - canvas.displayHeight = 1180; // 1080 + 2*50 - canvas.paddingX = 50; - canvas.paddingY = 50; - canvas.scale = 1.0; - canvas.scrollX = 0; - canvas.scrollY = 0; - canvas.scrollStyle = ScrollStyle.scrollbar; - canvas.size = Size(1920, 1080); + test('zoomed-out near edge applies edge correction', () { + final canvas = CanvasCoords() + ..scale = 0.5 + ..displayWidth = 200 + ..displayHeight = 200 + ..size = Size(100, 100); + final remoteRect = Rect.fromLTWH(0, 0, 200, 200); - final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); - - // Image (2020x1180) > view (1920x1080), no centering. - // Pointer at (50, 50), scrollX=0 → x=50/1.0=50, paddedX=50 - // x = 50 - 50(padding) + 0(rect.left) = 0 final result = computePointerPosition( - pointerX: 50, pointerY: 50, canvas: canvas, remoteRect: remoteRect); + pointerX: 99, pointerY: 99, canvas: canvas, remoteRect: remoteRect); expect(result, isNotNull); - expect(result!.$1, closeTo(0, 0.01)); - expect(result.$2, closeTo(0, 0.01)); - }); - - test('with margin — scrollbar style, 50% scroll offset', () { - final canvas = CanvasCoords(); - canvas.displayWidth = 2020; - canvas.displayHeight = 1180; - canvas.paddingX = 50; - canvas.paddingY = 50; - canvas.scale = 1.0; - canvas.scrollX = 0.5; - canvas.scrollY = 0.5; - canvas.scrollStyle = ScrollStyle.scrollbar; - canvas.size = Size(1920, 1080); - - final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); - - // With 50% scroll: x = 0 + 2020*0.5 = 1010 - // Image > view → no centering subtraction - // /scale(1.0) → 1010, minus padding(50) = 960 - final result = computePointerPosition( - pointerX: 0, pointerY: 0, canvas: canvas, remoteRect: remoteRect); - - expect(result, isNotNull); - expect(result!.$1, closeTo(960, 0.01)); - expect(result.$2, closeTo(540, 0.01)); - }); - - test('pointer in margin area clamps to remote rect boundary', () { - final canvas = CanvasCoords(); - canvas.displayWidth = 2120; - canvas.displayHeight = 1280; - canvas.paddingX = 100; - canvas.paddingY = 100; - canvas.scale = 1.0; - canvas.x = 0; - canvas.y = 0; - canvas.scrollStyle = ScrollStyle.scrollauto; - canvas.size = Size(2120, 1280); - - final remoteRect = Rect.fromLTWH(0, 0, 1920, 1080); - - // Pointer at (50, 50): inside padded rect, but maps to (-50, -50) - // in remote coords → clamped to (0, 0) - final result = computePointerPosition( - pointerX: 50, pointerY: 50, canvas: canvas, remoteRect: remoteRect); - - expect(result, isNotNull); - expect(result!.$1, closeTo(0, 0.01)); - expect(result.$2, closeTo(0, 0.01)); - }); - }); - - // =========================================================================== - // End-to-end: different scale ratios with margin - // =========================================================================== - group('Scale ratio scenarios with margin', () { - test('1:1 ratio, no margin', () { - final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(realRect, 0)!; - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: padded.width.toInt(), - displayHeight: padded.height.toInt(), - ); - expect(vs.scale, 1.0); - expect(computeDisplayWidth(padded), 1920); - expect(computeDisplayHeight(padded), 1080); - }); - - test('2x remote, 1x local, margin=100', () { - final realRect = Rect.fromLTWH(0, 0, 3840, 2160); - final padded = computePaddedRect(realRect, 100)!; - expect(padded.width, 4040); - expect(padded.height, 2360); - - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 4040, - displayHeight: 2360, - ); - expect(vs.scale, closeTo(1080.0 / 2360, 0.0001)); - }); - - test('1x remote, 2x local (HiDPI), margin=200', () { - final realRect = Rect.fromLTWH(0, 0, 1920, 1080); - final padded = computePaddedRect(realRect, 200)!; - expect(padded.width, 2320); - expect(padded.height, 1480); - - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 3840, - height: 2160, - displayWidth: 2320, - displayHeight: 1480, - ); - expect(vs.scale, closeTo(2160.0 / 1480, 0.0001)); - }); - - test('1.5x remote, 1x local, margin=50, original view style', () { - final realRect = Rect.fromLTWH(0, 0, 2880, 1620); - final padded = computePaddedRect(realRect, 50)!; - - final vs = ViewStyle( - style: kRemoteViewStyleOriginal, - width: 1920, - height: 1080, - displayWidth: padded.width.toInt(), - displayHeight: padded.height.toInt(), - ); - // Original always 1.0 - expect(vs.scale, 1.0); - }); - - test('portrait remote, landscape local, margin=100', () { - final realRect = Rect.fromLTWH(0, 0, 1080, 1920); - final padded = computePaddedRect(realRect, 100)!; - expect(padded.width, 1280); - expect(padded.height, 2120); - - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 1280, - displayHeight: 2120, - ); - // s1 = 1920/1280 = 1.5, s2 = 1080/2120 ≈ 0.5094 → 0.5094 - expect(vs.scale, closeTo(1080.0 / 2120, 0.0001)); - }); - - test('multi-monitor: wide remote, standard local, margin=100', () { - // Dual-monitor remote: 3840x1080, margin=100 → padded: 4040x1280 - // Local: 1920x1080 - final realRect = Rect.fromLTWH(0, 0, 3840, 1080); - final padded = computePaddedRect(realRect, 100)!; - expect(padded.width, 4040); - expect(padded.height, 1280); - - final vs = ViewStyle( - style: kRemoteViewStyleAdaptive, - width: 1920, - height: 1080, - displayWidth: 4040, - displayHeight: 1280, - ); - // s1 = 1920/4040 ≈ 0.4752, s2 = 1080/1280 = 0.84375 → 0.4752 - expect(vs.scale, closeTo(1920.0 / 4040, 0.0001)); + expect(result!.$1, closeTo(199, 0.01)); + expect(result.$2, closeTo(199, 0.01)); }); }); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d93cc98cd..488554a68 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -117,7 +117,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Bloquear entrada de utilizador"), ("Unblock user input", "Desbloquear entrada de utilizador"), ("Adjust Window", "Ajustar Janela"), - ("canvas_margin", "Margem da tela"), + ("canvas_margin", "Margem do ecrã"), ("Original", "Original"), ("Shrink", "Reduzir"), ("Stretch", "Aumentar"), From 996f0ad99e93f31e160d4037c1eaa88dd860af52 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 6 May 2026 11:43:51 +0800 Subject: [PATCH 11/14] feat(canvas): margin, refactor Signed-off-by: fufesou --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_setting_page.dart | 6 +++--- flutter/lib/desktop/widgets/remote_toolbar.dart | 7 ++++++- flutter/lib/models/model.dart | 16 ++++++++++------ flutter/test/canvas_margin_test.dart | 10 +++++----- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 9ace9da01..a59c135eb 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -82,6 +82,7 @@ const String kOptionViewStyle = "view_style"; const String kOptionScrollStyle = "scroll_style"; const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness"; const String kOptionRemoteCanvasMargin = "remote-canvas-margin"; +const double kMaxRemoteCanvasMargin = 400.0; const String kOptionImageQuality = "image_quality"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionTextureRender = "use-texture-render"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 6df1ce531..d49e38597 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1813,7 +1813,7 @@ class _DisplayState extends State<_Display> { Widget remoteCanvasMargin(BuildContext context) { onChanged(double value) async { - final normalizedValue = value.clamp(0, 400).round(); + final normalizedValue = value.clamp(0, kMaxRemoteCanvasMargin).round(); await bind.mainSetUserDefaultOption( key: kOptionRemoteCanvasMargin, value: normalizedValue.toString()); setState(() {}); @@ -1822,14 +1822,14 @@ class _DisplayState extends State<_Display> { final currentValue = (double.tryParse(bind.mainGetUserDefaultOption( key: kOptionRemoteCanvasMargin)) ?? 0) - .clamp(0, 400) + .clamp(0, kMaxRemoteCanvasMargin) .toDouble(); return _Card(title: 'canvas_margin', children: [ EdgeThicknessControl( value: currentValue, min: 0, - max: 400, + max: kMaxRemoteCanvasMargin, onChanged: onChanged, ), ]); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index e491d764b..f87b64211 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1129,6 +1129,11 @@ class _DisplayMenuState extends State<_DisplayMenu> { final supportsRemoteCanvasMargin = data['supportsRemoteCanvasMargin'] as bool; final remoteCanvasMargin = data['remoteCanvasMargin'] as double; + final hasVisibleControls = scrollVisible || supportsRemoteCanvasMargin; + + if (!hasVisibleControls) { + return SizedBox.shrink(); + } onChangeScrollStyle(String? value) async { if (value == null) return; @@ -1206,7 +1211,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { child: EdgeThicknessControl( value: remoteCanvasMargin, min: 0, - max: 400, + max: kMaxRemoteCanvasMargin, onChanged: onChangeRemoteCanvasMargin, colorScheme: colorScheme, ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 37e669513..9af4903b4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2210,7 +2210,7 @@ class CanvasModel with ChangeNotifier { if (!supportsRemoteCanvasMargin) { return; } - final normalizedValue = value.clamp(0, 400).round(); + final normalizedValue = value.clamp(0, kMaxRemoteCanvasMargin).round(); await bind.sessionSetFlutterOption( sessionId: sessionId, k: kOptionRemoteCanvasMargin, @@ -2231,7 +2231,9 @@ class CanvasModel with ChangeNotifier { final value = sessionValue?.isNotEmpty == true ? sessionValue : defaultValue; _remoteCanvasMargin = - (double.tryParse(value ?? '') ?? 0).clamp(0, 400).toDouble(); + (double.tryParse(value ?? '') ?? 0) + .clamp(0, kMaxRemoteCanvasMargin) + .toDouble(); _remoteCanvasMarginInitialized = true; } @@ -3322,11 +3324,13 @@ class CursorModel with ChangeNotifier { dy = newPos.y - _y; _x = newPos.x; _y = newPos.y; - if (tryMoveCanvasX && canvasDx != 0) { - parent.target?.canvasModel.panX(-canvasDx * scale); + final panDx = dx != 0 ? dx : canvasDx; + final panDy = dy != 0 ? dy : canvasDy; + if (tryMoveCanvasX && panDx != 0) { + parent.target?.canvasModel.panX(-panDx * scale); } - if (tryMoveCanvasY && canvasDy != 0) { - parent.target?.canvasModel.panY(-canvasDy * scale); + if (tryMoveCanvasY && panDy != 0) { + parent.target?.canvasModel.panY(-panDy * scale); } parent.target?.inputModel.moveMouse(_x, _y); diff --git a/flutter/test/canvas_margin_test.dart b/flutter/test/canvas_margin_test.dart index f55a34dfa..6206e9665 100644 --- a/flutter/test/canvas_margin_test.dart +++ b/flutter/test/canvas_margin_test.dart @@ -1,9 +1,9 @@ import 'dart:math'; import 'dart:ui'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_test/flutter_test.dart'; -const _maxRemoteCanvasMargin = 400.0; const _nearEdgeThreshold = 3.0; // Minimal test fixtures for the pure canvas-margin math. These intentionally @@ -25,11 +25,11 @@ class CanvasCoords { } double clampMargin(double value) { - return min(_maxRemoteCanvasMargin, max(0, value)); + return min(kMaxRemoteCanvasMargin, max(0.0, value)); } int normalizeMarginForStorage(double value) { - return value.clamp(0, _maxRemoteCanvasMargin).round(); + return value.clamp(0, kMaxRemoteCanvasMargin).round(); } Rect? computePaddedRect(Rect? realRect, double margin) { @@ -142,11 +142,11 @@ void main() { test('clamps and normalizes margin values', () { expect(clampMargin(-10), 0); expect(clampMargin(50), 50); - expect(clampMargin(999), _maxRemoteCanvasMargin); + expect(clampMargin(999), kMaxRemoteCanvasMargin); expect(normalizeMarginForStorage(50.7), 51); expect(normalizeMarginForStorage(-5), 0); - expect(normalizeMarginForStorage(999), _maxRemoteCanvasMargin.toInt()); + expect(normalizeMarginForStorage(999), kMaxRemoteCanvasMargin.toInt()); }); test('expands remote rect and derives display padding', () { From adf63f67b5e90d70b734da54f0803f5ea8fe01a8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 6 May 2026 14:19:43 +0800 Subject: [PATCH 12/14] feat(cavas): margin, refact toDouble() Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 6 +++++- flutter/lib/models/input_model.dart | 18 +++++++++++------- flutter/lib/models/model.dart | 10 ++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index f87b64211..6e2e6d77b 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1125,7 +1125,11 @@ class _DisplayMenuState extends State<_DisplayMenu> { final scrollVisible = data['scrollVisible'] as bool; final groupValue = data['scrollStyle'] as String; final edgeScrollEdgeThickness = - (data['edgeScrollEdgeThickness'] as int?) ?? 0; + ((data['edgeScrollEdgeThickness'] as int?) ?? + EdgeThicknessControl.kMin.round()) + .clamp(EdgeThicknessControl.kMin.round(), + EdgeThicknessControl.kMax.round()) + .toInt(); final supportsRemoteCanvasMargin = data['supportsRemoteCanvasMargin'] as bool; final remoteCanvasMargin = data['remoteCanvasMargin'] as double; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 76031559a..cbbb7afda 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -62,18 +62,21 @@ class CanvasCoords { static CanvasCoords fromJson(Map json) { final model = CanvasCoords(); - model.x = json['x']; - model.y = json['y']; - model.scale = json['scale']; - model.scrollX = json['scrollX']; - model.scrollY = json['scrollY']; + model.x = (json['x'] ?? 0).toDouble(); + model.y = (json['y'] ?? 0).toDouble(); + model.scale = (json['scale'] ?? 1).toDouble(); + model.scrollX = (json['scrollX'] ?? 0).toDouble(); + model.scrollY = (json['scrollY'] ?? 0).toDouble(); model.displayWidth = (json['displayWidth'] ?? 0).toDouble(); model.displayHeight = (json['displayHeight'] ?? 0).toDouble(); model.paddingX = (json['paddingX'] ?? 0).toDouble(); model.paddingY = (json['paddingY'] ?? 0).toDouble(); model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); - model.size = Size(json['size']['w'], json['size']['h']); + model.size = Size( + (json['size']['w'] ?? 0).toDouble(), + (json['size']['h'] ?? 0).toDouble(), + ); return model; } @@ -1461,7 +1464,8 @@ class InputModel { if (e is PointerScrollEvent) { final rawDx = e.scrollDelta.dx; final rawDy = e.scrollDelta.dy; - final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs(); + final dominantDelta = + rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs(); final isSmooth = dominantDelta < 1; final nowUs = DateTime.now().microsecondsSinceEpoch; final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 9af4903b4..afc1bdab0 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2226,14 +2226,16 @@ class CanvasModel with ChangeNotifier { } final sessionValue = await bind.sessionGetFlutterOption( sessionId: sessionId, k: kOptionRemoteCanvasMargin); + if (_remoteCanvasMarginInitialized || !supportsRemoteCanvasMargin) { + return; + } final defaultValue = bind.mainGetUserDefaultOption(key: kOptionRemoteCanvasMargin); final value = sessionValue?.isNotEmpty == true ? sessionValue : defaultValue; - _remoteCanvasMargin = - (double.tryParse(value ?? '') ?? 0) - .clamp(0, kMaxRemoteCanvasMargin) - .toDouble(); + _remoteCanvasMargin = (double.tryParse(value ?? '') ?? 0) + .clamp(0, kMaxRemoteCanvasMargin) + .toDouble(); _remoteCanvasMarginInitialized = true; } From 0ee87d5db5d9d222717c4392cc0cc1213aab6d82 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 6 May 2026 19:08:20 +0800 Subject: [PATCH 13/14] feat(cavas): margin, simple refactor Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 20 ++++++++++++++++++- flutter/lib/models/input_model.dart | 5 +++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 6e2e6d77b..e742995a4 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -941,6 +941,7 @@ class _DisplayMenu extends StatefulWidget { class _DisplayMenuState extends State<_DisplayMenu> { final RxInt _customPercent = 100.obs; + double? _remoteCanvasMarginPreview; late final ScreenAdjustor _screenAdjustor = ScreenAdjustor( id: widget.id, ffi: widget.ffi, @@ -1132,7 +1133,11 @@ class _DisplayMenuState extends State<_DisplayMenu> { .toInt(); final supportsRemoteCanvasMargin = data['supportsRemoteCanvasMargin'] as bool; - final remoteCanvasMargin = data['remoteCanvasMargin'] as double; + final savedRemoteCanvasMargin = data['remoteCanvasMargin'] as double; + final remoteCanvasMargin = + (_remoteCanvasMarginPreview ?? savedRemoteCanvasMargin) + .clamp(0, kMaxRemoteCanvasMargin) + .toDouble(); final hasVisibleControls = scrollVisible || supportsRemoteCanvasMargin; if (!hasVisibleControls) { @@ -1158,7 +1163,16 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChangeRemoteCanvasMargin(double? value) async { if (value == null) return; + _remoteCanvasMarginPreview = + value.clamp(0, kMaxRemoteCanvasMargin).toDouble(); + state.setState(() {}); + } + + onChangeRemoteCanvasMarginEnd(double value) async { + _remoteCanvasMarginPreview = + value.clamp(0, kMaxRemoteCanvasMargin).toDouble(); await widget.ffi.canvasModel.setRemoteCanvasMargin(value); + _remoteCanvasMarginPreview = null; state.setState(() {}); } @@ -1217,6 +1231,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { min: 0, max: kMaxRemoteCanvasMargin, onChanged: onChangeRemoteCanvasMargin, + onChangeEnd: onChangeRemoteCanvasMarginEnd, colorScheme: colorScheme, ), ), @@ -2796,6 +2811,7 @@ Widget _buildPointerTrackWidget(Widget child, FFI? ffi) { class EdgeThicknessControl extends StatelessWidget { final double value; final ValueChanged? onChanged; + final ValueChanged? onChangeEnd; final ColorScheme? colorScheme; final double min; final double max; @@ -2805,6 +2821,7 @@ class EdgeThicknessControl extends StatelessWidget { Key? key, required this.value, this.onChanged, + this.onChangeEnd, this.colorScheme, this.min = kMin, this.max = kMax, @@ -2843,6 +2860,7 @@ class EdgeThicknessControl extends StatelessWidget { semanticFormatterCallback: (double newValue) => "${newValue.round()}$unit", onChanged: onChanged, + onChangeEnd: onChangeEnd, ), ), ); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index cbbb7afda..5c0093a65 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -73,9 +73,10 @@ class CanvasCoords { model.paddingY = (json['paddingY'] ?? 0).toDouble(); model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); + final sizeMap = json['size']; model.size = Size( - (json['size']['w'] ?? 0).toDouble(), - (json['size']['h'] ?? 0).toDouble(), + (sizeMap?['w'] ?? 0).toDouble(), + (sizeMap?['h'] ?? 0).toDouble(), ); return model; } From ea5cfddccc48225c4410b500850a5b68f3ebec77 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 10 May 2026 20:33:23 +0800 Subject: [PATCH 14/14] feat(canvas): revert changes of pubspec.lock Signed-off-by: fufesou --- flutter/pubspec.lock | 83 ++++++++++---------------------------------- 1 file changed, 19 insertions(+), 64 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 24662b06a..c6f8aa1c2 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" auto_size_text: dependency: "direct main" description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" bot_toast: dependency: "direct main" description: @@ -417,14 +417,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" ffi: dependency: "direct main" description: @@ -652,11 +644,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -862,30 +849,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" - url: "https://pub.dev" - source: hosted - version: "10.0.5" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" - url: "https://pub.dev" - source: hosted - version: "3.0.5" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" lints: dependency: transitive description: @@ -914,10 +877,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -1255,10 +1218,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sqflite: dependency: "direct main" description: @@ -1279,18 +1242,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1303,10 +1266,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: @@ -1319,18 +1282,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.6" texture_rgba_renderer: dependency: "direct main" description: @@ -1510,7 +1473,7 @@ packages: source: hosted version: "1.1.16" vector_math: - dependency: "direct main" + dependency: transitive description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" @@ -1565,14 +1528,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" - url: "https://pub.dev" - source: hosted - version: "14.2.5" wakelock_plus: dependency: "direct main" description: