rustdesk/flutter/lib/desktop/widgets/tabbar_widget.dart

1135 lines
34 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide TabBarTheme;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
2022-09-27 18:34:05 +08:00
import 'package:flutter_svg/flutter_svg.dart';
import 'package:bot_toast/bot_toast.dart';
import '../../utils/multi_window_manager.dart';
const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
const double _kIconSize = 18;
const double _kDividerIndent = 10;
const double _kActionIconSize = 12;
class TabInfo {
final String key;
final String label;
final IconData? selectedIcon;
final IconData? unselectedIcon;
2022-09-08 21:03:20 +08:00
final bool closable;
2022-09-08 19:26:55 +08:00
final VoidCallback? onTabCloseButton;
final VoidCallback? onTap;
final Widget page;
TabInfo(
{required this.key,
required this.label,
this.selectedIcon,
this.unselectedIcon,
this.closable = true,
2022-09-08 19:26:55 +08:00
this.onTabCloseButton,
this.onTap,
required this.page});
}
enum DesktopTabType {
main,
cm,
remoteScreen,
fileTransfer,
portForward,
}
2022-08-24 20:17:51 +08:00
class DesktopTabState {
final List<TabInfo> tabs = [];
final ScrollPosController scrollController =
ScrollPosController(itemCount: 0);
final PageController pageController = PageController();
int selected = 0;
TabInfo get selectedTabInfo => tabs[selected];
2022-08-24 20:17:51 +08:00
DesktopTabState() {
scrollController.itemCount = tabs.length;
}
}
CancelFunc showRightMenu(ToastBuilder builder,
{BuildContext? context, Offset? target}) {
return BotToast.showAttachedWidget(
target: target,
targetContext: context,
verticalOffset: 0,
horizontalOffset: 0,
duration: Duration(seconds: 4),
animationDuration: Duration(milliseconds: 0),
animationReverseDuration: Duration(milliseconds: 0),
preferDirection: PreferDirection.rightTop,
ignoreContentClick: false,
onlyOne: true,
allowClick: true,
enableSafeArea: true,
backgroundColor: Color(0x00000000),
attachedBuilder: builder,
);
}
2022-08-24 20:17:51 +08:00
class DesktopTabController {
final state = DesktopTabState().obs;
final DesktopTabType tabType;
/// index, key
Function(int, String)? onRemoved;
Function(int, String)? onSelected;
2022-08-24 21:52:21 +08:00
DesktopTabController(
{required this.tabType, this.onRemoved, this.onSelected});
int get length => state.value.tabs.length;
2022-10-26 20:39:28 +08:00
void add(TabInfo tab) {
2022-08-23 15:25:18 +08:00
if (!isDesktop) return;
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
int toIndex;
if (index >= 0) {
toIndex = index;
} else {
state.update((val) {
val!.tabs.add(tab);
});
state.value.scrollController.itemCount = state.value.tabs.length;
toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0);
}
try {
jumpTo(toIndex);
} catch (e) {
// call before binding controller will throw
debugPrint("Failed to jumpTo: $e");
}
}
void remove(int index) {
2022-08-23 15:25:18 +08:00
if (!isDesktop) return;
final len = state.value.tabs.length;
if (index < 0 || index > len - 1) return;
final key = state.value.tabs[index].key;
final currentSelected = state.value.selected;
int toIndex = 0;
if (index == len - 1) {
toIndex = max(0, currentSelected - 1);
} else if (index < len - 1 && index < currentSelected) {
toIndex = max(0, currentSelected - 1);
}
state.value.tabs.removeAt(index);
state.value.scrollController.itemCount = state.value.tabs.length;
jumpTo(toIndex);
onRemoved?.call(index, key);
}
void jumpTo(int index) {
if (!isDesktop || index < 0) return;
state.update((val) {
val!.selected = index;
2022-10-26 20:02:20 +08:00
Future.delayed(Duration(milliseconds: 100), (() {
if (val.pageController.hasClients) {
val.pageController.jumpToPage(index);
}
2022-10-26 20:02:20 +08:00
val.scrollController.itemCount = val.tabs.length;
if (val.scrollController.hasClients &&
val.scrollController.itemCount > index) {
val.scrollController
.scrollToItem(index, center: false, animate: true);
}
}));
});
if (state.value.tabs.length > index) {
final key = state.value.tabs[index].key;
onSelected?.call(index, key);
}
}
2022-09-08 19:26:55 +08:00
void jumpBy(String key) {
if (!isDesktop) return;
final index = state.value.tabs.indexWhere((tab) => tab.key == key);
jumpTo(index);
}
void closeBy(String? key) {
if (!isDesktop) return;
assert(onRemoved != null);
if (key == null) {
if (state.value.selected < state.value.tabs.length) {
remove(state.value.selected);
}
} else {
2022-09-08 19:26:55 +08:00
final index = state.value.tabs.indexWhere((tab) => tab.key == key);
remove(index);
}
}
void clear() {
state.value.tabs.clear();
state.refresh();
}
}
class TabThemeConf {
double iconSize;
2022-10-24 16:44:43 +08:00
TabThemeConf({required this.iconSize});
}
typedef TabBuilder = Widget Function(
String key, Widget icon, Widget label, TabThemeConf themeConf);
typedef TabMenuBuilder = Widget Function(String key);
typedef LabelGetter = Rx<String> Function(String key);
/// [_lastClickTime], help to handle double click
int _lastClickTime =
DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
class DesktopTab extends StatelessWidget {
final bool showLogo;
final bool showTitle;
final bool showMinimize;
final bool showMaximize;
final bool showClose;
final Widget Function(Widget pageView)? pageViewBuilder;
// Right click tab menu
final TabMenuBuilder? tabMenuBuilder;
final Widget? tail;
final Future<bool> Function()? onWindowCloseButton;
final TabBuilder? tabBuilder;
final LabelGetter? labelGetter;
final double? maxLabelWidth;
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
2022-08-24 20:17:51 +08:00
final DesktopTabController controller;
2022-10-24 16:44:43 +08:00
Rx<DesktopTabState> get state => controller.state;
final isMaximized = false.obs;
final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
late final DesktopTabType tabType;
late final bool isMainWindow;
DesktopTab({
Key? key,
required this.controller,
this.showLogo = true,
this.showTitle = true,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
this.pageViewBuilder,
this.tabMenuBuilder,
this.tail,
2022-09-08 19:26:55 +08:00
this.onWindowCloseButton,
this.tabBuilder,
this.labelGetter,
this.maxLabelWidth,
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
}) : super(key: key) {
tabType = controller.tabType;
isMainWindow =
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
}
static RxString labelGetterAlias(String peerId) {
final opt = 'alias';
PeerStringOption.init(peerId, opt, () {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: opt);
return alias.isEmpty ? peerId : alias;
});
return PeerStringOption.find(peerId, opt);
}
@override
Widget build(BuildContext context) {
return Column(children: [
Obx(() => Offstage(
offstage: !stateGlobal.showTabBar.isTrue ||
(kUseCompatibleUiMode && isHideSingleItem()),
child: SizedBox(
2022-08-24 21:20:50 +08:00
height: _kTabBarHeight,
child: Column(
children: [
SizedBox(
2022-08-24 21:20:50 +08:00
height: _kTabBarHeight - 1,
child: _buildBar(),
),
const Divider(
2022-08-24 21:20:50 +08:00
height: 1,
thickness: 1,
),
],
),
))),
Expanded(
child: pageViewBuilder != null
? pageViewBuilder!(_buildPageView())
: _buildPageView())
]);
}
Widget _buildBlock({required Widget child}) {
if (tabType != DesktopTabType.main) {
return child;
}
var block = false.obs;
return Obx(() => MouseRegion(
onEnter: (_) async {
var access_mode = await bind.mainGetOption(key: 'access-mode');
var option = option2bool(
'allow-remote-config-modification',
await bind.mainGetOption(
key: 'allow-remote-config-modification'));
if (access_mode == 'view' || (access_mode.isEmpty && !option)) {
var time0 = DateTime.now().millisecondsSinceEpoch;
await bind.mainCheckMouseTime();
Timer(const Duration(milliseconds: 120), () async {
var d = time0 - await bind.mainGetMouseTime();
if (d < 120) {
block.value = true;
}
});
}
},
onExit: (_) => block.value = false,
child: Stack(
children: [
child,
Offstage(
offstage: !block.value,
child: Container(
color: Colors.black.withOpacity(0.5),
)),
],
),
));
}
Widget _buildPageView() {
return _buildBlock(
child: Obx(() => PageView(
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
children: state.value.tabs
.map((tab) => tab.page)
.toList(growable: false))));
}
/// Check whether to show ListView
///
/// Conditions:
/// - hide single item when only has one item (home) on [DesktopTabPage].
bool isHideSingleItem() {
return state.value.tabs.length == 1 &&
controller.tabType == DesktopTabType.main;
}
Widget _buildBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: GestureDetector(
// custom double tap handler
onTap: showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
_lastClickTime = current;
if (elapsed < bind.getDoubleClickTime()) {
// onDoubleTap
toggleMaximize(isMainWindow)
.then((value) => isMaximized.value = value);
}
}
: null,
2022-10-14 19:44:57 +08:00
onPanStart: (_) => startDragging(isMainWindow),
child: Row(
children: [
Offstage(
offstage: !Platform.isMacOS,
child: const SizedBox(
width: 78,
)),
Offstage(
offstage: kUseCompatibleUiMode,
child: Row(children: [
Offstage(
offstage: !showLogo,
child: SvgPicture.asset(
'assets/logo.svg',
width: 16,
height: 16,
)),
Offstage(
offstage: !showTitle,
child: const Text(
"RustDesk",
style: TextStyle(fontSize: 13),
).marginOnly(left: 2))
]).marginOnly(
left: 5,
right: 10,
),
),
Expanded(
child: Listener(
// handle mouse wheel
onPointerSignal: (e) {
if (e is PointerScrollEvent) {
final sc =
controller.state.value.scrollController;
if (!sc.canScroll) return;
_scrollDebounce.call(() {
sc.animateTo(sc.offset + e.scrollDelta.dy,
duration: Duration(milliseconds: 200),
curve: Curves.ease);
});
}
},
child: _ListView(
controller: controller,
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
labelGetter: labelGetter,
maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor:
selectedTabBackgroundColor,
unSelectedTabBackgroundColor:
unSelectedTabBackgroundColor))),
],
))),
// hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
2022-11-30 14:05:49 +08:00
WindowActionPanel(
isMainWindow: isMainWindow,
tabType: tabType,
state: state,
tail: tail,
isMaximized: isMaximized,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
onClose: onWindowCloseButton,
)
],
);
}
}
2022-10-14 09:48:33 +08:00
class WindowActionPanel extends StatefulWidget {
2022-10-14 19:44:57 +08:00
final bool isMainWindow;
final DesktopTabType tabType;
final Rx<DesktopTabState> state;
final RxBool isMaximized;
final bool showMinimize;
final bool showMaximize;
final bool showClose;
2022-10-14 19:44:57 +08:00
final Widget? tail;
final Future<bool> Function()? onClose;
const WindowActionPanel(
{Key? key,
2022-10-14 19:44:57 +08:00
required this.isMainWindow,
required this.tabType,
required this.state,
required this.isMaximized,
2022-10-14 19:44:57 +08:00
this.tail,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
this.onClose})
: super(key: key);
2022-10-14 09:48:33 +08:00
@override
State<StatefulWidget> createState() {
return WindowActionPanelState();
}
}
class WindowActionPanelState extends State<WindowActionPanel>
with MultiWindowListener, WindowListener {
@override
void initState() {
super.initState();
DesktopMultiWindow.addListener(this);
windowManager.addListener(this);
Future.delayed(Duration(milliseconds: 500), () {
if (widget.isMainWindow) {
windowManager.isMaximized().then((maximized) {
if (widget.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
}
});
} else {
final wc = WindowController.fromWindowId(windowId!);
wc.isMaximized().then((maximized) {
debugPrint("isMaximized $maximized");
if (widget.isMaximized.value != maximized) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => widget.isMaximized.value = maximized));
}
});
}
});
2022-10-14 09:48:33 +08:00
}
@override
void dispose() {
DesktopMultiWindow.removeListener(this);
windowManager.removeListener(this);
super.dispose();
}
@override
void onWindowMaximize() {
// catch maximize from system
if (!widget.isMaximized.value) {
widget.isMaximized.value = true;
2022-10-14 09:48:33 +08:00
}
super.onWindowMaximize();
}
@override
void onWindowUnmaximize() {
// catch unmaximize from system
if (widget.isMaximized.value) {
widget.isMaximized.value = false;
2022-10-14 09:48:33 +08:00
}
super.onWindowUnmaximize();
}
@override
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
await windowManager.hide();
rustDeskWinManager.unregisterActiveWindow(0);
} else {
widget.onClose?.call();
2022-11-09 15:14:11 +08:00
await WindowController.fromWindowId(windowId!).hide();
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": windowId!});
}
super.onWindowClose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
2022-10-14 19:44:57 +08:00
Offstage(offstage: widget.tail == null, child: widget.tail),
Offstage(
2022-11-30 14:05:49 +08:00
offstage: kUseCompatibleUiMode,
child: Row(
children: [
Offstage(
offstage: !widget.showMinimize,
child: ActionIcon(
message: 'Minimize',
icon: IconFont.min,
onTap: () {
if (widget.isMainWindow) {
windowManager.minimize();
} else {
WindowController.fromWindowId(windowId!).minimize();
}
},
isClose: false,
)),
Offstage(
offstage: !widget.showMaximize,
child: Obx(() => ActionIcon(
message:
widget.isMaximized.value ? "Restore" : "Maximize",
icon: widget.isMaximized.value
? IconFont.restore
: IconFont.max,
onTap: _toggleMaximize,
isClose: false,
))),
Offstage(
offstage: !widget.showClose,
child: ActionIcon(
message: 'Close',
icon: IconFont.close,
onTap: () async {
final res = await widget.onClose?.call() ?? true;
if (res) {
// hide for all window
// note: the main window can be restored by tray icon
Future.delayed(Duration.zero, () async {
if (widget.isMainWindow) {
await windowManager.close();
} else {
await WindowController.fromWindowId(windowId!)
.close();
}
});
}
},
isClose: true,
))
],
),
),
],
);
2022-10-14 19:44:57 +08:00
}
void _toggleMaximize() {
toggleMaximize(widget.isMainWindow).then((maximize) {
if (widget.isMaximized.value != maximize) {
// update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
widget.isMaximized.value = maximize;
2022-10-14 19:44:57 +08:00
}
});
}
}
void startDragging(bool isMainWindow) {
if (isMainWindow) {
windowManager.startDragging();
} else {
WindowController.fromWindowId(windowId!).startDragging();
}
}
/// return true -> window will be maximize
/// return false -> window will be unmaximize
Future<bool> toggleMaximize(bool isMainWindow) async {
if (isMainWindow) {
if (await windowManager.isMaximized()) {
windowManager.unmaximize();
return false;
} else {
windowManager.maximize();
return true;
}
} else {
final wc = WindowController.fromWindowId(windowId!);
if (await wc.isMaximized()) {
wc.unmaximize();
return false;
} else {
wc.maximize();
return true;
}
}
}
Future<bool> closeConfirmDialog() async {
var confirm = true;
final res = await gFFI.dialogManager.show<bool>((setState, close) {
submit() {
final opt = "enable-confirm-closing-tabs";
String value = bool2option(opt, confirm);
bind.mainSetOption(key: opt, value: value);
close(true);
}
return CustomAlertDialog(
title: Row(children: [
const Icon(Icons.warning_amber_sharp,
color: Colors.redAccent, size: 28),
const SizedBox(width: 10),
Text(translate("Warning")),
]),
content: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate("Disconnect all devices?")),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Confirm before closing multiple tabs"),
),
value: confirm,
onChanged: (v) {
if (v == null) return;
setState(() => confirm = v);
},
)
2022-10-24 16:44:43 +08:00
]),
// confirm checkbox
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
ElevatedButton(onPressed: submit, child: Text(translate("OK"))),
],
onSubmit: submit,
onCancel: close,
);
});
return res == true;
}
class _ListView extends StatelessWidget {
2022-08-24 20:17:51 +08:00
final DesktopTabController controller;
final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
final LabelGetter? labelGetter;
final double? maxLabelWidth;
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
Rx<DesktopTabState> get state => controller.state;
const _ListView({
required this.controller,
this.tabBuilder,
this.tabMenuBuilder,
this.labelGetter,
this.maxLabelWidth,
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
});
/// Check whether to show ListView
///
/// Conditions:
/// - hide single item when only has one item (home) on [DesktopTabPage].
bool isHideSingleItem() {
return state.value.tabs.length == 1 &&
controller.tabType == DesktopTabType.main;
}
@override
Widget build(BuildContext context) {
return Obx(() => ListView(
controller: state.value.scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
children: isHideSingleItem()
? List.empty()
: state.value.tabs.asMap().entries.map((e) {
final index = e.key;
final tab = e.value;
return _Tab(
key: ValueKey(tab.key),
index: index,
tabInfoKey: tab.key,
label: labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label),
selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon,
closable: tab.closable,
selected: state.value.selected,
onClose: () {
if (tab.onTabCloseButton != null) {
tab.onTabCloseButton!();
} else {
controller.remove(index);
}
},
onTap: () {
controller.jumpTo(index);
tab.onTap?.call();
},
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor: selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
);
}).toList()));
}
}
class _Tab extends StatefulWidget {
final int index;
final String tabInfoKey;
final Rx<String> label;
final IconData? selectedIcon;
final IconData? unselectedIcon;
final bool closable;
final int selected;
final Function() onClose;
final Function() onTap;
final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
final double? maxLabelWidth;
final Color? selectedTabBackgroundColor;
final Color? unSelectedTabBackgroundColor;
const _Tab({
Key? key,
required this.index,
required this.tabInfoKey,
required this.label,
this.selectedIcon,
this.unselectedIcon,
this.tabBuilder,
this.tabMenuBuilder,
required this.closable,
required this.selected,
required this.onClose,
required this.onTap,
this.maxLabelWidth,
this.selectedTabBackgroundColor,
this.unSelectedTabBackgroundColor,
}) : super(key: key);
@override
State<_Tab> createState() => _TabState();
}
class _TabState extends State<_Tab> with RestorationMixin {
final RestorableBool restoreHover = RestorableBool(false);
Widget _buildTabContent() {
bool showIcon =
widget.selectedIcon != null && widget.unselectedIcon != null;
bool isSelected = widget.index == widget.selected;
final icon = Offstage(
offstage: !showIcon,
child: Icon(
isSelected ? widget.selectedIcon : widget.unselectedIcon,
size: _kIconSize,
color: isSelected
? MyTheme.tabbar(context).selectedTabIconColor
: MyTheme.tabbar(context).unSelectedTabIconColor,
).paddingOnly(right: 5));
final labelWidget = Obx(() {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected
? MyTheme.tabbar(context).selectedTextColor
: MyTheme.tabbar(context).unSelectedTextColor),
overflow: TextOverflow.ellipsis,
));
});
Widget getWidgetWithBuilder() {
if (widget.tabBuilder == null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
labelWidget,
],
);
} else {
return widget.tabBuilder!(
widget.tabInfoKey,
icon,
labelWidget,
TabThemeConf(iconSize: _kIconSize),
);
}
}
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
if (e.buttons == 2) {
if (widget.tabMenuBuilder != null) {
showRightMenu(
(cacel) {
return widget.tabMenuBuilder!(widget.tabInfoKey);
},
target: e.position,
);
}
}
},
child: getWidgetWithBuilder(),
);
}
@override
Widget build(BuildContext context) {
bool isSelected = widget.index == widget.selected;
bool showDivider =
widget.index != widget.selected - 1 && widget.index != widget.selected;
RxBool hover = restoreHover.value.obs;
return Ink(
child: InkWell(
onHover: (value) {
hover.value = value;
restoreHover.value = value;
},
onTap: () => widget.onTap(),
child: Container(
color: isSelected
? widget.selectedTabBackgroundColor
: widget.unSelectedTabBackgroundColor,
child: Row(
children: [
SizedBox(
height: _kTabBarHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildTabContent(),
Obx((() => _CloseButton(
visiable: hover.value && widget.closable,
tabSelected: isSelected,
onClose: () => widget.onClose(),
)))
])).paddingSymmetric(horizontal: 10),
Offstage(
offstage: !showDivider,
child: VerticalDivider(
width: 1,
indent: _kDividerIndent,
endIndent: _kDividerIndent,
color: MyTheme.tabbar(context).dividerColor,
thickness: 1,
),
)
],
),
),
),
);
}
@override
String? get restorationId => "_Tab${widget.label.value}";
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(restoreHover, 'restoreHover');
}
}
class _CloseButton extends StatelessWidget {
final bool visiable;
final bool tabSelected;
final Function onClose;
const _CloseButton({
Key? key,
required this.visiable,
required this.tabSelected,
required this.onClose,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: _kIconSize,
child: Offstage(
offstage: !visiable,
child: InkWell(
customBorder: const RoundedRectangleBorder(),
onTap: () => onClose(),
child: Icon(
Icons.close,
size: _kIconSize,
color: tabSelected
? MyTheme.tabbar(context).selectedIconColor
: MyTheme.tabbar(context).unSelectedIconColor,
),
),
)).paddingOnly(left: 5);
}
}
2022-10-24 16:44:43 +08:00
class ActionIcon extends StatefulWidget {
final String? message;
final IconData icon;
final Function() onTap;
final bool isClose;
final double iconSize;
final double boxSize;
2022-10-24 16:44:43 +08:00
2022-10-14 22:50:13 +08:00
const ActionIcon(
{Key? key,
this.message,
2022-10-14 22:50:13 +08:00
required this.icon,
required this.onTap,
this.isClose = false,
this.iconSize = _kActionIconSize,
this.boxSize = _kTabBarHeight - 1})
2022-10-14 22:50:13 +08:00
: super(key: key);
2022-10-24 16:44:43 +08:00
@override
State<ActionIcon> createState() => _ActionIconState();
}
class _ActionIconState extends State<ActionIcon> {
var hover = false.obs;
@override
void initState() {
super.initState();
hover.value = false;
}
@override
Widget build(BuildContext context) {
2022-10-24 16:44:43 +08:00
return Tooltip(
message: widget.message != null ? translate(widget.message!) : "",
waitDuration: const Duration(seconds: 1),
child: Obx(
() => InkWell(
hoverColor: widget.isClose
? const Color.fromARGB(255, 196, 43, 28)
: MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value,
onTap: widget.onTap,
child: SizedBox(
height: widget.boxSize,
width: widget.boxSize,
child: Icon(
widget.icon,
color: hover.value && widget.isClose
? Colors.white
: MyTheme.tabbar(context).unSelectedIconColor,
size: widget.iconSize,
),
),
2022-10-24 16:44:43 +08:00
),
),
);
}
}
class AddButton extends StatelessWidget {
const AddButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ActionIcon(
message: 'New Connection',
icon: IconFont.add,
onTap: () => rustDeskWinManager.call(
WindowType.Main, kWindowMainWindowOnTop, ""),
isClose: false);
}
}
class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor;
final Color? selectedTextColor;
final Color? unSelectedTextColor;
final Color? selectedIconColor;
final Color? unSelectedIconColor;
final Color? dividerColor;
final Color? hoverColor;
const TabbarTheme(
{required this.selectedTabIconColor,
required this.unSelectedTabIconColor,
required this.selectedTextColor,
required this.unSelectedTextColor,
required this.selectedIconColor,
required this.unSelectedIconColor,
required this.dividerColor,
required this.hoverColor});
static const light = TabbarTheme(
selectedTabIconColor: MyTheme.accent,
unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241),
selectedTextColor: Color.fromARGB(255, 26, 26, 26),
unSelectedTextColor: Color.fromARGB(255, 96, 96, 96),
selectedIconColor: Color.fromARGB(255, 26, 26, 26),
unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
dividerColor: Color.fromARGB(255, 238, 238, 238),
hoverColor: Color.fromARGB(51, 158, 158, 158));
static const dark = TabbarTheme(
selectedTabIconColor: MyTheme.accent,
unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
selectedTextColor: Color.fromARGB(255, 255, 255, 255),
unSelectedTextColor: Color.fromARGB(255, 207, 207, 207),
selectedIconColor: Color.fromARGB(255, 215, 215, 215),
unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
dividerColor: Color.fromARGB(255, 64, 64, 64),
hoverColor: Colors.black26);
@override
ThemeExtension<TabbarTheme> copyWith({
Color? selectedTabIconColor,
Color? unSelectedTabIconColor,
Color? selectedTextColor,
Color? unSelectedTextColor,
Color? selectedIconColor,
Color? unSelectedIconColor,
Color? dividerColor,
Color? hoverColor,
}) {
return TabbarTheme(
selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor,
unSelectedTabIconColor:
unSelectedTabIconColor ?? this.unSelectedTabIconColor,
selectedTextColor: selectedTextColor ?? this.selectedTextColor,
unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor,
selectedIconColor: selectedIconColor ?? this.selectedIconColor,
unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor,
dividerColor: dividerColor ?? this.dividerColor,
hoverColor: hoverColor ?? this.hoverColor,
);
}
@override
ThemeExtension<TabbarTheme> lerp(
ThemeExtension<TabbarTheme>? other, double t) {
if (other is! TabbarTheme) {
return this;
}
return TabbarTheme(
selectedTabIconColor:
Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t),
unSelectedTabIconColor:
Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t),
selectedTextColor:
Color.lerp(selectedTextColor, other.selectedTextColor, t),
unSelectedTextColor:
Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t),
selectedIconColor:
Color.lerp(selectedIconColor, other.selectedIconColor, t),
unSelectedIconColor:
Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t),
dividerColor: Color.lerp(dividerColor, other.dividerColor, t),
hoverColor: Color.lerp(hoverColor, other.hoverColor, t),
);
}
static color(BuildContext context) {
return Theme.of(context).extension<ColorThemeExtension>()!;
}
}