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

YaruLandscapeLayout: allow left pane resizing #266

Merged
merged 16 commits into from
Oct 11, 2022
1 change: 1 addition & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ const kYaruDialogTitlePadding = 0.0;
const kYaruPageWidth = 500.0;
const kYaruContainerRadius = 8.0;
const kYaruButtonRadius = 6.0;
const kYaruMasterDetailBreakpoint = 620.0;
215 changes: 173 additions & 42 deletions lib/src/pages/layouts/yaru_landscape_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class YaruLandscapeLayout extends StatefulWidget {
required this.pageBuilder,
required this.onSelected,
required this.leftPaneWidth,
required this.allowLeftPaneResize,
required this.leftPaneMinWidth,
required this.pageMinWidth,
this.onLeftPaneWidthChange,
this.appBar,
});

Expand All @@ -36,9 +40,21 @@ class YaruLandscapeLayout extends StatefulWidget {
/// Callback that returns an index when the page changes.
final ValueChanged<int> onSelected;

/// Specifies the width of left pane.
/// Specifies the initial width of left pane.
final double leftPaneWidth;

/// If true, allow the left pane to be resized.
final bool allowLeftPaneResize;

/// If [allowLeftPaneResize], specifies the min-width of the left pane.
final double leftPaneMinWidth;

/// If [allowLeftPaneResize], callback called when the left pane is resizing.
final Function(double)? onLeftPaneWidthChange;

/// If [allowLeftPaneResize], specifies the min-width of the page.
final double pageMinWidth;

/// An optional [PreferredSizeWidget] used as the left [AppBar]
/// If provided, a second [AppBar] will be created right to it.
final PreferredSizeWidget? appBar;
Expand All @@ -47,13 +63,38 @@ class YaruLandscapeLayout extends StatefulWidget {
State<YaruLandscapeLayout> createState() => _YaruLandscapeLayoutState();
}

const _kLeftPaneResizingRegionWidth = 4.0;
const _kLeftPaneResizingRegionAnimationDuration = Duration(milliseconds: 250);

class _YaruLandscapeLayoutState extends State<YaruLandscapeLayout> {
late int _selectedIndex;
late double _leftPaneWidth;

double _initialPaneWidth = 0.0;
double _paneWidthMove = 0.0;

bool _isDragging = false;
bool _isHovering = false;

@override
void initState() {
_selectedIndex = widget.selectedIndex;
super.initState();
_selectedIndex = widget.selectedIndex;
_leftPaneWidth = widget.leftPaneWidth;
}

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

final width = MediaQuery.of(context).size.width;

// Avoid left pane to overflow when resizing the window
if (widget.allowLeftPaneResize &&
_leftPaneWidth >= width - widget.pageMinWidth) {
_leftPaneWidth = width - widget.pageMinWidth;
widget.onLeftPaneWidthChange?.call(_leftPaneWidth);
}
Jupi007 marked this conversation as resolved.
Show resolved Hide resolved
}

void _onTap(int index) {
Expand All @@ -63,51 +104,141 @@ class _YaruLandscapeLayoutState extends State<YaruLandscapeLayout> {

@override
Widget build(BuildContext context) {
final theme = YaruMasterDetailTheme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: widget.leftPaneWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
width: 1,
color: Colors.black.withOpacity(0.1),
),
),
),
child: Scaffold(
appBar: widget.appBar,
body: YaruMasterListView(
length: widget.length,
selectedIndex: _selectedIndex,
onTap: _onTap,
builder: widget.tileBuilder,
return _maybeBuildGlobalMouseRegion(
LayoutBuilder(
builder: (context, boxConstraints) => Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildLeftPane(),
Expanded(
child: widget.allowLeftPaneResize
? Stack(
children: [
_buildPage(context),
_buildLeftPaneResizer(context, boxConstraints),
],
)
: _buildPage(context),
),
],
),
),
);
}

Color get _separatorColor => Colors.black.withOpacity(0.1);

Widget _maybeBuildGlobalMouseRegion(Widget child) {
if (widget.allowLeftPaneResize) {
return MouseRegion(
cursor: _isHovering || _isDragging
? SystemMouseCursors.resizeColumn
: MouseCursor.defer,
child: child,
);
}

return child;
}

Widget _buildLeftPane() {
return Container(
width: _leftPaneWidth,
decoration: BoxDecoration(
border: Border(
right: BorderSide(
width: 1,
color: _separatorColor,
),
),
Expanded(
child: Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: theme.landscapeTransitions,
),
child: Navigator(
pages: [
MaterialPage(
key: ValueKey(_selectedIndex),
child: widget.length > _selectedIndex
? widget.pageBuilder(context, _selectedIndex)
: widget.pageBuilder(context, 0),
),
],
onPopPage: (route, result) => route.didPop(result),
observers: [HeroController()],
),
),
child: Scaffold(
appBar: widget.appBar,
body: YaruMasterListView(
length: widget.length,
selectedIndex: _selectedIndex,
onTap: _onTap,
builder: widget.tileBuilder,
),
),
);
}

Widget _buildPage(BuildContext context) {
final theme = YaruMasterDetailTheme.of(context);

return Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: theme.landscapeTransitions,
),
child: Navigator(
pages: [
MaterialPage(
key: ValueKey(_selectedIndex),
child: widget.length > _selectedIndex
? widget.pageBuilder(context, _selectedIndex)
: widget.pageBuilder(context, 0),
),
],
onPopPage: (route, result) => route.didPop(result),
observers: [HeroController()],
),
);
}

