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

feat: add top bar layout control #818

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
14 changes: 13 additions & 1 deletion example/lib/pages/dialog_page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

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

Expand Down Expand Up @@ -49,7 +51,17 @@ class _DialogPageState extends State<DialogPage> {
),
),
title: const Text('The Title'),
isClosable: isCloseable,
windowControlLayout: isCloseable
? ((Platform.isMacOS)
? const YaruWindowControlLayout(
[YaruWindowControlType.close],
[],
)
: const YaruWindowControlLayout(
[],
[YaruWindowControlType.close],
))
: const YaruWindowControlLayout([], []),
),
content: SizedBox(
height: 100,
Expand Down
172 changes: 62 additions & 110 deletions lib/src/widgets/yaru_title_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:yaru/yaru.dart';
import 'package:yaru_widgets/constants.dart';
import 'package:yaru_widgets/src/widgets/yaru_window_control_layout.dart';
import 'package:yaru_window/yaru_window.dart';

import 'yaru_title_bar_gesture_detector.dart';
import 'yaru_title_bar_theme.dart';
import 'yaru_window_control.dart';
import 'yaru_window_control_row.dart';

const _kYaruTitleBarHeroTag = '<YaruTitleBar hero tag>';

Expand All @@ -32,12 +34,10 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
this.shape,
this.border,
this.style,
this.windowControlLayout,
this.isActive,
this.isClosable,
this.isMaximized,
this.isDraggable,
this.isMaximizable,
this.isMinimizable,
this.isRestorable,
this.onClose,
this.onDrag,
this.onMaximize,
Expand Down Expand Up @@ -80,30 +80,21 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
/// The style.
final YaruTitleBarStyle? style;

/// The order and position of window controls
final YaruWindowControlLayout? windowControlLayout;

/// Whether the title bar visualized as active.
final bool? isActive;

/// Whether the title bar shows a close button.
final bool? isClosable;
/// Whether to show the restore rather than the maximize button.
final bool? isMaximized;

/// Whether the title bar can be dragged.
final bool? isDraggable;

/// Whether the title bar shows a maximize button.
final bool? isMaximizable;

/// Whether the title bar shows a minimize button.
final bool? isMinimizable;

/// Whether the title bar shows a restore button.
final bool? isRestorable;

/// Called when the close button is pressed.
final FutureOr<void> Function(BuildContext)? onClose;

/// Called when the title bar is dragged to move the window.
final FutureOr<void> Function(BuildContext)? onDrag;

/// Called when the maximize button is pressed or the title bar is
/// double-clicked while the window is not maximized.
final FutureOr<void> Function(BuildContext)? onMaximize;
Expand All @@ -115,6 +106,9 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
/// double-clicked while the window is maximized.
final FutureOr<void> Function(BuildContext)? onRestore;

/// Called when the title bar is dragged to move the window.
final FutureOr<void> Function(BuildContext)? onDrag;

/// Called when the secondary mouse button is pressed.
final FutureOr<void> Function(BuildContext)? onShowMenu;

Expand Down Expand Up @@ -230,11 +224,9 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
return TextFieldTapRegion(
child: YaruTitleBarGestureDetector(
onDrag: isDraggable == true ? (_) => onDrag?.call(context) : null,
onDoubleTap: () => isMaximizable == true
? onMaximize?.call(context)
: isRestorable == true
? onRestore?.call(context)
: null,
onDoubleTap: () => isMaximized == true
? onRestore?.call(context)
: onMaximize?.call(context),
onSecondaryTap: onShowMenu != null ? () => onShowMenu!(context) : null,
child: AppBar(
elevation: titleBarTheme.elevation,
Expand All @@ -254,54 +246,32 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (style == YaruTitleBarStyle.normal &&
windowControlLayout?.leftItems != null &&
windowControlLayout!.leftItems.isNotEmpty)
YaruWindowControlRow(
windowControls: windowControlLayout!.leftItems,
isMaximized: isMaximized ?? false,
buttonPadding: bPadding,
buttonSpacing: bSpacing,
onClose: onClose,
onMaximize: onMaximize,
onRestore: onRestore,
onMinimize: onMinimize,
),
...?actions,
if (style == YaruTitleBarStyle.normal &&
(isMinimizable == true ||
isRestorable == true ||
isMaximizable == true ||
isClosable == true))
Padding(
padding: bPadding,
child: Row(
children: [
if (isMinimizable == true)
YaruWindowControl(
platform: windowControlPlatform,
foregroundColor: foregroundColor,
type: YaruWindowControlType.minimize,
onTap: onMinimize != null
? () => onMinimize!(context)
: null,
),
if (isRestorable == true)
YaruWindowControl(
platform: windowControlPlatform,
foregroundColor: foregroundColor,
type: YaruWindowControlType.restore,
onTap: onRestore != null
? () => onRestore!(context)
: null,
),
if (isMaximizable == true)
YaruWindowControl(
platform: windowControlPlatform,
foregroundColor: foregroundColor,
type: YaruWindowControlType.maximize,
onTap: onMaximize != null
? () => onMaximize!(context)
: null,
),
if (isClosable == true)
YaruWindowControl(
platform: windowControlPlatform,
foregroundColor: foregroundColor,
type: YaruWindowControlType.close,
onTap: onClose != null
? () => onClose!(context)
: null,
),
].withSpacing(bSpacing),
),
windowControlLayout?.rightItems != null &&
windowControlLayout!.rightItems.isNotEmpty)
YaruWindowControlRow(
windowControls: windowControlLayout!.rightItems,
isMaximized: isMaximized ?? false,
buttonPadding: bPadding,
buttonSpacing: bSpacing,
onClose: onClose,
onMaximize: onMaximize,
onRestore: onRestore,
onMinimize: onMinimize,
),
],
),
Expand All @@ -314,15 +284,6 @@ class YaruTitleBar extends StatelessWidget implements PreferredSizeWidget {
}
}

extension _ListSpacing on List<Widget> {
List<Widget> withSpacing(double spacing) {
return expand((item) sync* {
yield SizedBox(width: spacing);
yield item;
}).skip(1).toList();
}
}

/// A window title bar.
///
/// `YaruWindowTitleBar` is a replacement for the native window title bar, that
Expand Down Expand Up @@ -425,12 +386,10 @@ class YaruWindowTitleBar extends StatelessWidget
this.shape,
this.border,
this.style,
this.windowControlLayout,
this.isActive,
this.isClosable,
this.isDraggable,
this.isMaximizable,
this.isMinimizable,
this.isRestorable,
this.isMaximized,
this.onClose = YaruWindow.close,
this.onDrag = YaruWindow.drag,
this.onMaximize = YaruWindow.maximize,
Expand Down Expand Up @@ -476,20 +435,14 @@ class YaruWindowTitleBar extends StatelessWidget
/// Whether the title bar visualized as active.
final bool? isActive;

/// Whether the title bar shows a close button.
final bool? isClosable;

/// Whether the title bar can be dragged to move the window.
final bool? isDraggable;

/// Whether the title bar shows a maximize button.
final bool? isMaximizable;

/// Whether the title bar shows a minimize button.
final bool? isMinimizable;
/// Whether to show the restore rather than the maximize button.
final bool? isMaximized;

/// Whether the title bar shows a restore button.
final bool? isRestorable;
/// The order and position of window controls
final YaruWindowControlLayout? windowControlLayout;

/// Called when the close button is pressed.
final FutureOr<void> Function(BuildContext)? onClose;
Expand Down Expand Up @@ -566,14 +519,16 @@ class YaruWindowTitleBar extends StatelessWidget
border: border,
style: style,
isActive: isActive ?? state?.isActive,
isClosable: isClosable ?? state?.isClosable?.exceptMacOS(context),
isDraggable: isDraggable ?? state?.isMovable,
isMaximizable:
isMaximizable ?? state?.isMaximizable?.exceptMacOS(context),
isMinimizable:
isMinimizable ?? state?.isMinimizable?.exceptMacOS(context),
isRestorable:
isRestorable ?? state?.isRestorable?.exceptMacOS(context),
isMaximized: isMaximized ?? state?.isMaximized,
windowControlLayout: windowControlLayout ??
(_onMacOS(context)
? const YaruWindowControlLayout([], [])
: const YaruWindowControlLayout([], [
YaruWindowControlType.minimize,
YaruWindowControlType.maximize,
YaruWindowControlType.close
])),
onClose: onClose,
onDrag: onDrag,
onMaximize: onMaximize,
Expand All @@ -585,6 +540,11 @@ class YaruWindowTitleBar extends StatelessWidget
},
);
}

