Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce YaruWindowTitleBar & YaruDialogTitleBar #455

Merged
merged 20 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/lib/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Future<void> showSettingsDialog(BuildContext context) {
animation: model,
builder: (context, child) {
return AlertDialog(
title: const YaruTitleBar(
title: const YaruDialogTitleBar(
title: Text('Settings'),
),
titlePadding: EdgeInsets.zero,
Expand Down
10 changes: 8 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ubuntu_service/ubuntu_service.dart';
import 'package:yaru/yaru.dart';
import 'package:yaru_widgets/yaru_widgets.dart';

import 'example.dart';
import 'theme.dart';

void main() {
Future<void> main() async {
await YaruWindowTitleBar.ensureInitialized();

registerService<Connectivity>(Connectivity.new);
runApp(
MultiProvider(
Expand All @@ -30,7 +33,10 @@ class Home extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: context.watch<LightTheme>().value,
darkTheme: context.watch<DarkTheme>().value,
home: Example.create(context),
home: Scaffold(
appBar: const YaruWindowTitleBar(),
body: Example.create(context),
),
);
}
}
8 changes: 8 additions & 0 deletions example/linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@
#include "generated_plugin_registrant.h"

#include <handy_window/handy_window_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <yaru/yaru_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) handy_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin");
handy_window_plugin_register_with_registrar(handy_window_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
g_autoptr(FlPluginRegistrar) yaru_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "YaruPlugin");
yaru_plugin_register_with_registrar(yaru_registrar);
Expand Down
2 changes: 2 additions & 0 deletions example/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

list(APPEND FLUTTER_PLUGIN_LIST
handy_window
screen_retriever
window_manager
yaru
)

Expand Down
167 changes: 165 additions & 2 deletions lib/src/controls/yaru_title_bar.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../constants.dart';
import 'yaru_title_bar_theme.dart';
import 'yaru_window.dart';
import 'yaru_window_control.dart';
import 'yaru_window_controller.dart';
import 'yaru_window_state.dart';

const _kTitleButtonPadding = EdgeInsets.symmetric(horizontal: 7);

Expand Down Expand Up @@ -112,8 +117,16 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
MaterialStateProperty.resolveAs(this.foregroundColor, states) ??
theme.foregroundColor?.resolve(states) ??
Theme.of(context).colorScheme.onSurface;
final titleTextStyle =
theme.titleTextStyle?.copyWith(color: foregroundColor);

final titleTextStyle = Theme.of(context)
.appBarTheme
.titleTextStyle!
.copyWith(
color: foregroundColor,
fontSize: 14,
fontWeight: FontWeight.w500,
)
.merge(theme.titleTextStyle);
final shape = theme.shape ??
Border(
bottom: BorderSide(
Expand Down Expand Up @@ -208,3 +221,153 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
);
}
}

class YaruWindowTitleBar extends StatefulWidget implements PreferredSizeWidget {
const YaruWindowTitleBar({
super.key,
this.leading,
this.title,
this.trailing,
this.centerTitle,
this.backgroundColor,
this.foregroundColor,
this.controller,
});

/// The primary title widget.
final Widget? title;

/// A widget to display before the [title] widget.
final Widget? leading;

/// A widget to display after the [title] widget.
final Widget? trailing;

/// Whether the title should be centered.
final bool? centerTitle;

/// The foreground color.
final Color? foregroundColor;

/// The background color.
final Color? backgroundColor;

/// An optional controller.
final YaruWindowController? controller;

@override
Size get preferredSize => const Size(0, kIsWeb ? 0 : kYaruTitleBarHeight);

@override
State<YaruWindowTitleBar> createState() => _YaruWindowTitleBarState();

static Future<void> ensureInitialized() => YaruWindow.ensureInitialized();
}

class _YaruWindowTitleBarState extends State<YaruWindowTitleBar> {
late YaruWindowController _controller;

YaruWindowController createController() {
return YaruWindowController(
state: kIsWeb
? const YaruWindowState(
closable: false,
movable: false,
maximizable: false,
minimizable: false,
)
: Platform.isMacOS
? const YaruWindowState(
closable: false,
maximizable: false,
minimizable: false,
)
: null,
);
}

bool get isVisible => !kIsWeb;

@override
void initState() {
super.initState();
_controller = widget.controller ?? createController();
_controller.init();
}

@override
void didUpdateWidget(covariant YaruWindowTitleBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
if (oldWidget.controller == null) _controller.dispose();
_controller = widget.controller ?? createController();
_controller.init();
}
}

@override
void dispose() {
if (widget.controller == null) _controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
if (!isVisible) return const SizedBox.shrink();
return AnimatedBuilder(
animation: _controller,
builder: (context, child) => YaruTitleBar(
leading: widget.leading,
title: widget.title ?? Text(_controller.state?.title ?? ''),
trailing: widget.trailing,
centerTitle: widget.centerTitle,
backgroundColor: widget.backgroundColor,
isActive: _controller.state?.active,
isClosable: _controller.state?.closable,
isMaximizable: _controller.state?.maximizable,
isMinimizable: _controller.state?.minimizable,
isRestorable: _controller.state?.restorable,
onClose: _controller.close,
onDrag: _controller.state?.movable == true ? _controller.drag : null,
onMaximize: _controller.maximize,
onMinimize: _controller.minimize,
onRestore: _controller.restore,
onShowMenu: _controller.showMenu,
),
);
}
}

class YaruDialogTitleBar extends YaruWindowTitleBar {
const YaruDialogTitleBar({
super.key,
super.leading,
super.title,
super.trailing,
super.centerTitle,
super.backgroundColor,
super.controller,
});

@override
State<YaruWindowTitleBar> createState() => _YaruDialogTitleBarState();
}

class _YaruDialogTitleBarState extends _YaruWindowTitleBarState {
@override
YaruWindowController createController() {
return YaruWindowController(
state: const YaruWindowState(
closable: true,
maximizable: false,
minimizable: false,
movable: !kIsWeb,
restorable: false,
),
close: Navigator.of(context).maybePop,
);
}

@override
bool get isVisible => true;
}
4 changes: 1 addition & 3 deletions lib/src/controls/yaru_title_bar_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

const _kTitleTextStyle = TextStyle(fontSize: 14, fontWeight: FontWeight.w500);

@immutable
class YaruTitleBarThemeData extends ThemeExtension<YaruTitleBarThemeData>
with Diagnosticable {
Expand All @@ -14,7 +12,7 @@ class YaruTitleBarThemeData extends ThemeExtension<YaruTitleBarThemeData>
this.titleSpacing,
this.foregroundColor,
this.backgroundColor,
this.titleTextStyle = _kTitleTextStyle,
this.titleTextStyle,
this.shape,
});

Expand Down
122 changes: 122 additions & 0 deletions lib/src/controls/yaru_window.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:window_manager/window_manager.dart';

import 'yaru_window_state.dart';

class YaruWindow {
@visibleForTesting
static WindowManager wm = WindowManager.instance;

static Future<void> close(_) => wm.close();
static Future<void> drag(_) => wm.startDragging();
static Future<void> maximize(_) => wm.maximize();
static Future<void> minimize(_) => wm.minimize();
static Future<void> restore(_) => wm.unmaximize();
static Future<void> showMenu(_) => wm.popUpWindowMenu().catchError((_) {});
static Future<YaruWindowState> state() => wm.state();
static Stream<YaruWindowState> states() async* {
final listener = YaruWindowListener(wm);
try {
yield* listener.listen();
} finally {
await listener.close();
}
}

static Future<void> ensureInitialized() async {
WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb) {
await wm.ensureInitialized();
await wm.setTitleBarStyle(TitleBarStyle.hidden);
}
}
}

extension YaruWindowManagerX on WindowManager {
Future<YaruWindowState> state() {
return Future.wait([
isFocused().catchError((_) => true),
isClosable().catchError((_) => true),
isFullScreen().catchError((_) => false),
isMaximizable().catchError((_) => true),
isMaximized().catchError((_) => false),
isMinimizable().catchError((_) => true),
isMinimized().catchError((_) => false),
isMovable().catchError((_) => true),
getTitle().catchError((_) => ''),
]).then((values) {
final active = values[0] as bool;
final closable = values[1] as bool;
final fullscreen = values[2] as bool;
final maximizable = values[3] as bool;
final maximized = values[4] as bool;
final minimizable = values[5] as bool;
final minimized = values[6] as bool;
final movable = values[7] as bool;
final title = values[8] as String;
return YaruWindowState(
active: active,
closable: closable,
fullscreen: fullscreen,
maximizable: maximizable && !maximized,
maximized: maximized,
minimizable: minimizable && !minimized,
minimized: minimized,
movable: movable,
restorable: fullscreen || maximized || minimized,
title: title,
);
});
}
}

class YaruWindowListener implements WindowListener {
YaruWindowListener(this._wm);

final WindowManager _wm;
final _controller = StreamController<YaruWindowState>();

Stream<YaruWindowState> listen() {
_wm.addListener(this);
return _controller.stream;
}

Future<void> close() async {
_wm.removeListener(this);
await _controller.close();
}

Future<void> _emitState() async => _controller.add(await _wm.state());

@override
void onWindowBlur() => _emitState();
@override
void onWindowFocus() => _emitState();
@override
void onWindowEnterFullScreen() => _emitState();
@override
void onWindowLeaveFullScreen() => _emitState();
@override
void onWindowMaximize() => _emitState();
@override
void onWindowUnmaximize() => _emitState();
@override
void onWindowMinimize() => _emitState();
@override
void onWindowRestore() => _emitState();
@override
void onWindowClose() {}
@override
void onWindowResize() {}
@override
void onWindowResized() {}
@override
void onWindowMove() {}
@override
void onWindowMoved() {}
@override
void onWindowEvent(String eventName) {}
}
Loading