Widget _buildLeftPaneResizer(
BuildContext context,
BoxConstraints boxConstraints,
) {
return Positioned(
child: AnimatedContainer(
duration: _kLeftPaneResizingRegionAnimationDuration,
color:
_isHovering || _isDragging ? _separatorColor : Colors.transparent,
child: MouseRegion(
cursor: SystemMouseCursors.resizeColumn,
onEnter: (event) => setState(() {
_isHovering = true;
}),
onExit: (event) => setState(() {
_isHovering = false;
}),
child: GestureDetector(
onPanStart: (details) => setState(() {
_isDragging = true;
_initialPaneWidth = _leftPaneWidth;
}),
onPanUpdate: (details) => setState(() {
_paneWidthMove += details.delta.dx;
final width = _initialPaneWidth + _paneWidthMove;
final maxWidth = boxConstraints.maxWidth - widget.pageMinWidth;

final previousPaneWidth = _leftPaneWidth;

if (width >= maxWidth) {
_leftPaneWidth = maxWidth;
} else if (width < widget.leftPaneMinWidth) {
_leftPaneWidth = widget.leftPaneMinWidth;
} else {
_leftPaneWidth = width;
}

if (previousPaneWidth != _leftPaneWidth) {
widget.onLeftPaneWidthChange?.call(width);
}
}),
onPanEnd: (details) => setState(() {
_isDragging = false;
_paneWidthMove = 0.0;
}),
),
),
],
),
height: MediaQuery.of(context).size.height,
Jupi007 marked this conversation as resolved.
Show resolved Hide resolved
width: _kLeftPaneResizingRegionWidth,
top: 0,
left: 0,
);
}
}
30 changes: 27 additions & 3 deletions lib/src/pages/layouts/yaru_master_detail_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import '../../constants.dart';
import 'yaru_detail_page.dart';
import 'yaru_landscape_layout.dart';
import 'yaru_master_detail_theme.dart';
import 'yaru_master_tile.dart';
import 'yaru_portrait_layout.dart';

typedef YaruMasterDetailBuilder = Widget Function(
Expand All @@ -13,7 +16,7 @@ class YaruMasterDetailPage extends StatefulWidget {
/// Creates a basic responsive layout with yaru theme,
/// renders layout based on [width] constrain.
///
/// * if [constraints.maxWidth] < 620 the widget will render [YaruPotraitLayout]
/// * if [constraints.maxWidth] < 620 the widget will render [YaruPortraitLayout]
/// * if [constraints.maxWidth] > 620 widget will render [YaruLandscapeLayout]
///
/// for example:
Expand All @@ -32,6 +35,9 @@ class YaruMasterDetailPage extends StatefulWidget {
required this.titleBuilder,
required this.pageBuilder,
required this.leftPaneWidth,
this.allowLeftPaneResize = true,
this.leftPaneMinWidth = 175.0,
this.pageMinWidth = kYaruMasterDetailBreakpoint / 2,
this.appBar,
this.initialIndex,
this.onSelected,
Expand All @@ -55,9 +61,20 @@ class YaruMasterDetailPage extends StatefulWidget {
/// * [YaruDetailPage]
final IndexedWidgetBuilder pageBuilder;

/// Specifies the width of left pane.
/// Specifies the initial width of left pane.
final double leftPaneWidth;

/// If true, allow the left pane to be resized in landscape layout.
final bool allowLeftPaneResize;

/// If [allowLeftPaneResize], specifies the min-width of the left pane.
/// Defaults to 175
final double leftPaneMinWidth;

/// If [allowLeftPaneResize], specifies the min-width of the page.
/// Defaults to 310
final double pageMinWidth;

/// An optional custom AppBar for the left pane.
final PreferredSizeWidget? appBar;

Expand All @@ -75,6 +92,8 @@ class _YaruMasterDetailPageState extends State<YaruMasterDetailPage> {
var _index = -1;
var _previousIndex = 0;

late double _leftPaneWidth;

void _setIndex(int index) {
_previousIndex = _index;
_index = index;
Expand All @@ -85,6 +104,7 @@ class _YaruMasterDetailPageState extends State<YaruMasterDetailPage> {
void initState() {
super.initState();
_index = widget.initialIndex ?? -1;
_leftPaneWidth = widget.leftPaneWidth;
}

@override
Expand Down Expand Up @@ -119,7 +139,11 @@ class _YaruMasterDetailPageState extends State<YaruMasterDetailPage> {
titleBuilder: widget.titleBuilder,
pageBuilder: widget.pageBuilder,
onSelected: _setIndex,
leftPaneWidth: widget.leftPaneWidth,
leftPaneWidth: _leftPaneWidth,
allowLeftPaneResize: widget.allowLeftPaneResize,
leftPaneMinWidth: widget.leftPaneMinWidth,
pageMinWidth: widget.pageMinWidth,
onLeftPaneWidthChange: (panWidth) => _leftPaneWidth = panWidth,
appBar: widget.appBar,
);
}
Expand Down
4 changes: 3 additions & 1 deletion lib/src/pages/layouts/yaru_master_detail_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:yaru/yaru.dart';

import '../../constants.dart';

@immutable
class YaruMasterDetailThemeData with Diagnosticable {
/// Creates a theme that can be used with [YaruMasterDetailPage].
Expand All @@ -15,7 +17,7 @@ class YaruMasterDetailThemeData with Diagnosticable {

factory YaruMasterDetailThemeData.fallback() {
return const YaruMasterDetailThemeData(
breakpoint: 620,
breakpoint: kYaruMasterDetailBreakpoint,
tileSpacing: 6,
listPadding: EdgeInsets.symmetric(vertical: 8),
portraitTransitions: YaruPageTransitionsTheme.horizontal,
Expand Down