feat: custom windows title bar (#5311)

This commit is contained in:
Mathias Mogensen 2024-05-12 22:09:55 +02:00 committed by GitHub
parent a0ed043cb8
commit cdcb393efd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 179 additions and 36 deletions

View File

@ -1,6 +1,37 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:window_manager/window_manager.dart';
class WindowsButtonListener extends WindowListener {
WindowsButtonListener();
final ValueNotifier<bool> isMaximized = ValueNotifier(false);
@override
void onWindowMaximize() {
isMaximized.value = true;
}
@override
void onWindowUnmaximize() {
isMaximized.value = false;
}
void dispose() {
isMaximized.dispose();
}
}
class CocoaWindowChannel {
CocoaWindowChannel._();
@ -26,29 +57,117 @@ class CocoaWindowChannel {
}
class MoveWindowDetector extends StatefulWidget {
const MoveWindowDetector({super.key, this.child});
const MoveWindowDetector({
super.key,
this.child,
this.showTitleBar = false,
});
final Widget? child;
final bool showTitleBar;
@override
MoveWindowDetectorState createState() => MoveWindowDetectorState();
}
class MoveWindowDetectorState extends State<MoveWindowDetector> {
late final WindowsButtonListener? windowsButtonListener;
double winX = 0;
double winY = 0;
bool isMaximized = false;
@override
void initState() {
if (PlatformExtension.isWindows) {
windowsButtonListener = WindowsButtonListener();
windowManager.addListener(windowsButtonListener!);
windowsButtonListener!.isMaximized.addListener(() {
if (mounted) {
setState(
() => isMaximized = windowsButtonListener!.isMaximized.value,
);
}
});
} else {
windowsButtonListener = null;
}
windowManager.isMaximized().then(
(v) => mounted ? setState(() => isMaximized = v) : null,
);
super.initState();
}
@override
void dispose() {
if (windowsButtonListener != null) {
windowManager.removeListener(windowsButtonListener!);
windowsButtonListener?.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!Platform.isMacOS) {
if (!Platform.isMacOS && !Platform.isWindows) {
return widget.child ?? const SizedBox.shrink();
}
if (Platform.isWindows) {
final brightness = Theme.of(context).brightness;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showTitleBar) ...[
Container(
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
),
child: DragToMoveArea(
child: Row(
children: [
const HSpace(4),
_buildToggleMenuButton(context),
const Spacer(),
WindowCaptionButton.minimize(
brightness: brightness,
onPressed: () => windowManager.minimize(),
),
if (isMaximized) ...[
WindowCaptionButton.unmaximize(
brightness: brightness,
onPressed: () => windowManager.unmaximize(),
),
] else ...[
WindowCaptionButton.maximize(
brightness: brightness,
onPressed: () => windowManager.maximize(),
),
],
WindowCaptionButton.close(
brightness: brightness,
onPressed: () => windowManager.close(),
),
],
),
),
),
] else ...[
const SizedBox(height: 5),
],
widget.child ?? const SizedBox.shrink(),
],
);
}
return GestureDetector(
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
behavior: HitTestBehavior.translucent,
onDoubleTap: () async {
await CocoaWindowChannel.instance.zoom();
},
onDoubleTap: () async => CocoaWindowChannel.instance.zoom(),
onPanStart: (DragStartDetails details) {
winX = details.globalPosition.dx;
winY = details.globalPosition.dy;
@ -65,4 +184,29 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
child: widget.child,
);
}
Widget _buildToggleMenuButton(BuildContext context) {
if (!context.read<HomeSettingBloc>().state.isMenuCollapsed) {
return const SizedBox.shrink();
}
return FlowyTooltip(
richMessage: TextSpan(
children: [
TextSpan(text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n'),
const TextSpan(text: 'Ctrl+\\'),
],
),
child: FlowyIconButton(
hoverColor: Colors.transparent,
onPressed: () => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
? const FlowySvg(FlowySvgs.show_menu_s)
: const FlowySvg(FlowySvgs.hide_menu_m),
),
);
}
}

View File

@ -6,8 +6,6 @@ import 'package:flutter/material.dart';
class AppFlowyApplication implements EntryPoint {
@override
Widget create(LaunchConfiguration config) {
return SplashScreen(
isAnon: config.isAnon,
);
return SplashScreen(isAnon: config.isAnon);
}
}

View File

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:appflowy/core/helpers/helpers.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scaled_app/scaled_app.dart';
@ -46,6 +47,11 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
await windowManager.show();
await windowManager.focus();
if (PlatformExtension.isWindows) {
// Hide title bar on Windows, we implement a custom solution elsewhere
await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
final position = await windowsManager.getPosition();
if (position != null) {
await windowManager.setPosition(position);
@ -54,8 +60,7 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
unawaited(
windowsManager.getScaleFactor().then(
(value) =>
ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value,
(v) => ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => v,
),
);
}

View File

@ -16,10 +16,7 @@ import 'package:go_router/go_router.dart';
class SplashScreen extends StatelessWidget {
/// Root Page of the app.
const SplashScreen({
super.key,
required this.isAnon,
});
const SplashScreen({super.key, required this.isAnon});
final bool isAnon;

View File

@ -220,9 +220,10 @@ class PageManager {
],
child: Selector<PageNotifier, Widget>(
selector: (context, notifier) => notifier.titleWidget,
builder: (context, widget, child) {
return MoveWindowDetector(child: HomeTopBar(layout: layout));
},
builder: (_, __, child) => MoveWindowDetector(
showTitleBar: true,
child: HomeTopBar(layout: layout),
),
),
);
}

View File

@ -6,6 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
@ -25,20 +26,18 @@ class SidebarTopMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) {
return SizedBox(
height: HomeSizes.topBarHeight,
child: MoveWindowDetector(
child: Row(
children: [
_buildLogoIcon(context),
const Spacer(),
_buildCollapseMenuButton(context),
],
),
builder: (context, _) => SizedBox(
height: !PlatformExtension.isWindows ? HomeSizes.topBarHeight : 45,
child: MoveWindowDetector(
child: Row(
children: [
_buildLogoIcon(context),
const Spacer(),
_buildCollapseMenuButton(context),
],
),
);
},
),
),
);
}
@ -72,15 +71,13 @@ class SidebarTopMenu extends StatelessWidget {
return FlowyTooltip(
richMessage: textSpan,
child: FlowyIconButton(
width: 28,
width: PlatformExtension.isWindows ? 30 : 28,
hoverColor: Colors.transparent,
onPressed: () => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: const FlowySvg(
FlowySvgs.hide_menu_m,
),
icon: const FlowySvg(FlowySvgs.hide_menu_m),
),
);
}

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -63,7 +64,7 @@ class FlowyNavigation extends StatelessWidget {
return BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
builder: (context, state) {
if (state.isMenuCollapsed) {
if (!PlatformExtension.isWindows && state.isMenuCollapsed) {
return RotationTransition(
turns: const AlwaysStoppedAnimation(180 / 360),
child: FlowyTooltip(