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

Add window controls #378

Merged
merged 9 commits into from
Nov 12, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions example/lib/example_page_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'pages/selectable_container_page.dart';
import 'pages/switch_button_page.dart';
import 'pages/tabbed_page_page.dart';
import 'pages/tile_page.dart';
import 'pages/window_controls_page.dart';

class PageItem {
const PageItem({
Expand Down Expand Up @@ -194,4 +195,10 @@ final examplePageItems = <PageItem>[
: const Icon(YaruIcons.information),
pageBuilder: (_) => const DialogPage(),
),
PageItem(
titleBuilder: (context) => const Text('YaruWindowControl'),
tooltipMessage: 'YaruWindowControl',
iconBuilder: (context, selected) => const Icon(YaruIcons.window_top_bar),
pageBuilder: (_) => const WindowControlsPage(),
),
];
50 changes: 50 additions & 0 deletions example/lib/pages/window_controls_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:yaru_widgets/yaru_widgets.dart';

class WindowControlsPage extends StatefulWidget {
const WindowControlsPage({super.key});

@override
State<WindowControlsPage> createState() => _WindowControlsPageState();
}

class _WindowControlsPageState extends State<WindowControlsPage> {
bool _maximized = false;

@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(kYaruPagePadding),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
YaruWindowControl(
type: YaruWindowControlType.minimize,
onTap: () {},
),
const SizedBox(width: 10),
YaruWindowControl(
type: _maximized
? YaruWindowControlType.maximize
: YaruWindowControlType.restore,
onTap: () => setState(() => _maximized = !_maximized),
),
const SizedBox(width: 10),
YaruWindowControl(
type: YaruWindowControlType.close,
onTap: () {},
),
],
),
YaruTile(
title: const Text('Maximized'),
trailing: YaruSwitch(
value: _maximized,
onChanged: (v) => setState(() => _maximized = v),
),
),
],
);
}
}
256 changes: 256 additions & 0 deletions lib/src/controls/yaru_window_control.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import 'package:flutter/material.dart';

const _kWindowControlSize = 24.0;
const _kWindowControlIconSize = 8.0;
const _kWindowControlIconStrokeWidth = 1.0;
const _kWindowControlIconStrokeAlign = _kWindowControlIconStrokeWidth / 2;
const _kWindowControlIconAnimationDuration = Duration(milliseconds: 500);
const _kWindowControlAnimationCurve = Curves.linear;
const _kWindowControlBackgroundAnimationDuration = Duration(milliseconds: 200);
const _kWindowControlBackgroundOpacity = 0.1;
const _kWindowControlBackgroundOpacityHover = 0.15;
const _kWindowControlBackgroundOpacityActive = 0.2;

/// Defines the look of a [YaruWindowControl]
enum YaruWindowControlType {
close,
maximize,
restore,
minimize,
}

class YaruWindowControl extends StatefulWidget {
const YaruWindowControl({
super.key,
required this.type,
required this.onTap,
});

final YaruWindowControlType type;

final GestureTapCallback? onTap;

@override
State<YaruWindowControl> createState() {
return _YaruWindowControlState();
}
}

