diff --git a/lib/core/widgets/material/search/m3_search.dart b/lib/core/widgets/material/search/m3_search.dart deleted file mode 100644 index 9e7b78d..0000000 --- a/lib/core/widgets/material/search/m3_search.dart +++ /dev/null @@ -1,602 +0,0 @@ -//TODO: REMOVE THIS WHEN NATIVE MATERIAL FLUTTER SEARCH IS RELEASED -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; - -/// Shows a full screen search page and returns the search result selected by -/// the user when the page is closed. -/// -/// The search page consists of an app bar with a search field and a body which -/// can either show suggested search queries or the search results. -/// -/// The appearance of the search page is determined by the provided -/// `delegate`. The initial query string is given by `query`, which defaults -/// to the empty string. When `query` is set to null, `delegate.query` will -/// be used as the initial query. -/// -/// This method returns the selected search result, which can be set in the -/// [SearchDelegate.close] call. If the search page is closed with the system -/// back button, it returns null. -/// -/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search] -/// call. Call [SearchDelegate.close] before re-using the same delegate instance -/// for another [showMaterial3Search] call. -/// -/// The `useRootNavigator` argument is used to determine whether to push the -/// search page to the [Navigator] furthest from or nearest to the given -/// `context`. By default, `useRootNavigator` is `false` and the search page -/// route created by this method is pushed to the nearest navigator to the -/// given `context`. It can not be `null`. -/// -/// The transition to the search page triggered by this method looks best if the -/// screen triggering the transition contains an [AppBar] at the top and the -/// transition is called from an [IconButton] that's part of [AppBar.actions]. -/// The animation provided by [SearchDelegate.transitionAnimation] can be used -/// to trigger additional animations in the underlying page while the search -/// page fades in or out. This is commonly used to animate an [AnimatedIcon] in -/// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow -/// used to exit the search page. -/// -/// ## Handling emojis and other complex characters -/// {@macro flutter.widgets.EditableText.onChanged} -/// -/// See also: -/// -/// * [SearchDelegate] to define the content of the search page. -Future showMaterial3Search({ - required BuildContext context, - required SearchDelegate delegate, - String? query = '', - bool useRootNavigator = false, -}) { - delegate.query = query ?? delegate.query; - delegate._currentBody = _SearchBody.suggestions; - return Navigator.of(context, rootNavigator: useRootNavigator) - .push(_SearchPageRoute( - delegate: delegate, - )); -} - -/// Delegate for [showMaterial3Search] to define the content of the search page. -/// -/// The search page always shows an [AppBar] at the top where users can -/// enter their search queries. The buttons shown before and after the search -/// query text field can be customized via [SearchDelegate.buildLeading] -/// and [SearchDelegate.buildActions]. Additionally, a widget can be placed -/// across the bottom of the [AppBar] via [SearchDelegate.buildBottom]. -/// -/// The body below the [AppBar] can either show suggested queries (returned by -/// [SearchDelegate.buildSuggestions]) or - once the user submits a search - the -/// results of the search as returned by [SearchDelegate.buildResults]. -/// -/// [SearchDelegate.query] always contains the current query entered by the user -/// and should be used to build the suggestions and results. -/// -/// The results can be brought on screen by calling [SearchDelegate.showResults] -/// and you can go back to showing the suggestions by calling -/// [SearchDelegate.showSuggestions]. -/// -/// Once the user has selected a search result, [SearchDelegate.close] should be -/// called to remove the search page from the top of the navigation stack and -/// to notify the caller of [showMaterial3Search] about the selected search result. -/// -/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search] -/// call. Call [SearchDelegate.close] before re-using the same delegate instance -/// for another [showMaterial3Search] call. -/// -/// ## Handling emojis and other complex characters -/// {@macro flutter.widgets.EditableText.onChanged} -abstract class SearchDelegate { - /// Constructor to be called by subclasses which may specify - /// [searchFieldLabel], either [searchFieldStyle] or [searchFieldDecorationTheme], - /// [keyboardType] and/or [textInputAction]. Only one of [searchFieldLabel] - /// and [searchFieldDecorationTheme] may be non-null. - /// - /// {@tool snippet} - /// ```dart - /// class CustomSearchHintDelegate extends SearchDelegate { - /// CustomSearchHintDelegate({ - /// required String hintText, - /// }) : super( - /// searchFieldLabel: hintText, - /// keyboardType: TextInputType.text, - /// textInputAction: TextInputAction.search, - /// ); - /// - /// @override - /// Widget buildLeading(BuildContext context) => const Text('leading'); - /// - /// @override - /// PreferredSizeWidget buildBottom(BuildContext context) { - /// return const PreferredSize( - /// preferredSize: Size.fromHeight(56.0), - /// child: Text('bottom')); - /// } - /// - /// @override - /// Widget buildSuggestions(BuildContext context) => const Text('suggestions'); - /// - /// @override - /// Widget buildResults(BuildContext context) => const Text('results'); - /// - /// @override - /// List buildActions(BuildContext context) => []; - /// } - /// ``` - /// {@end-tool} - SearchDelegate({ - this.searchFieldLabel, - this.searchFieldStyle, - this.searchFieldDecorationTheme, - this.keyboardType, - this.textInputAction = TextInputAction.search, - }) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null); - - /// Suggestions shown in the body of the search page while the user types a - /// query into the search field. - /// - /// The delegate method is called whenever the content of [query] changes. - /// The suggestions should be based on the current [query] string. If the query - /// string is empty, it is good practice to show suggested queries based on - /// past queries or the current context. - /// - /// Usually, this method will return a [ListView] with one [ListTile] per - /// suggestion. When [ListTile.onTap] is called, [query] should be updated - /// with the corresponding suggestion and the results page should be shown - /// by calling [showResults]. - Widget buildSuggestions(BuildContext context); - - /// The results shown after the user submits a search from the search page. - /// - /// The current value of [query] can be used to determine what the user - /// searched for. - /// - /// This method might be applied more than once to the same query. - /// If your [buildResults] method is computationally expensive, you may want - /// to cache the search results for one or more queries. - /// - /// Typically, this method returns a [ListView] with the search results. - /// When the user taps on a particular search result, [close] should be called - /// with the selected result as argument. This will close the search page and - /// communicate the result back to the initial caller of [showMaterial3Search]. - Widget buildResults(BuildContext context); - - /// A widget to display before the current query in the [AppBar]. - /// - /// Typically an [IconButton] configured with a [BackButtonIcon] that exits - /// the search with [close]. One can also use an [AnimatedIcon] driven by - /// [transitionAnimation], which animates from e.g. a hamburger menu to the - /// back button as the search overlay fades in. - /// - /// Returns null if no widget should be shown. - /// - /// See also: - /// - /// * [AppBar.leading], the intended use for the return value of this method. - Widget? buildLeading(BuildContext context); - - /// Widgets to display after the search query in the [AppBar]. - /// - /// If the [query] is not empty, this should typically contain a button to - /// clear the query and show the suggestions again (via [showSuggestions]) if - /// the results are currently shown. - /// - /// Returns null if no widget should be shown. - /// - /// See also: - /// - /// * [AppBar.actions], the intended use for the return value of this method. - List? buildActions(BuildContext context); - - /// Widget to display across the bottom of the [AppBar]. - /// - /// Returns null by default, i.e. a bottom widget is not included. - /// - /// See also: - /// - /// * [AppBar.bottom], the intended use for the return value of this method. - /// - PreferredSizeWidget? buildBottom(BuildContext context) => null; - - /// The theme used to configure the search page. - /// - /// The returned [ThemeData] will be used to wrap the entire search page, - /// so it can be used to configure any of its components with the appropriate - /// theme properties. - /// - /// Unless overridden, the default theme will configure the AppBar containing - /// the search input text field with a white background and black text on light - /// themes. For dark themes the default is a dark grey background with light - /// color text. - /// - /// See also: - /// - /// * [AppBarTheme], which configures the AppBar's appearance. - /// * [InputDecorationTheme], which configures the appearance of the search - /// text field. - ThemeData appBarTheme(BuildContext context) { - final ThemeData theme = Theme.of(context); - final ColorScheme colorScheme = theme.colorScheme; - return theme.copyWith( - appBarTheme: AppBarTheme( - systemOverlayStyle: colorScheme.brightness == Brightness.light - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - backgroundColor: colorScheme.brightness == Brightness.dark - ? Colors.grey[900] - : Colors.white, - iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), - ), - inputDecorationTheme: searchFieldDecorationTheme ?? - InputDecorationTheme( - hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle, - border: InputBorder.none, - ), - ); - } - - /// The current query string shown in the [AppBar]. - /// - /// The user manipulates this string via the keyboard. - /// - /// If the user taps on a suggestion provided by [buildSuggestions] this - /// string should be updated to that suggestion via the setter. - String get query => _queryTextController.text; - - /// Changes the current query string. - /// - /// Setting the query string programmatically moves the cursor to the end of the text field. - set query(String value) { - _queryTextController.text = value; - if (_queryTextController.text.isNotEmpty) { - _queryTextController.selection = TextSelection.fromPosition( - TextPosition(offset: _queryTextController.text.length)); - } - } - - /// Transition from the suggestions returned by [buildSuggestions] to the - /// [query] results returned by [buildResults]. - /// - /// If the user taps on a suggestion provided by [buildSuggestions] the - /// screen should typically transition to the page showing the search - /// results for the suggested query. This transition can be triggered - /// by calling this method. - /// - /// See also: - /// - /// * [showSuggestions] to show the search suggestions again. - void showResults(BuildContext context) { - _focusNode?.unfocus(); - _currentBody = _SearchBody.results; - } - - /// Transition from showing the results returned by [buildResults] to showing - /// the suggestions returned by [buildSuggestions]. - /// - /// Calling this method will also put the input focus back into the search - /// field of the [AppBar]. - /// - /// If the results are currently shown this method can be used to go back - /// to showing the search suggestions. - /// - /// See also: - /// - /// * [showResults] to show the search results. - void showSuggestions(BuildContext context) { - assert(_focusNode != null, - '_focusNode must be set by route before showSuggestions is called.'); - _focusNode!.requestFocus(); - _currentBody = _SearchBody.suggestions; - } - - /// Closes the search page and returns to the underlying route. - /// - /// The value provided for `result` is used as the return value of the call - /// to [showMaterial3Search] that launched the search initially. - void close(BuildContext context, T result) { - _currentBody = null; - _focusNode?.unfocus(); - Navigator.of(context) - ..popUntil((Route route) => route == _route) - ..pop(result); - } - - /// The hint text that is shown in the search field when it is empty. - /// - /// If this value is set to null, the value of - /// `MaterialLocalizationS.of(context)!.searchFieldLabel` will be used instead. - final String? searchFieldLabel; - - /// The style of the [searchFieldLabel]. - /// - /// If this value is set to null, the value of the ambient [Theme]'s - /// [InputDecorationTheme.hintStyle] will be used instead. - /// - /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can - /// be non-null. - final TextStyle? searchFieldStyle; - - /// The [InputDecorationTheme] used to configure the search field's visuals. - /// - /// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can - /// be non-null. - final InputDecorationTheme? searchFieldDecorationTheme; - - /// The type of action button to use for the keyboard. - /// - /// Defaults to the default value specified in [TextField]. - final TextInputType? keyboardType; - - /// The text input action configuring the soft keyboard to a particular action - /// button. - /// - /// Defaults to [TextInputAction.search]. - final TextInputAction textInputAction; - - /// [Animation] triggered when the search pages fades in or out. - /// - /// This animation is commonly used to animate [AnimatedIcon]s of - /// [IconButton]s returned by [buildLeading] or [buildActions]. It can also be - /// used to animate [IconButton]s contained within the route below the search - /// page. - Animation get transitionAnimation => _proxyAnimation; - - // The focus node to use for manipulating focus on the search page. This is - // managed, owned, and set by the _SearchPageRoute using this delegate. - FocusNode? _focusNode; - - final TextEditingController _queryTextController = TextEditingController(); - - final ProxyAnimation _proxyAnimation = - ProxyAnimation(kAlwaysDismissedAnimation); - - final ValueNotifier<_SearchBody?> _currentBodyNotifier = - ValueNotifier<_SearchBody?>(null); - - _SearchBody? get _currentBody => _currentBodyNotifier.value; - set _currentBody(_SearchBody? value) { - _currentBodyNotifier.value = value; - } - - _SearchPageRoute? _route; -} - -/// Describes the body that is currently shown under the [AppBar] in the -/// search page. -enum _SearchBody { - /// Suggested queries are shown in the body. - /// - /// The suggested queries are generated by [SearchDelegate.buildSuggestions]. - suggestions, - - /// Search results are currently shown in the body. - /// - /// The search results are generated by [SearchDelegate.buildResults]. - results, -} - -class _SearchPageRoute extends PageRoute { - _SearchPageRoute({ - required this.delegate, - }) { - assert( - delegate._route == null, - 'The ${delegate.runtimeType} instance is currently used by another active ' - 'search. Please close that search by calling close() on the SearchDelegate ' - 'before opening another search with the same delegate instance.', - ); - delegate._route = this; - } - - final SearchDelegate delegate; - - @override - Color? get barrierColor => null; - - @override - String? get barrierLabel => null; - - @override - Duration get transitionDuration => const Duration(milliseconds: 300); - - @override - bool get maintainState => false; - - @override - Widget buildTransitions( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return FadeTransition( - opacity: animation, - child: child, - ); - } - - @override - Animation createAnimation() { - final Animation animation = super.createAnimation(); - delegate._proxyAnimation.parent = animation; - return animation; - } - - @override - Widget buildPage( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - ) { - return _SearchPage( - delegate: delegate, - animation: animation, - ); - } - - @override - void didComplete(T? result) { - super.didComplete(result); - assert(delegate._route == this); - delegate._route = null; - delegate._currentBody = null; - } -} - -class _SearchPage extends StatefulWidget { - const _SearchPage({ - required this.delegate, - required this.animation, - }); - - final SearchDelegate delegate; - final Animation animation; - - @override - State createState() => _SearchPageState(); -} - -class _SearchPageState extends State<_SearchPage> { - // This node is owned, but not hosted by, the search page. Hosting is done by - // the text field. - FocusNode focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - widget.delegate._queryTextController.addListener(_onQueryChanged); - widget.animation.addStatusListener(_onAnimationStatusChanged); - widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); - focusNode.addListener(_onFocusChanged); - widget.delegate._focusNode = focusNode; - } - - @override - void dispose() { - super.dispose(); - widget.delegate._queryTextController.removeListener(_onQueryChanged); - widget.animation.removeStatusListener(_onAnimationStatusChanged); - widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); - widget.delegate._focusNode = null; - focusNode.dispose(); - } - - void _onAnimationStatusChanged(AnimationStatus status) { - if (status != AnimationStatus.completed) { - return; - } - widget.animation.removeStatusListener(_onAnimationStatusChanged); - if (widget.delegate._currentBody == _SearchBody.suggestions) { - focusNode.requestFocus(); - } - } - - @override - void didUpdateWidget(_SearchPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.delegate != oldWidget.delegate) { - oldWidget.delegate._queryTextController.removeListener(_onQueryChanged); - widget.delegate._queryTextController.addListener(_onQueryChanged); - oldWidget.delegate._currentBodyNotifier - .removeListener(_onSearchBodyChanged); - widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); - oldWidget.delegate._focusNode = null; - widget.delegate._focusNode = focusNode; - } - } - - void _onFocusChanged() { - if (focusNode.hasFocus && - widget.delegate._currentBody != _SearchBody.suggestions) { - widget.delegate.showSuggestions(context); - } - } - - void _onQueryChanged() { - setState(() { - // rebuild ourselves because query changed. - }); - } - - void _onSearchBodyChanged() { - setState(() { - // rebuild ourselves because search body changed. - }); - } - - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterialLocalizations(context)); - final ThemeData theme = widget.delegate.appBarTheme(context); - final String searchFieldLabel = - widget.delegate.searchFieldLabel ?? S.of(context)!.search; - Widget? body; - switch (widget.delegate._currentBody) { - case _SearchBody.suggestions: - body = KeyedSubtree( - key: const ValueKey<_SearchBody>(_SearchBody.suggestions), - child: widget.delegate.buildSuggestions(context), - ); - break; - case _SearchBody.results: - body = KeyedSubtree( - key: const ValueKey<_SearchBody>(_SearchBody.results), - child: widget.delegate.buildResults(context), - ); - break; - case null: - break; - } - - late final String routeName; - switch (theme.platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - routeName = ''; - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - routeName = searchFieldLabel; - } - - return Semantics( - explicitChildNodes: true, - scopesRoute: true, - namesRoute: true, - label: routeName, - child: Theme( - data: theme, - child: Scaffold( - appBar: AppBar( - toolbarHeight: 72, - leading: widget.delegate.buildLeading(context), - title: TextField( - controller: widget.delegate._queryTextController, - focusNode: focusNode, - style: widget.delegate.searchFieldStyle ?? - theme.textTheme.titleLarge, - textInputAction: widget.delegate.textInputAction, - keyboardType: widget.delegate.keyboardType, - onSubmitted: (String _) { - widget.delegate.showResults(context); - }, - decoration: InputDecoration(hintText: searchFieldLabel), - ), - actions: widget.delegate.buildActions(context), - bottom: widget.delegate.buildBottom(context), - ), - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: body, - ), - ), - ), - ); - } -} diff --git a/lib/core/widgets/material/search/m3_search_bar.dart b/lib/core/widgets/material/search/m3_search_bar.dart deleted file mode 100644 index dafcea1..0000000 --- a/lib/core/widgets/material/search/m3_search_bar.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -class SearchBar extends StatelessWidget { - const SearchBar({ - Key? key, - this.height = 56, - required this.leadingIcon, - this.trailingIcon, - required this.supportingText, - required this.onTap, - }) : super(key: key); - - final double height; - double get effectiveHeight { - return max(height, 48); - } - - final VoidCallback onTap; - final Widget leadingIcon; - final Widget? trailingIcon; - - final String supportingText; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - final TextTheme textTheme = Theme.of(context).textTheme; - - return Container( - constraints: const BoxConstraints(minWidth: 360, maxWidth: 720), - width: double.infinity, - height: effectiveHeight, - child: Material( - elevation: 1, - color: colorScheme.surface, - shadowColor: colorScheme.shadow, - surfaceTintColor: colorScheme.surfaceTint, - borderRadius: BorderRadius.circular(effectiveHeight / 2), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(effectiveHeight / 2), - highlightColor: Colors.transparent, - splashFactory: InkRipple.splashFactory, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row(children: [ - leadingIcon, - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: TextField( - onTap: onTap, - readOnly: true, - enabled: false, - cursorColor: colorScheme.primary, - style: textTheme.bodyLarge, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - isCollapsed: true, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - hintText: supportingText, - hintStyle: textTheme.bodyLarge?.apply( - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ), - if (trailingIcon != null) trailingIcon!, - ]), - ), - ), - ), - ); - } -} diff --git a/lib/core/widgets/material/search/search_anchor.dart b/lib/core/widgets/material/search/search_anchor.dart deleted file mode 100644 index 6acadaa..0000000 --- a/lib/core/widgets/material/search/search_anchor.dart +++ /dev/null @@ -1,1883 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// TODO: Remove once these changes were merged into stable release. -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -const int _kOpenViewMilliseconds = 600; -const Duration _kOpenViewDuration = - Duration(milliseconds: _kOpenViewMilliseconds); -const Duration _kAnchorFadeDuration = Duration(milliseconds: 150); -const Curve _kViewFadeOnInterval = Interval(0.0, 1 / 2); -const Curve _kViewIconsFadeOnInterval = Interval(1 / 6, 2 / 6); -const Curve _kViewDividerFadeOnInterval = Interval(0.0, 1 / 6); -const Curve _kViewListFadeOnInterval = - Interval(133 / _kOpenViewMilliseconds, 233 / _kOpenViewMilliseconds); - -/// Signature for a function that creates a [Widget] which is used to open a search view. -/// -/// The `controller` callback provided to [SearchAnchor.builder] can be used -/// to open the search view and control the editable field on the view. -typedef SearchAnchorChildBuilder = Widget Function( - BuildContext context, SearchController controller); - -/// Signature for a function that creates a [Widget] to build the suggestion list -/// based on the input in the search bar. -/// -/// The `controller` callback provided to [SearchAnchor.suggestionsBuilder] can be used -/// to close the search view and control the editable field on the view. -typedef SuggestionsBuilder = Iterable Function( - BuildContext context, SearchController controller); - -/// Signature for a function that creates a [Widget] to layout the suggestion list. -/// -/// Parameter `suggestions` is the content list that this function wants to lay out. -typedef ViewBuilder = Widget Function(Iterable suggestions); - -/// Manages a "search view" route that allows the user to select one of the -/// suggested completions for a search query. -/// -/// The search view's route can either be shown by creating a [SearchController] -/// and then calling [SearchController.openView] or by tapping on an anchor. -/// When the anchor is tapped or [SearchController.openView] is called, the search view either -/// grows to a specific size, or grows to fill the entire screen. By default, -/// the search view only shows full screen on mobile platforms. Use [SearchAnchor.isFullScreen] -/// to override the default setting. -/// -/// The search view is usually opened by a [SearchBar], an [IconButton] or an [Icon]. -/// If [builder] returns an Icon, or any un-tappable widgets, we don't have -/// to explicitly call [SearchController.openView]. -/// -/// {@tool dartpad} -/// This example shows how to use an IconButton to open a search view in a [SearchAnchor]. -/// It also shows how to use [SearchController] to open or close the search view route. -/// -/// ** See code in examples/api/lib/material/search_anchor/search_anchor.2.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This example shows how to set up a floating (or pinned) AppBar with a -/// [SearchAnchor] for a title. -/// -/// ** See code in examples/api/lib/material/search_anchor/search_anchor.1.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SearchBar], a widget that defines a search bar. -/// * [SearchBarTheme], a widget that overrides the default configuration of a search bar. -/// * [SearchViewTheme], a widget that overrides the default configuration of a search view. -class SearchAnchor extends StatefulWidget { - /// Creates a const [SearchAnchor]. - /// - /// The [builder] and [suggestionsBuilder] arguments are required. - const SearchAnchor({ - super.key, - this.isFullScreen, - this.searchController, - this.viewBuilder, - this.viewLeading, - this.viewTrailing, - this.viewHintText, - this.viewBackgroundColor, - this.viewElevation, - this.viewSurfaceTintColor, - this.viewSide, - this.viewShape, - this.headerTextStyle, - this.headerHintStyle, - this.dividerColor, - this.viewConstraints, - required this.builder, - required this.suggestionsBuilder, - }); - - /// Create a [SearchAnchor] that has a [SearchBar] which opens a search view. - /// - /// All the barX parameters are used to customize the anchor. Similarly, all the - /// viewX parameters are used to override the view's defaults. - /// - /// {@tool dartpad} - /// This example shows how to use a [SearchAnchor.bar] which uses a default search - /// bar to open a search view route. - /// - /// ** See code in examples/api/lib/material/search_anchor/search_anchor.0.dart ** - /// {@end-tool} - /// - /// The [suggestionsBuilder] argument must not be null. - factory SearchAnchor.bar( - {Widget? barLeading, - Iterable? barTrailing, - String? barHintText, - GestureTapCallback? onTap, - MaterialStateProperty? barElevation, - MaterialStateProperty? barBackgroundColor, - MaterialStateProperty? barOverlayColor, - MaterialStateProperty? barSide, - MaterialStateProperty? barShape, - MaterialStateProperty? barPadding, - MaterialStateProperty? barTextStyle, - MaterialStateProperty? barHintStyle, - Widget? viewLeading, - Iterable? viewTrailing, - String? viewHintText, - Color? viewBackgroundColor, - double? viewElevation, - BorderSide? viewSide, - OutlinedBorder? viewShape, - TextStyle? viewHeaderTextStyle, - TextStyle? viewHeaderHintStyle, - Color? dividerColor, - BoxConstraints? constraints, - bool? isFullScreen, - SearchController searchController, - required SuggestionsBuilder suggestionsBuilder}) = - _SearchAnchorWithSearchBar; - - /// Whether the search view grows to fill the entire screen when the - /// [SearchAnchor] is tapped. - /// - /// By default, the search view is full-screen on mobile devices. On other - /// platforms, the search view only grows to a specific size that is determined - /// by the anchor and the default size. - final bool? isFullScreen; - - /// An optional controller that allows opening and closing of the search view from - /// other widgets. - /// - /// If this is null, one internal search controller is created automatically - /// and it is used to open the search view when the user taps on the anchor. - final SearchController? searchController; - - /// Optional callback to obtain a widget to lay out the suggestion list of the - /// search view. - /// - /// Default view uses a [ListView] with a vertical scroll direction. - final ViewBuilder? viewBuilder; - - /// An optional widget to display before the text input field when the search - /// view is open. - /// - /// Typically the [viewLeading] widget is an [Icon] or an [IconButton]. - /// - /// Defaults to a back button which pops the view. - final Widget? viewLeading; - - /// An optional widget list to display after the text input field when the search - /// view is open. - /// - /// Typically the [viewTrailing] widget list only has one or two widgets. - /// - /// Defaults to an icon button which clears the text in the input field. - final Iterable? viewTrailing; - - /// Text that is displayed when the search bar's input field is empty. - final String? viewHintText; - - /// The search view's background fill color. - /// - /// If null, the value of [SearchViewThemeData.backgroundColor] will be used. - /// If this is also null, then the default value is [ColorScheme.surface]. - final Color? viewBackgroundColor; - - /// The elevation of the search view's [Material]. - /// - /// If null, the value of [SearchViewThemeData.elevation] will be used. If this - /// is also null, then default value is 6.0. - final double? viewElevation; - - /// The surface tint color of the search view's [Material]. - /// - /// See [Material.surfaceTintColor] for more details. - /// - /// If null, the value of [SearchViewThemeData.surfaceTintColor] will be used. - /// If this is also null, then the default value is [ColorScheme.surfaceTint]. - final Color? viewSurfaceTintColor; - - /// The color and weight of the search view's outline. - /// - /// This value is combined with [viewShape] to create a shape decorated - /// with an outline. This will be ignored if the view is full-screen. - /// - /// If null, the value of [SearchViewThemeData.side] will be used. If this is - /// also null, the search view doesn't have a side by default. - final BorderSide? viewSide; - - /// The shape of the search view's underlying [Material]. - /// - /// This shape is combined with [viewSide] to create a shape decorated - /// with an outline. - /// - /// If null, the value of [SearchViewThemeData.shape] will be used. - /// If this is also null, then the default value is a rectangle shape for full-screen - /// mode and a [RoundedRectangleBorder] shape with a 28.0 radius otherwise. - final OutlinedBorder? viewShape; - - /// The style to use for the text being edited on the search view. - /// - /// If null, defaults to the `bodyLarge` text style from the current [Theme]. - /// The default text color is [ColorScheme.onSurface]. - final TextStyle? headerTextStyle; - - /// The style to use for the [viewHintText] on the search view. - /// - /// If null, the value of [SearchViewThemeData.headerHintStyle] will be used. - /// If this is also null, the value of [headerTextStyle] will be used. If this is also null, - /// defaults to the `bodyLarge` text style from the current [Theme]. The default - /// text color is [ColorScheme.onSurfaceVariant]. - final TextStyle? headerHintStyle; - - /// The color of the divider on the search view. - /// - /// If this property is null, then [SearchViewThemeData.dividerColor] is used. - /// If that is also null, the default value is [ColorScheme.outline]. - final Color? dividerColor; - - /// Optional size constraints for the search view. - /// - /// If null, the value of [SearchViewThemeData.constraints] will be used. If - /// this is also null, then the constraints defaults to: - /// ```dart - /// const BoxConstraints(minWidth: 360.0, minHeight: 240.0) - /// ``` - final BoxConstraints? viewConstraints; - - /// Called to create a widget which can open a search view route when it is tapped. - /// - /// The widget returned by this builder is faded out when it is tapped. - /// At the same time a search view route is faded in. - /// - /// This must not be null. - final SearchAnchorChildBuilder builder; - - /// Called to get the suggestion list for the search view. - /// - /// By default, the list returned by this builder is laid out in a [ListView]. - /// To get a different layout, use [viewBuilder] to override. - final SuggestionsBuilder suggestionsBuilder; - - @override - State createState() => _SearchAnchorState(); -} - -class _SearchAnchorState extends State { - bool _anchorIsVisible = true; - final GlobalKey _anchorKey = GlobalKey(); - bool get _viewIsOpen => !_anchorIsVisible; - late SearchController? _internalSearchController; - SearchController get _searchController => - widget.searchController ?? _internalSearchController!; - - @override - void initState() { - super.initState(); - if (widget.searchController == null) { - _internalSearchController = SearchController(); - } - _searchController._attach(this); - } - - @override - void dispose() { - super.dispose(); - _searchController._detach(this); - _internalSearchController = null; - } - - void _openView() { - Navigator.of(context).push(_SearchViewRoute( - viewLeading: widget.viewLeading, - viewTrailing: widget.viewTrailing, - viewHintText: widget.viewHintText, - viewBackgroundColor: widget.viewBackgroundColor, - viewElevation: widget.viewElevation, - viewSurfaceTintColor: widget.viewSurfaceTintColor, - viewSide: widget.viewSide, - viewShape: widget.viewShape, - viewHeaderTextStyle: widget.headerTextStyle, - viewHeaderHintStyle: widget.headerHintStyle, - dividerColor: widget.dividerColor, - viewConstraints: widget.viewConstraints, - showFullScreenView: getShowFullScreenView(), - toggleVisibility: toggleVisibility, - textDirection: Directionality.of(context), - viewBuilder: widget.viewBuilder, - anchorKey: _anchorKey, - searchController: _searchController, - suggestionsBuilder: widget.suggestionsBuilder, - )); - } - - void _closeView(String? selectedText) { - if (selectedText != null) { - _searchController.text = selectedText; - } - Navigator.of(context).pop(); - } - - Rect? getRect(GlobalKey key) { - final BuildContext? context = key.currentContext; - if (context != null) { - final RenderBox searchBarBox = context.findRenderObject()! as RenderBox; - final Size boxSize = searchBarBox.size; - final Offset boxLocation = searchBarBox.localToGlobal(Offset.zero); - return boxLocation & boxSize; - } - return null; - } - - bool toggleVisibility() { - setState(() { - _anchorIsVisible = !_anchorIsVisible; - }); - return _anchorIsVisible; - } - - bool getShowFullScreenView() { - if (widget.isFullScreen != null) { - return widget.isFullScreen!; - } - - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: - return true; - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - return false; - } - } - - @override - Widget build(BuildContext context) { - return AnimatedOpacity( - key: _anchorKey, - opacity: _anchorIsVisible ? 1.0 : 0.0, - duration: _kAnchorFadeDuration, - child: GestureDetector( - onTap: _openView, - child: widget.builder(context, _searchController), - ), - ); - } -} - -class _SearchViewRoute extends PopupRoute<_SearchViewRoute> { - _SearchViewRoute({ - this.toggleVisibility, - this.textDirection, - this.viewBuilder, - this.viewLeading, - this.viewTrailing, - this.viewHintText, - this.viewBackgroundColor, - this.viewElevation, - this.viewSurfaceTintColor, - this.viewSide, - this.viewShape, - this.viewHeaderTextStyle, - this.viewHeaderHintStyle, - this.dividerColor, - this.viewConstraints, - required this.showFullScreenView, - required this.anchorKey, - required this.searchController, - required this.suggestionsBuilder, - }); - - final ValueGetter? toggleVisibility; - final TextDirection? textDirection; - final ViewBuilder? viewBuilder; - final Widget? viewLeading; - final Iterable? viewTrailing; - final String? viewHintText; - final Color? viewBackgroundColor; - final double? viewElevation; - final Color? viewSurfaceTintColor; - final BorderSide? viewSide; - final OutlinedBorder? viewShape; - final TextStyle? viewHeaderTextStyle; - final TextStyle? viewHeaderHintStyle; - final Color? dividerColor; - final BoxConstraints? viewConstraints; - final bool showFullScreenView; - final GlobalKey anchorKey; - final SearchController searchController; - final SuggestionsBuilder suggestionsBuilder; - - @override - Color? get barrierColor => Colors.transparent; - - @override - bool get barrierDismissible => true; - - @override - String? get barrierLabel => 'Dismiss'; - - late final SearchViewThemeData viewDefaults; - late final SearchViewThemeData viewTheme; - late final DividerThemeData dividerTheme; - final RectTween _rectTween = RectTween(); - - Rect? getRect() { - final BuildContext? context = anchorKey.currentContext; - if (context != null) { - final RenderBox searchBarBox = context.findRenderObject()! as RenderBox; - final Size boxSize = searchBarBox.size; - final Offset boxLocation = searchBarBox.localToGlobal(Offset.zero); - return boxLocation & boxSize; - } - return null; - } - - @override - TickerFuture didPush() { - assert(anchorKey.currentContext != null); - updateViewConfig(anchorKey.currentContext!); - updateTweens(anchorKey.currentContext!); - toggleVisibility?.call(); - return super.didPush(); - } - - @override - bool didPop(_SearchViewRoute? result) { - assert(anchorKey.currentContext != null); - updateTweens(anchorKey.currentContext!); - toggleVisibility?.call(); - return super.didPop(result); - } - - void updateViewConfig(BuildContext context) { - viewDefaults = - _SearchViewDefaultsM3(context, isFullScreen: showFullScreenView); - viewTheme = SearchViewTheme.of(context); - dividerTheme = DividerTheme.of(context); - } - - void updateTweens(BuildContext context) { - final Size screenSize = MediaQuery.of(context).size; - final Rect anchorRect = getRect() ?? Rect.zero; - - // Check if the search view goes off the screen. - final BoxConstraints effectiveConstraints = - viewConstraints ?? viewTheme.constraints ?? viewDefaults.constraints!; - final double verticalDistanceToEdge = screenSize.height - anchorRect.top; - final double endHeight = math.max(effectiveConstraints.minHeight, - math.min(screenSize.height * 2 / 3, verticalDistanceToEdge)); - _rectTween.begin = anchorRect; - - switch (textDirection ?? TextDirection.ltr) { - case TextDirection.ltr: - final double viewEdgeToScreenEdge = screenSize.width - anchorRect.left; - final double endWidth = math.max(effectiveConstraints.minWidth, - math.min(anchorRect.width, viewEdgeToScreenEdge)); - final Size endSize = Size(endWidth, endHeight); - _rectTween.end = showFullScreenView - ? Offset.zero & screenSize - : (anchorRect.topLeft & endSize); - return; - case TextDirection.rtl: - final double viewEdgeToScreenEdge = anchorRect.right; - final double endWidth = math.max(effectiveConstraints.minWidth, - math.min(anchorRect.width, viewEdgeToScreenEdge)); - final Offset topLeft = - Offset(math.max(anchorRect.right - endWidth, 0.0), anchorRect.top); - final Size endSize = Size(endWidth, endHeight); - _rectTween.end = - showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize); - } - } - - @override - Widget buildPage(BuildContext context, Animation animation, - Animation secondaryAnimation) { - return Directionality( - textDirection: textDirection ?? TextDirection.ltr, - child: AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { - final Animation curvedAnimation = CurvedAnimation( - parent: animation, - curve: Curves.easeInOutCubicEmphasized, - reverseCurve: Curves.easeInOutCubicEmphasized.flipped, - ); - - final Rect viewRect = _rectTween.evaluate(curvedAnimation)!; - final double topPadding = showFullScreenView - ? lerpDouble(0.0, MediaQuery.of(context).padding.top, - curvedAnimation.value)! - : 0.0; - - return FadeTransition( - opacity: CurvedAnimation( - parent: animation, - curve: _kViewFadeOnInterval, - reverseCurve: _kViewFadeOnInterval.flipped, - ), - child: _ViewContent( - viewLeading: viewLeading, - viewTrailing: viewTrailing, - viewHintText: viewHintText, - viewBackgroundColor: viewBackgroundColor, - viewElevation: viewElevation, - viewSurfaceTintColor: viewSurfaceTintColor, - viewSide: viewSide, - viewShape: viewShape, - viewHeaderTextStyle: viewHeaderTextStyle, - viewHeaderHintStyle: viewHeaderHintStyle, - dividerColor: dividerColor, - viewConstraints: viewConstraints, - showFullScreenView: showFullScreenView, - animation: curvedAnimation, - getRect: getRect, - topPadding: topPadding, - viewRect: viewRect, - viewDefaults: viewDefaults, - viewTheme: viewTheme, - dividerTheme: dividerTheme, - viewBuilder: viewBuilder, - searchController: searchController, - suggestionsBuilder: suggestionsBuilder, - ), - ); - }), - ); - } - - @override - Duration get transitionDuration => _kOpenViewDuration; -} - -class _ViewContent extends StatefulWidget { - const _ViewContent({ - this.viewBuilder, - this.viewLeading, - this.viewTrailing, - this.viewHintText, - this.viewBackgroundColor, - this.viewElevation, - this.viewSurfaceTintColor, - this.viewSide, - this.viewShape, - this.viewHeaderTextStyle, - this.viewHeaderHintStyle, - this.dividerColor, - this.viewConstraints, - required this.showFullScreenView, - required this.getRect, - required this.topPadding, - required this.animation, - required this.viewRect, - required this.viewDefaults, - required this.viewTheme, - required this.dividerTheme, - required this.searchController, - required this.suggestionsBuilder, - }); - - final ViewBuilder? viewBuilder; - final Widget? viewLeading; - final Iterable? viewTrailing; - final String? viewHintText; - final Color? viewBackgroundColor; - final double? viewElevation; - final Color? viewSurfaceTintColor; - final BorderSide? viewSide; - final OutlinedBorder? viewShape; - final TextStyle? viewHeaderTextStyle; - final TextStyle? viewHeaderHintStyle; - final Color? dividerColor; - final BoxConstraints? viewConstraints; - final bool showFullScreenView; - final ValueGetter getRect; - final double topPadding; - final Animation animation; - final Rect viewRect; - final SearchViewThemeData viewDefaults; - final SearchViewThemeData viewTheme; - final DividerThemeData dividerTheme; - final SearchController searchController; - final SuggestionsBuilder suggestionsBuilder; - - @override - State<_ViewContent> createState() => _ViewContentState(); -} - -class _ViewContentState extends State<_ViewContent> { - Size? _screenSize; - late Rect _viewRect; - late final SearchController _controller; - late Iterable result; - final FocusNode _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - _viewRect = widget.viewRect; - _controller = widget.searchController; - result = widget.suggestionsBuilder(context, _controller); - if (!_focusNode.hasFocus) { - _focusNode.requestFocus(); - } - } - - @override - void didUpdateWidget(covariant _ViewContent oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.viewRect != oldWidget.viewRect) { - setState(() { - _viewRect = widget.viewRect; - }); - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final Size updatedScreenSize = MediaQuery.of(context).size; - if (_screenSize != updatedScreenSize) { - _screenSize = updatedScreenSize; - setState(() { - final Rect anchorRect = widget.getRect() ?? _viewRect; - final BoxConstraints constraints = widget.viewConstraints ?? - widget.viewTheme.constraints ?? - widget.viewDefaults.constraints!; - final Size updatedViewSize = Size( - math.max(constraints.minWidth, anchorRect.width), _viewRect.height); - switch (Directionality.of(context)) { - case TextDirection.ltr: - final Offset updatedPosition = anchorRect.topLeft; - _viewRect = updatedPosition & updatedViewSize; - return; - case TextDirection.rtl: - final Offset topLeft = Offset( - math.max(anchorRect.right - updatedViewSize.width, 0.0), - anchorRect.top); - _viewRect = topLeft & updatedViewSize; - } - }); - } - } - - Widget viewBuilder(Iterable suggestions) { - if (widget.viewBuilder == null) { - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ListView(children: suggestions.toList()), - ); - } - return widget.viewBuilder!(suggestions); - } - - void updateSuggestions() { - setState(() { - result = widget.suggestionsBuilder(context, _controller); - }); - } - - @override - Widget build(BuildContext context) { - final Widget defaultLeading = IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.of(context).pop(); - }, - style: const ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), - ); - - final List defaultTrailing = [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _controller.clear(); - updateSuggestions(); - }, - ), - ]; - - final Color effectiveBackgroundColor = widget.viewBackgroundColor ?? - widget.viewTheme.backgroundColor ?? - widget.viewDefaults.backgroundColor!; - final Color effectiveSurfaceTint = widget.viewSurfaceTintColor ?? - widget.viewTheme.surfaceTintColor ?? - widget.viewDefaults.surfaceTintColor!; - final double effectiveElevation = widget.viewElevation ?? - widget.viewTheme.elevation ?? - widget.viewDefaults.elevation!; - final BorderSide? effectiveSide = - widget.viewSide ?? widget.viewTheme.side ?? widget.viewDefaults.side; - OutlinedBorder effectiveShape = widget.viewShape ?? - widget.viewTheme.shape ?? - widget.viewDefaults.shape!; - if (effectiveSide != null) { - effectiveShape = effectiveShape.copyWith(side: effectiveSide); - } - final Color effectiveDividerColor = widget.dividerColor ?? - widget.viewTheme.dividerColor ?? - widget.dividerTheme.color ?? - widget.viewDefaults.dividerColor!; - final TextStyle? effectiveTextStyle = widget.viewHeaderTextStyle ?? - widget.viewTheme.headerTextStyle ?? - widget.viewDefaults.headerTextStyle; - final TextStyle? effectiveHintStyle = widget.viewHeaderHintStyle ?? - widget.viewTheme.headerHintStyle ?? - widget.viewHeaderTextStyle ?? - widget.viewTheme.headerTextStyle ?? - widget.viewDefaults.headerHintStyle; - - final Widget viewDivider = DividerTheme( - data: widget.dividerTheme.copyWith(color: effectiveDividerColor), - child: const Divider(height: 1), - ); - - return Align( - alignment: Alignment.topLeft, - child: Transform.translate( - offset: _viewRect.topLeft, - child: SizedBox( - width: _viewRect.width, - height: _viewRect.height, - child: Material( - shape: effectiveShape, - color: effectiveBackgroundColor, - surfaceTintColor: effectiveSurfaceTint, - elevation: effectiveElevation, - child: FadeTransition( - opacity: CurvedAnimation( - parent: widget.animation, - curve: _kViewIconsFadeOnInterval, - reverseCurve: _kViewIconsFadeOnInterval.flipped, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.only(top: widget.topPadding), - child: SafeArea( - top: false, - bottom: false, - child: SearchBar( - constraints: widget.showFullScreenView - ? BoxConstraints( - minHeight: - _SearchViewDefaultsM3.fullScreenBarHeight) - : null, - focusNode: _focusNode, - leading: widget.viewLeading ?? defaultLeading, - trailing: widget.viewTrailing ?? defaultTrailing, - hintText: widget.viewHintText, - backgroundColor: const MaterialStatePropertyAll( - Colors.transparent), - overlayColor: const MaterialStatePropertyAll( - Colors.transparent), - elevation: const MaterialStatePropertyAll(0.0), - textStyle: MaterialStatePropertyAll( - effectiveTextStyle), - hintStyle: MaterialStatePropertyAll( - effectiveHintStyle), - controller: _controller, - onChanged: (_) { - updateSuggestions(); - }, - ), - ), - ), - FadeTransition( - opacity: CurvedAnimation( - parent: widget.animation, - curve: _kViewDividerFadeOnInterval, - reverseCurve: _kViewFadeOnInterval.flipped, - ), - child: viewDivider), - Expanded( - child: FadeTransition( - opacity: CurvedAnimation( - parent: widget.animation, - curve: _kViewListFadeOnInterval, - reverseCurve: _kViewListFadeOnInterval.flipped, - ), - child: viewBuilder(result), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _SearchAnchorWithSearchBar extends SearchAnchor { - _SearchAnchorWithSearchBar( - {Widget? barLeading, - Iterable? barTrailing, - String? barHintText, - GestureTapCallback? onTap, - MaterialStateProperty? barElevation, - MaterialStateProperty? barBackgroundColor, - MaterialStateProperty? barOverlayColor, - MaterialStateProperty? barSide, - MaterialStateProperty? barShape, - MaterialStateProperty? barPadding, - MaterialStateProperty? barTextStyle, - MaterialStateProperty? barHintStyle, - super.viewLeading, - super.viewTrailing, - String? viewHintText, - super.viewBackgroundColor, - super.viewElevation, - super.viewSide, - super.viewShape, - TextStyle? viewHeaderTextStyle, - TextStyle? viewHeaderHintStyle, - super.dividerColor, - BoxConstraints? constraints, - super.isFullScreen, - super.searchController, - required super.suggestionsBuilder}) - : super( - viewHintText: viewHintText ?? barHintText, - headerTextStyle: viewHeaderTextStyle, - headerHintStyle: viewHeaderHintStyle, - builder: (BuildContext context, SearchController controller) { - return SearchBar( - constraints: constraints, - controller: controller, - onTap: () { - controller.openView(); - onTap?.call(); - }, - onChanged: (_) { - controller.openView(); - }, - hintText: barHintText, - hintStyle: barHintStyle, - textStyle: barTextStyle, - elevation: barElevation, - backgroundColor: barBackgroundColor, - overlayColor: barOverlayColor, - side: barSide, - shape: barShape, - padding: barPadding ?? - const MaterialStatePropertyAll( - EdgeInsets.symmetric(horizontal: 16.0)), - leading: barLeading ?? const Icon(Icons.search), - trailing: barTrailing, - ); - }); -} - -/// A controller to manage a search view created by [SearchAnchor]. -/// -/// A [SearchController] is used to control a menu after it has been created, -/// with methods such as [openView] and [closeView]. It can also control the text in the -/// input field. -/// -/// See also: -/// -/// * [SearchAnchor], a widget that defines a region that opens a search view. -/// * [TextEditingController], A controller for an editable text field. -class SearchController extends TextEditingController { - // The anchor that this controller controls. - // - // This is set automatically when a [SearchController] is given to the anchor - // it controls. - _SearchAnchorState? _anchor; - - /// Whether or not the associated search view is currently open. - bool get isOpen { - assert(_anchor != null); - return _anchor!._viewIsOpen; - } - - /// Opens the search view that this controller is associated with. - void openView() { - assert(_anchor != null); - _anchor!._openView(); - } - - /// Close the search view that this search controller is associated with. - /// - /// If `selectedText` is given, then the text value of the controller is set to - /// `selectedText`. - void closeView(String? selectedText) { - assert(_anchor != null); - _anchor!._closeView(selectedText); - } - - // ignore: use_setters_to_change_properties - void _attach(_SearchAnchorState anchor) { - _anchor = anchor; - } - - void _detach(_SearchAnchorState anchor) { - if (_anchor == anchor) { - _anchor = null; - } - } -} - -/// A Material Design search bar. -/// -/// Search bars include a [leading] Search icon, a text input field and optional -/// [trailing] icons. A search bar is typically used to open a search view. -/// It is the default trigger for a search view. -/// -/// For [TextDirection.ltr], the [leading] widget is on the left side of the bar. -/// It should contain either a navigational action (such as a menu or up-arrow) -/// or a non-functional search icon. -/// -/// The [trailing] is an optional list that appears at the other end of -/// the search bar. Typically only one or two action icons are included. -/// These actions can represent additional modes of searching (like voice search), -/// a separate high-level action (such as current location) or an overflow menu. -class SearchBar extends StatefulWidget { - /// Creates a Material Design search bar. - const SearchBar({ - super.key, - this.controller, - this.focusNode, - this.hintText, - this.leading, - this.trailing, - this.onTap, - this.onChanged, - this.constraints, - this.elevation, - this.backgroundColor, - this.shadowColor, - this.surfaceTintColor, - this.overlayColor, - this.side, - this.shape, - this.padding, - this.textStyle, - this.hintStyle, - }); - - /// Controls the text being edited in the search bar's text field. - /// - /// If null, this widget will create its own [TextEditingController]. - final TextEditingController? controller; - - /// {@macro flutter.widgets.Focus.focusNode} - final FocusNode? focusNode; - - /// Text that suggests what sort of input the field accepts. - /// - /// Displayed at the same location on the screen where text may be entered - /// when the input is empty. - /// - /// Defaults to null. - final String? hintText; - - /// A widget to display before the text input field. - /// - /// Typically the [leading] widget is an [Icon] or an [IconButton]. - final Widget? leading; - - /// A list of Widgets to display in a row after the text field. - /// - /// Typically these actions can represent additional modes of searching - /// (like voice search), an avatar, a separate high-level action (such as - /// current location) or an overflow menu. There should not be more than - /// two trailing actions. - final Iterable? trailing; - - /// Called when the user taps this search bar. - final GestureTapCallback? onTap; - - /// Invoked upon user input. - final ValueChanged? onChanged; - - /// Optional size constraints for the search bar. - /// - /// If null, the value of [SearchBarThemeData.constraints] will be used. If - /// this is also null, then the constraints defaults to: - /// ```dart - /// const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0) - /// ``` - final BoxConstraints? constraints; - - /// The elevation of the search bar's [Material]. - /// - /// If null, the value of [SearchBarThemeData.elevation] will be used. If this - /// is also null, then default value is 6.0. - final MaterialStateProperty? elevation; - - /// The search bar's background fill color. - /// - /// If null, the value of [SearchBarThemeData.backgroundColor] will be used. - /// If this is also null, then the default value is [ColorScheme.surface]. - final MaterialStateProperty? backgroundColor; - - /// The shadow color of the search bar's [Material]. - /// - /// If null, the value of [SearchBarThemeData.shadowColor] will be used. - /// If this is also null, then the default value is [ColorScheme.shadow]. - final MaterialStateProperty? shadowColor; - - /// The surface tint color of the search bar's [Material]. - /// - /// See [Material.surfaceTintColor] for more details. - /// - /// If null, the value of [SearchBarThemeData.surfaceTintColor] will be used. - /// If this is also null, then the default value is [ColorScheme.surfaceTint]. - final MaterialStateProperty? surfaceTintColor; - - /// The highlight color that's typically used to indicate that - /// the search bar is focused, hovered, or pressed. - final MaterialStateProperty? overlayColor; - - /// The color and weight of the search bar's outline. - /// - /// This value is combined with [shape] to create a shape decorated - /// with an outline. - /// - /// If null, the value of [SearchBarThemeData.side] will be used. If this is - /// also null, the search bar doesn't have a side by default. - final MaterialStateProperty? side; - - /// The shape of the search bar's underlying [Material]. - /// - /// This shape is combined with [side] to create a shape decorated - /// with an outline. - /// - /// If null, the value of [SearchBarThemeData.shape] will be used. - /// If this is also null, defaults to [StadiumBorder]. - final MaterialStateProperty? shape; - - /// The padding between the search bar's boundary and its contents. - /// - /// If null, the value of [SearchBarThemeData.padding] will be used. - /// If this is also null, then the default value is 16.0 horizontally. - final MaterialStateProperty? padding; - - /// The style to use for the text being edited. - /// - /// If null, defaults to the `bodyLarge` text style from the current [Theme]. - /// The default text color is [ColorScheme.onSurface]. - final MaterialStateProperty? textStyle; - - /// The style to use for the [hintText]. - /// - /// If null, the value of [SearchBarThemeData.hintStyle] will be used. If this - /// is also null, the value of [textStyle] will be used. If this is also null, - /// defaults to the `bodyLarge` text style from the current [Theme]. - /// The default text color is [ColorScheme.onSurfaceVariant]. - final MaterialStateProperty? hintStyle; - - @override - State createState() => _SearchBarState(); -} - -class _SearchBarState extends State { - late final MaterialStatesController _internalStatesController; - late final FocusNode _focusNode; - - @override - void initState() { - super.initState(); - _internalStatesController = MaterialStatesController(); - _internalStatesController.addListener(() { - setState(() {}); - }); - _focusNode = widget.focusNode ?? FocusNode(); - } - - @override - void dispose() { - _internalStatesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final TextDirection textDirection = Directionality.of(context); - final ColorScheme colorScheme = Theme.of(context).colorScheme; - final IconThemeData iconTheme = IconTheme.of(context); - final SearchBarThemeData searchBarTheme = SearchBarTheme.of(context); - final SearchBarThemeData defaults = _SearchBarDefaultsM3(context); - - T? resolve( - MaterialStateProperty? widgetValue, - MaterialStateProperty? themeValue, - MaterialStateProperty? defaultValue, - ) { - final Set states = _internalStatesController.value; - return widgetValue?.resolve(states) ?? - themeValue?.resolve(states) ?? - defaultValue?.resolve(states); - } - - final TextStyle? effectiveTextStyle = resolve( - widget.textStyle, searchBarTheme.textStyle, defaults.textStyle); - final double? effectiveElevation = resolve( - widget.elevation, searchBarTheme.elevation, defaults.elevation); - final Color? effectiveShadowColor = resolve( - widget.shadowColor, searchBarTheme.shadowColor, defaults.shadowColor); - final Color? effectiveBackgroundColor = resolve( - widget.backgroundColor, - searchBarTheme.backgroundColor, - defaults.backgroundColor); - final Color? effectiveSurfaceTintColor = resolve( - widget.surfaceTintColor, - searchBarTheme.surfaceTintColor, - defaults.surfaceTintColor); - final OutlinedBorder? effectiveShape = resolve( - widget.shape, searchBarTheme.shape, defaults.shape); - final BorderSide? effectiveSide = - resolve(widget.side, searchBarTheme.side, defaults.side); - final EdgeInsetsGeometry? effectivePadding = resolve( - widget.padding, searchBarTheme.padding, defaults.padding); - final MaterialStateProperty? effectiveOverlayColor = - widget.overlayColor ?? - searchBarTheme.overlayColor ?? - defaults.overlayColor; - - final Set states = _internalStatesController.value; - final TextStyle? effectiveHintStyle = widget.hintStyle?.resolve(states) ?? - searchBarTheme.hintStyle?.resolve(states) ?? - widget.textStyle?.resolve(states) ?? - searchBarTheme.textStyle?.resolve(states) ?? - defaults.hintStyle?.resolve(states); - - final bool isDark = Theme.of(context).brightness == Brightness.dark; - bool isIconThemeColorDefault(Color? color) { - if (isDark) { - return color == kDefaultIconLightColor; - } - return color == kDefaultIconDarkColor; - } - - Widget? leading; - if (widget.leading != null) { - leading = IconTheme.merge( - data: isIconThemeColorDefault(iconTheme.color) - ? IconThemeData(color: colorScheme.onSurface) - : iconTheme, - child: widget.leading!, - ); - } - - List? trailing; - if (widget.trailing != null) { - trailing = widget.trailing - ?.map((Widget trailing) => IconTheme.merge( - data: isIconThemeColorDefault(iconTheme.color) - ? IconThemeData(color: colorScheme.onSurfaceVariant) - : iconTheme, - child: trailing, - )) - .toList(); - } - - return ConstrainedBox( - constraints: widget.constraints ?? - searchBarTheme.constraints ?? - defaults.constraints!, - child: Material( - elevation: effectiveElevation!, - shadowColor: effectiveShadowColor, - color: effectiveBackgroundColor, - surfaceTintColor: effectiveSurfaceTintColor, - shape: effectiveShape?.copyWith(side: effectiveSide), - child: InkWell( - onTap: () { - widget.onTap?.call(); - _focusNode.requestFocus(); - }, - overlayColor: effectiveOverlayColor, - customBorder: effectiveShape?.copyWith(side: effectiveSide), - statesController: _internalStatesController, - child: Padding( - padding: effectivePadding!, - child: Row( - textDirection: textDirection, - children: [ - if (leading != null) leading, - Expanded( - child: IgnorePointer( - child: Padding( - padding: effectivePadding, - child: TextField( - focusNode: _focusNode, - onChanged: widget.onChanged, - controller: widget.controller, - style: effectiveTextStyle, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.hintText, - hintStyle: effectiveHintStyle, - ), - ), - ), - )), - if (trailing != null) ...trailing, - ], - ), - ), - ), - ), - ); - } -} - -// BEGIN GENERATED TOKEN PROPERTIES - SearchBar - -// Do not edit by hand. The code between the "BEGIN GENERATED" and -// "END GENERATED" comments are generated from data in the Material -// Design token database by the script: -// dev/tools/gen_defaults/bin/gen_defaults.dart. - -// Token database version: v0_162 - -class _SearchBarDefaultsM3 extends SearchBarThemeData { - _SearchBarDefaultsM3(this.context); - - final BuildContext context; - late final ColorScheme _colors = Theme.of(context).colorScheme; - late final TextTheme _textTheme = Theme.of(context).textTheme; - - @override - MaterialStateProperty? get backgroundColor => - MaterialStatePropertyAll(_colors.surface); - - @override - MaterialStateProperty? get elevation => - const MaterialStatePropertyAll(6.0); - - @override - MaterialStateProperty? get shadowColor => - MaterialStatePropertyAll(_colors.shadow); - - @override - MaterialStateProperty? get surfaceTintColor => - MaterialStatePropertyAll(_colors.surfaceTint); - - @override - MaterialStateProperty? get overlayColor => - MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.pressed)) { - return _colors.onSurface.withOpacity(0.12); - } - if (states.contains(MaterialState.hovered)) { - return _colors.onSurface.withOpacity(0.08); - } - if (states.contains(MaterialState.focused)) { - return Colors.transparent; - } - return Colors.transparent; - }); - - // No default side - - @override - MaterialStateProperty? get shape => - const MaterialStatePropertyAll(StadiumBorder()); - - @override - MaterialStateProperty? get padding => - const MaterialStatePropertyAll( - EdgeInsets.symmetric(horizontal: 8.0)); - - @override - MaterialStateProperty get textStyle => - MaterialStatePropertyAll( - _textTheme.bodyLarge?.copyWith(color: _colors.onSurface)); - - @override - MaterialStateProperty get hintStyle => - MaterialStatePropertyAll( - _textTheme.bodyLarge?.copyWith(color: _colors.onSurfaceVariant)); - - @override - BoxConstraints get constraints => - const BoxConstraints(minWidth: 360.0, maxWidth: 800.0, minHeight: 56.0); -} - -// END GENERATED TOKEN PROPERTIES - SearchBar - -// BEGIN GENERATED TOKEN PROPERTIES - SearchView - -// Do not edit by hand. The code between the "BEGIN GENERATED" and -// "END GENERATED" comments are generated from data in the Material -// Design token database by the script: -// dev/tools/gen_defaults/bin/gen_defaults.dart. - -// Token database version: v0_162 - -class _SearchViewDefaultsM3 extends SearchViewThemeData { - _SearchViewDefaultsM3(this.context, {required this.isFullScreen}); - - final BuildContext context; - final bool isFullScreen; - late final ColorScheme _colors = Theme.of(context).colorScheme; - late final TextTheme _textTheme = Theme.of(context).textTheme; - - static double fullScreenBarHeight = 72.0; - - @override - Color? get backgroundColor => _colors.surface; - - @override - double? get elevation => 6.0; - - @override - Color? get surfaceTintColor => _colors.surfaceTint; - - // No default side - - @override - OutlinedBorder? get shape => isFullScreen - ? const RoundedRectangleBorder() - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(28.0))); - - @override - TextStyle? get headerTextStyle => - _textTheme.bodyLarge?.copyWith(color: _colors.onSurface); - - @override - TextStyle? get headerHintStyle => - _textTheme.bodyLarge?.copyWith(color: _colors.onSurfaceVariant); - - @override - BoxConstraints get constraints => - const BoxConstraints(minWidth: 360.0, minHeight: 240.0); - - @override - Color? get dividerColor => _colors.outline; -} - -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Examples can assume: -// late BuildContext context; - -/// Defines default property values for descendant [SearchBar] widgets. -/// -/// Descendant widgets obtain the current [SearchBarThemeData] object using -/// `SearchBarTheme.of(context)`. Instances of [SearchBarThemeData] can be customized -/// with [SearchBarThemeData.copyWith]. -/// -/// Typically a [SearchBarThemeData] is specified as part of the overall [Theme] -/// with [ThemeData.searchBarTheme]. -/// -/// All [SearchBarThemeData] properties are `null` by default. When null, the -/// [SearchBar] will use the values from [ThemeData] if they exist, otherwise it -/// will provide its own defaults based on the overall [Theme]'s colorScheme. -/// See the individual [SearchBar] properties for details. -/// -/// See also: -/// -/// * [ThemeData], which describes the overall theme information for the -/// application. -@immutable -class SearchBarThemeData with Diagnosticable { - /// Creates a theme that can be used for [ThemeData.searchBarTheme]. - const SearchBarThemeData({ - this.elevation, - this.backgroundColor, - this.shadowColor, - this.surfaceTintColor, - this.overlayColor, - this.side, - this.shape, - this.padding, - this.textStyle, - this.hintStyle, - this.constraints, - }); - - /// Overrides the default value of the [SearchBar.elevation]. - final MaterialStateProperty? elevation; - - /// Overrides the default value of the [SearchBar.backgroundColor]. - final MaterialStateProperty? backgroundColor; - - /// Overrides the default value of the [SearchBar.shadowColor]. - final MaterialStateProperty? shadowColor; - - /// Overrides the default value of the [SearchBar.surfaceTintColor]. - final MaterialStateProperty? surfaceTintColor; - - /// Overrides the default value of the [SearchBar.overlayColor]. - final MaterialStateProperty? overlayColor; - - /// Overrides the default value of the [SearchBar.side]. - final MaterialStateProperty? side; - - /// Overrides the default value of the [SearchBar.shape]. - final MaterialStateProperty? shape; - - /// Overrides the default value for [SearchBar.padding]. - final MaterialStateProperty? padding; - - /// Overrides the default value for [SearchBar.textStyle]. - final MaterialStateProperty? textStyle; - - /// Overrides the default value for [SearchBar.hintStyle]. - final MaterialStateProperty? hintStyle; - - /// Overrides the value of size constraints for [SearchBar]. - final BoxConstraints? constraints; - - /// Creates a copy of this object but with the given fields replaced with the - /// new values. - SearchBarThemeData copyWith({ - MaterialStateProperty? elevation, - MaterialStateProperty? backgroundColor, - MaterialStateProperty? shadowColor, - MaterialStateProperty? surfaceTintColor, - MaterialStateProperty? overlayColor, - MaterialStateProperty? side, - MaterialStateProperty? shape, - MaterialStateProperty? padding, - MaterialStateProperty? textStyle, - MaterialStateProperty? hintStyle, - BoxConstraints? constraints, - }) { - return SearchBarThemeData( - elevation: elevation ?? this.elevation, - backgroundColor: backgroundColor ?? this.backgroundColor, - shadowColor: shadowColor ?? this.shadowColor, - surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, - overlayColor: overlayColor ?? this.overlayColor, - side: side ?? this.side, - shape: shape ?? this.shape, - padding: padding ?? this.padding, - textStyle: textStyle ?? this.textStyle, - hintStyle: hintStyle ?? this.hintStyle, - constraints: constraints ?? this.constraints, - ); - } - - /// Linearly interpolate between two [SearchBarThemeData]s. - /// - /// {@macro dart.ui.shadow.lerp} - static SearchBarThemeData? lerp( - SearchBarThemeData? a, SearchBarThemeData? b, double t) { - if (identical(a, b)) { - return a; - } - return SearchBarThemeData( - elevation: MaterialStateProperty.lerp( - a?.elevation, b?.elevation, t, lerpDouble), - backgroundColor: MaterialStateProperty.lerp( - a?.backgroundColor, b?.backgroundColor, t, Color.lerp), - shadowColor: MaterialStateProperty.lerp( - a?.shadowColor, b?.shadowColor, t, Color.lerp), - surfaceTintColor: MaterialStateProperty.lerp( - a?.surfaceTintColor, b?.surfaceTintColor, t, Color.lerp), - overlayColor: MaterialStateProperty.lerp( - a?.overlayColor, b?.overlayColor, t, Color.lerp), - side: _lerpSides(a?.side, b?.side, t), - shape: MaterialStateProperty.lerp( - a?.shape, b?.shape, t, OutlinedBorder.lerp), - padding: MaterialStateProperty.lerp( - a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp), - textStyle: MaterialStateProperty.lerp( - a?.textStyle, b?.textStyle, t, TextStyle.lerp), - hintStyle: MaterialStateProperty.lerp( - a?.hintStyle, b?.hintStyle, t, TextStyle.lerp), - constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), - ); - } - - @override - int get hashCode => Object.hash( - elevation, - backgroundColor, - shadowColor, - surfaceTintColor, - overlayColor, - side, - shape, - padding, - textStyle, - hintStyle, - constraints, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - return other is SearchBarThemeData && - other.elevation == elevation && - other.backgroundColor == backgroundColor && - other.shadowColor == shadowColor && - other.surfaceTintColor == surfaceTintColor && - other.overlayColor == overlayColor && - other.side == side && - other.shape == shape && - other.padding == padding && - other.textStyle == textStyle && - other.hintStyle == hintStyle && - other.constraints == constraints; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty>( - 'elevation', elevation, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'backgroundColor', backgroundColor, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'shadowColor', shadowColor, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'surfaceTintColor', surfaceTintColor, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'overlayColor', overlayColor, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'side', side, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'shape', shape, - defaultValue: null)); - properties.add( - DiagnosticsProperty>( - 'padding', padding, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'textStyle', textStyle, - defaultValue: null)); - properties.add(DiagnosticsProperty>( - 'hintStyle', hintStyle, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'constraints', constraints, - defaultValue: null)); - } - - // Special case because BorderSide.lerp() doesn't support null arguments - static MaterialStateProperty? _lerpSides( - MaterialStateProperty? a, - MaterialStateProperty? b, - double t) { - if (identical(a, b)) { - return a; - } - return _LerpSides(a, b, t); - } -} - -class _LerpSides implements MaterialStateProperty { - const _LerpSides(this.a, this.b, this.t); - - final MaterialStateProperty? a; - final MaterialStateProperty? b; - final double t; - - @override - BorderSide? resolve(Set states) { - final BorderSide? resolvedA = a?.resolve(states); - final BorderSide? resolvedB = b?.resolve(states); - if (identical(resolvedA, resolvedB)) { - return resolvedA; - } - if (resolvedA == null) { - return BorderSide.lerp( - BorderSide(width: 0, color: resolvedB!.color.withAlpha(0)), - resolvedB, - t); - } - if (resolvedB == null) { - return BorderSide.lerp(resolvedA, - BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), t); - } - return BorderSide.lerp(resolvedA, resolvedB, t); - } -} - -/// Applies a search bar theme to descendant [SearchBar] widgets. -/// -/// Descendant widgets obtain the current theme's [SearchBarTheme] object using -/// [SearchBarTheme.of]. When a widget uses [SearchBarTheme.of], it is automatically -/// rebuilt if the theme later changes. -/// -/// A search bar theme can be specified as part of the overall Material theme using -/// [ThemeData.searchBarTheme]. -/// -/// See also: -/// -/// * [SearchBarThemeData], which describes the actual configuration of a search bar -/// theme. -class SearchBarTheme extends InheritedWidget { - /// Constructs a search bar theme that configures all descendant [SearchBar] widgets. - const SearchBarTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The properties used for all descendant [SearchBar] widgets. - final SearchBarThemeData data; - - /// Returns the configuration [data] from the closest [SearchBarTheme] ancestor. - /// If there is no ancestor, it returns [ThemeData.searchBarTheme]. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// SearchBarThemeData theme = SearchBarTheme.of(context); - /// ``` - static SearchBarThemeData of(BuildContext context) { - final SearchBarTheme? searchBarTheme = - context.dependOnInheritedWidgetOfExactType(); - return searchBarTheme?.data ?? const SearchBarThemeData(); - } - - @override - bool updateShouldNotify(SearchBarTheme oldWidget) => data != oldWidget.data; -} - -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Examples can assume: -// late BuildContext context; - -/// Defines the configuration of the search views created by the [SearchAnchor] -/// widget. -/// -/// Descendant widgets obtain the current [SearchViewThemeData] object using -/// `SearchViewTheme.of(context)`. -/// -/// Typically, a [SearchViewThemeData] is specified as part of the overall [Theme] -/// with [ThemeData.searchViewTheme]. Otherwise, [SearchViewTheme] can be used -/// to configure its own widget subtree. -/// -/// All [SearchViewThemeData] properties are `null` by default. If any of these -/// properties are null, the search view will provide its own defaults. -/// -/// See also: -/// -/// * [ThemeData], which describes the overall theme for the application. -/// * [SearchBarThemeData], which describes the theme for the search bar itself in a -/// [SearchBar] widget. -/// * [SearchAnchor], which is used to open a search view route. -@immutable -class SearchViewThemeData with Diagnosticable { - /// Creates a theme that can be used for [ThemeData.searchViewTheme]. - const SearchViewThemeData({ - this.backgroundColor, - this.elevation, - this.surfaceTintColor, - this.constraints, - this.side, - this.shape, - this.headerTextStyle, - this.headerHintStyle, - this.dividerColor, - }); - - /// Overrides the default value of the [SearchAnchor.viewBackgroundColor]. - final Color? backgroundColor; - - /// Overrides the default value of the [SearchAnchor.viewElevation]. - final double? elevation; - - /// Overrides the default value of the [SearchAnchor.viewSurfaceTintColor]. - final Color? surfaceTintColor; - - /// Overrides the default value of the [SearchAnchor.viewSide]. - final BorderSide? side; - - /// Overrides the default value of the [SearchAnchor.viewShape]. - final OutlinedBorder? shape; - - /// Overrides the default value for [SearchAnchor.headerTextStyle]. - final TextStyle? headerTextStyle; - - /// Overrides the default value for [SearchAnchor.headerHintStyle]. - final TextStyle? headerHintStyle; - - /// Overrides the value of size constraints for [SearchAnchor.viewConstraints]. - final BoxConstraints? constraints; - - /// Overrides the value of the divider color for [SearchAnchor.dividerColor]. - final Color? dividerColor; - - /// Creates a copy of this object but with the given fields replaced with the - /// new values. - SearchViewThemeData copyWith({ - Color? backgroundColor, - double? elevation, - Color? surfaceTintColor, - BorderSide? side, - OutlinedBorder? shape, - TextStyle? headerTextStyle, - TextStyle? headerHintStyle, - BoxConstraints? constraints, - Color? dividerColor, - }) { - return SearchViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - elevation: elevation ?? this.elevation, - surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, - side: side ?? this.side, - shape: shape ?? this.shape, - headerTextStyle: headerTextStyle ?? this.headerTextStyle, - headerHintStyle: headerHintStyle ?? this.headerHintStyle, - constraints: constraints ?? this.constraints, - dividerColor: dividerColor ?? this.dividerColor, - ); - } - - /// Linearly interpolate between two [SearchViewThemeData]s. - static SearchViewThemeData? lerp( - SearchViewThemeData? a, SearchViewThemeData? b, double t) { - if (identical(a, b)) { - return a; - } - return SearchViewThemeData( - backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), - elevation: lerpDouble(a?.elevation, b?.elevation, t), - surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), - side: _lerpSides(a?.side, b?.side, t), - shape: OutlinedBorder.lerp(a?.shape, b?.shape, t), - headerTextStyle: - TextStyle.lerp(a?.headerTextStyle, b?.headerTextStyle, t), - headerHintStyle: - TextStyle.lerp(a?.headerTextStyle, b?.headerTextStyle, t), - constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), - dividerColor: Color.lerp(a?.dividerColor, b?.dividerColor, t), - ); - } - - @override - int get hashCode => Object.hash( - backgroundColor, - elevation, - surfaceTintColor, - side, - shape, - headerTextStyle, - headerHintStyle, - constraints, - dividerColor, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - return other is SearchViewThemeData && - other.backgroundColor == backgroundColor && - other.elevation == elevation && - other.surfaceTintColor == surfaceTintColor && - other.side == side && - other.shape == shape && - other.headerTextStyle == headerTextStyle && - other.headerHintStyle == headerHintStyle && - other.constraints == constraints && - other.dividerColor == dividerColor; - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'backgroundColor', backgroundColor, - defaultValue: null)); - properties.add(DiagnosticsProperty('elevation', elevation, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'surfaceTintColor', surfaceTintColor, - defaultValue: null)); - properties.add( - DiagnosticsProperty('side', side, defaultValue: null)); - properties.add(DiagnosticsProperty('shape', shape, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'headerTextStyle', headerTextStyle, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'headerHintStyle', headerHintStyle, - defaultValue: null)); - properties.add(DiagnosticsProperty( - 'constraints', constraints, - defaultValue: null)); - properties.add(DiagnosticsProperty('dividerColor', dividerColor, - defaultValue: null)); - } - - // Special case because BorderSide.lerp() doesn't support null arguments - static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) { - if (a == null || b == null) { - return null; - } - if (identical(a, b)) { - return a; - } - return BorderSide.lerp(a, b, t); - } -} - -/// An inherited widget that defines the configuration in this widget's -/// descendants for search view created by the [SearchAnchor] widget. -/// -/// A search view theme can be specified as part of the overall Material theme using -/// [ThemeData.searchViewTheme]. -/// -/// See also: -/// -/// * [SearchViewThemeData], which describes the actual configuration of a search view -/// theme. -class SearchViewTheme extends InheritedWidget { - /// Creates a const theme that controls the configurations for the search view - /// created by the [SearchAnchor] widget. - const SearchViewTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The properties used for all descendant [SearchAnchor] widgets. - final SearchViewThemeData data; - - /// Returns the configuration [data] from the closest [SearchViewTheme] ancestor. - /// If there is no ancestor, it returns [ThemeData.searchViewTheme]. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// SearchViewThemeData theme = SearchViewTheme.of(context); - /// ``` - static SearchViewThemeData of(BuildContext context) { - final SearchViewTheme? searchViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return searchViewTheme?.data ?? const SearchViewThemeData(); - } - - @override - bool updateShouldNotify(SearchViewTheme oldWidget) => data != oldWidget.data; -} diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 9afa344..74dd3a1 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -56,7 +56,7 @@ class DocumentSearchCubit extends Cubit with DocumentPaging final searchFilter = DocumentFilter( query: TextQuery.extended(query), ); - + await updateFilter(filter: searchFilter); emit( state.copyWith( diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index 2d4f118..b13f0e5 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -8,14 +8,12 @@ import 'package:hive_flutter/adapters.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; -import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; @@ -40,67 +38,140 @@ class _DocumentSearchBarState extends State { _controller.addListener(() { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 500), () { + print("Searching for $query"); context.read().suggest(query); }); }); } - late final DocumentSearchCubit _searchCubit; String get query => _controller.text; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _searchCubit = context.watch(); - } - @override Widget build(BuildContext context) { - return SearchAnchor.bar( - searchController: _controller, - barLeading: IconButton( - icon: const Icon(Icons.menu), - onPressed: Scaffold.of(context).openDrawer, + return Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: const InputDecorationTheme(), ), - barHintText: S.of(context)!.searchDocuments, - barTrailing: [ - IconButton( - icon: GlobalSettingsBuilder( - builder: (context, settings) { - return ValueListenableBuilder( - valueListenable: - Hive.box(HiveBoxes.localUserAccount).listenable(), - builder: (context, box, _) { - final account = box.get(settings.currentLoggedInUser!)!; - return UserAvatar( - userId: settings.currentLoggedInUser!, - account: account, + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.only(top: 4), + child: SearchAnchor( + searchController: _controller, + viewHintText: S.of(context)!.searchDocuments, + builder: (context, controller) { + return SearchBar( + focusNode: FocusNode(), + controller: controller, + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + trailing: [ + IconButton( + icon: GlobalSettingsBuilder( + builder: (context, settings) { + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount).listenable(), + builder: (context, box, _) { + final account = box.get(settings.currentLoggedInUser!)!; + return UserAvatar( + userId: settings.currentLoggedInUser!, + account: account, + ); + }, + ); + }, + ), + onPressed: () { + final apiVersion = context.read(); + showDialog( + context: context, + builder: (context) => Provider.value( + value: apiVersion, + child: const ManageAccountsPage(), + ), + ); + }, + ), + ], + hintText: S.of(context)!.searchDocuments, + onTap: () { + controller.openView().then((value) => FocusScope.of(context).unfocus()); + }, + ); + }, + suggestionsBuilder: (context, controller) { + switch (state.view) { + case SearchView.suggestions: + return _buildSuggestionItems(state); + case SearchView.results: + return _buildResultsList(state); + } + }, + ), + ); + + return SearchAnchor.bar( + barPadding: MaterialStatePropertyAll(EdgeInsets.only(left: 8, right: 0)), + viewLeading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + // FocusManager.instance.primaryFocus?.unfocus(); + _controller.clear(); + _controller.closeView(null); + Future.delayed(const Duration(milliseconds: 100), () { + FocusManager.instance.primaryFocus?.unfocus(); + }); + }, + ), + searchController: _controller, + barLeading: IconButton( + icon: const Icon(Icons.menu), + onPressed: Scaffold.of(context).openDrawer, + ), + barHintText: S.of(context)!.searchDocuments, + barTrailing: [ + IconButton( + icon: GlobalSettingsBuilder( + builder: (context, settings) { + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount).listenable(), + builder: (context, box, _) { + final account = box.get(settings.currentLoggedInUser!)!; + return UserAvatar( + userId: settings.currentLoggedInUser!, + account: account, + ); + }, + ); + }, + ), + onPressed: () { + final apiVersion = context.read(); + showDialog( + context: context, + builder: (context) => Provider.value( + value: apiVersion, + child: const ManageAccountsPage(), + ), ); }, - ); - }, - ), - onPressed: () { - final apiVersion = context.read(); - showDialog( - context: context, - builder: (context) => Provider.value( - value: apiVersion, - child: const ManageAccountsPage(), ), - ); - }, - ), - ], - suggestionsBuilder: (context, controller) { - switch (_searchCubit.state.view) { - case SearchView.suggestions: - return _buildSuggestionItems(_searchCubit.state); - case SearchView.results: - // TODO: Handle this case. - break; - } - }, + ], + suggestionsBuilder: (context, controller) { + switch (state.view) { + case SearchView.suggestions: + return _buildSuggestionItems(state); + case SearchView.results: + return _buildResultsList(state); + } + }, + ); + }, + ), ); } @@ -155,6 +226,31 @@ class _DocumentSearchBarState extends State { ); } + Iterable _buildResultsList(DocumentSearchState state) sync* { + if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) { + yield Center( + child: Text(S.of(context)!.noMatchesFound), + ); + return; + } + yield DefaultAdaptiveDocumentsView( + viewType: state.viewType, + documents: state.documents, + hasInternetConnection: true, + isLabelClickable: false, + isLoading: state.isLoading, + hasLoaded: state.hasLoaded, + enableHeroAnimation: false, + onTap: (document) { + pushDocumentDetailsRoute( + context, + document: document, + isLabelClickable: false, + ); + }, + ); + } + Widget _buildResultsView(DocumentSearchState state) { final header = Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index d39b41d..64e3563 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -3,19 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; -import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; -import 'package:paperless_mobile/core/navigation/push_routes.dart'; -import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart' as s; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart'; -import 'package:paperless_mobile/features/home/view/model/api_version.dart'; -import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; -import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; class SliverSearchBar extends StatelessWidget { final bool floating; @@ -51,42 +42,4 @@ class SliverSearchBar extends StatelessWidget { ), ); } - - s.SearchBar _buildOld(BuildContext context) { - return s.SearchBar( - height: kToolbarHeight, - supportingText: S.of(context)!.searchDocuments, - onTap: () => pushDocumentSearchPage(context), - leadingIcon: IconButton( - icon: const Icon(Icons.menu), - onPressed: Scaffold.of(context).openDrawer, - ), - trailingIcon: IconButton( - icon: GlobalSettingsBuilder( - builder: (context, settings) { - return ValueListenableBuilder( - valueListenable: Hive.box(HiveBoxes.localUserAccount).listenable(), - builder: (context, box, _) { - final account = box.get(settings.currentLoggedInUser!)!; - return UserAvatar( - userId: settings.currentLoggedInUser!, - account: account, - ); - }, - ); - }, - ), - onPressed: () { - final apiVersion = context.read(); - showDialog( - context: context, - builder: (context) => Provider.value( - value: apiVersion, - child: const ManageAccountsPage(), - ), - ); - }, - ), - ); - } } diff --git a/lib/theme.dart b/lib/theme.dart index 46ab95d..d1543ba 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -26,7 +26,7 @@ ThemeData buildTheme({ final classicScheme = ColorScheme.fromSeed( seedColor: _classicThemeColorSeed, brightness: brightness, - ).harmonized(); + ); late ColorScheme colorScheme; switch (preferredColorScheme) { case ColorSchemeOption.classic: