diff --git a/CHANGELOG.md b/CHANGELOG.md index e51dcbf52..8e13b3e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - **BREAKING** feat: `TreeViewItem.children` is now unmodifiable. Use `TreeViewController` methods (`addItem()`, `addItems()`, `removeItem()`, `moveItem()`) to modify tree structure. - feat: `TitleBar` now supports double-click callback to maximize or restore the window ([#1298](https://github.com/bdlukaa/fluent_ui/issues/1298)) - fix: Correctly apply `TitleBar`'s `isBackButtonEnabled` ([#1298](https://github.com/bdlukaa/fluent_ui/issues/1298)) +- fix: `TitleBar` height no longer shrinks when the window is resized to a smaller width ([#1340](https://github.com/bdlukaa/fluent_ui/issues/1340)) ## 4.14.0 diff --git a/example/lib/main.dart b/example/lib/main.dart index de8ec8089..ed5184b00 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -211,7 +211,7 @@ class _MyHomePageState extends State with WindowListener { appTheme.mode = ThemeMode.light; } }, - child: const Icon(WindowsIcons.lightbulb), + child: const Icon(WindowsIcons.lightbulb, size: 16), ), ), captionControls: const WindowButtons(), diff --git a/lib/src/controls/navigation/navigation_view/title_bar.dart b/lib/src/controls/navigation/navigation_view/title_bar.dart index 811f04aa1..6fc3e8261 100644 --- a/lib/src/controls/navigation/navigation_view/title_bar.dart +++ b/lib/src/controls/navigation/navigation_view/title_bar.dart @@ -31,6 +31,7 @@ class TitleBar extends StatelessWidget { this.subtitle, this.content, this.endHeader, + this.height, this.captionControls, this.onDragStarted, this.onDragEnded, @@ -84,8 +85,20 @@ class TitleBar extends StatelessWidget { /// Usually an [AutoSuggestBox] widget. final Widget? content; + /// The right header widget. + /// + /// Usually an [Icon] widget. final Widget? endHeader; + /// The height of the title bar. + /// + /// If not provided, the height is calculated based on the [content]. + /// + /// See also: + /// + /// * [calculateHeight], which calculates the height based on the content. + final double? height; + /// The controls of the window, if any. final Widget? captionControls; @@ -104,6 +117,15 @@ class TitleBar extends StatelessWidget { /// The callback that is called when the title bar is double-tapped. final VoidCallback? onDoubleTap; + static double calculateHeight(Widget? titleBar) { + if (titleBar == null) return 0; + if (titleBar is TitleBar) { + if (titleBar.height != null) return titleBar.height!; + if (titleBar.content != null) return 48; + } + return 32; + } + @override Widget build(BuildContext context) { assert(debugCheckHasFluentTheme(context)); @@ -121,11 +143,10 @@ class TitleBar extends StatelessWidget { onPanUpdate: (_) => onDragUpdated?.call(), onDoubleTap: () => onDoubleTap?.call(), child: ConstrainedBox( - constraints: BoxConstraints( + constraints: BoxConstraints.tightFor( // according to documentation, increase the size of the title bar if // there is content - minHeight: content != null ? 48 : 32, - maxHeight: 48, + height: TitleBar.calculateHeight(this), ), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -331,13 +352,9 @@ class _RenderTitleSubtitleOverflow extends RenderBox var height = 0.0; var child = firstChild; while (child != null) { - final childParentData = - child.parentData! as _TitleSubtitleOverflowParentData; - if (!childParentData.isHidden) { - height = height > child.getMinIntrinsicHeight(width) - ? height - : child.getMinIntrinsicHeight(width); - } + height = height > child.getMinIntrinsicHeight(width) + ? height + : child.getMinIntrinsicHeight(width); child = childAfter(child); } return height; @@ -348,13 +365,9 @@ class _RenderTitleSubtitleOverflow extends RenderBox var height = 0.0; var child = firstChild; while (child != null) { - final childParentData = - child.parentData! as _TitleSubtitleOverflowParentData; - if (!childParentData.isHidden) { - height = height > child.getMaxIntrinsicHeight(width) - ? height - : child.getMaxIntrinsicHeight(width); - } + height = height > child.getMaxIntrinsicHeight(width) + ? height + : child.getMaxIntrinsicHeight(width); child = childAfter(child); } return height; diff --git a/lib/src/controls/navigation/navigation_view/view.dart b/lib/src/controls/navigation/navigation_view/view.dart index 79364eb64..48870d1de 100644 --- a/lib/src/controls/navigation/navigation_view/view.dart +++ b/lib/src/controls/navigation/navigation_view/view.dart @@ -230,7 +230,6 @@ class NavigationView extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('titleBar', titleBar)) ..add(DiagnosticsProperty('pane', pane)) ..add( DiagnosticsProperty( @@ -825,7 +824,7 @@ class NavigationViewState extends State { children: [ Padding( padding: EdgeInsetsDirectional.only( - top: 38, + top: TitleBar.calculateHeight(widget.titleBar), start: pane.size?.compactWidth ?? kCompactNavigationPaneWidth, ), child: content, diff --git a/test/navigation_view_test.dart b/test/navigation_view_test.dart index 622418ff7..22d524af2 100644 --- a/test/navigation_view_test.dart +++ b/test/navigation_view_test.dart @@ -970,221 +970,188 @@ void main() { ); }); - // Regression test for https://github.com/bdlukaa/fluent_ui/issues/XXX - - // NavigationView compact pane flyout too small in RTL directionality - group('Issue - NavigationView compact pane flyout in RTL directionality', () { - testWidgets('PaneItemExpander flyout opens correctly in RTL compact mode', ( - tester, - ) async { - await tester.pumpWidget( - FluentApp( - home: Directionality( - textDirection: TextDirection.rtl, - child: SizedBox( + // Regression test for https://github.com/bdlukaa/fluent_ui/issues/1340 + // TitleBar should not shrink in height when window is resized to smaller width + group('Issue #1340 - TitleBar height stability on window resize', () { + testWidgets( + 'TitleBar with content maintains 48px height at large window width', + (tester) async { + await tester.pumpWidget( + FluentApp( + home: SizedBox( width: 1200, height: 800, child: NavigationView( + titleBar: const TitleBar( + title: Text('My App'), + content: SizedBox(width: 200, height: 32), + ), pane: NavigationPane( selected: 0, displayMode: PaneDisplayMode.compact, items: [ - PaneItemExpander( - icon: const Icon(FluentIcons.folder), - title: const Text('Files'), - body: const Center(child: Text('Files Page')), - items: [ - PaneItem( - icon: const Icon(FluentIcons.document), - title: const Text('Documents'), - body: const Center(child: Text('Documents Page')), - ), - ], + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const Center(child: Text('Home Page')), ), ], ), ), ), ), - ), - ); - - await tester.pumpAndSettle(); - - // Tap the PaneItemExpander icon to open the flyout - await tester.tap(find.byIcon(FluentIcons.folder)); - await tester.pumpAndSettle(); + ); - // The flyout should be open and contain the child item - expect(find.byType(MenuFlyout), findsOneWidget); + await tester.pumpAndSettle(); - // The flyout should have enough width (more than just the compact pane width) - final flyoutBox = tester.renderObject(find.byType(MenuFlyout)); - expect(flyoutBox.size.width, greaterThan(kCompactNavigationPaneWidth)); - }); - }); + final titleBar = find.byType(TitleBar); + expect(titleBar, findsOneWidget); + expect(tester.getSize(titleBar).height, 48.0); + }, + ); - // Tests for auto display mode transition - group('Auto display mode transition', () { testWidgets( - 'No overlay when resizing from minimal to compact', + 'TitleBar with content maintains 48px height at small window width', (tester) async { - // Start in minimal mode (width <= 640) - double viewWidth = 300; - await tester.pumpWidget( FluentApp( - home: StatefulBuilder( - builder: (context, setState) { - return SizedBox( - width: viewWidth, - height: 800, - child: NavigationView( - pane: NavigationPane( - selected: 0, - items: [ - PaneItem( - icon: const Icon(FluentIcons.home), - title: const Text('Home'), - body: const SizedBox(), - ), - ], + home: SizedBox( + width: 300, + height: 800, + child: NavigationView( + titleBar: const TitleBar( + title: Text('My App'), + content: SizedBox(width: 200, height: 32), + ), + pane: NavigationPane( + selected: 0, + displayMode: PaneDisplayMode.compact, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const Center(child: Text('Home Page')), ), - ), - ); - }, + ], + ), + ), ), ), ); await tester.pumpAndSettle(); - expect(find.byType(NavigationView), findsOneWidget); - // Resize to compact mode (641–1007px) - viewWidth = 800; - await tester.pumpWidget( - FluentApp( - home: StatefulBuilder( - builder: (context, setState) { - return SizedBox( - width: viewWidth, - height: 800, - child: NavigationView( - pane: NavigationPane( - selected: 0, - items: [ - PaneItem( - icon: const Icon(FluentIcons.home), - title: const Text('Home'), - body: const SizedBox(), - ), - ], + final titleBar = find.byType(TitleBar); + expect(titleBar, findsOneWidget); + expect(tester.getSize(titleBar).height, 48.0); + }, + ); + + testWidgets( + 'TitleBar height does not change when window width is reduced', + (tester) async { + Widget buildWithWidth(double width) { + return FluentApp( + home: SizedBox( + width: width, + height: 800, + child: NavigationView( + titleBar: const TitleBar( + title: Text('My App'), + content: SizedBox(width: 200, height: 32), + ), + pane: NavigationPane( + selected: 0, + displayMode: PaneDisplayMode.compact, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const Center(child: Text('Home Page')), ), - ), - ); - }, + ], + ), + ), ), - ), - ); - - // After a single frame (not pumpAndSettle), verify the compact pane - // is at compact width – not at open-pane width as it would be if the - // AnimatedContainer state were incorrectly reused from minimal mode. - await tester.pump(); + ); + } - expect(find.byType(NavigationView), findsOneWidget); + await tester.pumpWidget(buildWithWidth(1200)); + await tester.pumpAndSettle(); - // The render box of the NavigationView should not show the full-width - // open pane (320 px). The pane should appear at compact width (50 px) - // immediately, with no ongoing width animation. - final navViewBox = tester.renderObject( - find.byType(NavigationView), - ); - expect(navViewBox.size.width, 800); + final titleBar = find.byType(TitleBar); + final heightAtLargeWidth = tester.getSize(titleBar).height; + expect(heightAtLargeWidth, 48.0); - // Settle any remaining animations and verify no errors occur. + // Simulate window resize to smaller width + await tester.pumpWidget(buildWithWidth(300)); await tester.pumpAndSettle(); - expect(find.byType(NavigationView), findsOneWidget); + + final heightAtSmallWidth = tester.getSize(find.byType(TitleBar)).height; + expect(heightAtSmallWidth, 48.0); + expect(heightAtLargeWidth, equals(heightAtSmallWidth)); }, ); testWidgets( - 'Minimal pane open state is reset when transitioning to compact', + 'TitleBar without content maintains 32px height regardless of window width', (tester) async { - final navKey = GlobalKey(); - double viewWidth = 300; - + // At large window width await tester.pumpWidget( FluentApp( - home: StatefulBuilder( - builder: (context, setState) { - return SizedBox( - width: viewWidth, - height: 800, - child: NavigationView( - key: navKey, - pane: NavigationPane( - selected: 0, - items: [ - PaneItem( - icon: const Icon(FluentIcons.home), - title: const Text('Home'), - body: const SizedBox(), - ), - ], + home: SizedBox( + width: 1200, + height: 800, + child: NavigationView( + titleBar: const TitleBar(title: Text('My App')), + pane: NavigationPane( + selected: 0, + displayMode: PaneDisplayMode.expanded, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const Center(child: Text('Home Page')), ), - ), - ); - }, + ], + ), + ), ), ), ); await tester.pumpAndSettle(); - // Verify we are in minimal mode. - expect(navKey.currentState?.displayMode, PaneDisplayMode.minimal); - - // Open the minimal pane. - navKey.currentState?.isMinimalPaneOpen = true; - await tester.pumpAndSettle(); - expect(navKey.currentState?.isMinimalPaneOpen, true); + final titleBar = find.byType(TitleBar); + expect(tester.getSize(titleBar).height, 32.0); - // Resize to compact mode. - viewWidth = 800; + // At small window width await tester.pumpWidget( FluentApp( - home: StatefulBuilder( - builder: (context, setState) { - return SizedBox( - width: viewWidth, - height: 800, - child: NavigationView( - key: navKey, - pane: NavigationPane( - selected: 0, - items: [ - PaneItem( - icon: const Icon(FluentIcons.home), - title: const Text('Home'), - body: const SizedBox(), - ), - ], + home: SizedBox( + width: 400, + height: 800, + child: NavigationView( + titleBar: const TitleBar(title: Text('My App')), + pane: NavigationPane( + selected: 0, + displayMode: PaneDisplayMode.expanded, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const Center(child: Text('Home Page')), ), - ), - ); - }, + ], + ), + ), ), ), ); await tester.pumpAndSettle(); - // Display mode should now be compact. - expect(navKey.currentState?.displayMode, PaneDisplayMode.compact); - - // The minimal-pane-open flag must have been reset during the - // mode transition so it doesn't leak into the compact mode. - expect(navKey.currentState?.isMinimalPaneOpen, false); + expect(tester.getSize(find.byType(TitleBar)).height, 32.0); }, ); });