feat: frameless window for mac

This commit is contained in:
Vincent Chan 2022-05-27 10:34:12 +08:00
parent ef0d59ff30
commit c4db17f73c
7 changed files with 183 additions and 18 deletions

View File

@ -0,0 +1,67 @@
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'dart:io' show Platform;
class CocoaWindowChannel {
CocoaWindowChannel._();
final MethodChannel _channel = const MethodChannel("flutter/cocoaWindow");
static final CocoaWindowChannel instance = CocoaWindowChannel._();
Future<void> setWindowPosition(Offset offset) async {
await _channel.invokeMethod("setWindowPosition", [offset.dx, offset.dy]);
}
Future<List<double>> getWindowPosition() async {
final raw = await _channel.invokeMethod("getWindowPosition");
final arr = raw as List<dynamic>;
final List<double> result = arr.map((s) => s as double).toList();
return result;
}
Future<void> zoom() async {
await _channel.invokeMethod("zoom");
}
}
class MoveWindowDetector extends StatefulWidget {
const MoveWindowDetector({Key? key, this.child}) : super(key: key);
final Widget? child;
@override
_MoveWindowDetectorState createState() => _MoveWindowDetectorState();
}
class _MoveWindowDetectorState extends State<MoveWindowDetector> {
double winX = 0;
double winY = 0;
@override
Widget build(BuildContext context) {
if (!Platform.isMacOS) {
return widget.child ?? Container();
}
return GestureDetector(
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
behavior: HitTestBehavior.translucent,
onDoubleTap: () async {
await CocoaWindowChannel.instance.zoom();
},
onPanStart: (DragStartDetails details) {
winX = details.globalPosition.dx;
winY = details.globalPosition.dy;
},
onPanUpdate: (DragUpdateDetails details) async {
final windowPos = await CocoaWindowChannel.instance.getWindowPosition();
final double dx = windowPos[0];
final double dy = windowPos[1];
final deltaX = details.globalPosition.dx - winX;
final deltaY = details.globalPosition.dy - winY;
await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
},
child: widget.child,
);
}
}

View File

@ -49,6 +49,9 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
unauthorized: (_Unauthorized value) {
emit(state.copyWith(unauthorized: true));
},
collapseMenu: (e) {
emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
},
);
});
}
@ -77,6 +80,7 @@ class HomeEvent with _$HomeEvent {
const factory HomeEvent.dismissEditPannel() = _DismissEditPannel;
const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSetting setting) = _DidReceiveWorkspaceSetting;
const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
const factory HomeEvent.collapseMenu() = _CollapseMenu;
}
@freezed
@ -87,6 +91,7 @@ class HomeState with _$HomeState {
required Option<EditPannelContext> pannelContext,
required CurrentWorkspaceSetting workspaceSetting,
required bool unauthorized,
required bool isMenuCollapsed,
}) = _HomeState;
factory HomeState.initial(CurrentWorkspaceSetting workspaceSetting) => HomeState(
@ -95,5 +100,6 @@ class HomeState with _$HomeState {
pannelContext: none(),
workspaceSetting: workspaceSetting,
unauthorized: false,
isMenuCollapsed: false,
);
}

View File

