From d601ff99d0d755035b710a6c67f76a603a001fba Mon Sep 17 00:00:00 2001 From: Reuben Turner Date: Tue, 28 Feb 2023 12:44:21 -0500 Subject: [PATCH] feat: Sliver toolbar (#368) * feat: `SliverToolBar` * chore(example): address lints * docs(SliverToolbar): update `floating` doc * feat(example): add page for SliverToolbar * chore: dart format * docs(SliverToolBar): slight change to `pinned` docs * docs(SliverToolBar): add section to readme * docs(SliverToolBar): tweak sample * test(SliverAppBar): add initial tests * chore: update version & changelog * chore: pub upgrade * chore: remove unused imports * test: remove ignore lint --- CHANGELOG.md | 3 + README.md | 30 +- example/analysis_options.yaml | 1 + .../sf_symbols/macwindow.on.rectangle_2x.png | Bin 0 -> 1042 bytes .../menubar.arrow.down.rectangle_2x.png | Bin 0 -> 892 bytes .../menubar.arrow.up.rectangle_2x.png | Bin 0 -> 869 bytes .../sf_symbols/menubar.rectangle_2x.png | Bin 0 -> 556 bytes example/lib/main.dart | 34 +- example/lib/pages/buttons_page.dart | 104 ++--- example/lib/pages/fields_page.dart | 428 +++++++++--------- example/lib/pages/sliver_toolbar_page.dart | 137 ++++++ example/lib/pages/toolbar_page.dart | 96 ++-- example/macos/Podfile.lock | 2 +- example/pubspec.lock | 2 +- lib/macos_ui.dart | 1 + .../layout/toolbar/custom_toolbar_item.dart | 21 +- lib/src/layout/toolbar/sliver_toolbar.dart | 317 +++++++++++++ lib/src/layout/toolbar/toolbar.dart | 27 +- lib/src/library.dart | 1 + macos/macos_ui.podspec | 2 +- pubspec.lock | 4 +- pubspec.yaml | 4 +- test/layout/sliver_toolbar_test.dart | 182 ++++++++ 23 files changed, 1058 insertions(+), 338 deletions(-) create mode 100644 example/assets/sf_symbols/macwindow.on.rectangle_2x.png create mode 100644 example/assets/sf_symbols/menubar.arrow.down.rectangle_2x.png create mode 100644 example/assets/sf_symbols/menubar.arrow.up.rectangle_2x.png create mode 100644 example/assets/sf_symbols/menubar.rectangle_2x.png create mode 100644 example/lib/pages/sliver_toolbar_page.dart create mode 100644 lib/src/layout/toolbar/sliver_toolbar.dart create mode 100644 test/layout/sliver_toolbar_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c38001..cefe920e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [1.12.0] +✨ New widget: `SliverToolBar` + ## [1.11.1] * Fixed an issue where the `MacosSearchField` would not perform an action when an item was selected. diff --git a/README.md b/README.md index 1a1a1451..cd74c79c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ should avoid allowing your application window to be resized below the height of - [MacosScaffold](#macosscaffold) - [Modern Window Look](#modern-window-look) - [ToolBar](#toolbar) + - [SliverToolBar](#SliverToolBar) - [MacosListTile](#MacosListTile) - [MacosTabView](#MacosTabView) @@ -407,6 +408,33 @@ CustomToolbarItem( ), ``` +## `SliverToolBar` + + +`SliverToolbar` is a variant of the standard `ToolBar`, with the key difference being that (as the name implies), it +is compatible with scrollable widgets like `CustomScrollView` and `NestedScrollView`. There are three additional +properties on `SliverToolBar`: +* `pinned`, which determines if the toolbar should remain visible while scrolling +* `floating`, which determines if the toolbar should become visible as soon as the use starts scrolling upwards +* `opacity`, which manages the translucency effect of the toolbar + +This widget enables developers to achieve the toolbar behaviors seen in Apple's App Store. + +Sample usage: +```dart +return CustomScrollView( + controller: scrollController, + slivers: [ + SliverToolBar( + title: const Text('SliverToolbar'), + pinned: true, + toolbarOpacity: 0.75, + ), + // Other slivers below + ], +); +``` + ## MacosListTile A widget that aims to approximate the [`ListTile`](https://api.flutter.dev/flutter/material/ListTile-class.html) widget found in @@ -414,7 +442,7 @@ Flutter's material library. ![MacosListTile](https://imgur.com/pQB99M2.png) -Usage: +Sample usage: ```dart MacosListTile( leading: const Icon(CupertinoIcons.lightbulb), diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 9c4b285c..2d96245c 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -23,6 +23,7 @@ linter: # producing the lint. rules: - use_super_parameters + - prefer_single_quotes # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/example/assets/sf_symbols/macwindow.on.rectangle_2x.png b/example/assets/sf_symbols/macwindow.on.rectangle_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e17fbdb76017df4b17053cdbb9adc9ebf4268e0a GIT binary patch literal 1042 zcmV+t1nv8YP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91F`xqg1ONa40RR91C;$Ke01N?p3;+NFd`Uz>R9Fe^nMrI-Q5eVj`phac zT?iT?9*H3~#YTt_3kfQe7%CPPLaazkOBMt>-5?P*L_`cbL1JMjT^S;wG^vd!f)+LQ z`TehRYfu)9PNJAC8x!*DHd z3wTHS4rl>|AmyO=vq9PftmfgKq>#s^-6NBO1&+lNn)P^wU;|7*<>ggG zDTWiwAh9EH6rjMweFQr%*-*v`|KciQm}AuB$Z?3zouF=Bj=H<3B3#n$5GQpw!Mo<> zbafWJ4Lc7@R!a*zCt?9=j1I)fpd9oEQPFrDy*JJ(h_%acthb~MJOlNtAsvtb5dQ0Ov(A2y1kVb>6h(O*-A?030U^tq9Q@RA*$5Ysdt{U7neXa1N=(oUEfgeVH z5nTE29r=Lu8}s6PxH;MA*Tz zLGL7}AO+2aXgcjG3Gjt5-Ht^oX_oC-XF6^*kSChwLpeeO}wB4R2SzRKv z{aTuVpjS@YEY@CYfpW zoRzlNdtjU~t6}@n)!_RH%@&O9}I#I&T#!^$lC=D-+iJ#*A5D4#?yf?Zxpx${o$~_ zB`M+=-nUP%XT8_*5o8aT%+w8Kk&0pv2pz`;Q)f&c&j M07*qoM6N<$g5eXv*#H0l literal 0 HcmV?d00001 diff --git a/example/assets/sf_symbols/menubar.arrow.down.rectangle_2x.png b/example/assets/sf_symbols/menubar.arrow.down.rectangle_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9fe7a281005cf2a340ab8155c9b324fac998e0 GIT binary patch literal 892 zcmV-?1B3jDP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91E}#Pd1ONa40RR91Bme*a02m@Xr2qf|=1D|BR9Fe^n8|AtK@`UAxF9YF zE(juupdcQF1Uwm0^rl`Un297oRPav_PogI=7eN9Z^dNfBh)2B&;vtNPh#Mj*cyNul zV2sQ7`#M$fGIU_3yPBziJox4Nl^KWR{a!t+o!c7j%J@U_MAdE2T#AaqtdQ z!IdOQo}nj+L3$7NDKJ;Th~*F5PD1{20_hDnH^7{T30rEq3Oyp*4=m~H_&0$rPYkjG z65Bx!Q0WSTBCMMBa+S+)+&huz$MCpKWvJE%9H*`h4E+(kjCdnLea&!MrX5%J@Y3nJ zi}z#Fi!B-bIiK1~t055}Y^>E-ab<2ki$ zsmu`fmr$C^x)cn9i(r)<3uTwJ(ZF<4J+_wWyK*Zr0`*#+G( zXK}m11pZcz`Q(P1Qbl%LuTXjilxzm)A)@CA$?r*({-$&4NNeCP&>FcUH*t3A7a5O$ zFD|nuzYzb2i%E?`S});#rxnxqQPvoflSgtBS27acNA-dC@)40A2HH(8I>8~(?E*T{ zL5}_Zz~tghsUS~>?C2V@UI43I_OEEwzQo!e@DD=zNjlQR4>`g=9YW+a9P}~ix#QwV z%k}mf?A|=2-I`NWp;ECcLFg@h1w62EXTL!2XU?mPv0R~G#FZUlpHTzgF7WaYqvB4? z#|rD*eSEsi5d2O(9%ft}M_&-mDK_2wyz$_~MFgS~D7C-RM5Xf6tiHt9v6)l2iMa2< zBDy;^wuaAMV~?p+MuG3Y73x#B;ak9-5N^vv{aYVB`wfGtZa|v|jDVmW#w01j?Cao< z^oW&D0DGG*BUz|5Ps4ds-L#kLdZ3$FbbirX?#^HAAV!6itiH@JUkILnW! S%RUbP0000Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91E}#Pd1ONa40RR91Bme*a02m@Xr2qf|&q+iC$Mb&7MWRS;v*;0*Qyy$OTf!Jh=~S18-X zs5lh|_{*VR)_q3KaSSDX;|cN? zM6qF}-F-LGkF#bK{XDUal2&!~nqh3Pl>TrFFVYLncm~-5Ddhn5IM+p($EW^P!kypB vC`V8y()PDaPU;cPEB*=VV?2IWDOYo@u_m z3|c@o2Loe!CIbsd2@p#GF#`kh0!9XAAk7F8TfhXD)my*}XRCk|N+{VZ1uC27>Eaj? z!TENktv;)xNZVQ7&O^OS7c)eJF5dsmE@D+;{!#D`Z{Xd4&U!@=yG2W;uE^UKD4=C3 zsLT4gGPz%-?!cX!DSn@x&AF-jY-ag+zfGGH7j$jXS*{r#Anm&-Q6#{AamuIiFFjl( zGENmL{~H+hy6kOlD=~cdqa|1T$6|&rJk8uDGYmGkUk-l3QN~>PqWIW!kAC&m84O8s zr4ypUZwYOvy7j2#_=e%hq(zpMIMJ9w-Zknx`8dW7TPda&3?&_3+ z>XwZ~Wj50;oB5aBnbR?K^W%+kwpDI={LE#|9NGS-Th30FHCIo1`ldo>dG5S7J1hmY zPk-b$zqw*{u=f8AoPUlA>g}@HT6B|f@5_os?aNkmudeZZZSsZdCu`{T-;=(Su6OpA zJr^ZnHE%}p663>Gdsk%ho(p`}@NSWQNc)ZUo!@`2W_g~zFwR{!Wq(jmM28vw*4#$E c#(kn!%+(+H8?;%>JqwB{Pgg&ebxsLQ0ORu5rT_o{ literal 0 HcmV?d00001 diff --git a/example/lib/main.dart b/example/lib/main.dart index 447f1fcd..3d1a24e7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:example/pages/dialogs_page.dart'; import 'package:example/pages/fields_page.dart'; import 'package:example/pages/indicators_page.dart'; import 'package:example/pages/selectors_page.dart'; +import 'package:example/pages/sliver_toolbar_page.dart'; import 'package:example/pages/tabview_page.dart'; import 'package:example/pages/toolbar_page.dart'; import 'package:flutter/cupertino.dart'; @@ -68,8 +69,9 @@ class _WidgetGalleryState extends State { ), const DialogsPage(), const ToolbarPage(), - const SelectorsPage(), + const SliverToolbarPage(), const TabViewPage(), + const SelectorsPage(), ]; @override @@ -234,8 +236,30 @@ class _WidgetGalleryState extends State { label: Text('Dialogs & Sheets'), ), const SidebarItem( - leading: MacosIcon(CupertinoIcons.macwindow), - label: Text('Toolbar'), + leading: MacosImageIcon( + AssetImage( + 'assets/sf_symbols/macwindow.on.rectangle_2x.png', + ), + ), + label: Text('Layout'), + disclosureItems: [ + SidebarItem( + leading: MacosIcon(CupertinoIcons.macwindow), + label: Text('Toolbar'), + ), + SidebarItem( + leading: MacosImageIcon( + AssetImage( + 'assets/sf_symbols/menubar.rectangle_2x.png', + ), + ), + label: Text('SliverToolbar'), + ), + SidebarItem( + leading: MacosIcon(CupertinoIcons.uiwindow_split_2x1), + label: Text('TabView'), + ), + ], ), const SidebarItem( leading: MacosImageIcon( @@ -245,10 +269,6 @@ class _WidgetGalleryState extends State { ), label: Text('Selectors'), ), - const SidebarItem( - leading: MacosIcon(CupertinoIcons.uiwindow_split_2x1), - label: Text('TabView'), - ), ], ); }, diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart index 6533484b..6c373f5f 100644 --- a/example/lib/pages/buttons_page.dart +++ b/example/lib/pages/buttons_page.dart @@ -230,52 +230,52 @@ class _ButtonsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ MacosPulldownButton( - title: "PDF", + title: 'PDF', items: [ MacosPulldownMenuItem( title: const Text('Open in Preview'), onTap: () => - debugPrint("Opening in preview..."), + debugPrint('Opening in preview...'), ), MacosPulldownMenuItem( title: const Text('Save as PDF...'), - onTap: () => debugPrint("Saving as PDF..."), + onTap: () => debugPrint('Saving as PDF...'), ), MacosPulldownMenuItem( enabled: false, title: const Text('Save as Postscript'), onTap: () => - debugPrint("Saving as Postscript..."), + debugPrint('Saving as Postscript...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( enabled: false, title: const Text('Save to iCloud Drive'), onTap: () => - debugPrint("Saving to iCloud..."), + debugPrint('Saving to iCloud...'), ), MacosPulldownMenuItem( enabled: false, title: const Text('Save to Web Receipts'), onTap: () => - debugPrint("Saving to Web Receipts..."), + debugPrint('Saving to Web Receipts...'), ), MacosPulldownMenuItem( title: const Text('Send in Mail...'), onTap: () => - debugPrint("Sending via Mail..."), + debugPrint('Sending via Mail...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( title: const Text('Edit Menu...'), - onTap: () => debugPrint("Editing menu..."), + onTap: () => debugPrint('Editing menu...'), ), ], ), const SizedBox(width: 20), const MacosPulldownButton( - title: "PDF", - disabledTitle: "Disabled", + title: 'PDF', + disabledTitle: 'Disabled', items: [], ), ], @@ -290,34 +290,34 @@ class _ButtonsPageState extends State { MacosPulldownMenuItem( title: const Text('New Folder'), onTap: () => - debugPrint("Creating new folder..."), + debugPrint('Creating new folder...'), ), MacosPulldownMenuItem( title: const Text('Open'), - onTap: () => debugPrint("Opening..."), + onTap: () => debugPrint('Opening...'), ), MacosPulldownMenuItem( title: const Text('Open with...'), - onTap: () => debugPrint("Opening with..."), + onTap: () => debugPrint('Opening with...'), ), MacosPulldownMenuItem( title: const Text('Import from iPhone...'), - onTap: () => debugPrint("Importing..."), + onTap: () => debugPrint('Importing...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( enabled: false, title: const Text('Remove'), - onTap: () => debugPrint("Deleting..."), + onTap: () => debugPrint('Deleting...'), ), MacosPulldownMenuItem( title: const Text('Move to Bin'), - onTap: () => debugPrint("Moving to Bin..."), + onTap: () => debugPrint('Moving to Bin...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( title: const Text('Tags...'), - onTap: () => debugPrint("Tags..."), + onTap: () => debugPrint('Tags...'), ), ], ), @@ -353,7 +353,7 @@ class _ButtonsPageState extends State { ), const SizedBox(width: 20), MacosPopupButton( - disabledHint: const Text("Disabled"), + disabledHint: const Text('Disabled'), onChanged: null, items: null, ), @@ -474,38 +474,38 @@ class _ButtonsPageState extends State { } const languages = [ - "Mandarin Chinese", - "Spanish", - "English", - "Hindi/Urdu", - "Arabic", - "Bengali", - "Portuguese", - "Russian", - "Japanese", - "German", - "Thai", - "Greek", - "Nepali", - "Punjabi", - "Wu", - "French", - "Telugu", - "Vietnamese", - "Marathi", - "Korean", - "Tamil", - "Italian", - "Turkish", - "Cantonese/Yue", - "Urdu", - "Javanese", - "Egyptian Arabic", - "Gujarati", - "Iranian Persian", - "Indonesian", - "Polish", - "Ukrainian", - "Romanian", - "Dutch" + 'Mandarin Chinese', + 'Spanish', + 'English', + 'Hindi/Urdu', + 'Arabic', + 'Bengali', + 'Portuguese', + 'Russian', + 'Japanese', + 'German', + 'Thai', + 'Greek', + 'Nepali', + 'Punjabi', + 'Wu', + 'French', + 'Telugu', + 'Vietnamese', + 'Marathi', + 'Korean', + 'Tamil', + 'Italian', + 'Turkish', + 'Cantonese/Yue', + 'Urdu', + 'Javanese', + 'Egyptian Arabic', + 'Gujarati', + 'Iranian Persian', + 'Indonesian', + 'Polish', + 'Ukrainian', + 'Romanian', + 'Dutch' ]; diff --git a/example/lib/pages/fields_page.dart b/example/lib/pages/fields_page.dart index 2172019e..94785708 100644 --- a/example/lib/pages/fields_page.dart +++ b/example/lib/pages/fields_page.dart @@ -109,7 +109,7 @@ class _FieldsPageState extends State { emptyWidget: const Center( child: Padding( padding: EdgeInsets.all(8.0), - child: Text("No action found!"), + child: Text('No action found!'), )), placeholder: 'Search for an action...', onResultSelected: (resultItem) { @@ -142,251 +142,251 @@ class _FieldsPageState extends State { } const countries = [ - "Afghanistan", - "Albania", - "Algeria", - "Andorra", - "Angola", - "Anguilla", - "Antigua and Barbuda", - "Argentina", - "Armenia", - "Aruba", - "Australia", - "Austria", - "Azerbaijan", - "Bahamas", - "Bahrain", - "Bangladesh", - "Barbados", - "Belarus", - "Belgium", - "Belize", - "Benin", - "Bermuda", - "Bhutan", - "Bolivia", - "Bosnia and Herzegovina", - "Botswana", - "Brazil", - "British Virgin Islands", - "Brunei", - "Bulgaria", - "Burkina Faso", - "Burundi", - "Cambodia", - "Cameroon", - "Cape Verde", - "Cayman Islands", - "Chad", - "Chile", - "China", - "Colombia", - "Congo", - "Cook Islands", - "Costa Rica", - "Cote D Ivoire", - "Croatia", - "Cruise Ship", - "Cuba", - "Cyprus", - "Czech Republic", - "Denmark", - "Djibouti", - "Dominica", - "Dominican Republic", - "Ecuador", - "Egypt", - "El Salvador", - "Equatorial Guinea", - "Estonia", - "Ethiopia", - "Falkland Islands", - "Faroe Islands", - "Fiji", - "Finland", - "France", - "French Polynesia", - "French West Indies", - "Gabon", - "Gambia", - "Georgia", - "Germany", - "Ghana", - "Gibraltar", - "Greece", - "Greenland", - "Grenada", - "Guam", - "Guatemala", - "Guernsey", - "Guinea", - "Guinea Bissau", - "Guyana", - "Haiti", - "Honduras", - "Hong Kong", - "Hungary", - "Iceland", - "India", - "Indonesia", - "Iran", - "Iraq", - "Ireland", - "Isle of Man", - "Israel", - "Italy", - "Jamaica", - "Japan", - "Jersey", - "Jordan", - "Kazakhstan", - "Kenya", - "Kuwait", - "Kyrgyz Republic", - "Laos", - "Latvia", - "Lebanon", - "Lesotho", - "Liberia", - "Libya", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Macau", - "Macedonia", - "Madagascar", - "Malawi", - "Malaysia", - "Maldives", - "Mali", - "Malta", - "Mauritania", - "Mauritius", - "Mexico", - "Moldova", - "Monaco", - "Mongolia", - "Montenegro", - "Montserrat", - "Morocco", - "Mozambique", - "Namibia", - "Nepal", - "Netherlands", - "Netherlands Antilles", - "New Caledonia", - "New Zealand", - "Nicaragua", - "Niger", - "Nigeria", - "Norway", - "Oman", - "Pakistan", - "Palestine", - "Panama", - "Papua New Guinea", - "Paraguay", - "Peru", - "Philippines", - "Poland", - "Portugal", - "Puerto Rico", - "Qatar", - "Reunion", - "Romania", - "Russia", - "Rwanda", - "Saint Pierre and Miquelon", - "Samoa", - "San Marino", - "Satellite", - "Saudi Arabia", - "Senegal", - "Serbia", - "Seychelles", - "Sierra Leone", - "Singapore", - "Slovakia", - "Slovenia", - "South Africa", - "South Korea", - "Spain", - "Sri Lanka", - "St Kitts and Nevis", - "St Lucia", - "St Vincent", - "St. Lucia", - "Sudan", - "Suriname", - "Swaziland", - "Sweden", - "Switzerland", - "Syria", - "Taiwan", - "Tajikistan", - "Tanzania", - "Thailand", + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Anguilla', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Aruba', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bermuda', + 'Bhutan', + 'Bolivia', + 'Bosnia and Herzegovina', + 'Botswana', + 'Brazil', + 'British Virgin Islands', + 'Brunei', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Cape Verde', + 'Cayman Islands', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Congo', + 'Cook Islands', + 'Costa Rica', + 'Cote D Ivoire', + 'Croatia', + 'Cruise Ship', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Estonia', + 'Ethiopia', + 'Falkland Islands', + 'Faroe Islands', + 'Fiji', + 'Finland', + 'France', + 'French Polynesia', + 'French West Indies', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Gibraltar', + 'Greece', + 'Greenland', + 'Grenada', + 'Guam', + 'Guatemala', + 'Guernsey', + 'Guinea', + 'Guinea Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hong Kong', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland', + 'Isle of Man', + 'Israel', + 'Italy', + 'Jamaica', + 'Japan', + 'Jersey', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kuwait', + 'Kyrgyz Republic', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macau', + 'Macedonia', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Montserrat', + 'Morocco', + 'Mozambique', + 'Namibia', + 'Nepal', + 'Netherlands', + 'Netherlands Antilles', + 'New Caledonia', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Norway', + 'Oman', + 'Pakistan', + 'Palestine', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Puerto Rico', + 'Qatar', + 'Reunion', + 'Romania', + 'Russia', + 'Rwanda', + 'Saint Pierre and Miquelon', + 'Samoa', + 'San Marino', + 'Satellite', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'South Africa', + 'South Korea', + 'Spain', + 'Sri Lanka', + 'St Kitts and Nevis', + 'St Lucia', + 'St Vincent', + 'St. Lucia', + 'Sudan', + 'Suriname', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', "Timor L'Este", - "Togo", - "Tonga", - "Trinidad and Tobago", - "Tunisia", - "Turkey", - "Turkmenistan", - "Turks and Caicos", - "Uganda", - "Ukraine", - "United Arab Emirates", - "United Kingdom", - "Uruguay", - "Uzbekistan", - "Venezuela", - "Vietnam", - "Virgin Islands (US)", - "Yemen", - "Zambia", - "Zimbabwe" + 'Togo', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Turks and Caicos', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'Uruguay', + 'Uzbekistan', + 'Venezuela', + 'Vietnam', + 'Virgin Islands (US)', + 'Yemen', + 'Zambia', + 'Zimbabwe' ]; var actionResults = [ SearchResultItem( - "Build project", + 'Build project', child: Row( children: const [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.hammer), ), - Text("Build project"), + Text('Build project'), ], ), - onSelected: () => debugPrint("Will build the project"), + onSelected: () => debugPrint('Will build the project'), ), SearchResultItem( - "Debug project", + 'Debug project', child: Row( children: const [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.tickets), ), - Text("Debug project"), + Text('Debug project'), ], ), - onSelected: () => debugPrint("Will debug the project"), + onSelected: () => debugPrint('Will debug the project'), ), SearchResultItem( - "Open containing folder", + 'Open containing folder', child: Row( children: const [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.folder), ), - Text("Open containing folder"), + Text('Open containing folder'), ], ), - onSelected: () => debugPrint("Will open containing folder"), + onSelected: () => debugPrint('Will open containing folder'), ), ]; diff --git a/example/lib/pages/sliver_toolbar_page.dart b/example/lib/pages/sliver_toolbar_page.dart new file mode 100644 index 00000000..8c2039a5 --- /dev/null +++ b/example/lib/pages/sliver_toolbar_page.dart @@ -0,0 +1,137 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class SliverToolbarPage extends StatefulWidget { + const SliverToolbarPage({super.key}); + + @override + State createState() => _SliverToolbarPageState(); +} + +class _SliverToolbarPageState extends State { + bool pinned = true; + bool floating = false; + double opacity = .9; + + @override + Widget build(BuildContext context) { + return MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return CustomScrollView( + slivers: [ + SliverToolBar( + title: const Text('SliverToolbar'), + floating: floating, + pinned: pinned, + toolbarOpacity: opacity, + actions: [ + ToolBarIconButton( + label: 'Pinned', + icon: MacosIcon( + pinned ? CupertinoIcons.pin_fill : CupertinoIcons.pin, + ), + tooltipMessage: pinned ? 'Unpin' : 'Pin', + showLabel: false, + onPressed: () { + setState(() => pinned = !pinned); + }, + ), + ToolBarIconButton( + label: 'Floating', + icon: MacosImageIcon( + AssetImage( + floating + ? 'assets/sf_symbols/menubar.arrow.down.rectangle_2x.png' + : 'assets/sf_symbols/menubar.arrow.up.rectangle_2x.png', + ), + ), + tooltipMessage: floating ? 'Unfloat' : 'Float', + showLabel: false, + onPressed: () { + setState(() => floating = !floating); + }, + ), + CustomToolbarItem( + inToolbarBuilder: (context) { + return MacosTooltip( + message: 'Toolbar opacity', + child: MacosPopupButton( + value: opacity, + items: const [ + MacosPopupMenuItem( + value: 0.25, + child: Text('25%'), + ), + MacosPopupMenuItem( + value: 0.5, + child: Text('50%'), + ), + MacosPopupMenuItem( + value: 0.75, + child: Text('75%'), + ), + MacosPopupMenuItem( + value: 0.9, + child: Text('90% (Default)'), + ), + ], + onChanged: (opacity) { + if (opacity == 0.25) { + setState(() => this.opacity = 0.25); + } else if (opacity == 0.5) { + setState(() => this.opacity = 0.5); + } else if (opacity == 0.75) { + setState(() => this.opacity = 0.75); + } else if (opacity == 0.9) { + setState(() => this.opacity = 0.9); + } + }, + ), + ); + }, + ), + ], + ), + const SliverPadding( + padding: EdgeInsets.all(16), + sliver: SliverToBoxAdapter( + child: Text( + 'SliverToolbar is nearly identical to the standard ' + 'Toolbar widget, except that it can be used in a ' + 'CustomScrollView. It can be pinned, floating, or ' + 'neither.', + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + Row( + children: [ + ...List.generate( + 3, + (index) => const FlutterLogo(size: 150), + ) + ], + ), + ...List.generate( + 100, + (index) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text('Item ${index + 1}'), + ), + ), + ], + ), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/example/lib/pages/toolbar_page.dart b/example/lib/pages/toolbar_page.dart index af01f229..6e1754ce 100644 --- a/example/lib/pages/toolbar_page.dart +++ b/example/lib/pages/toolbar_page.dart @@ -1,6 +1,5 @@ +import 'package:flutter/cupertino.dart'; import 'package:macos_ui/macos_ui.dart'; -// ignore: implementation_imports -import 'package:macos_ui/src/library.dart'; class ToolbarPage extends StatefulWidget { const ToolbarPage({super.key}); @@ -21,92 +20,92 @@ class _ToolbarPageState extends State { icon: const MacosIcon( CupertinoIcons.folder_badge_plus, ), - onPressed: () => debugPrint("New Folder..."), - label: "New Folder", + onPressed: () => debugPrint('New Folder...'), + label: 'New Folder', showLabel: true, - tooltipMessage: "This is a beautiful tooltip", + tooltipMessage: 'This is a beautiful tooltip', ), ToolBarIconButton( icon: const MacosIcon( CupertinoIcons.add_circled, ), - onPressed: () => debugPrint("Add..."), - label: "Add", + onPressed: () => debugPrint('Add...'), + label: 'Add', showLabel: true, - tooltipMessage: "This is another beautiful tooltip", + tooltipMessage: 'This is another beautiful tooltip', ), const ToolBarSpacer(), ToolBarIconButton( - label: "Delete", + label: 'Delete', icon: const MacosIcon( CupertinoIcons.trash, ), - onPressed: () => debugPrint("pressed"), + onPressed: () => debugPrint('pressed'), showLabel: false, ), const ToolBarIconButton( - label: "Change View", + label: 'Change View', icon: MacosIcon( CupertinoIcons.list_bullet, ), showLabel: false, ), ToolBarPullDownButton( - label: "Actions", + label: 'Actions', icon: CupertinoIcons.ellipsis_circle, - tooltipMessage: "Perform tasks with the selected items", + tooltipMessage: 'Perform tasks with the selected items', items: [ MacosPulldownMenuItem( - label: "New Folder", - title: const Text("New Folder"), - onTap: () => debugPrint("Creating new folder..."), + label: 'New Folder', + title: const Text('New Folder'), + onTap: () => debugPrint('Creating new folder...'), ), MacosPulldownMenuItem( - label: "Open", - title: const Text("Open"), - onTap: () => debugPrint("Opening..."), + label: 'Open', + title: const Text('Open'), + onTap: () => debugPrint('Opening...'), ), MacosPulldownMenuItem( - label: "Open with...", + label: 'Open with...', title: const Text('Open with...'), - onTap: () => debugPrint("Opening with..."), + onTap: () => debugPrint('Opening with...'), ), MacosPulldownMenuItem( - label: "Import from iPhone...", + label: 'Import from iPhone...', title: const Text('Import from iPhone...'), - onTap: () => debugPrint("Importing..."), + onTap: () => debugPrint('Importing...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( - label: "Remove", + label: 'Remove', enabled: false, title: const Text('Remove'), - onTap: () => debugPrint("Deleting..."), + onTap: () => debugPrint('Deleting...'), ), MacosPulldownMenuItem( - label: "Move to Bin", + label: 'Move to Bin', title: const Text('Move to Bin'), - onTap: () => debugPrint("Moving to Bin..."), + onTap: () => debugPrint('Moving to Bin...'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( - label: "Tags...", + label: 'Tags...', title: const Text('Tags...'), - onTap: () => debugPrint("Tags..."), + onTap: () => debugPrint('Tags...'), ), ], ), const ToolBarDivider(), ToolBarIconButton( - label: "Table", + label: 'Table', icon: const MacosIcon( CupertinoIcons.square_grid_3x2, ), - onPressed: () => debugPrint("Table..."), + onPressed: () => debugPrint('Table...'), showLabel: false, ), ToolBarIconButton( - label: "Toggle Sidebar", + label: 'Toggle Sidebar', icon: const MacosIcon( CupertinoIcons.sidebar_left, ), @@ -114,43 +113,43 @@ class _ToolbarPageState extends State { showLabel: false, ), ToolBarPullDownButton( - label: "Group", + label: 'Group', icon: CupertinoIcons.rectangle_grid_3x2, items: [ MacosPulldownMenuItem( - label: "None", + label: 'None', title: const Text('None'), - onTap: () => debugPrint("Remove sorting"), + onTap: () => debugPrint('Remove sorting'), ), const MacosPulldownMenuDivider(), MacosPulldownMenuItem( - label: "Name", + label: 'Name', title: const Text('Name'), - onTap: () => debugPrint("Sorting by name"), + onTap: () => debugPrint('Sorting by name'), ), MacosPulldownMenuItem( - label: "Kind", + label: 'Kind', title: const Text('Kind'), - onTap: () => debugPrint("Sorting by kind"), + onTap: () => debugPrint('Sorting by kind'), ), MacosPulldownMenuItem( - label: "Size", + label: 'Size', title: const Text('Size'), - onTap: () => debugPrint("Sorting by size"), + onTap: () => debugPrint('Sorting by size'), ), MacosPulldownMenuItem( - label: "Date Added", + label: 'Date Added', title: const Text('Date Added'), - onTap: () => debugPrint("Sorting by date"), + onTap: () => debugPrint('Sorting by date'), ), ], ), ToolBarIconButton( - label: "Share", + label: 'Share', icon: const MacosIcon( CupertinoIcons.share, ), - onPressed: () => debugPrint("pressed"), + onPressed: () => debugPrint('pressed'), showLabel: false, ), ], @@ -159,19 +158,16 @@ class _ToolbarPageState extends State { ContentArea( builder: (context, scrollController) { return SingleChildScrollView( + controller: scrollController, padding: const EdgeInsets.all(30), child: Center( child: Column( children: const [ Text( - "The toolbar appears below the title bar of the macOS app or integrates with it.", + 'A toolbar provides convenient access to frequently used commands and controls that perform actions relevant to the current view.', textAlign: TextAlign.center, ), SizedBox(height: 20.0), - Text( - "It provides convenient access to frequently used commands and features.", - textAlign: TextAlign.center, - ), ], ), ), diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 90ae1621..67acca3c 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -15,7 +15,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - macos_ui: 125c911559d646194386d84c017ad6819122e2db + macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/example/pubspec.lock b/example/pubspec.lock index abfba059..8e15ffe7 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -97,7 +97,7 @@ packages: path: ".." relative: true source: path - version: "1.11.1" + version: "1.12.0" matcher: dependency: transitive description: diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index 4bd80c30..6a71ceec 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -53,6 +53,7 @@ export 'src/layout/tab_view/tab_controller.dart'; export 'src/layout/tab_view/tab_view.dart'; export 'src/layout/title_bar.dart'; export 'src/layout/toolbar/custom_toolbar_item.dart'; +export 'src/layout/toolbar/sliver_toolbar.dart'; export 'src/layout/toolbar/toolbar.dart'; export 'src/layout/toolbar/toolbar_divider.dart'; export 'src/layout/toolbar/toolbar_overflow_menu.dart'; diff --git a/lib/src/layout/toolbar/custom_toolbar_item.dart b/lib/src/layout/toolbar/custom_toolbar_item.dart index 257866ca..bae382fe 100644 --- a/lib/src/layout/toolbar/custom_toolbar_item.dart +++ b/lib/src/layout/toolbar/custom_toolbar_item.dart @@ -10,17 +10,26 @@ class CustomToolbarItem extends ToolbarItem { /// ```dart /// // Add a grey vertical line as a custom toolbar item: /// CustomToolbarItem( - /// inToolbarBuilder: (context) => Padding( + /// inToolbarBuilder: (context) => Padding( /// padding: const EdgeInsets.all(8.0), - /// child: Container(color: Colors.grey, width: 1, height: 30), + /// child: Container( + /// color: Colors.grey, + /// width: 1, + /// height: 30, + /// ), + /// ), + /// inOverflowedBuilder: (context) => Container( + /// color: Colors.grey, + /// width: 30, + /// height: 1, /// ), - /// inOverflowedBuilder: (context) => - /// Container(color: Colors.grey, width: 30, height: 1), /// ), /// // Add a search field as a custom toolbar item: /// CustomToolbarItem( - /// inToolbarBuilder: (context) => - /// const SizedBox(width: 200, child: MacosSearchField()), + /// inToolbarBuilder: (context) => const SizedBox( + /// width: 200, + /// child: MacosSearchField(), + /// ), /// ), /// ``` /// diff --git a/lib/src/layout/toolbar/sliver_toolbar.dart b/lib/src/layout/toolbar/sliver_toolbar.dart new file mode 100644 index 00000000..8ec1b087 --- /dev/null +++ b/lib/src/layout/toolbar/sliver_toolbar.dart @@ -0,0 +1,317 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; + +/// Defines the height of a regular-sized [SliverToolBar] +const _kToolbarHeight = 52.0; + +/// Defines the width of the [SliverToolBar]'s title. +const _kTitleWidth = 150.0; + +/// {@template sliverToolBar} +/// A variant of [ToolBar] that is compatible with slivers. +/// +/// It is nearly identical to [ToolBar], with the exception that this widget +/// must only be used [ScrollView]s that allow slivers, such as +/// [CustomScrollView] and [NestedScrollView]. It contains three additional +/// properties that are relevant to its usage in such [ScrollView]s: [pinned], +/// [floating], and [toolbarOpacity]. +/// +/// See also: +/// * [SliverAppBar] (package:material) +/// {@endtemplate} +class SliverToolBar extends StatefulWidget with Diagnosticable { + /// {@macro sliverToolBar} + const SliverToolBar({ + super.key, + this.height = _kToolbarHeight, + this.alignment = Alignment.center, + this.title, + this.titleWidth = _kTitleWidth, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 4.0), + this.decoration, + this.leading, + this.automaticallyImplyLeading = true, + this.actions, + this.centerTitle = false, + this.dividerColor, + this.pinned = true, + this.floating = false, + this.toolbarOpacity = 0.9, + }); + + /// Specifies the height of this [ToolBar]. + /// + /// Defaults to [_kToolbarHeight] which is 52.0. + final double height; + + /// Aligns the [title] within the [ToolBar]. + /// + /// Defaults to [Alignment.center]. + /// + /// The [ToolBar] will expand to fill its parent and position its + /// child within itself according to the given value. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final Alignment alignment; + + /// The [title] of the toolbar. + /// + /// Typically, a [Text] widget. + final Widget? title; + + /// Specifies the width of the title of the [ToolBar]. + /// + /// Defaults to [_kTitleWidth] which is 150.0. + final double titleWidth; + + /// The decoration to paint behind the [title]. + final BoxDecoration? decoration; + + /// Empty space to inscribe inside the toolbar. The [title], if any, is + /// placed inside this padding. + /// + /// Defaults to ` EdgeInsets.symmetric(horizontal: 8, vertical: 4.0)`. + final EdgeInsets padding; + + /// A widget to display before the toolbar's [title]. + /// + /// Typically the [leading] widget is a [MacosIcon] or a [MacosIconButton]. + final Widget? leading; + + /// Controls whether we should try to imply the leading widget if null. + /// + /// If `true` and [leading] is null, automatically try to deduce what the leading + /// widget should be. If `false` and [leading] is null, leading space is given to [title]. + /// If leading widget is not null, this parameter has no effect. + final bool automaticallyImplyLeading; + + /// A list of [ToolbarItem] widgets to display in a row after the [title] widget, + /// as the toolbar actions. + /// + /// Toolbar items include [ToolBarIconButton], [ToolBarPulldownButton], + /// [ToolBarSpacer], and [CustomToolbarItem] widgets. + /// + /// If the toolbar actions exceed the available toolbar width (e.g. when the + /// window is resized), the overflowed actions are displayed via a + /// [ToolbarOverflowMenu], that can be opened from the [ToolbarOverflowButton] + /// at the right edge of the toolbar. + final List? actions; + + /// Whether the title should be centered. + final bool centerTitle; + + /// The color of the divider below the toolbar. + /// + /// Defaults to MacosTheme.of(context).dividerColor. + /// + /// Set it to MacosColors.transparent to remove. + final Color? dividerColor; + + /// Whether the toolbar should remain visible at the start of the scroll view. + /// + /// Defaults to `true`. + final bool pinned; + + /// Whether the toolbar should become visible as soon as the user scrolls + /// upwards. + /// + /// Otherwise, the user will need to scroll near the top of the scroll view + /// to reveal the toolbar. + /// + /// Defaults to `false`. + final bool floating; + + /// The opacity of the toolbar when content is scrolled underneath it. + /// + /// Adjust this value to tweak the blur effect the toolbar creates. Note that + /// the blur is only applied when content is being scrolled underneath the + /// toolbar. + /// + /// Defaults to `0.9`. + final double toolbarOpacity; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty('alignment', alignment)); + properties.add(DiagnosticsProperty('title', title)); + properties.add(DoubleProperty('titleWidth', titleWidth)); + properties + .add(DiagnosticsProperty('decoration', decoration)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(DiagnosticsProperty('leading', leading)); + properties.add(FlagProperty( + 'automaticallyImplyLeading', + value: automaticallyImplyLeading, + ifTrue: 'automatically imply leading', + )); + properties.add(DiagnosticsProperty>('actions', actions)); + properties.add( + FlagProperty('centerTitle', value: centerTitle, ifTrue: 'center title'), + ); + properties.add(DiagnosticsProperty('dividerColor', dividerColor)); + properties.add(FlagProperty('pinned', value: pinned, ifTrue: 'pinned')); + properties + .add(FlagProperty('floating', value: floating, ifTrue: 'floating')); + } + + @override + State createState() => _SliverToolBarState(); +} + +class _SliverToolBarState extends State + with TickerProviderStateMixin { + int overflowedActionsCount = 0; + + @override + void didUpdateWidget(SliverToolBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.actions != null && + widget.actions!.length != oldWidget.actions!.length) { + overflowedActionsCount = 0; + } + } + + @override + Widget build(BuildContext context) { + return MediaQuery.removePadding( + context: context, + removeBottom: true, + child: SliverPersistentHeader( + floating: widget.floating, + pinned: widget.pinned, + delegate: _SliverToolBarDelegate( + height: widget.height, + alignment: widget.alignment, + title: widget.title, + titleWidth: widget.titleWidth, + decoration: widget.decoration, + padding: widget.padding, + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + actions: widget.actions, + centerTitle: widget.centerTitle, + dividerColor: widget.dividerColor, + floating: widget.floating, + pinned: widget.pinned, + toolbarOpacity: widget.toolbarOpacity, + vsync: this, + ), + ), + ); + } +} + +class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { + _SliverToolBarDelegate({ + required this.height, + required this.alignment, + required this.title, + required this.titleWidth, + required this.decoration, + required this.padding, + required this.leading, + required this.automaticallyImplyLeading, + required this.actions, + required this.centerTitle, + required this.dividerColor, + required this.vsync, + required this.floating, + required this.pinned, + required this.toolbarOpacity, + }); + + final double height; + final Alignment alignment; + final Widget? title; + final double titleWidth; + final BoxDecoration? decoration; + final EdgeInsets padding; + final Widget? leading; + final bool automaticallyImplyLeading; + final List? actions; + final bool centerTitle; + final Color? dividerColor; + final bool floating; + final bool pinned; + final double toolbarOpacity; + + @override + double get minExtent => _kToolbarHeight; + + @override + double get maxExtent => _kToolbarHeight; + + @override + final TickerProvider vsync; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + final bool isScrolledUnder = overlapsContent || + (pinned || floating && shrinkOffset > maxExtent - minExtent); + final double opacity = + pinned || floating && isScrolledUnder ? toolbarOpacity : 1.0; + + BoxDecoration? effectiveDecoration; + if (isScrolledUnder) { + effectiveDecoration = decoration?.copyWith( + color: decoration?.color?.withOpacity(opacity), + ) ?? + BoxDecoration( + color: MacosTheme.of(context).canvasColor.withOpacity(opacity), + ); + } + + final Widget toolBar = FlexibleSpaceBar.createSettings( + minExtent: minExtent, + maxExtent: maxExtent, + currentExtent: math.max(minExtent, maxExtent - shrinkOffset), + toolbarOpacity: opacity, + isScrolledUnder: isScrolledUnder, + child: ToolBar( + automaticallyImplyLeading: automaticallyImplyLeading, + leading: leading, + title: title, + titleWidth: titleWidth, + decoration: effectiveDecoration, + padding: padding, + actions: actions, + centerTitle: centerTitle, + dividerColor: dividerColor, + alignment: alignment, + height: height, + ), + ); + return toolBar; + } + + @override + bool shouldRebuild(covariant _SliverToolBarDelegate oldDelegate) { + return leading != oldDelegate.leading || + automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading || + alignment != oldDelegate.alignment || + title != oldDelegate.title || + titleWidth != oldDelegate.titleWidth || + decoration != oldDelegate.decoration || + padding != oldDelegate.padding || + leading != oldDelegate.leading || + actions != oldDelegate.actions || + centerTitle != oldDelegate.centerTitle || + dividerColor != oldDelegate.dividerColor || + floating != oldDelegate.floating || + pinned != oldDelegate.pinned; + } +} diff --git a/lib/src/layout/toolbar/toolbar.dart b/lib/src/layout/toolbar/toolbar.dart index 294a24a6..a9685f6a 100644 --- a/lib/src/layout/toolbar/toolbar.dart +++ b/lib/src/layout/toolbar/toolbar.dart @@ -15,7 +15,7 @@ const _kLeadingWidth = 20.0; const _kTitleWidth = 150.0; /// A toolbar to use in a [MacosScaffold]. -class ToolBar extends StatefulWidget { +class ToolBar extends StatefulWidget with Diagnosticable { /// Creates a toolbar in the [MacosScaffold]. The toolbar appears below the /// title bar (if present) of the macOS app or integrates with it. /// @@ -120,6 +120,31 @@ class ToolBar extends StatefulWidget { /// Set this to `MacosColors.transparent` to remove. final Color? dividerColor; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty('alignment', alignment)); + properties.add(DiagnosticsProperty('title', title)); + properties.add(DoubleProperty('titleWidth', titleWidth)); + properties + .add(DiagnosticsProperty('decoration', decoration)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(DiagnosticsProperty('leading', leading)); + properties.add(FlagProperty( + 'automaticallyImplyLeading', + value: automaticallyImplyLeading, + ifTrue: 'automatically imply leading', + )); + properties.add(DiagnosticsProperty>('actions', actions)); + properties.add(FlagProperty( + 'centerTitle', + value: centerTitle, + ifTrue: 'center title', + )); + properties.add(DiagnosticsProperty('dividerColor', dividerColor)); + } + @override State createState() => _ToolBarState(); } diff --git a/lib/src/library.dart b/lib/src/library.dart index 9d05ec7e..37fb1228 100644 --- a/lib/src/library.dart +++ b/lib/src/library.dart @@ -28,6 +28,7 @@ export 'package:flutter/material.dart' DateUtils, TimeOfDay, DayPeriod, + FlexibleSpaceBar, MaterialState; export 'package:flutter/widgets.dart'; diff --git a/macos/macos_ui.podspec b/macos/macos_ui.podspec index 4f45d04e..947c74a3 100644 --- a/macos/macos_ui.podspec +++ b/macos/macos_ui.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.description = <<-DESC A new flutter plugin project. DESC - s.homepage = 'https://github.com/GroovinChip/macos_ui' + s.homepage = 'https://github.com/macosui/macos_ui' s.license = { :file => '../LICENSE' } s.author = { 'GroovinChip' => 'groovinchip@gmail.com' } s.source = { :path => '.' } diff --git a/pubspec.lock b/pubspec.lock index 75773850..6cfa9f14 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -420,10 +420,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" url: "https://pub.dev" source: hosted - version: "0.10.11" + version: "0.10.12" source_span: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d1760775..9604bd3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: macos_ui description: Flutter widgets and themes implementing the current macOS design language. -version: 1.11.1 +version: 1.12.0 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" @@ -23,5 +23,5 @@ flutter: plugin: platforms: macos: - package: dev.groovinchip.macos_ui + package: dev.macosui.macos_ui pluginClass: MacOSUiPlugin diff --git a/test/layout/sliver_toolbar_test.dart b/test/layout/sliver_toolbar_test.dart new file mode 100644 index 00000000..bcfeb9d4 --- /dev/null +++ b/test/layout/sliver_toolbar_test.dart @@ -0,0 +1,182 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; + +void main() { + Future pumpScrollableWithSliverToolbar( + WidgetTester tester, + ScrollController controller, { + bool pinned = true, + bool floating = false, + double opacity = 0.9, + }) async { + await tester.pumpWidget( + MacosApp( + home: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return CustomScrollView( + controller: controller, + slivers: [ + SliverToolBar( + title: const Text('Title'), + pinned: pinned, + floating: floating, + toolbarOpacity: 0.9, + ), + const SliverToBoxAdapter( + child: SizedBox(height: 1200), + ), + ], + ); + }, + ), + ], + ), + ), + ); + } + + testWidgets( + 'MediaQuery bottom padding is removed as expected', + (tester) async { + final Key leadingKey = GlobalKey(); + final Key titleKey = GlobalKey(); + await tester.pumpWidget( + MacosApp( + home: MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + bottom: 30.0, + ), + ), + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return CustomScrollView( + controller: scrollController, + slivers: [ + SliverToolBar( + leading: Placeholder(key: leadingKey), + title: Text('Title', key: titleKey), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 1200), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + + // location of bottomLeft is unchanged by applying padding + expect( + tester.getBottomLeft(find.byKey(leadingKey)), + const Offset(8.0, 47.0), + ); + }, + ); + + testWidgets( + 'Y-Offsets and sizes are correct', + (tester) async { + final ScrollController controller = ScrollController(); + await pumpScrollableWithSliverToolbar(tester, controller); + + final toolbar = find.byType(ToolBar); + final navToolbar = find.byType(NavigationToolbar); + + expect(tester.getTopLeft(toolbar).dy, 0.0); + expect(tester.getTopLeft(navToolbar).dy, 4.0); + expect(tester.getSize(toolbar).height, 52.0); + expect(tester.getSize(navToolbar).height, 43.0); + }, + ); + + testWidgets( + 'Scrolling down while pinned=true keeps the toolbar in view', + (tester) async { + final ScrollController controller = ScrollController(); + await pumpScrollableWithSliverToolbar(tester, controller); + + final toolbar = find.byType(ToolBar); + final navToolbar = find.byType(NavigationToolbar); + + expect(controller.offset, 0.0); + + controller.jumpTo(600.0); + await tester.pump(); + + expect(tester.getTopLeft(toolbar).dy, 0.0); + expect(tester.getTopLeft(navToolbar).dy, 4.0); + }, + ); + + testWidgets( + 'Scrolling down while pinned=false does not keep the toolbar in view', + (tester) async { + final ScrollController controller = ScrollController(); + await pumpScrollableWithSliverToolbar(tester, controller, pinned: false); + + final toolbar = find.byType(ToolBar); + final navToolbar = find.byType(NavigationToolbar); + + expect(controller.offset, 0.0); + expect(tester.getTopLeft(toolbar).dy, 0.0); + expect(tester.getTopLeft(navToolbar).dy, 4.0); + + controller.jumpTo(600.0); + await tester.pump(); + + expect(toolbar, findsNothing); + expect(navToolbar, findsNothing); + }, + ); + + testWidgets( + 'Scrolling down and back up while pinned=false and floating=true brings the toolbar back into view', + (tester) async { + final ScrollController controller = ScrollController(); + await pumpScrollableWithSliverToolbar( + tester, + controller, + pinned: false, + floating: true, + ); + + final toolbar = find.byType(ToolBar); + final navToolbar = find.byType(NavigationToolbar); + + expect(controller.offset, 0.0); + expect(tester.getTopLeft(toolbar).dy, 0.0); + expect(tester.getTopLeft(navToolbar).dy, 4.0); + + controller.jumpTo(600.0); + await tester.pump(); + + expect(toolbar, findsNothing); + expect(navToolbar, findsNothing); + + final Offset scrollEventLocation = + tester.getCenter(find.byType(CustomScrollView)); + final TestPointer testPointer = TestPointer(1, PointerDeviceKind.mouse); + testPointer.hover(scrollEventLocation); + await tester + .sendEventToBinding(testPointer.scroll(const Offset(0.0, -52.0))); + await tester.pumpAndSettle(); + + expect(controller.offset, 548.0); + expect(toolbar, findsOneWidget); + expect(tester.getTopLeft(toolbar).dy, 0.0); + expect(navToolbar, findsOneWidget); + expect(tester.getTopLeft(navToolbar).dy, 4.0); + }, + ); +}