diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 40c4d46e0..b6ed6b519 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3214,3 +3214,46 @@ Widget buildPresetPasswordWarning() { }, ); } + +// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44 +Widget buildVirtualWindowFrame(BuildContext context, Widget child) { + boxShadow() => isMainDesktopWindow + ? [ + if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse) + BoxShadow( + color: Colors.black.withOpacity(0.1), + offset: Offset( + 0.0, + stateGlobal.isFocused.isTrue + ? kFrameBoxShadowOffsetFocused + : kFrameBoxShadowOffsetUnfocused), + blurRadius: kFrameBoxShadowBlurRadius, + ), + ] + : null; + return Obx( + () => Container( + decoration: BoxDecoration( + color: isMainDesktopWindow ? Colors.transparent : Theme.of(context).colorScheme.background, + border: Border.all( + color: Theme.of(context).dividerColor, + width: stateGlobal.windowBorderWidth.value, + ), + borderRadius: BorderRadius.circular( + (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue) + ? 0 + : kFrameBorderRadius, + ), + boxShadow: boxShadow(), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue) + ? 0 + : kFrameClipRRectBorderRadius, + ), + child: child, + ), + ), + ); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 1b0e72f05..14f17fa29 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -154,9 +154,14 @@ const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; -var kWindowEdgeSize = isWindows ? 1.0 : 5.0; -const kWindowBorderWidth = 1.0; +final kWindowEdgeSize = isWindows ? 1.0 : 5.0; +final kWindowBorderWidth = isLinux ? 1.0 : 0.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); +const kFrameBorderRadius = 12.0; +const kFrameClipRRectBorderRadius = 12.0; +const kFrameBoxShadowBlurRadius = 32.0; +const kFrameBoxShadowOffsetFocused = 4.0; +const kFrameBoxShadowOffsetUnfocused = 2.0; const kInvalidValueStr = 'InvalidValueStr'; diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index be3ba2497..b9d4f274e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -91,18 +91,21 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: Theme.of(context).cardColor, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton(), - labelGetter: DesktopTab.tablabelGetter, - )), - ); + final child = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + labelGetter: DesktopTab.tablabelGetter, + )); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + ); return isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index edd995adc..ac94890b6 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -97,21 +97,30 @@ class _PortForwardTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: () async { - tabController.clear(); - return true; - }, - tail: AddButton(), - labelGetter: DesktopTab.tablabelGetter, - )), + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: () async { + tabController.clear(); + return true; + }, + tail: AddButton(), + labelGetter: DesktopTab.tablabelGetter, + ), ); + final tabWidget = isLinux + ? buildVirtualWindowFrame( + context, + Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: child), + ) + : Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + ); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ecd9e62bb..9ce4c1827 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -7,6 +7,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; import '../../common/widgets/overlay.dart'; @@ -165,6 +166,7 @@ class _RemotePageState extends State // and let OS to handle events instead. _rawKeyFocusNode.unfocus(); } + stateGlobal.isFocused.value = false; } @override @@ -174,6 +176,7 @@ class _RemotePageState extends State if (isWindows) { _isWindowBlur = false; } + stateGlobal.isFocused.value = true; } @override diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9d7d0c221..350bdf79d 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -131,103 +131,103 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Obx( - () => Container( - decoration: BoxDecoration( - border: Border.all( - color: MyTheme.color(context).border!, - width: stateGlobal.windowBorderWidth.value), - ), - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton(), - pageViewBuilder: (pageView) => pageView, - labelGetter: DesktopTab.tablabelGetter, - tabBuilder: (key, icon, label, themeConf) => Obx(() { - final connectionType = ConnectionTypeState.find(key); - if (!connectionType.isValid()) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - label, - ], - ); - } else { - bool secure = - connectionType.secure.value == ConnectionType.strSecure; - bool direct = - connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } - var msgFingerprint = '${translate('Fingerprint')}:\n'; - var fingerprint = FingerprintState.find(key).value; - if (fingerprint.isEmpty) { - fingerprint = 'N/A'; - } - if (fingerprint.length > 5 * 8) { - var first = fingerprint.substring(0, 39); - var second = fingerprint.substring(40); - msgFingerprint += '$first\n$second'; - } else { - msgFingerprint += fingerprint; - } + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn; + if (secure && direct) { + msgConn = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + msgConn = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + msgConn = translate("Direct and unencrypted connection"); + } else { + msgConn = translate("Relayed and unencrypted connection"); + } + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } - final tab = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Tooltip( - message: '$msgConn\n$msgFingerprint', - child: SvgPicture.asset( - 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', - width: themeConf.iconSize, - height: themeConf.iconSize, - ).paddingOnly(right: 5), - ), - label, - unreadMessageCountBuilder(UnreadChatCountState.find(key)) - .marginOnly(left: 4), - ], - ); + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); - return Listener( - onPointerDown: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) { - return; - } - final remotePage = tabController.state.value.tabs - .firstWhere((tab) => tab.key == key) - .page as RemotePage; - if (remotePage.ffi.ffiModel.pi.isSet.isTrue && - e.buttons == 2) { - showRightMenu( - (CancelFunc cancelFunc) { - return _tabMenuBuilder(key, cancelFunc); - }, - target: e.position, - ); - } - }, - child: tab, - ); - } - }), - ), - ), + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), ), ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + )); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx(() => SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 6d5466add..5730bb81d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -77,14 +77,20 @@ class _DesktopServerPageState extends State ChangeNotifierProvider.value(value: gFFI.chatModel), ], child: Consumer( - builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( + builder: (context, serverModel, child) { + final body = Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: ConnectionManager(), - ), - ), + ); + return isLinux + ? buildVirtualWindowFrame(context, body) + : Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: body, + ); + }, ), ); } diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart index 694f18ace..f76603337 100644 --- a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.canvasModel), ], child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, body: FileManagerTabPage( params: params, ), diff --git a/flutter/lib/desktop/screen/desktop_port_forward_screen.dart b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart index c7c163a57..c586a5837 100644 --- a/flutter/lib/desktop/screen/desktop_port_forward_screen.dart +++ b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart @@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.ffiModel), ], child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, body: PortForwardTabPage( params: params, ), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 0f13ce2d9..431379b2a 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,6 +548,16 @@ class WindowActionPanelState extends State setState(() {}); } + @override + void onWindowFocus() { + stateGlobal.isFocused.value = true; + } + + @override + void onWindowBlur() { + stateGlobal.isFocused.value = false; + } + @override void onWindowMinimize() { stateGlobal.setMinimized(true); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 3df2e04b8..2089d525e 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -440,6 +440,9 @@ class _AppState extends State { if (isDesktop && desktopType == DesktopType.main) { child = keyListenerBuilder(context, child); } + if (isLinux) { + child = buildVirtualWindowFrame(context, child); + } return child; }, ), diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 58e216c34..f32cbe2d9 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -20,6 +20,7 @@ class StateGlobal { final svcStatus = SvcStatus.notReady.obs; // Only used for macOS bool? closeOnFullscreen; + final RxBool isFocused = false.obs; String _inputSource = ''; diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index b96c16f3f..56b85ccae 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -21,7 +21,6 @@ static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - // we have custom window frame gtk_window_set_decorated(window, FALSE); // try setting icon for rustdesk, which uses the system cache GtkIconTheme* theme = gtk_icon_theme_get_default(); @@ -75,12 +74,7 @@ static void my_application_activate(GApplication* application) { FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); - - auto border_frame = gtk_frame_new(nullptr); - gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN); - gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view)); - gtk_widget_show(GTK_WIDGET(border_frame)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view));