@ -25,10 +25,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
listener.start(addAppCallback: _handleAppsOrFail);
await _fetchApps(emit);
},
collapse: (e) async {
final isCollapse = state.isCollapse;
emit(state.copyWith(isCollapse: !isCollapse));
},
openPage: (e) async {
emit(state.copyWith(plugin: e.plugin));
},
@ -94,7 +90,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
@freezed
class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.collapse() = _Collapse;
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
@ -104,14 +99,12 @@ class MenuEvent with _$MenuEvent {
@freezed
class MenuState with _$MenuState {
const factory MenuState({
required bool isCollapse,
required List<App> apps,
required Either<Unit, FlowyError> successOrFailure,
required Plugin plugin,
}) = _MenuState;
factory MenuState.initial() => MenuState(
isCollapse: false,
apps: [],
successOrFailure: left(unit),
plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),

View File

@ -1,8 +1,12 @@
import 'dart:io' show Platform;
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:time/time.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -11,6 +15,7 @@ import 'package:app_flowy/plugin/plugin.dart';
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:app_flowy/workspace/presentation/home/navigation.dart';
import 'package:app_flowy/core/frameless_window.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flowy_infra/notifier.dart';
@ -152,7 +157,7 @@ class HomeStackManager {
child: Selector<HomeStackNotifier, Widget>(
selector: (context, notifier) => notifier.titleWidget,
builder: (context, widget, child) {
return const HomeTopBar();
return const MoveWindowDetector(child: HomeTopBar());
},
),
);
@ -191,6 +196,14 @@ class HomeTopBar extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlocBuilder<HomeBloc, HomeState>(
buildWhen: ((previous, current) => previous.isMenuCollapsed != current.isMenuCollapsed),
builder: (context, state) {
if (state.isMenuCollapsed && Platform.isMacOS) {
return const HSpace(80);
}
return const HSpace(0);
}),
const FlowyNavigation(),
const HSpace(16),
ChangeNotifierProvider.value(

View File

@ -1,6 +1,8 @@
export './app/header/header.dart';
export './app/menu_app.dart';
import 'dart:io' show Platform;
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart';
import 'package:flowy_infra/notifier.dart';
@ -18,7 +20,9 @@ import 'package:expandable/expandable.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/core/frameless_window.dart';
// import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -59,10 +63,10 @@ class HomeMenu extends StatelessWidget {
getIt<HomeStackManager>().setPlugin(state.plugin);
},
),
BlocListener<MenuBloc, MenuState>(
listenWhen: (p, c) => p.isCollapse != c.isCollapse,
BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
listener: (context, state) {
_collapsedNotifier.value = state.isCollapse;
_collapsedNotifier.value = state.isMenuCollapsed;
},
)
],
@ -179,6 +183,17 @@ class MenuSharedState {
class MenuTopBar extends StatelessWidget {
const MenuTopBar({Key? key}) : super(key: key);
Widget renderIcon(BuildContext context) {
if (Platform.isMacOS) {
return Container();
}
final theme = context.watch<AppTheme>();
return (theme.isDark
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
: svgWithSize("flowy_logo_with_text", const Size(92, 17)));
}
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
@ -186,20 +201,19 @@ class MenuTopBar extends StatelessWidget {
builder: (context, state) {
return SizedBox(
height: HomeSizes.topBarHeight,
child: Row(
child: MoveWindowDetector(
child: Row(
children: [
(theme.isDark
? svgWithSize("flowy_logo_dark_mode", const Size(92, 17))
: svgWithSize("flowy_logo_with_text", const Size(92, 17))),
renderIcon(context),
const Spacer(),
FlowyIconButton(
width: 28,
onPressed: () => context.read<MenuBloc>().add(const MenuEvent.collapse()),
onPressed: () => context.read<HomeBloc>().add(const HomeEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: svgWidget("home/hide_menu", color: theme.iconColor),
)
],
),
)),
);
},
);

View File

@ -1,3 +1,4 @@
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/notifier.dart';
@ -95,6 +96,7 @@ class FlowyNavigation extends StatelessWidget {
width: 24,
onPressed: () {
notifier.value = false;
ctx.read<HomeBloc>().add(const HomeEvent.collapseMenu());
},
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
icon: svgWidget("home/hide_menu", color: theme.iconColor),

View File

@ -1,12 +1,82 @@
import Cocoa
import FlutterMacOS
private let kTrafficLightOffetTop = 22
class MainFlutterWindow: NSWindow {
func registerMethodChannel(flutterViewController: FlutterViewController) {
let cocoaWindowChannel = FlutterMethodChannel(name: "flutter/cocoaWindow", binaryMessenger: flutterViewController.engine.binaryMessenger)
cocoaWindowChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: FlutterResult) -> Void in
if call.method == "setWindowPosition" {
guard let position = call.arguments as? NSArray else {
result(nil)
return
}
let nX = position[0] as! NSNumber
let nY = position[1] as! NSNumber
let x = nX.doubleValue
let y = nY.doubleValue
self.setFrameOrigin(NSPoint(x: x, y: y))
result(nil)
return
} else if call.method == "getWindowPosition" {
let frame = self.frame
result([frame.origin.x, frame.origin.y])
return
} else if call.method == "zoom" {
self.zoom(self)
result(nil)
return
}
result(FlutterMethodNotImplemented)
})
}
func layoutTrafficLightButton(titlebarView: NSView, button: NSButton, offsetTop: CGFloat, offsetLeft: CGFloat) {
button.translatesAutoresizingMaskIntoConstraints = false;
titlebarView.addConstraint(NSLayoutConstraint.init(
item: button,
attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: offsetTop))
titlebarView.addConstraint(NSLayoutConstraint.init(
item: button,
attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: offsetLeft))
}
func layoutTrafficLights() {
let closeButton = self.standardWindowButton(ButtonType.closeButton)!
let minButton = self.standardWindowButton(ButtonType.miniaturizeButton)!
let zoomButton = self.standardWindowButton(ButtonType.zoomButton)!
let titlebarView = closeButton.superview!
self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20)
self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38)
self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56)
let customToolbar = NSTitlebarAccessoryViewController()
let newView = NSView()
newView.frame = NSRect(origin: CGPoint(), size: CGSize(width: 0, height: 40)) // only the height is cared
customToolbar.view = newView
self.addTitlebarAccessoryViewController(customToolbar)
}
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.registerMethodChannel(flutterViewController: flutterViewController)
self.setFrame(windowFrame, display: true)
self.titlebarAppearsTransparent = true
self.titleVisibility = .hidden
self.styleMask.insert(StyleMask.fullSizeContentView)
self.isMovableByWindowBackground = true
self.isMovable = false
self.layoutTrafficLights()
RegisterGeneratedPlugins(registry: flutterViewController)