From a378bc697f4fb77f5a1f9a8d2da49790b53a68b4 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 12 Mar 2026 16:26:14 +0100 Subject: [PATCH] update search box design --- .../search/presentation/screens/search.dart | 197 +++++++++--------- .../presentation/widgets/search_field.dart | 145 +++++++------ 2 files changed, 178 insertions(+), 164 deletions(-) diff --git a/app/lib/features/geckoview/features/search/presentation/screens/search.dart b/app/lib/features/geckoview/features/search/presentation/screens/search.dart index c7718945..3ea7736f 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -232,7 +232,8 @@ class SearchScreen extends HookConsumerWidget { final measuredHeight = getTextFieldHeight(textFieldKey); if (measuredHeight != null) { - preferredHeight.value = measuredHeight; + // Add 2px to account for SearchField container border + preferredHeight.value = measuredHeight + 2; return; } } @@ -458,7 +459,7 @@ class SearchScreen extends HookConsumerWidget { title: isEditMode ? null : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Builder( builder: (context) { final tabTypeSwitcher = Focus( @@ -529,111 +530,105 @@ class SearchScreen extends HookConsumerWidget { ), bottom: PreferredSize( preferredSize: Size.fromHeight(preferredHeight.value), - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: SearchField( - textFieldKey: textFieldKey, - showBangIcon: showBangIcon, - textEditingController: searchTextController, - focusNode: searchFocusNode, - maxLines: isEditMode ? 3 : 1, - autofocus: true, - label: const Text('Search or enter URL'), - unfocusOnTapOutside: false, - onSubmitted: (value) async { - if (value.isNotEmpty) { - final classification = classifyAddressBarInput(value); - Uri? newUrl; - String? searchQuery; - - switch (classification) { - case NavigateInputClassification(:final uri): - newUrl = uri; - case SearchInputClassification(:final query): - searchQuery = query; - case InvalidInputClassification(): - if (context.mounted) { - ui_helper.showErrorMessage( - context, - 'Invalid address', - ); - } - return; - } - - if (newUrl == null && searchQuery != null) { - // Read from both providers - use site if set, otherwise global - final siteBang = isEditMode - ? ref.read( - selectedBangDataProvider( - domain: existingTabState.url.host, - ), - ) - : null; - final globalBang = ref.read( - selectedBangDataProvider(), - ); - final bang = - siteBang ?? - globalBang ?? - await ref.read( - defaultSearchBangDataProvider.future, - ); - - if (bang != null) { - newUrl = bang.getTemplateUrl(searchQuery); - - if (!privateTabMode) { - await ref - .read(bangSearchProvider.notifier) - .triggerBangSearch(bang, searchQuery); - } - } - } - - if (newUrl != null) { - if (isEditMode) { - // Load into existing tab - await ref - .read( - tabSessionProvider(tabId: tabId).notifier, - ) - .loadUrl(url: newUrl); - } else { - // Create new tab - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: newUrl, - tabMode: effectiveTabMode, - parentId: - (selectedTabType.value == TabType.child) - ? ref.read(selectedTabProvider) - : null, - launchedFromIntent: launchedFromIntent, - selectTab: true, - containerSelection: - selectedContainer == null - ? const TabContainerSelection.unassigned() - : TabContainerSelection.specific( - selectedContainer, - ), - ); - } + child: SearchField( + textFieldKey: textFieldKey, + showBangIcon: showBangIcon, + textEditingController: searchTextController, + focusNode: searchFocusNode, + maxLines: isEditMode ? 3 : 1, + autofocus: true, + label: const Text('Search or enter URL'), + unfocusOnTapOutside: false, + onSubmitted: (value) async { + if (value.isNotEmpty) { + final classification = classifyAddressBarInput(value); + Uri? newUrl; + String? searchQuery; + switch (classification) { + case NavigateInputClassification(:final uri): + newUrl = uri; + case SearchInputClassification(:final query): + searchQuery = query; + case InvalidInputClassification(): if (context.mounted) { - ref - .read(bottomSheetControllerProvider.notifier) - .requestDismiss(); + ui_helper.showErrorMessage( + context, + 'Invalid address', + ); + } + return; + } - const BrowserRoute().go(context); + if (newUrl == null && searchQuery != null) { + // Read from both providers - use site if set, otherwise global + final siteBang = isEditMode + ? ref.read( + selectedBangDataProvider( + domain: existingTabState.url.host, + ), + ) + : null; + final globalBang = ref.read( + selectedBangDataProvider(), + ); + final bang = + siteBang ?? + globalBang ?? + await ref.read( + defaultSearchBangDataProvider.future, + ); + + if (bang != null) { + newUrl = bang.getTemplateUrl(searchQuery); + + if (!privateTabMode) { + await ref + .read(bangSearchProvider.notifier) + .triggerBangSearch(bang, searchQuery); } } } - }, - activeBang: activeBang, - showSuggestions: true, - ), + + if (newUrl != null) { + if (isEditMode) { + // Load into existing tab + await ref + .read(tabSessionProvider(tabId: tabId).notifier) + .loadUrl(url: newUrl); + } else { + // Create new tab + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: newUrl, + tabMode: effectiveTabMode, + parentId: + (selectedTabType.value == TabType.child) + ? ref.read(selectedTabProvider) + : null, + launchedFromIntent: launchedFromIntent, + selectTab: true, + containerSelection: selectedContainer == null + ? const TabContainerSelection.unassigned() + : TabContainerSelection.specific( + selectedContainer, + ), + ); + } + + if (context.mounted) { + ref + .read(bottomSheetControllerProvider.notifier) + .requestDismiss(); + + const BrowserRoute().go(context); + } + } + } + }, + activeBang: activeBang, + showSuggestions: true, ), ), ), diff --git a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart index 8688c46b..da2c9fd9 100644 --- a/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart +++ b/app/lib/features/geckoview/features/search/presentation/widgets/search_field.dart @@ -107,72 +107,91 @@ class SearchField extends HookConsumerWidget { ); } - return AutoSuggestTextField( - controller: textEditingController, - suggestion: suggestion.value, - enableIMEPersonalizedLearning: !incognitoEnabled, - focusNode: safeFocusNode, - maxLines: maxLines, - textFieldKey: textFieldKey, - textInputAction: (maxLines == null || maxLines! > 1) - ? TextInputAction.done - : null, - minLines: minLines, - autofocus: autofocus, - decoration: InputDecoration( - border: InputBorder.none, - prefixIcon: (showBangIcon && activeBang != null) - ? Padding( - padding: const EdgeInsetsDirectional.all(12.0), - child: UrlIcon([activeBang!.getDefaultUrl()], iconSize: 24.0), - ) - : null, - label: label, - hint: hint, - floatingLabelBehavior: FloatingLabelBehavior.always, - suffixIcon: hasText - ? IconButton( - onPressed: () { - textEditingController.clear(); - }, - icon: const Icon(Icons.clear), - ) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - QrScannerButton( - onScanResult: (scanResult) { - if (scanResult?.code != null) { - textEditingController.text = scanResult!.code!; - } - }, - ), - SpeechToTextButton( - onTextReceived: (data) { - textEditingController.text = data; - }, - ), - ], - ), + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8.0), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: colorScheme.outlineVariant), ), - onTapOutside: unfocusOnTapOutside - ? (event) { - safeFocusNode.unfocus(); - } - : null, - onSubmitted: onSubmitted, - onTap: () { - if (suggestion.value != null) { - unawaited(HapticFeedback.lightImpact()); + child: AutoSuggestTextField( + controller: textEditingController, + suggestion: suggestion.value, + enableIMEPersonalizedLearning: !incognitoEnabled, + focusNode: safeFocusNode, + maxLines: maxLines, + textFieldKey: textFieldKey, + textInputAction: (maxLines == null || maxLines! > 1) + ? TextInputAction.done + : null, + minLines: minLines, + autofocus: autofocus, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: const EdgeInsetsDirectional.fromSTEB(12, 12, 0, 12), + prefixIcon: (showBangIcon && activeBang != null) + ? Padding( + padding: const EdgeInsetsDirectional.all(12.0), + child: UrlIcon([activeBang!.getDefaultUrl()], iconSize: 24.0), + ) + : null, + label: label, + hint: hint, + floatingLabelBehavior: FloatingLabelBehavior.always, + suffixIconConstraints: const BoxConstraints(minHeight: 48), + suffixIcon: hasText + ? Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: IconButton( + onPressed: () { + textEditingController.clear(); + }, + icon: const Icon(Icons.clear), + ), + ) + : Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + QrScannerButton( + onScanResult: (scanResult) { + if (scanResult?.code != null) { + textEditingController.text = scanResult!.code!; + } + }, + ), + SpeechToTextButton( + onTextReceived: (data) { + textEditingController.text = data; + }, + ), + ], + ), + ), + ), + onTapOutside: unfocusOnTapOutside + ? (event) { + safeFocusNode.unfocus(); + } + : null, + onSubmitted: onSubmitted, + onTap: () { + if (suggestion.value != null) { + unawaited(HapticFeedback.lightImpact()); - textEditingController.text = suggestion.value!; - textEditingController.selection = TextSelection.collapsed( - offset: suggestion.value!.length, - ); + textEditingController.text = suggestion.value!; + textEditingController.selection = TextSelection.collapsed( + offset: suggestion.value!.length, + ); - suggestion.value = null; - } - }, + suggestion.value = null; + } + }, + ), ); } }