bool _onMacOS(BuildContext context) {
final platform = Theme.of(context).platform;
return !kIsWeb && platform == TargetPlatform.macOS;
}
}

/// A dialog title bar.
Expand All @@ -607,11 +567,10 @@ class YaruDialogTitleBar extends YaruWindowTitleBar {
super.border,
super.style = YaruTitleBarStyle.normal,
super.isActive,
super.isClosable = true,
super.isDraggable,
super.isMaximizable = false,
super.isMinimizable = false,
super.isRestorable = false,
super.windowControlLayout =
const YaruWindowControlLayout([], [YaruWindowControlType.close]),
super.isMaximized = false,
super.onClose = _maybePop,
super.onDrag = YaruWindow.drag,
super.onMaximize = null,
Expand All @@ -634,10 +593,3 @@ class YaruDialogTitleBar extends YaruWindowTitleBar {
return Navigator.maybePop(context);
}
}

extension on bool? {
bool? exceptMacOS(BuildContext context) {
final platform = Theme.of(context).platform;
return !kIsWeb && platform == TargetPlatform.macOS ? false : this;
}
}
56 changes: 56 additions & 0 deletions lib/src/widgets/yaru_window_control_layout.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'yaru_window_control.dart';

/// Defines the order and position in which [YaruWindowControl] items are presented.
///
/// YaruWindowControlType.maximize and YaruWindowControlType.restore are treated as the same button.
/// Only put one of these types in your YaruWindowControlLayout, otherwise the button will appear twice.

