diff --git a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart index 0e242ec8..5819af27 100644 --- a/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart +++ b/ui/flutter/lib/features/tasks/presentation/pages/create_task_window_page.dart @@ -37,6 +37,7 @@ import '../../../../shared/widgets/app_toast.dart'; import '../../../../l10n/l10n.dart'; import '../../../../util/util.dart'; import '../../application/pending_create_task.dart'; +import '../widgets/history_search_field.dart'; import '../widgets/create_task_action_buttons.dart'; import '../widgets/resolve_file_tree.dart'; @@ -1247,20 +1248,9 @@ class _CreateTaskWindowPageState extends ConsumerState { height: screenSize.height < 560 ? screenSize.height * 0.55 : 360, child: Column( children: [ - SizedBox( - height: 38, - child: AppTextField( - key: const ValueKey('create-history-filter'), - controller: filterController, - placeholder: Text( - dialogContext.l10n.searchHistory, - style: TextStyle(color: palette.searchHint, fontSize: 13), - ), - features: [ - InputFeature.leading(Icon(Icons.search_rounded, size: 15, color: palette.textMuted)), - ], - onChanged: (value) => setDialogState(() => filterQuery = value), - ), + HistorySearchField( + controller: filterController, + onChanged: (value) => setDialogState(() => filterQuery = value), ), const SizedBox(height: 10), Expanded( diff --git a/ui/flutter/lib/features/tasks/presentation/widgets/history_search_field.dart b/ui/flutter/lib/features/tasks/presentation/widgets/history_search_field.dart new file mode 100644 index 00000000..b46485f0 --- /dev/null +++ b/ui/flutter/lib/features/tasks/presentation/widgets/history_search_field.dart @@ -0,0 +1,38 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +import '../../../../l10n/l10n.dart'; +import '../../../../shared/theme/app_design_tokens.dart'; +import '../../../../shared/theme/app_palette.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class HistorySearchField extends StatelessWidget { + const HistorySearchField({super.key, required this.controller, required this.onChanged}); + + final TextEditingController controller; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final palette = AppPalette.of(context); + const textStyle = TextStyle(fontSize: 13); + return ConstrainedBox( + // Allow large accessibility text to grow instead of clipping the line. + constraints: const BoxConstraints(minHeight: 38), + child: AppTextField( + key: const ValueKey('create-history-filter'), + controller: controller, + style: textStyle, + padding: AppDesignTokens.compactTextFieldPadding, + placeholder: Text( + context.l10n.searchHistory, + // Match the editable text metrics so the cursor and hint align. + style: textStyle.copyWith(color: palette.searchHint), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + features: [InputFeature.leading(Icon(Icons.search_rounded, size: 15, color: palette.textMuted))], + onChanged: onChanged, + ), + ); + } +} diff --git a/ui/flutter/test/features/tasks/presentation/history_search_field_test.dart b/ui/flutter/test/features/tasks/presentation/history_search_field_test.dart new file mode 100644 index 00000000..0bf81cc1 --- /dev/null +++ b/ui/flutter/test/features/tasks/presentation/history_search_field_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:gopeed/features/tasks/presentation/widgets/history_search_field.dart'; +import 'package:gopeed/l10n/l10n.dart'; +import 'package:gopeed/shared/theme/app_component_themes.dart'; +import 'package:gopeed/shared/theme/app_theme.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart' as shad; + +void main() { + for (final platform in [TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.macOS]) { + for (final scale in [1.0, 1.5, 2.0]) { + testWidgets('history hint and input line align on $platform at scale $scale', (tester) async { + debugDefaultTargetPlatformOverride = platform; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + final controller = TextEditingController(); + addTearDown(controller.dispose); + String? query; + for (final width in [280.0, 360.0, 600.0]) { + await tester.pumpWidget( + shad.ShadcnApp( + theme: AppTheme.light(), + materialTheme: AppTheme.materialLight(), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: AppComponentThemes( + child: Builder( + builder: (context) => Localizations.override( + context: context, + locale: const Locale('zh'), + child: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(scale)), + child: Center( + child: SizedBox( + width: width, + child: HistorySearchField(controller: controller, onChanged: (value) => query = value), + ), + ), + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final editable = tester.state(find.byType(EditableText)).renderEditable; + final hint = tester.renderObject(find.text('搜索历史记录…')); + expect(editable.preferredLineHeight, lessThanOrEqualTo(editable.size.height)); + expect(hint.size.height, closeTo(editable.preferredLineHeight, 0.1)); + expect(hint.localToGlobal(Offset.zero).dy, closeTo(editable.localToGlobal(Offset.zero).dy, 0.1)); + expect(tester.takeException(), isNull); + } + await tester.enterText(find.byType(EditableText), 'example'); + expect(query, 'example'); + debugDefaultTargetPlatformOverride = null; + }); + } + } +}