class _YaruWindowControlState extends State<YaruWindowControl>
with TickerProviderStateMixin {
bool _hover = false;
bool _active = false;

late YaruWindowControlType oldType;

late CurvedAnimation _position;
late AnimationController _positionController;

@override
void initState() {
super.initState();

oldType = widget.type;

_positionController = AnimationController(
Jupi007 marked this conversation as resolved.
Show resolved Hide resolved
duration: _kWindowControlIconAnimationDuration,
value: widget.type == YaruWindowControlType.maximize ? 0.0 : 1.0,
vsync: this,
);
_position = CurvedAnimation(
parent: _positionController,
curve: _kWindowControlAnimationCurve,
);
}

@override
void didUpdateWidget(covariant YaruWindowControl oldWidget) {
super.didUpdateWidget(oldWidget);

if (oldWidget.type != widget.type) {
oldType = widget.type;

if (oldWidget.type == YaruWindowControlType.maximize &&
widget.type == YaruWindowControlType.restore) {
_positionController.forward();
} else if (oldWidget.type == YaruWindowControlType.restore &&
widget.type == YaruWindowControlType.maximize) {
_positionController.reverse();
} else if (widget.type == YaruWindowControlType.restore) {
_positionController.value = 0.0;
} else if (widget.type == YaruWindowControlType.maximize) {
_positionController.value = 1.0;
}
}
}

@override
void dispose() {
_positionController.dispose();

super.dispose();
}

void _handleHover(bool hover) {
setState(() {
_hover = hover;

if (!hover) {
_active = false;
}
});
}

void _handleActive(bool active) {
setState(() {
_active = active;
});
}

Color _getColor(BuildContext context) {
final onSurface = Theme.of(context).colorScheme.onSurface;
return _active
? onSurface.withOpacity(_kWindowControlBackgroundOpacityActive)
: _hover
? onSurface.withOpacity(_kWindowControlBackgroundOpacityHover)
: onSurface.withOpacity(_kWindowControlBackgroundOpacity);
}

Widget _buildEventDetectors(Widget child) {
return MouseRegion(
onEnter: (_) => _handleHover(true),
onExit: (_) => _handleHover(false),
child: GestureDetector(
onTap: widget.onTap,
onTapDown: (_) => _handleActive(true),
onTapUp: (_) => _handleActive(false),
child: child,
),
);
}

@override
Widget build(BuildContext context) {
return _buildEventDetectors(
AnimatedContainer(
duration: _kWindowControlBackgroundAnimationDuration,
decoration: BoxDecoration(
color: _getColor(context),
shape: BoxShape.circle,
),
child: SizedBox.square(
dimension: _kWindowControlSize,
child: Center(
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _position,
builder: (context, child) => CustomPaint(
size: const Size.square(_kWindowControlIconSize),
painter: _YaruWindowControlPainter(
type: widget.type,
oldType: oldType,
iconColor: Theme.of(context).colorScheme.onSurface,
position: _position.value,
),
),
),
),
),
),
),
);
}
}

class _YaruWindowControlPainter extends CustomPainter {
_YaruWindowControlPainter({
required this.type,
required this.oldType,
required this.iconColor,
required this.position,
});

final YaruWindowControlType type;
final YaruWindowControlType oldType;

final Color iconColor;

final double position;

@override
void paint(Canvas canvas, Size size) {
const rect = Rect.fromLTWH(
_kWindowControlIconStrokeAlign,
_kWindowControlIconStrokeAlign,
_kWindowControlIconSize - _kWindowControlIconStrokeAlign * 2,
_kWindowControlIconSize - _kWindowControlIconStrokeAlign * 2,
);

switch (type) {
case YaruWindowControlType.close:
_drawClose(canvas, size, rect);
break;
case YaruWindowControlType.minimize:
_drawMinimize(canvas, size, rect);
break;
case YaruWindowControlType.restore:
case YaruWindowControlType.maximize:
_drawRestoreMaximize(canvas, size, rect);
break;
}
}

void _drawClose(Canvas canvas, Size size, Rect rect) {
canvas.drawLine(rect.topLeft, rect.bottomRight, _getIconPaint());
canvas.drawLine(rect.topRight, rect.bottomLeft, _getIconPaint());
}

void _drawRestoreMaximize(Canvas canvas, Size size, Rect drawRect) {
const gap = _kWindowControlIconStrokeWidth + 1;

final rect = Rect.fromLTRB(
drawRect.left,
drawRect.top + gap * position,
drawRect.right - gap * position,
drawRect.bottom,
);

final path = Path()
..moveTo(
drawRect.topLeft.dx + gap,
drawRect.topLeft.dy,
)
..lineTo(
drawRect.topRight.dx,
drawRect.topRight.dy,
)
..lineTo(
drawRect.bottomRight.dx,
drawRect.bottomRight.dy - gap,
);

final color = _getIconPaint().color;

canvas.drawRect(rect, _getIconPaint());
canvas.drawPath(path, _getIconPaint()..color = color.withOpacity(position));
}

void _drawMinimize(Canvas canvas, Size size, Rect rect) {
canvas.drawLine(
Offset(rect.bottomLeft.dx, rect.bottomLeft.dy - 1.0),
Offset(rect.bottomRight.dx, rect.bottomRight.dy - 1.0),
_getIconPaint(),
);
}

Paint _getIconPaint() {
return Paint()
..style = PaintingStyle.stroke
..strokeWidth = _kWindowControlIconStrokeWidth
..color = iconColor
..strokeCap = StrokeCap.square;
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
1 change: 1 addition & 0 deletions lib/yaru_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export 'src/controls/yaru_switch_button.dart';
export 'src/controls/yaru_title_bar.dart';
export 'src/controls/yaru_toggle_button.dart';
export 'src/controls/yaru_toggle_button_theme.dart';
export 'src/controls/yaru_window_control.dart';
// Extensions
export 'src/extensions/border_radius_extension.dart';
// Layouts
Expand Down