class YaruWindowControlLayout {
const YaruWindowControlLayout(this.leftItems, this.rightItems);

final List<YaruWindowControlType> leftItems;
final List<YaruWindowControlType> rightItems;

/// Parses the [gtk-decoration-layout](https://docs.gtk.org/gtk4/property.Settings.gtk-decoration-layout.html)
/// string, as provided by GTK settings.
///
/// It is up to the developer how to retrieve the string. For example, they can
/// use the [gtk](https://pub.dev/packages/gtk) package.
static YaruWindowControlLayout parseGTKSetting(
String gtkDecorationLayoutString,
) {
final splitSideStrings = gtkDecorationLayoutString.split(':');
final leftItemStrings = splitSideStrings.first.split(',');
final rightItemStrings = (splitSideStrings.length > 1)
? splitSideStrings.last.split(',')
: <String>[];

return YaruWindowControlLayout(
_getControlTypesFromStrings(leftItemStrings),
_getControlTypesFromStrings(rightItemStrings),
);
}

static List<YaruWindowControlType> _getControlTypesFromStrings(
List<String> gtkControls,
) {
final decorations = List<YaruWindowControlType>.empty(growable: true);
for (final gtkControl in gtkControls) {
if (gtkControl.isNotEmpty) {
switch (gtkControl) {
case 'close':
decorations.add(YaruWindowControlType.close);
break;
case 'minimize':
decorations.add(YaruWindowControlType.minimize);
break;
case 'maximize':
decorations.add(YaruWindowControlType.maximize);
break;
default: // anything else is not supported, including "icon" and "menu"
}
}
}
return decorations;
}
}
Loading
Loading