view_camera_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. import 'dart:async';
  2. import 'package:desktop_multi_window/desktop_multi_window.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_hbb/common/widgets/remote_input.dart';
  6. import 'package:get/get.dart';
  7. import 'package:provider/provider.dart';
  8. import 'package:wakelock_plus/wakelock_plus.dart';
  9. import 'package:flutter_hbb/models/state_model.dart';
  10. import '../../consts.dart';
  11. import '../../common/widgets/overlay.dart';
  12. import '../../common.dart';
  13. import '../../common/widgets/dialog.dart';
  14. import '../../common/widgets/toolbar.dart';
  15. import '../../models/model.dart';
  16. import '../../models/platform_model.dart';
  17. import '../../common/shared_state.dart';
  18. import '../../utils/image.dart';
  19. import '../widgets/remote_toolbar.dart';
  20. import '../widgets/kb_layout_type_chooser.dart';
  21. import '../widgets/tabbar_widget.dart';
  22. import 'package:flutter_hbb/native/custom_cursor.dart'
  23. if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
  24. final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
  25. // Used to skip session close if "move to new window" is clicked.
  26. final Map<String, bool> closeSessionOnDispose = {};
  27. class ViewCameraPage extends StatefulWidget {
  28. ViewCameraPage({
  29. Key? key,
  30. required this.id,
  31. required this.toolbarState,
  32. this.sessionId,
  33. this.tabWindowId,
  34. this.password,
  35. this.display,
  36. this.displays,
  37. this.tabController,
  38. this.connToken,
  39. this.forceRelay,
  40. this.isSharedPassword,
  41. }) : super(key: key) {
  42. initSharedStates(id);
  43. }
  44. final String id;
  45. final SessionID? sessionId;
  46. final int? tabWindowId;
  47. final int? display;
  48. final List<int>? displays;
  49. final String? password;
  50. final ToolbarState toolbarState;
  51. final bool? forceRelay;
  52. final bool? isSharedPassword;
  53. final String? connToken;
  54. final SimpleWrapper<State<ViewCameraPage>?> _lastState = SimpleWrapper(null);
  55. final DesktopTabController? tabController;
  56. FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi;
  57. @override
  58. State<ViewCameraPage> createState() {
  59. final state = _ViewCameraPageState(id);
  60. _lastState.value = state;
  61. return state;
  62. }
  63. }
  64. class _ViewCameraPageState extends State<ViewCameraPage>
  65. with AutomaticKeepAliveClientMixin, MultiWindowListener {
  66. Timer? _timer;
  67. String keyboardMode = "legacy";
  68. bool _isWindowBlur = false;
  69. final _cursorOverImage = false.obs;
  70. var _blockableOverlayState = BlockableOverlayState();
  71. final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
  72. // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
  73. // to identify the toolbar instance and its callback function.
  74. int? _instanceIdOnEnterOrLeaveImage4Toolbar;
  75. Function(bool)? _onEnterOrLeaveImage4Toolbar;
  76. late FFI _ffi;
  77. SessionID get sessionId => _ffi.sessionId;
  78. _ViewCameraPageState(String id) {
  79. _initStates(id);
  80. }
  81. void _initStates(String id) {}
  82. @override
  83. void initState() {
  84. super.initState();
  85. _ffi = FFI(widget.sessionId);
  86. Get.put<FFI>(_ffi, tag: widget.id);
  87. _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
  88. showKBLayoutTypeChooserIfNeeded(
  89. _ffi.ffiModel.pi.platform, _ffi.dialogManager);
  90. _ffi.recordingModel
  91. .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
  92. });
  93. _ffi.start(
  94. widget.id,
  95. isViewCamera: true,
  96. password: widget.password,
  97. isSharedPassword: widget.isSharedPassword,
  98. forceRelay: widget.forceRelay,
  99. tabWindowId: widget.tabWindowId,
  100. display: widget.display,
  101. displays: widget.displays,
  102. connToken: widget.connToken,
  103. );
  104. WidgetsBinding.instance.addPostFrameCallback((_) {
  105. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
  106. _ffi.dialogManager
  107. .showLoading(translate('Connecting...'), onCancel: closeConnection);
  108. });
  109. if (!isLinux) {
  110. WakelockPlus.enable();
  111. }
  112. _ffi.ffiModel.updateEventListener(sessionId, widget.id);
  113. if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
  114. _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
  115. _ffi.dialogManager.loadMobileActionsOverlayVisible();
  116. DesktopMultiWindow.addListener(this);
  117. // if (!_isCustomCursorInited) {
  118. // customCursorController.registerNeedUpdateCursorCallback(
  119. // (String? lastKey, String? currentKey) async {
  120. // if (_firstEnterImage.value) {
  121. // _firstEnterImage.value = false;
  122. // return true;
  123. // }
  124. // return lastKey == null || lastKey != currentKey;
  125. // });
  126. // _isCustomCursorInited = true;
  127. // }
  128. _blockableOverlayState.applyFfi(_ffi);
  129. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
  130. WidgetsBinding.instance.addPostFrameCallback((_) {
  131. widget.tabController?.onSelected?.call(widget.id);
  132. });
  133. }
  134. @override
  135. void onWindowBlur() {
  136. super.onWindowBlur();
  137. // On windows, we use `focus` way to handle keyboard better.
  138. // Now on Linux, there's some rdev issues which will break the input.
  139. // We disable the `focus` way for non-Windows temporarily.
  140. if (isWindows) {
  141. _isWindowBlur = true;
  142. // unfocus the primary-focus when the whole window is lost focus,
  143. // and let OS to handle events instead.
  144. _rawKeyFocusNode.unfocus();
  145. }
  146. stateGlobal.isFocused.value = false;
  147. }
  148. @override
  149. void onWindowFocus() {
  150. super.onWindowFocus();
  151. // See [onWindowBlur].
  152. if (isWindows) {
  153. _isWindowBlur = false;
  154. }
  155. stateGlobal.isFocused.value = true;
  156. }
  157. @override
  158. void onWindowRestore() {
  159. super.onWindowRestore();
  160. // On windows, we use `onWindowRestore` way to handle window restore from
  161. // a minimized state.
  162. if (isWindows) {
  163. _isWindowBlur = false;
  164. }
  165. if (!isLinux) {
  166. WakelockPlus.enable();
  167. }
  168. }
  169. // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
  170. @override
  171. void onWindowMaximize() {
  172. super.onWindowMaximize();
  173. if (!isLinux) {
  174. WakelockPlus.enable();
  175. }
  176. }
  177. @override
  178. void onWindowMinimize() {
  179. super.onWindowMinimize();
  180. if (!isLinux) {
  181. WakelockPlus.disable();
  182. }
  183. }
  184. @override
  185. void onWindowEnterFullScreen() {
  186. super.onWindowEnterFullScreen();
  187. if (isMacOS) {
  188. stateGlobal.setFullscreen(true);
  189. }
  190. }
  191. @override
  192. void onWindowLeaveFullScreen() {
  193. super.onWindowLeaveFullScreen();
  194. if (isMacOS) {
  195. stateGlobal.setFullscreen(false);
  196. }
  197. }
  198. @override
  199. Future<void> dispose() async {
  200. final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
  201. // https://github.com/flutter/flutter/issues/64935
  202. super.dispose();
  203. debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}");
  204. _ffi.textureModel.onViewCameraPageDispose(closeSession);
  205. if (closeSession) {
  206. // ensure we leave this session, this is a double check
  207. _ffi.inputModel.enterOrLeave(false);
  208. }
  209. DesktopMultiWindow.removeListener(this);
  210. _ffi.dialogManager.hideMobileActionsOverlay();
  211. _ffi.imageModel.disposeImage();
  212. _ffi.cursorModel.disposeImages();
  213. _rawKeyFocusNode.dispose();
  214. await _ffi.close(closeSession: closeSession);
  215. _timer?.cancel();
  216. _ffi.dialogManager.dismissAll();
  217. if (closeSession) {
  218. await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
  219. overlays: SystemUiOverlay.values);
  220. }
  221. if (!isLinux) {
  222. await WakelockPlus.disable();
  223. }
  224. await Get.delete<FFI>(tag: widget.id);
  225. removeSharedStates(widget.id);
  226. }
  227. Widget emptyOverlay() => BlockableOverlay(
  228. /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
  229. /// see override build() in [BlockableOverlay]
  230. state: _blockableOverlayState,
  231. underlying: Container(
  232. color: Colors.transparent,
  233. ),
  234. );
  235. Widget buildBody(BuildContext context) {
  236. remoteToolbar(BuildContext context) => RemoteToolbar(
  237. id: widget.id,
  238. ffi: _ffi,
  239. state: widget.toolbarState,
  240. onEnterOrLeaveImageSetter: (id, func) {
  241. _instanceIdOnEnterOrLeaveImage4Toolbar = id;
  242. _onEnterOrLeaveImage4Toolbar = func;
  243. },
  244. onEnterOrLeaveImageCleaner: (id) {
  245. // If _instanceIdOnEnterOrLeaveImage4Toolbar != id
  246. // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
  247. if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
  248. _instanceIdOnEnterOrLeaveImage4Toolbar = null;
  249. _onEnterOrLeaveImage4Toolbar = null;
  250. }
  251. },
  252. setRemoteState: setState,
  253. );
  254. bodyWidget() {
  255. return Stack(
  256. children: [
  257. Container(
  258. color: kColorCanvas,
  259. child: getBodyForDesktop(context),
  260. ),
  261. Stack(
  262. children: [
  263. _ffi.ffiModel.pi.isSet.isTrue &&
  264. _ffi.ffiModel.waitForFirstImage.isTrue
  265. ? emptyOverlay()
  266. : () {
  267. if (!_ffi.ffiModel.isPeerAndroid) {
  268. return Offstage();
  269. } else {
  270. return Obx(() => Offstage(
  271. offstage: _ffi.dialogManager
  272. .mobileActionsOverlayVisible.isFalse,
  273. child: Overlay(initialEntries: [
  274. makeMobileActionsOverlayEntry(
  275. () => _ffi.dialogManager
  276. .setMobileActionsOverlayVisible(false),
  277. ffi: _ffi,
  278. )
  279. ]),
  280. ));
  281. }
  282. }(),
  283. // Use Overlay to enable rebuild every time on menu button click.
  284. _ffi.ffiModel.pi.isSet.isTrue
  285. ? Overlay(
  286. initialEntries: [OverlayEntry(builder: remoteToolbar)])
  287. : remoteToolbar(context),
  288. _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
  289. ],
  290. ),
  291. ],
  292. );
  293. }
  294. return Scaffold(
  295. backgroundColor: Theme.of(context).colorScheme.background,
  296. body: Obx(() {
  297. final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
  298. _ffi.ffiModel.waitForFirstImage.isFalse;
  299. if (imageReady) {
  300. // If the privacy mode(disable physical displays) is switched,
  301. // we should not dismiss the dialog immediately.
  302. if (DateTime.now().difference(togglePrivacyModeTime) >
  303. const Duration(milliseconds: 3000)) {
  304. // `dismissAll()` is to ensure that the state is clean.
  305. // It's ok to call dismissAll() here.
  306. _ffi.dialogManager.dismissAll();
  307. // Recreate the block state to refresh the state.
  308. _blockableOverlayState = BlockableOverlayState();
  309. _blockableOverlayState.applyFfi(_ffi);
  310. }
  311. // Block the whole `bodyWidget()` when dialog shows.
  312. return BlockableOverlay(
  313. underlying: bodyWidget(),
  314. state: _blockableOverlayState,
  315. );
  316. } else {
  317. // `_blockableOverlayState` is not recreated here.
  318. // The toolbar's block state won't work properly when reconnecting, but that's okay.
  319. return bodyWidget();
  320. }
  321. }),
  322. );
  323. }
  324. @override
  325. Widget build(BuildContext context) {
  326. super.build(context);
  327. return WillPopScope(
  328. onWillPop: () async {
  329. clientClose(sessionId, _ffi.dialogManager);
  330. return false;
  331. },
  332. child: MultiProvider(providers: [
  333. ChangeNotifierProvider.value(value: _ffi.ffiModel),
  334. ChangeNotifierProvider.value(value: _ffi.imageModel),
  335. ChangeNotifierProvider.value(value: _ffi.cursorModel),
  336. ChangeNotifierProvider.value(value: _ffi.canvasModel),
  337. ChangeNotifierProvider.value(value: _ffi.recordingModel),
  338. ], child: buildBody(context)));
  339. }
  340. void enterView(PointerEnterEvent evt) {
  341. _cursorOverImage.value = true;
  342. _firstEnterImage.value = true;
  343. if (_onEnterOrLeaveImage4Toolbar != null) {
  344. try {
  345. _onEnterOrLeaveImage4Toolbar!(true);
  346. } catch (e) {
  347. //
  348. }
  349. }
  350. // See [onWindowBlur].
  351. if (!isWindows) {
  352. if (!_rawKeyFocusNode.hasFocus) {
  353. _rawKeyFocusNode.requestFocus();
  354. }
  355. _ffi.inputModel.enterOrLeave(true);
  356. }
  357. }
  358. void leaveView(PointerExitEvent evt) {
  359. if (_ffi.ffiModel.keyboard) {
  360. _ffi.inputModel.tryMoveEdgeOnExit(evt.position);
  361. }
  362. _cursorOverImage.value = false;
  363. _firstEnterImage.value = false;
  364. if (_onEnterOrLeaveImage4Toolbar != null) {
  365. try {
  366. _onEnterOrLeaveImage4Toolbar!(false);
  367. } catch (e) {
  368. //
  369. }
  370. }
  371. // See [onWindowBlur].
  372. if (!isWindows) {
  373. _ffi.inputModel.enterOrLeave(false);
  374. }
  375. }
  376. Widget _buildRawTouchAndPointerRegion(
  377. Widget child,
  378. PointerEnterEventListener? onEnter,
  379. PointerExitEventListener? onExit,
  380. ) {
  381. return RawTouchGestureDetectorRegion(
  382. child: _buildRawPointerMouseRegion(child, onEnter, onExit),
  383. ffi: _ffi,
  384. isCamera: true,
  385. );
  386. }
  387. Widget _buildRawPointerMouseRegion(
  388. Widget child,
  389. PointerEnterEventListener? onEnter,
  390. PointerExitEventListener? onExit,
  391. ) {
  392. return CameraRawPointerMouseRegion(
  393. onEnter: onEnter,
  394. onExit: onExit,
  395. onPointerDown: (event) {
  396. // A double check for blur status.
  397. // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
  398. // Sometimes the system does not send the necessary focus event to flutter. We should manually
  399. // handle this inconsistent status by setting `_isWindowBlur` to false. So we can
  400. // ensure the grab-key thread is running when our users are clicking the remote canvas.
  401. if (_isWindowBlur) {
  402. debugPrint(
  403. "Unexpected status: onPointerDown is triggered while the remote window is in blur status");
  404. _isWindowBlur = false;
  405. }
  406. if (!_rawKeyFocusNode.hasFocus) {
  407. _rawKeyFocusNode.requestFocus();
  408. }
  409. },
  410. inputModel: _ffi.inputModel,
  411. child: child,
  412. );
  413. }
  414. Widget getBodyForDesktop(BuildContext context) {
  415. var paints = <Widget>[
  416. MouseRegion(onEnter: (evt) {
  417. if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
  418. }, onExit: (evt) {
  419. if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
  420. }, child: LayoutBuilder(builder: (context, constraints) {
  421. final c = Provider.of<CanvasModel>(context, listen: false);
  422. Future.delayed(Duration.zero, () => c.updateViewStyle());
  423. final peerDisplay = CurrentDisplayState.find(widget.id);
  424. return Obx(
  425. () => _ffi.ffiModel.pi.isSet.isFalse
  426. ? Container(color: Colors.transparent)
  427. : Obx(() {
  428. widget.toolbarState.initShow(sessionId);
  429. _ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
  430. return ImagePaint(
  431. id: widget.id,
  432. cursorOverImage: _cursorOverImage,
  433. listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
  434. child, enterView, leaveView),
  435. ffi: _ffi,
  436. );
  437. }),
  438. );
  439. }))
  440. ];
  441. paints.add(
  442. Positioned(
  443. top: 10,
  444. right: 10,
  445. child: _buildRawTouchAndPointerRegion(
  446. QualityMonitor(_ffi.qualityMonitorModel), null, null),
  447. ),
  448. );
  449. return Stack(
  450. children: paints,
  451. );
  452. }
  453. @override
  454. bool get wantKeepAlive => true;
  455. }
  456. class ImagePaint extends StatefulWidget {
  457. final FFI ffi;
  458. final String id;
  459. final RxBool cursorOverImage;
  460. final Widget Function(Widget)? listenerBuilder;
  461. ImagePaint(
  462. {Key? key,
  463. required this.ffi,
  464. required this.id,
  465. required this.cursorOverImage,
  466. this.listenerBuilder})
  467. : super(key: key);
  468. @override
  469. State<StatefulWidget> createState() => _ImagePaintState();
  470. }
  471. class _ImagePaintState extends State<ImagePaint> {
  472. String get id => widget.id;
  473. RxBool get cursorOverImage => widget.cursorOverImage;
  474. Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
  475. @override
  476. Widget build(BuildContext context) {
  477. final m = Provider.of<ImageModel>(context);
  478. var c = Provider.of<CanvasModel>(context);
  479. final s = c.scale;
  480. bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
  481. if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
  482. final paintWidth = c.getDisplayWidth() * s;
  483. final paintHeight = c.getDisplayHeight() * s;
  484. final paintSize = Size(paintWidth, paintHeight);
  485. final paintWidget =
  486. m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
  487. ? _BuildPaintTextureRender(
  488. c, s, Offset.zero, paintSize, isViewOriginal())
  489. : _buildScrollbarNonTextureRender(m, paintSize, s);
  490. return NotificationListener<ScrollNotification>(
  491. onNotification: (notification) {
  492. c.updateScrollPercent();
  493. return false;
  494. },
  495. child: Container(
  496. child: _buildCrossScrollbarFromLayout(
  497. context,
  498. _buildListener(paintWidget),
  499. c.size,
  500. paintSize,
  501. c.scrollHorizontal,
  502. c.scrollVertical,
  503. )),
  504. );
  505. } else {
  506. if (c.size.width > 0 && c.size.height > 0) {
  507. final paintWidget =
  508. m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
  509. ? _BuildPaintTextureRender(
  510. c,
  511. s,
  512. Offset(
  513. isLinux ? c.x.toInt().toDouble() : c.x,
  514. isLinux ? c.y.toInt().toDouble() : c.y,
  515. ),
  516. c.size,
  517. isViewOriginal())
  518. : _buildScrollAutoNonTextureRender(m, c, s);
  519. return Container(child: _buildListener(paintWidget));
  520. } else {
  521. return Container();
  522. }
  523. }
  524. }
  525. Widget _buildScrollbarNonTextureRender(
  526. ImageModel m, Size imageSize, double s) {
  527. return CustomPaint(
  528. size: imageSize,
  529. painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
  530. );
  531. }
  532. Widget _buildScrollAutoNonTextureRender(
  533. ImageModel m, CanvasModel c, double s) {
  534. return CustomPaint(
  535. size: Size(c.size.width, c.size.height),
  536. painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
  537. );
  538. }
  539. Widget _BuildPaintTextureRender(
  540. CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
  541. final ffiModel = c.parent.target!.ffiModel;
  542. final displays = ffiModel.pi.getCurDisplays();
  543. final children = <Widget>[];
  544. final rect = ffiModel.rect;
  545. if (rect == null) {
  546. return Container();
  547. }
  548. final curDisplay = ffiModel.pi.currentDisplay;
  549. for (var i = 0; i < displays.length; i++) {
  550. final textureId = widget.ffi.textureModel
  551. .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
  552. if (true) {
  553. // both "textureId.value != -1" and "true" seems ok
  554. children.add(Positioned(
  555. left: (displays[i].x - rect.left) * s + offset.dx,
  556. top: (displays[i].y - rect.top) * s + offset.dy,
  557. width: displays[i].width * s,
  558. height: displays[i].height * s,
  559. child: Obx(() => Texture(
  560. textureId: textureId.value,
  561. filterQuality:
  562. isViewOriginal ? FilterQuality.none : FilterQuality.low,
  563. )),
  564. ));
  565. }
  566. }
  567. return SizedBox(
  568. width: size.width,
  569. height: size.height,
  570. child: Stack(children: children),
  571. );
  572. }
  573. MouseCursor _buildCustomCursor(BuildContext context, double scale) {
  574. final cursor = Provider.of<CursorModel>(context);
  575. final cache = cursor.cache ?? preDefaultCursor.cache;
  576. return buildCursorOfCache(cursor, scale, cache);
  577. }
  578. MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
  579. final cursor = Provider.of<CursorModel>(context);
  580. final cache = preForbiddenCursor.cache;
  581. return buildCursorOfCache(cursor, scale, cache);
  582. }
  583. Widget _buildCrossScrollbarFromLayout(
  584. BuildContext context,
  585. Widget child,
  586. Size layoutSize,
  587. Size size,
  588. ScrollController horizontal,
  589. ScrollController vertical,
  590. ) {
  591. var widget = child;
  592. if (layoutSize.width < size.width) {
  593. widget = ScrollConfiguration(
  594. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
  595. child: SingleChildScrollView(
  596. controller: horizontal,
  597. scrollDirection: Axis.horizontal,
  598. physics: cursorOverImage.isTrue
  599. ? const NeverScrollableScrollPhysics()
  600. : null,
  601. child: widget,
  602. ),
  603. );
  604. } else {
  605. widget = Row(
  606. children: [
  607. Container(
  608. width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
  609. ),
  610. widget,
  611. ],
  612. );
  613. }
  614. if (layoutSize.height < size.height) {
  615. widget = ScrollConfiguration(
  616. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
  617. child: SingleChildScrollView(
  618. controller: vertical,
  619. physics: cursorOverImage.isTrue
  620. ? const NeverScrollableScrollPhysics()
  621. : null,
  622. child: widget,
  623. ),
  624. );
  625. } else {
  626. widget = Column(
  627. children: [
  628. Container(
  629. height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
  630. ),
  631. widget,
  632. ],
  633. );
  634. }
  635. if (layoutSize.width < size.width) {
  636. widget = RawScrollbar(
  637. thickness: kScrollbarThickness,
  638. thumbColor: Colors.grey,
  639. controller: horizontal,
  640. thumbVisibility: false,
  641. trackVisibility: false,
  642. notificationPredicate: layoutSize.height < size.height
  643. ? (notification) => notification.depth == 1
  644. : defaultScrollNotificationPredicate,
  645. child: widget,
  646. );
  647. }
  648. if (layoutSize.height < size.height) {
  649. widget = RawScrollbar(
  650. thickness: kScrollbarThickness,
  651. thumbColor: Colors.grey,
  652. controller: vertical,
  653. thumbVisibility: false,
  654. trackVisibility: false,
  655. child: widget,
  656. );
  657. }
  658. return Container(
  659. child: widget,
  660. width: layoutSize.width,
  661. height: layoutSize.height,
  662. );
  663. }
  664. Widget _buildListener(Widget child) {
  665. if (listenerBuilder != null) {
  666. return listenerBuilder!(child);
  667. } else {
  668. return child;
  669. }
  670. }
  671. }