diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart index 7e16585e..bc0efd14 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/edit_url_dialog.dart @@ -1,31 +1,67 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/search/domain/providers/engine_suggestions.dart'; +import 'package:lensai/presentation/hooks/listenable_callback.dart'; +import 'package:lensai/presentation/widgets/auto_suggest_text_field.dart'; import 'package:lensai/utils/form_validators.dart'; import 'package:lensai/utils/uri_parser.dart' as uri_parser; -class EditUrlDialog extends HookWidget { +class EditUrlDialog extends HookConsumerWidget { final Uri initialUrl; const EditUrlDialog({required this.initialUrl}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey()); - final urlController = useTextEditingController(text: initialUrl.toString()); + final addressTextController = useTextEditingController( + text: initialUrl.toString(), + ); return AlertDialog( title: const Text('Edit Address'), - content: Form( - key: formKey, - child: TextFormField( - controller: urlController, - maxLines: null, - keyboardType: TextInputType.url, - decoration: const InputDecoration(hintText: 'Enter URL'), - validator: (value) { - return validateUrl(value, requireAuthority: false); - }, - ), + content: HookBuilder( + builder: (context) { + final addressTextFocusNode = useFocusNode(); + + final suggestion = useState(null); + + useListenableCallback(addressTextController, () async { + if (addressTextController.text.isNotEmpty) { + final result = await ref + .read(engineSuggestionsProvider.notifier) + .getAutocompleteSuggestion(addressTextController.text); + + suggestion.value = result; + } + }); + + return Form( + key: formKey, + child: AutoSuggestTextField( + controller: addressTextController, + focusNode: addressTextFocusNode, + suggestion: suggestion.value, + // enableIMEPersonalizedLearning: !incognitoEnabled, + maxLines: null, + keyboardType: TextInputType.url, + decoration: const InputDecoration(hintText: 'Enter URL'), + onTap: () { + if (!addressTextFocusNode.hasFocus) { + // Select all text when the field is tapped + addressTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: addressTextController.text.length, + ); + } + }, + validator: (value) { + return validateUrl(value, requireAuthority: false); + }, + ), + ); + }, ), actions: [ TextButton( @@ -36,7 +72,10 @@ class EditUrlDialog extends HookWidget { onPressed: () { if (formKey.currentState?.validate() ?? false) { Navigator.of(context).pop( - uri_parser.tryParseUrl(urlController.text, eagerParsing: true), + uri_parser.tryParseUrl( + addressTextController.text, + eagerParsing: true, + ), ); } }, diff --git a/app/lib/presentation/widgets/auto_suggest_text_field.dart b/app/lib/presentation/widgets/auto_suggest_text_field.dart index 213f22ec..a6f95cd7 100644 --- a/app/lib/presentation/widgets/auto_suggest_text_field.dart +++ b/app/lib/presentation/widgets/auto_suggest_text_field.dart @@ -19,6 +19,7 @@ class AutoSuggestTextField extends HookWidget { final int? maxLength; final ValueChanged? onChanged; final VoidCallback? onEditingComplete; + final FormFieldValidator? validator; final ValueChanged? onSubmitted; final List? inputFormatters; final bool? enabled; @@ -46,6 +47,7 @@ class AutoSuggestTextField extends HookWidget { this.maxLength, this.onChanged, this.onEditingComplete, + this.validator, this.onSubmitted, this.inputFormatters, this.enabled, @@ -62,13 +64,64 @@ class AutoSuggestTextField extends HookWidget { controller.text.isNotEmpty && suggestion!.startsWith(controller.text); + static int _getLineCountUsingBoxes( + String text, + TextStyle style, + double maxWidth, + ) { + final textSpan = TextSpan(text: text, style: style); + + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + + textPainter.layout(maxWidth: maxWidth); + + // Select all text + final selection = TextSelection(baseOffset: 0, extentOffset: text.length); + + // Each box represents one line + final lines = textPainter.getBoxesForSelection(selection); + return lines.length; + } + @override Widget build(BuildContext context) { + final textFieldKey = useMemoized(() => GlobalKey()); + + final showSuggestion = useListenableSelector(controller, () { + if (maxLines != 1) { + final box = textFieldKey.currentContext?.findRenderObject(); + if (box case final RenderBox box) { + final width = box.size.width; + + final lines = _getLineCountUsingBoxes( + controller.text, + style ?? Theme.of(context).textTheme.bodyLarge!, + width, + ); + + final suggestionLines = suggestion.mapNotNull( + (suggestion) => _getLineCountUsingBoxes( + suggestion, + style ?? Theme.of(context).textTheme.bodyLarge!, + width, + ), + ); + + return lines == 1 && suggestionLines == 1; + } + } + + return true; + }); + final baseDecoration = decoration ?? const InputDecoration(); return Stack( children: [ - if (suggestion != null) + if (showSuggestion && suggestion != null) AbsorbPointer( child: TextField( minLines: minLines, @@ -143,7 +196,8 @@ class AutoSuggestTextField extends HookWidget { ), ), ), - TextField( + TextFormField( + key: textFieldKey, controller: controller, focusNode: focusNode, decoration: baseDecoration.copyWith( @@ -163,7 +217,8 @@ class AutoSuggestTextField extends HookWidget { maxLength: maxLength, onChanged: onChanged, onEditingComplete: onEditingComplete, - onSubmitted: onSubmitted.mapNotNull( + validator: validator, + onFieldSubmitted: onSubmitted.mapNotNull( (onSubmitted) => (value) { if (_suggestionHasMatch()) { onSubmitted(suggestion!);