diff --git a/example/lib/main.dart b/example/lib/main.dart index 6ed75f8..ffec06d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -59,11 +59,7 @@ class HomePage extends StatelessWidget { title: Row( mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - 'assets/logo.png', - width: 24, - height: 24, - ), + Image.asset('assets/logo.png', width: 24, height: 24), const SizedBox(width: 10), Column( mainAxisSize: MainAxisSize.min, @@ -71,10 +67,7 @@ class HomePage extends StatelessWidget { children: [ const Text( 'Virtual Keypad', - style: TextStyle( - fontWeight: FontWeight.w800, - fontSize: 18, - ), + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18), ), Text( 'Flutter on-screen keyboard', @@ -92,10 +85,7 @@ class HomePage extends StatelessWidget { body: ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: [ - const _SectionHeader( - title: 'Input Types', - icon: Icons.input_rounded, - ), + const _SectionHeader(title: 'Input Types', icon: Icons.input_rounded), _ExampleCard( icon: Icons.dialpad_rounded, title: 'Numeric Input', @@ -144,10 +134,7 @@ class HomePage extends StatelessWidget { onTap: () => _navigate(context, const MultilineTextExample()), ), const SizedBox(height: 4), - const _SectionHeader( - title: 'Features', - icon: Icons.stars_rounded, - ), + const _SectionHeader(title: 'Features', icon: Icons.stars_rounded), _ExampleCard( icon: Icons.bolt_rounded, title: 'Standalone Mode', @@ -160,8 +147,7 @@ class HomePage extends StatelessWidget { title: 'Auto-Hide Keyboard', subtitle: 'Focus-aware animated transitions', gradient: const [Color(0xFF30cfd0), Color(0xFF330867)], - onTap: () => - _navigate(context, const AutoHideKeyboardExample()), + onTap: () => _navigate(context, const AutoHideKeyboardExample()), ), _ExampleCard( icon: Icons.palette_rounded, @@ -175,8 +161,7 @@ class HomePage extends StatelessWidget { title: 'Language Switching', subtitle: 'Toggle English ↔ Bengali ↔ French', gradient: const [Color(0xFF89f7fe), Color(0xFF66a6ff)], - onTap: () => - _navigate(context, const LanguageSwitchingExample()), + onTap: () => _navigate(context, const LanguageSwitchingExample()), ), ], ), @@ -190,18 +175,15 @@ class HomePage extends StatelessWidget { pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( - opacity: CurvedAnimation( - parent: animation, - curve: Curves.easeOut, - ), + opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), child: SlideTransition( - position: Tween( - begin: const Offset(0.04, 0), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: animation, - curve: Curves.easeOut, - )), + position: + Tween( + begin: const Offset(0.04, 0), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: animation, curve: Curves.easeOut), + ), child: child, ), ); @@ -351,8 +333,9 @@ class _ExampleCard extends StatelessWidget { subtitle, style: TextStyle( fontSize: 12, - color: - colorScheme.onSurface.withValues(alpha: 0.55), + color: colorScheme.onSurface.withValues( + alpha: 0.55, + ), ), ), ], diff --git a/example/lib/screens/auto_hide_keyboard_example.dart b/example/lib/screens/auto_hide_keyboard_example.dart index a9ecf19..9b62c2d 100644 --- a/example/lib/screens/auto_hide_keyboard_example.dart +++ b/example/lib/screens/auto_hide_keyboard_example.dart @@ -74,17 +74,19 @@ class _AutoHideKeyboardExampleState extends State { borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: (_isAnyFocused - ? const Color(0xFF30cfd0) - : colorScheme.shadow) - .withValues(alpha: 0.1), + color: + (_isAnyFocused + ? const Color(0xFF30cfd0) + : colorScheme.shadow) + .withValues(alpha: 0.1), blurRadius: 12, offset: const Offset(0, 3), ), ], border: Border.all( - color: colorScheme.outlineVariant - .withValues(alpha: 0.1), + color: colorScheme.outlineVariant.withValues( + alpha: 0.1, + ), ), ), child: ClipRRect( @@ -99,8 +101,9 @@ class _AutoHideKeyboardExampleState extends State { left: BorderSide( color: _isAnyFocused ? const Color(0xFF30cfd0) - : colorScheme.outlineVariant - .withValues(alpha: 0.4), + : colorScheme.outlineVariant.withValues( + alpha: 0.4, + ), width: 4, ), ), @@ -108,21 +111,22 @@ class _AutoHideKeyboardExampleState extends State { child: Row( children: [ AnimatedContainer( - duration: - const Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: _isAnyFocused ? const Color(0xFF30cfd0) - : colorScheme.outline - .withValues(alpha: 0.35), + : colorScheme.outline.withValues( + alpha: 0.35, + ), boxShadow: _isAnyFocused ? [ BoxShadow( - color: const Color(0xFF30cfd0) - .withValues(alpha: 0.5), + color: const Color( + 0xFF30cfd0, + ).withValues(alpha: 0.5), blurRadius: 8, spreadRadius: 1, ), @@ -163,8 +167,7 @@ class _AutoHideKeyboardExampleState extends State { ), ), AnimatedSwitcher( - duration: - const Duration(milliseconds: 300), + duration: const Duration(milliseconds: 300), child: Icon( _isAnyFocused ? Icons.keyboard_rounded @@ -172,8 +175,9 @@ class _AutoHideKeyboardExampleState extends State { key: ValueKey(_isAnyFocused), color: _isAnyFocused ? const Color(0xFF30cfd0) - : colorScheme.outline - .withValues(alpha: 0.35), + : colorScheme.outline.withValues( + alpha: 0.35, + ), size: 22, ), ), @@ -208,8 +212,7 @@ class _AutoHideKeyboardExampleState extends State { width: 2, ), ), - prefixIcon: - const Icon(Icons.text_fields_rounded), + prefixIcon: const Icon(Icons.text_fields_rounded), filled: true, fillColor: colorScheme.surfaceContainerLowest, contentPadding: const EdgeInsets.symmetric( @@ -264,8 +267,7 @@ class _AutoHideKeyboardExampleState extends State { borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), - prefixIcon: - const Icon(Icons.lock_outline_rounded), + prefixIcon: const Icon(Icons.lock_outline_rounded), filled: true, fillColor: colorScheme.surfaceContainerHighest .withValues(alpha: 0.5), @@ -308,8 +310,11 @@ class _SectionLabel extends StatelessWidget { children: [ Row( children: [ - Icon(icon, size: 18, - color: colorScheme.onSurface.withValues(alpha: 0.7)), + Icon( + icon, + size: 18, + color: colorScheme.onSurface.withValues(alpha: 0.7), + ), const SizedBox(width: 8), Text( label.toUpperCase(), diff --git a/example/lib/screens/custom_theme_example.dart b/example/lib/screens/custom_theme_example.dart index 614722a..2531bd1 100644 --- a/example/lib/screens/custom_theme_example.dart +++ b/example/lib/screens/custom_theme_example.dart @@ -127,32 +127,32 @@ class _CustomThemeExampleState extends State { onTap: () => setState(() => _selectedTheme = index), child: AnimatedContainer( - duration: - const Duration(milliseconds: 250), + duration: const Duration(milliseconds: 250), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), decoration: BoxDecoration( color: isSelected - ? theme.accent - .withValues(alpha: 0.2) - : current.appBarBg - .withValues(alpha: 0.6), + ? theme.accent.withValues(alpha: 0.2) + : current.appBarBg.withValues( + alpha: 0.6, + ), borderRadius: BorderRadius.circular(26), border: Border.all( color: isSelected - ? theme.accent - .withValues(alpha: 0.6) - : current.textColor - .withValues(alpha: 0.08), + ? theme.accent.withValues(alpha: 0.6) + : current.textColor.withValues( + alpha: 0.08, + ), width: isSelected ? 2 : 1, ), boxShadow: isSelected ? [ BoxShadow( - color: theme.accent - .withValues(alpha: 0.25), + color: theme.accent.withValues( + alpha: 0.25, + ), blurRadius: 12, offset: const Offset(0, 3), ), @@ -164,8 +164,7 @@ class _CustomThemeExampleState extends State { children: [ Text( theme.emoji, - style: - const TextStyle(fontSize: 18), + style: const TextStyle(fontSize: 18), ), const SizedBox(width: 8), Text( @@ -177,8 +176,9 @@ class _CustomThemeExampleState extends State { : FontWeight.w400, color: isSelected ? theme.accent - : current.textColor - .withValues(alpha: 0.55), + : current.textColor.withValues( + alpha: 0.55, + ), ), ), ], @@ -239,13 +239,11 @@ class _CustomThemeExampleState extends State { decoration: InputDecoration( labelText: 'Type here', labelStyle: TextStyle( - color: - current.textColor.withValues(alpha: 0.5), + color: current.textColor.withValues(alpha: 0.5), ), hintText: 'Try the themed keyboard', hintStyle: TextStyle( - color: - current.textColor.withValues(alpha: 0.2), + color: current.textColor.withValues(alpha: 0.2), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), @@ -254,8 +252,7 @@ class _CustomThemeExampleState extends State { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide( - color: - current.accent.withValues(alpha: 0.15), + color: current.accent.withValues(alpha: 0.15), ), ), focusedBorder: OutlineInputBorder( @@ -269,8 +266,7 @@ class _CustomThemeExampleState extends State { fillColor: current.theme.keyColor, prefixIcon: Icon( Icons.palette_outlined, - color: - current.textColor.withValues(alpha: 0.35), + color: current.textColor.withValues(alpha: 0.35), ), contentPadding: const EdgeInsets.symmetric( horizontal: 18, diff --git a/example/lib/screens/email_url_example.dart b/example/lib/screens/email_url_example.dart index 4fcdd97..ec80aae 100644 --- a/example/lib/screens/email_url_example.dart +++ b/example/lib/screens/email_url_example.dart @@ -82,8 +82,9 @@ class _EmailUrlExampleState extends State { labelText: 'Email', hintText: 'user@example.com', hintStyle: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.25), + color: colorScheme.onSurface.withValues( + alpha: 0.25, + ), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), @@ -97,10 +98,9 @@ class _EmailUrlExampleState extends State { ), ), prefixIcon: const Icon(Icons.email_outlined), - suffixIcon: - _isValidEmail(_emailController.text) - ? const _AnimatedCheckmark() - : null, + suffixIcon: _isValidEmail(_emailController.text) + ? const _AnimatedCheckmark() + : null, filled: true, fillColor: colorScheme.surfaceContainerLowest, ), @@ -119,21 +119,24 @@ class _EmailUrlExampleState extends State { gradient: LinearGradient( colors: [ Colors.transparent, - colorScheme.outlineVariant - .withValues(alpha: 0.4), + colorScheme.outlineVariant.withValues( + alpha: 0.4, + ), ], ), ), ), ), Padding( - padding: - const EdgeInsets.symmetric(horizontal: 14), + padding: const EdgeInsets.symmetric( + horizontal: 14, + ), child: Icon( Icons.more_horiz_rounded, size: 18, - color: colorScheme.outlineVariant - .withValues(alpha: 0.5), + color: colorScheme.outlineVariant.withValues( + alpha: 0.5, + ), ), ), Expanded( @@ -142,8 +145,9 @@ class _EmailUrlExampleState extends State { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - colorScheme.outlineVariant - .withValues(alpha: 0.4), + colorScheme.outlineVariant.withValues( + alpha: 0.4, + ), Colors.transparent, ], ), @@ -173,8 +177,9 @@ class _EmailUrlExampleState extends State { labelText: 'Website', hintText: 'https://example.com', hintStyle: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.25), + color: colorScheme.onSurface.withValues( + alpha: 0.25, + ), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), @@ -187,8 +192,7 @@ class _EmailUrlExampleState extends State { width: 2, ), ), - prefixIcon: - const Icon(Icons.language_rounded), + prefixIcon: const Icon(Icons.language_rounded), suffixIcon: _isValidUrl(_urlController.text) ? const _AnimatedCheckmark() : null, @@ -200,12 +204,13 @@ class _EmailUrlExampleState extends State { SnackBar( content: Row( children: [ - const Icon(Icons.open_in_browser, - color: Colors.white, size: 18), - const SizedBox(width: 10), - Text( - 'Opening ${_urlController.text}', + const Icon( + Icons.open_in_browser, + color: Colors.white, + size: 18, ), + const SizedBox(width: 10), + Text('Opening ${_urlController.text}'), ], ), behavior: SnackBarBehavior.floating, @@ -319,8 +324,7 @@ class _InputCard extends StatelessWidget { description, style: TextStyle( fontSize: 12, - color: - colorScheme.onSurface.withValues(alpha: 0.45), + color: colorScheme.onSurface.withValues(alpha: 0.45), ), ), ], @@ -390,11 +394,7 @@ class _AnimatedCheckmarkState extends State<_AnimatedCheckmark> ), ], ), - child: const Icon( - Icons.check_rounded, - color: Colors.white, - size: 16, - ), + child: const Icon(Icons.check_rounded, color: Colors.white, size: 16), ), ); } diff --git a/example/lib/screens/language_switching_example.dart b/example/lib/screens/language_switching_example.dart index 38d36e3..fdd2064 100644 --- a/example/lib/screens/language_switching_example.dart +++ b/example/lib/screens/language_switching_example.dart @@ -14,9 +14,27 @@ class _LanguageSwitchingExampleState extends State { String _currentLanguage = 'en'; final _languages = [ - ('en', 'English', '🇺🇸', 'QWERTY', [const Color(0xFF89f7fe), const Color(0xFF66a6ff)]), - ('bn', 'বাংলা', '🇧🇩', 'Bengali', [const Color(0xFF43e97b), const Color(0xFF38f9d7)]), - ('fr', 'Français', '🇫🇷', 'AZERTY', [const Color(0xFFf093fb), const Color(0xFFf5576c)]), + ( + 'en', + 'English', + '🇺🇸', + 'QWERTY', + [const Color(0xFF89f7fe), const Color(0xFF66a6ff)], + ), + ( + 'bn', + 'বাংলা', + '🇧🇩', + 'Bengali', + [const Color(0xFF43e97b), const Color(0xFF38f9d7)], + ), + ( + 'fr', + 'Français', + '🇫🇷', + 'AZERTY', + [const Color(0xFFf093fb), const Color(0xFFf5576c)], + ), ]; @override @@ -99,8 +117,7 @@ class _LanguageSwitchingExampleState extends State { borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( - color: colorScheme.shadow - .withValues(alpha: 0.05), + color: colorScheme.shadow.withValues(alpha: 0.05), blurRadius: 16, offset: const Offset(0, 4), ), @@ -112,15 +129,13 @@ class _LanguageSwitchingExampleState extends State { minLines: 3, onChanged: (_) => setState(() {}), textAlignVertical: TextAlignVertical.top, - style: const TextStyle( - fontSize: 15, - height: 1.6, - ), + style: const TextStyle(fontSize: 15, height: 1.6), decoration: InputDecoration( hintText: hintText, hintStyle: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.2), + color: colorScheme.onSurface.withValues( + alpha: 0.2, + ), fontSize: 15, ), border: OutlineInputBorder( @@ -130,8 +145,9 @@ class _LanguageSwitchingExampleState extends State { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(18), borderSide: BorderSide( - color: colorScheme.outlineVariant - .withValues(alpha: 0.15), + color: colorScheme.outlineVariant.withValues( + alpha: 0.15, + ), ), ), focusedBorder: OutlineInputBorder( @@ -157,8 +173,9 @@ class _LanguageSwitchingExampleState extends State { vertical: 8, ), decoration: BoxDecoration( - color: colorScheme.surfaceContainerHigh - .withValues(alpha: 0.3), + color: colorScheme.surfaceContainerHigh.withValues( + alpha: 0.3, + ), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -166,8 +183,9 @@ class _LanguageSwitchingExampleState extends State { Icon( Icons.text_snippet_outlined, size: 14, - color: colorScheme.onSurface - .withValues(alpha: 0.35), + color: colorScheme.onSurface.withValues( + alpha: 0.35, + ), ), const SizedBox(width: 6), Text( @@ -175,8 +193,9 @@ class _LanguageSwitchingExampleState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: colorScheme.onSurface - .withValues(alpha: 0.45), + color: colorScheme.onSurface.withValues( + alpha: 0.45, + ), ), ), const Spacer(), @@ -200,8 +219,9 @@ class _LanguageSwitchingExampleState extends State { Icon( Icons.clear_rounded, size: 14, - color: colorScheme.error - .withValues(alpha: 0.6), + color: colorScheme.error.withValues( + alpha: 0.6, + ), ), const SizedBox(width: 4), Text( @@ -209,8 +229,9 @@ class _LanguageSwitchingExampleState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: colorScheme.error - .withValues(alpha: 0.6), + color: colorScheme.error.withValues( + alpha: 0.6, + ), ), ), ], diff --git a/example/lib/screens/multi_field_example.dart b/example/lib/screens/multi_field_example.dart index acfcb1f..4d81e4a 100644 --- a/example/lib/screens/multi_field_example.dart +++ b/example/lib/screens/multi_field_example.dart @@ -34,10 +34,10 @@ class _MultiFieldExampleState extends State { } List get _stepsDone => [ - _nameController.text.isNotEmpty, - _emailController.text.isNotEmpty, - _passwordController.text.isNotEmpty, - ]; + _nameController.text.isNotEmpty, + _emailController.text.isNotEmpty, + _passwordController.text.isNotEmpty, + ]; @override Widget build(BuildContext context) { @@ -86,21 +86,15 @@ class _MultiFieldExampleState extends State { // ── Header ── Text( 'Create Account', - style: Theme.of(context) - .textTheme - .headlineSmall + style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.w800), ), const SizedBox(height: 4), Text( 'Fill in your details to get started', - style: Theme.of(context) - .textTheme - .bodyMedium + style: Theme.of(context).textTheme.bodyMedium ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface + color: Theme.of(context).colorScheme.onSurface .withValues(alpha: 0.55), ), ), @@ -159,11 +153,13 @@ class _MultiFieldExampleState extends State { SnackBar( content: Row( children: [ - const Icon(Icons.check_circle, - color: Colors.white, size: 18), + const Icon( + Icons.check_circle, + color: Colors.white, + size: 18, + ), const SizedBox(width: 10), - Text( - 'Welcome, ${_nameController.text}!'), + Text('Welcome, ${_nameController.text}!'), ], ), behavior: SnackBarBehavior.floating, @@ -223,8 +219,10 @@ class _MultiFieldExampleState extends State { prefixIcon: Icon(icon), filled: true, fillColor: colorScheme.surfaceContainerLowest, - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), ), ); } @@ -238,8 +236,9 @@ class _ProgressDots extends StatelessWidget { @override Widget build(BuildContext context) { - final muted = - Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.4); + final muted = Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.4); return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -256,7 +255,9 @@ class _ProgressDots extends StatelessWidget { borderRadius: BorderRadius.circular(2), color: i < filled - 1 ? _kGreenComplete - : (i < filled ? _kGradientStart.withValues(alpha: 0.5) : muted), + : (i < filled + ? _kGradientStart.withValues(alpha: 0.5) + : muted), ), ), ], @@ -271,8 +272,9 @@ class _Dot extends StatelessWidget { @override Widget build(BuildContext context) { - final muted = - Theme.of(context).colorScheme.outlineVariant.withValues(alpha: 0.4); + final muted = Theme.of( + context, + ).colorScheme.outlineVariant.withValues(alpha: 0.4); return AnimatedContainer( duration: const Duration(milliseconds: 300), @@ -310,10 +312,7 @@ class _StepperColumn extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: steps, - ); + return Column(mainAxisSize: MainAxisSize.min, children: steps); } } @@ -358,8 +357,9 @@ class _StepItem extends StatelessWidget { border: done ? null : Border.all( - color: colorScheme.outlineVariant - .withValues(alpha: 0.6), + color: colorScheme.outlineVariant.withValues( + alpha: 0.6, + ), ), boxShadow: done ? [ @@ -375,18 +375,21 @@ class _StepItem extends StatelessWidget { child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), child: done - ? const Icon(Icons.check, + ? const Icon( + Icons.check, key: ValueKey('check'), size: 14, - color: Colors.white) + color: Colors.white, + ) : Text( stepNumber, key: ValueKey('num$stepNumber'), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: colorScheme.onSurface - .withValues(alpha: 0.5), + color: colorScheme.onSurface.withValues( + alpha: 0.5, + ), ), ), ), @@ -401,8 +404,9 @@ class _StepItem extends StatelessWidget { painter: _DottedLinePainter( color: done ? _kGreenComplete.withValues(alpha: 0.5) - : colorScheme.outlineVariant - .withValues(alpha: 0.35), + : colorScheme.outlineVariant.withValues( + alpha: 0.35, + ), ), child: const SizedBox(width: 2), ), @@ -444,7 +448,11 @@ class _DottedLinePainter extends CustomPainter { var y = 0.0; while (y < size.height) { - canvas.drawLine(Offset(centerX, y), Offset(centerX, y + dashHeight), paint); + canvas.drawLine( + Offset(centerX, y), + Offset(centerX, y + dashHeight), + paint, + ); y += dashHeight + gap; } } diff --git a/example/lib/screens/multiline_text_example.dart b/example/lib/screens/multiline_text_example.dart index 64ad0fc..2298378 100644 --- a/example/lib/screens/multiline_text_example.dart +++ b/example/lib/screens/multiline_text_example.dart @@ -92,13 +92,15 @@ class _MultilineTextExampleState extends State { color: const Color(0xFFFDFCFF), borderRadius: BorderRadius.circular(20), border: Border.all( - color: colorScheme.outlineVariant - .withValues(alpha: 0.25), + color: colorScheme.outlineVariant.withValues( + alpha: 0.25, + ), ), boxShadow: [ BoxShadow( - color: const Color(0xFFa18cd1) - .withValues(alpha: 0.06), + color: const Color( + 0xFFa18cd1, + ).withValues(alpha: 0.06), blurRadius: 24, offset: const Offset(0, 6), ), @@ -120,8 +122,9 @@ class _MultilineTextExampleState extends State { Icon( Icons.edit_note_rounded, size: 48, - color: const Color(0xFFa18cd1) - .withValues(alpha: 0.25), + color: const Color( + 0xFFa18cd1, + ).withValues(alpha: 0.25), ), const SizedBox(height: 8), Text( @@ -129,8 +132,9 @@ class _MultilineTextExampleState extends State { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: colorScheme.onSurface - .withValues(alpha: 0.2), + color: colorScheme.onSurface.withValues( + alpha: 0.2, + ), letterSpacing: 0.2, ), ), @@ -147,8 +151,9 @@ class _MultilineTextExampleState extends State { style: TextStyle( fontSize: 15, height: 1.6, - color: colorScheme.onSurface - .withValues(alpha: 0.85), + color: colorScheme.onSurface.withValues( + alpha: 0.85, + ), ), decoration: InputDecoration( hintText: '', @@ -185,8 +190,9 @@ class _MultilineTextExampleState extends State { icon: Icons.text_fields_rounded, value: '${_controller.text.length}', label: 'chars', - backgroundColor: - const Color(0xFFa18cd1).withValues(alpha: 0.10), + backgroundColor: const Color( + 0xFFa18cd1, + ).withValues(alpha: 0.10), iconColor: const Color(0xFFa18cd1), ), const SizedBox(width: 10), @@ -194,8 +200,9 @@ class _MultilineTextExampleState extends State { icon: Icons.short_text_rounded, value: '${_wordCount(_controller.text)}', label: 'words', - backgroundColor: - const Color(0xFFfbc2eb).withValues(alpha: 0.18), + backgroundColor: const Color( + 0xFFfbc2eb, + ).withValues(alpha: 0.18), iconColor: const Color(0xFFc97db8), ), const SizedBox(width: 10), @@ -203,8 +210,9 @@ class _MultilineTextExampleState extends State { icon: Icons.format_line_spacing_rounded, value: '${_lineCount(_controller.text)}', label: 'lines', - backgroundColor: - const Color(0xFF90CAF9).withValues(alpha: 0.18), + backgroundColor: const Color( + 0xFF90CAF9, + ).withValues(alpha: 0.18), iconColor: const Color(0xFF5C9CE6), ), ], diff --git a/example/lib/screens/numeric_input_example.dart b/example/lib/screens/numeric_input_example.dart index eed9c2a..61bfc5a 100644 --- a/example/lib/screens/numeric_input_example.dart +++ b/example/lib/screens/numeric_input_example.dart @@ -84,18 +84,16 @@ class _NumericInputExampleState extends State { const SizedBox(height: 16), Text( 'Enter Amount', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 4), Text( 'How much would you like to send?', style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: - colorScheme.onSurface.withValues(alpha: 0.5), - ), + color: colorScheme.onSurface.withValues(alpha: 0.5), + ), ), const SizedBox(height: 24), @@ -112,8 +110,9 @@ class _NumericInputExampleState extends State { color: colorScheme.surfaceContainerLowest, boxShadow: [ BoxShadow( - color: const Color(0xFF667eea) - .withValues(alpha: 0.08), + color: const Color( + 0xFF667eea, + ).withValues(alpha: 0.08), blurRadius: 24, offset: const Offset(0, 8), ), @@ -129,16 +128,18 @@ class _NumericInputExampleState extends State { keyboardType: KeyboardType.number, decoration: InputDecoration( prefixIcon: Padding( - padding: - const EdgeInsets.only(left: 20, right: 4), + padding: const EdgeInsets.only( + left: 20, + right: 4, + ), child: ShaderMask( shaderCallback: (bounds) => const LinearGradient( - colors: [ - Color(0xFF667eea), - Color(0xFF764ba2), - ], - ).createShader(bounds), + colors: [ + Color(0xFF667eea), + Color(0xFF764ba2), + ], + ).createShader(bounds), child: const Text( '\$', style: TextStyle( @@ -155,8 +156,9 @@ class _NumericInputExampleState extends State { ), hintText: '0.00', hintStyle: TextStyle( - color: - colorScheme.onSurface.withValues(alpha: 0.15), + color: colorScheme.onSurface.withValues( + alpha: 0.15, + ), fontSize: 36, fontWeight: FontWeight.w800, ), @@ -224,8 +226,9 @@ class _NumericInputExampleState extends State { ), boxShadow: [ BoxShadow( - color: const Color(0xFF764ba2) - .withValues(alpha: 0.35), + color: const Color( + 0xFF764ba2, + ).withValues(alpha: 0.35), blurRadius: 16, offset: const Offset(0, 6), ), @@ -256,10 +259,7 @@ class _NumericInputExampleState extends State { ), ), ), - VirtualKeypad( - type: KeyboardType.number, - height: 260, - ), + VirtualKeypad(type: KeyboardType.number, height: 260), ], ), ), diff --git a/example/lib/screens/password_entry_example.dart b/example/lib/screens/password_entry_example.dart index 53b65e3..ade2dd9 100644 --- a/example/lib/screens/password_entry_example.dart +++ b/example/lib/screens/password_entry_example.dart @@ -116,20 +116,14 @@ class _PasswordEntryExampleState extends State { Text( 'Welcome Back', - style: Theme.of(context) - .textTheme - .headlineSmall + style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 6), Text( 'Sign in to continue', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith( - color: colorScheme.onSurfaceVariant, - ), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 28), @@ -151,8 +145,9 @@ class _PasswordEntryExampleState extends State { width: 2, ), ), - prefixIcon: - const Icon(Icons.person_outline_rounded), + prefixIcon: const Icon( + Icons.person_outline_rounded, + ), filled: true, fillColor: colorScheme.surfaceContainerLowest, ), @@ -178,8 +173,7 @@ class _PasswordEntryExampleState extends State { width: 2, ), ), - prefixIcon: - const Icon(Icons.lock_outline_rounded), + prefixIcon: const Icon(Icons.lock_outline_rounded), suffixIcon: IconButton( icon: Icon( _obscureText @@ -213,7 +207,7 @@ class _PasswordEntryExampleState extends State { color: active ? _strengthColor(strength) : colorScheme.outlineVariant - .withValues(alpha: 0.3), + .withValues(alpha: 0.3), ), ), ); @@ -239,8 +233,9 @@ class _PasswordEntryExampleState extends State { onPressed: () {}, style: TextButton.styleFrom( foregroundColor: const Color(0xFF4facfe), - padding: - const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), ), child: const Text( 'Forgot Password?', diff --git a/example/lib/screens/pin_pad_example.dart b/example/lib/screens/pin_pad_example.dart index 83e78ef..335401a 100644 --- a/example/lib/screens/pin_pad_example.dart +++ b/example/lib/screens/pin_pad_example.dart @@ -182,10 +182,11 @@ class _PinPadExampleState extends State : _gradient, boxShadow: [ BoxShadow( - color: (_showSuccess - ? const Color(0xFF43A047) - : const Color(0xFFf5576c)) - .withValues(alpha: 0.45), + color: + (_showSuccess + ? const Color(0xFF43A047) + : const Color(0xFFf5576c)) + .withValues(alpha: 0.45), blurRadius: 24, spreadRadius: 2, ), @@ -219,9 +220,7 @@ class _PinPadExampleState extends State child: Text( _showSuccess ? 'Unlocked!' : 'Enter PIN', key: ValueKey(_showSuccess), - style: Theme.of(context) - .textTheme - .titleLarge + style: Theme.of(context).textTheme.titleLarge ?.copyWith( fontWeight: FontWeight.w700, color: _showSuccess @@ -241,8 +240,9 @@ class _PinPadExampleState extends State _showSuccess ? 'granted' : 'enter', ), style: TextStyle( - color: - colorScheme.onSurface.withValues(alpha: 0.45), + color: colorScheme.onSurface.withValues( + alpha: 0.45, + ), fontSize: 13, ), ), @@ -269,8 +269,9 @@ class _PinPadExampleState extends State isActive: isActive, isSuccess: _showSuccess, pulseAnimation: _pulseAnimation, - outlineColor: - colorScheme.outline.withValues(alpha: 0.35), + outlineColor: colorScheme.outline.withValues( + alpha: 0.35, + ), ); }), ), @@ -342,10 +343,9 @@ class _PinDot extends StatelessWidget { border: Border.all(color: const Color(0xFFf5576c), width: 2), boxShadow: [ BoxShadow( - color: - const Color(0xFFf093fb).withValues( - alpha: pulseAnimation.value, - ), + color: const Color( + 0xFFf093fb, + ).withValues(alpha: pulseAnimation.value), blurRadius: 14, spreadRadius: 2, ), @@ -369,8 +369,8 @@ class _PinDot extends StatelessWidget { colors: [Color(0xFF43A047), Color(0xFF66BB6A)], ) : isFilled - ? _dotGradient - : null, + ? _dotGradient + : null, border: isFilled || isSuccess ? null : Border.all(color: outlineColor, width: 2), @@ -383,14 +383,14 @@ class _PinDot extends StatelessWidget { ), ] : isSuccess - ? [ - BoxShadow( - color: const Color(0xFF43A047).withValues(alpha: 0.4), - blurRadius: 10, - spreadRadius: 1, - ), - ] - : null, + ? [ + BoxShadow( + color: const Color(0xFF43A047).withValues(alpha: 0.4), + blurRadius: 10, + spreadRadius: 1, + ), + ] + : null, ), ); } diff --git a/example/lib/screens/standalone_example.dart b/example/lib/screens/standalone_example.dart index 8ac3198..74f3e53 100644 --- a/example/lib/screens/standalone_example.dart +++ b/example/lib/screens/standalone_example.dart @@ -67,9 +67,7 @@ class _StandaloneExampleState extends State { accentEnd.withValues(alpha: 0.04), ], ), - border: Border.all( - color: accent.withValues(alpha: 0.15), - ), + border: Border.all(color: accent.withValues(alpha: 0.15)), ), child: Row( children: [ @@ -105,8 +103,9 @@ class _StandaloneExampleState extends State { 'Using standard Flutter TextFields with VirtualKeypad(standalone: true)', style: TextStyle( fontSize: 11, - color: colorScheme.onSurface - .withValues(alpha: 0.55), + color: colorScheme.onSurface.withValues( + alpha: 0.55, + ), ), ), ], @@ -116,7 +115,6 @@ class _StandaloneExampleState extends State { ), ), const SizedBox(height: 24), - _SectionLabel( icon: Icons.person_outline_rounded, label: 'Contact Info', @@ -124,7 +122,6 @@ class _StandaloneExampleState extends State { accent: accent, ), const SizedBox(height: 14), - TextField( controller: _nameController, keyboardType: TextInputType.name, @@ -139,10 +136,7 @@ class _StandaloneExampleState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: accent, - width: 2, - ), + borderSide: const BorderSide(color: accent, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerLowest, @@ -153,7 +147,6 @@ class _StandaloneExampleState extends State { ), ), const SizedBox(height: 14), - TextField( controller: _emailController, keyboardType: TextInputType.emailAddress, @@ -161,18 +154,14 @@ class _StandaloneExampleState extends State { decoration: InputDecoration( labelText: 'Email', hintText: 'you@example.com', - prefixIcon: - const Icon(Icons.alternate_email_rounded), + prefixIcon: const Icon(Icons.alternate_email_rounded), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: accent, - width: 2, - ), + borderSide: const BorderSide(color: accent, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerLowest, @@ -183,7 +172,6 @@ class _StandaloneExampleState extends State { ), ), const SizedBox(height: 14), - TextField( controller: _phoneController, keyboardType: TextInputType.phone, @@ -198,10 +186,7 @@ class _StandaloneExampleState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: accent, - width: 2, - ), + borderSide: const BorderSide(color: accent, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerLowest, @@ -212,7 +197,6 @@ class _StandaloneExampleState extends State { ), ), const SizedBox(height: 24), - _SectionLabel( icon: Icons.note_alt_outlined, label: 'Notes', @@ -220,7 +204,6 @@ class _StandaloneExampleState extends State { accent: accent, ), const SizedBox(height: 14), - TextField( controller: _notesController, keyboardType: TextInputType.multiline, @@ -240,10 +223,7 @@ class _StandaloneExampleState extends State { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: const BorderSide( - color: accent, - width: 2, - ), + borderSide: const BorderSide(color: accent, width: 2), ), filled: true, fillColor: colorScheme.surfaceContainerLowest, @@ -258,10 +238,7 @@ class _StandaloneExampleState extends State { ), ), ), - VirtualKeypad( - standalone: true, - hideWhenUnfocused: true, - ), + VirtualKeypad(standalone: true, hideWhenUnfocused: true), ], ), ); @@ -289,8 +266,11 @@ class _SectionLabel extends StatelessWidget { children: [ Row( children: [ - Icon(icon, size: 18, - color: colorScheme.onSurface.withValues(alpha: 0.7)), + Icon( + icon, + size: 18, + color: colorScheme.onSurface.withValues(alpha: 0.7), + ), const SizedBox(width: 8), Text( label.toUpperCase(), diff --git a/example/pubspec.lock b/example/pubspec.lock index 9ade9f3..bccb95e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -206,7 +206,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0" + version: "0.3.1" vm_service: dependency: transitive description: diff --git a/lib/src/layouts/languages/bengali.dart b/lib/src/layouts/languages/bengali.dart index 5093e08..bfc2329 100644 --- a/lib/src/layouts/languages/bengali.dart +++ b/lib/src/layouts/languages/bengali.dart @@ -53,7 +53,11 @@ final KeyboardLayout _textLayoutPrimary = [ ], [ VirtualKey.action( - action: KeyAction.symbols, label: '১২৩', altLabel: 'কখগ', flex: 1), + action: KeyAction.symbols, + label: '১২৩', + altLabel: 'কখগ', + flex: 1, + ), VirtualKey.character(text: 'স'), VirtualKey.character(text: 'হ'), VirtualKey.character(text: '্'), @@ -92,7 +96,11 @@ final KeyboardLayout _textLayoutSecondary = [ ], [ VirtualKey.action( - action: KeyAction.symbolsAlt, label: '#+=', altLabel: '১২৩', flex: 1), + action: KeyAction.symbolsAlt, + label: '#+=', + altLabel: '১২৩', + flex: 1, + ), VirtualKey.character(text: '-'), VirtualKey.character(text: '/'), VirtualKey.character(text: ':'), @@ -104,7 +112,11 @@ final KeyboardLayout _textLayoutSecondary = [ ], [ VirtualKey.action( - action: KeyAction.symbols, label: '১২৩', altLabel: 'কখগ', flex: 1), + action: KeyAction.symbols, + label: '১২৩', + altLabel: 'কখগ', + flex: 1, + ), VirtualKey.character(text: ','), VirtualKey.character(text: '্'), VirtualKey.action(action: KeyAction.space, flex: 3), @@ -142,7 +154,11 @@ final KeyboardLayout _textLayoutTertiary = [ ], [ VirtualKey.action( - action: KeyAction.symbolsAlt, label: '#+=', altLabel: '১২৩', flex: 1), + action: KeyAction.symbolsAlt, + label: '#+=', + altLabel: '১২৩', + flex: 1, + ), VirtualKey.character(text: '.'), VirtualKey.character(text: ','), VirtualKey.character(text: '?'), @@ -154,7 +170,11 @@ final KeyboardLayout _textLayoutTertiary = [ ], [ VirtualKey.action( - action: KeyAction.symbols, label: '১২৩', altLabel: 'কখগ', flex: 2), + action: KeyAction.symbols, + label: '১২৩', + altLabel: 'কখগ', + flex: 2, + ), VirtualKey.character(text: ','), VirtualKey.action(action: KeyAction.space, flex: 4), VirtualKey.character(text: '।'), diff --git a/lib/src/layouts/languages/french.dart b/lib/src/layouts/languages/french.dart index d0304c7..a6d0131 100644 --- a/lib/src/layouts/languages/french.dart +++ b/lib/src/layouts/languages/french.dart @@ -246,25 +246,76 @@ final KeyboardLayout _urlLayoutTertiary = _textLayoutTertiary; /// Number, signed number, and phone layouts remain the same as in English. final KeyboardLayout _numberLayout = [ - [VirtualKey.character(text: '1'), VirtualKey.character(text: '2'), VirtualKey.character(text: '3')], - [VirtualKey.character(text: '4'), VirtualKey.character(text: '5'), VirtualKey.character(text: '6')], - [VirtualKey.character(text: '7'), VirtualKey.character(text: '8'), VirtualKey.character(text: '9')], - [VirtualKey.character(text: '.'), VirtualKey.character(text: '0'), VirtualKey.action(action: KeyAction.backSpace)], + [ + VirtualKey.character(text: '1'), + VirtualKey.character(text: '2'), + VirtualKey.character(text: '3'), + ], + [ + VirtualKey.character(text: '4'), + VirtualKey.character(text: '5'), + VirtualKey.character(text: '6'), + ], + [ + VirtualKey.character(text: '7'), + VirtualKey.character(text: '8'), + VirtualKey.character(text: '9'), + ], + [ + VirtualKey.character(text: '.'), + VirtualKey.character(text: '0'), + VirtualKey.action(action: KeyAction.backSpace), + ], ]; final KeyboardLayout _signedNumberLayout = [ - [VirtualKey.character(text: '1'), VirtualKey.character(text: '2'), VirtualKey.character(text: '3')], - [VirtualKey.character(text: '4'), VirtualKey.character(text: '5'), VirtualKey.character(text: '6')], - [VirtualKey.character(text: '7'), VirtualKey.character(text: '8'), VirtualKey.character(text: '9')], - [VirtualKey.character(text: '-'), VirtualKey.character(text: '0'), VirtualKey.character(text: '.')], - [VirtualKey.action(action: KeyAction.backSpace, flex: 2), VirtualKey.action(action: KeyAction.done)], + [ + VirtualKey.character(text: '1'), + VirtualKey.character(text: '2'), + VirtualKey.character(text: '3'), + ], + [ + VirtualKey.character(text: '4'), + VirtualKey.character(text: '5'), + VirtualKey.character(text: '6'), + ], + [ + VirtualKey.character(text: '7'), + VirtualKey.character(text: '8'), + VirtualKey.character(text: '9'), + ], + [ + VirtualKey.character(text: '-'), + VirtualKey.character(text: '0'), + VirtualKey.character(text: '.'), + ], + [ + VirtualKey.action(action: KeyAction.backSpace, flex: 2), + VirtualKey.action(action: KeyAction.done), + ], ]; final KeyboardLayout _phoneLayout = [ - [VirtualKey.character(text: '1'), VirtualKey.character(text: '2'), VirtualKey.character(text: '3')], - [VirtualKey.character(text: '4'), VirtualKey.character(text: '5'), VirtualKey.character(text: '6')], - [VirtualKey.character(text: '7'), VirtualKey.character(text: '8'), VirtualKey.character(text: '9')], - [VirtualKey.character(text: '*'), VirtualKey.character(text: '0'), VirtualKey.character(text: '#')], + [ + VirtualKey.character(text: '1'), + VirtualKey.character(text: '2'), + VirtualKey.character(text: '3'), + ], + [ + VirtualKey.character(text: '4'), + VirtualKey.character(text: '5'), + VirtualKey.character(text: '6'), + ], + [ + VirtualKey.character(text: '7'), + VirtualKey.character(text: '8'), + VirtualKey.character(text: '9'), + ], + [ + VirtualKey.character(text: '*'), + VirtualKey.character(text: '0'), + VirtualKey.character(text: '#'), + ], [ VirtualKey.character(text: '+'), VirtualKey.action(action: KeyAction.backSpace), diff --git a/lib/src/models.dart b/lib/src/models.dart index 3bf525d..2a28ea1 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -10,11 +10,8 @@ class VirtualKey { /// - [text]: The character to insert (and display in lowercase mode). /// - [capsText]: Optional uppercase variant. Defaults to [text.toUpperCase()]. /// - [flex]: Relative width of the key. Default is 1. - VirtualKey.character({ - required this.text, - String? capsText, - this.flex = 1, - }) : capsText = capsText ?? text?.toUpperCase(), + VirtualKey.character({required this.text, String? capsText, this.flex = 1}) + : capsText = capsText ?? text?.toUpperCase(), keyType = KeyType.character, action = null, label = null, diff --git a/lib/src/scope.dart b/lib/src/scope.dart index b7a67a3..4961c52 100644 --- a/lib/src/scope.dart +++ b/lib/src/scope.dart @@ -21,10 +21,7 @@ import 'enums.dart'; /// ``` class VirtualKeypadScope extends StatefulWidget { /// Creates a scope for managing keyboard-textfield connections. - const VirtualKeypadScope({ - super.key, - required this.child, - }); + const VirtualKeypadScope({super.key, required this.child}); /// The child widget tree containing text fields and keyboard. final Widget child; @@ -193,10 +190,7 @@ class VirtualKeypadScopeState extends State { @override Widget build(BuildContext context) { - return _VirtualKeypadScopeInherited( - state: this, - child: widget.child, - ); + return _VirtualKeypadScopeInherited(state: this, child: widget.child); } } diff --git a/lib/src/standalone_input_control.dart b/lib/src/standalone_input_control.dart index cb8abc0..84fcf48 100644 --- a/lib/src/standalone_input_control.dart +++ b/lib/src/standalone_input_control.dart @@ -30,22 +30,19 @@ class StandaloneInputControl with TextInputControl { bool get isAttached => _attached; /// Derives a [KeyboardType] from the attached field's [TextInputType]. - KeyboardType get keyboardType => - _toKeyboardType(_configuration?.inputType); + KeyboardType get keyboardType => _toKeyboardType(_configuration?.inputType); /// The input action from the attached field's configuration. TextInputAction get inputAction => _configuration?.inputAction ?? TextInputAction.done; /// Performs an input action (e.g. done, go, search) on the active client. - void performAction(TextInputAction action) => - _client?.performAction(action); + void performAction(TextInputAction action) => _client?.performAction(action); @override void attach(TextInputClient client, TextInputConfiguration configuration) { _client = client; - _currentValue = - client.currentTextEditingValue ?? TextEditingValue.empty; + _currentValue = client.currentTextEditingValue ?? TextEditingValue.empty; _configuration = configuration; _attached = true; } @@ -94,8 +91,7 @@ class StandaloneInputControl with TextInputControl { final newText = _currentValue.text.replaceRange(start, sel.end, ''); _updateEditingValue(newText, start); } else if (!sel.isCollapsed) { - final newText = - _currentValue.text.replaceRange(sel.start, sel.end, ''); + final newText = _currentValue.text.replaceRange(sel.start, sel.end, ''); _updateEditingValue(newText, sel.start); } } diff --git a/lib/src/standalone_scope.dart b/lib/src/standalone_scope.dart new file mode 100644 index 0000000..7ced132 --- /dev/null +++ b/lib/src/standalone_scope.dart @@ -0,0 +1,47 @@ +import 'package:flutter/widgets.dart'; + +/// Scopes a [VirtualKeypad] in standalone mode to only respond to text fields +/// within its subtree. +/// +/// Wrap both your text fields and [VirtualKeypad] with this widget to prevent +/// the keyboard from appearing when a text field outside the scope gains focus. +/// This is especially useful in tools like Widgetbook where multiple widget +/// previews are shown simultaneously. +/// +/// ```dart +/// VirtualKeypadStandaloneScope( +/// child: Column( +/// children: [ +/// TextField(controller: myController), +/// VirtualKeypad(standalone: true), +/// ], +/// ), +/// ) +/// ``` +/// +/// Without this wrapper, [VirtualKeypad] in standalone mode responds to any +/// focused text field in the application. +class VirtualKeypadStandaloneScope extends StatefulWidget { + /// Creates a standalone scope for [VirtualKeypad]. + const VirtualKeypadStandaloneScope({super.key, required this.child}); + + /// The child widget tree containing text fields and the keyboard. + final Widget child; + + /// Returns the nearest [VirtualKeypadStandaloneScopeState] ancestor, or + /// null if there is none. + static VirtualKeypadStandaloneScopeState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + State createState() => + VirtualKeypadStandaloneScopeState(); +} + +/// State for [VirtualKeypadStandaloneScope]. +class VirtualKeypadStandaloneScopeState + extends State { + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/lib/src/widgets/keyboard.dart b/lib/src/widgets/keyboard.dart index f16ca8f..ac32ca5 100644 --- a/lib/src/widgets/keyboard.dart +++ b/lib/src/widgets/keyboard.dart @@ -8,6 +8,7 @@ import '../layouts/keyboard_layout_provider.dart'; import '../models.dart'; import '../scope.dart'; import '../standalone_input_control.dart'; +import '../standalone_scope.dart'; import '../theme.dart'; /// A customizable virtual on-screen keyboard widget. @@ -133,11 +134,7 @@ class _VirtualKeypadState extends State { void _initStandalone() { _inputControl = StandaloneInputControl( - onShow: () { - if (!mounted) return; - setState(() => _standaloneVisible = true); - _onStandaloneFieldChanged(); - }, + onShow: _onStandaloneShow, onHide: () { if (!mounted) return; setState(() => _standaloneVisible = false); @@ -163,9 +160,47 @@ class _VirtualKeypadState extends State { if (_standaloneVisible) { setState(() => _standaloneVisible = false); } + return; + } + + // If wrapped in a scope, hide when focus moves outside that scope + final myScope = VirtualKeypadStandaloneScope.maybeOf(context); + if (myScope != null && _standaloneVisible) { + final focusedScope = VirtualKeypadStandaloneScope.maybeOf(focus.context!); + if (focusedScope != myScope) { + setState(() => _standaloneVisible = false); + } } } + /// Called when the [StandaloneInputControl] requests the keyboard to show. + /// + /// If the keyboard is wrapped in a [VirtualKeypadStandaloneScope], the + /// focused text field must be within the same scope for the keyboard to + /// appear. This prevents the keyboard from responding to text fields that + /// belong to a different part of the widget tree. + void _onStandaloneShow() { + if (!mounted) return; + + // If this keyboard is inside a VirtualKeypadStandaloneScope, only show + // when the focused widget is in the same scope. + final myScope = VirtualKeypadStandaloneScope.maybeOf(context); + if (myScope != null) { + final focusedContext = FocusManager.instance.primaryFocus?.context; + final focusedScope = focusedContext != null + ? VirtualKeypadStandaloneScope.maybeOf(focusedContext) + : null; + if (focusedScope != myScope) { + // The focused field is outside our scope – hide the keyboard. + if (_standaloneVisible) setState(() => _standaloneVisible = false); + return; + } + } + + setState(() => _standaloneVisible = true); + _onStandaloneFieldChanged(); + } + void _onStandaloneFieldChanged() { if (!mounted || _inputControl == null) return; final newType = _inputControl!.keyboardType; @@ -441,7 +476,8 @@ class _VirtualKeypadState extends State { Widget build(BuildContext context) { final bool shouldShowKeyboard; if (widget.standalone) { - shouldShowKeyboard = _standaloneVisible && (_inputControl?.isAttached ?? false); + shouldShowKeyboard = + _standaloneVisible && (_inputControl?.isAttached ?? false); } else { final hasController = _scope?.hasActiveController ?? false; final allowPhysical = _scope?.allowPhysicalKeyboard ?? false; @@ -621,10 +657,7 @@ class _KeyWidgetState extends State<_KeyWidget> { child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, - offset: Offset( - -(popupWidth - keyWidth) / 2, - -popupHeight - gap, - ), + offset: Offset(-(popupWidth - keyWidth) / 2, -popupHeight - gap), child: _KeyPreviewBubble( text: widget.virtualKey.getDisplayText( shift: widget.shift, @@ -684,9 +717,7 @@ class _KeyWidgetState extends State<_KeyWidget> { _showKeyPreview(); widget.onPressed(key); }, - child: Center( - child: _buildKeyContent(), - ), + child: Center(child: _buildKeyContent()), ), ), ), diff --git a/lib/src/widgets/text_field.dart b/lib/src/widgets/text_field.dart index 2ca3c98..a0c8735 100644 --- a/lib/src/widgets/text_field.dart +++ b/lib/src/widgets/text_field.dart @@ -257,8 +257,9 @@ class _VirtualKeypadTextFieldState extends State { if (element.widget is EditableText) { final editableTextState = (element as StatefulElement).state as EditableTextState; - editableTextState - .bringIntoView(TextPosition(offset: selection.baseOffset)); + editableTextState.bringIntoView( + TextPosition(offset: selection.baseOffset), + ); return; } element.visitChildren(visitChildren); @@ -324,50 +325,60 @@ class _VirtualKeypadTextFieldState extends State { } Widget _buildContextMenu( - BuildContext context, EditableTextState editableTextState) { + BuildContext context, + EditableTextState editableTextState, + ) { final List buttonItems = []; if (!editableTextState.textEditingValue.selection.isCollapsed) { - buttonItems.add(ContextMenuButtonItem( - label: 'Cut', - onPressed: () { - final selection = widget.controller.selection; - if (selection.isValid && !selection.isCollapsed) { - final selectedText = widget.controller.text.substring( - selection.start, - selection.end, - ); - Clipboard.setData(ClipboardData(text: selectedText)); - widget.controller.deleteRange(selection.start, selection.end); - } - editableTextState.hideToolbar(); - }, - )); + buttonItems.add( + ContextMenuButtonItem( + label: 'Cut', + onPressed: () { + final selection = widget.controller.selection; + if (selection.isValid && !selection.isCollapsed) { + final selectedText = widget.controller.text.substring( + selection.start, + selection.end, + ); + Clipboard.setData(ClipboardData(text: selectedText)); + widget.controller.deleteRange(selection.start, selection.end); + } + editableTextState.hideToolbar(); + }, + ), + ); } if (!editableTextState.textEditingValue.selection.isCollapsed) { - buttonItems.add(ContextMenuButtonItem( - label: 'Copy', + buttonItems.add( + ContextMenuButtonItem( + label: 'Copy', + onPressed: () { + editableTextState.copySelection(SelectionChangedCause.toolbar); + }, + ), + ); + } + + buttonItems.add( + ContextMenuButtonItem( + label: 'Paste', onPressed: () { - editableTextState.copySelection(SelectionChangedCause.toolbar); + _handlePaste(); + editableTextState.hideToolbar(); }, - )); - } + ), + ); - buttonItems.add(ContextMenuButtonItem( - label: 'Paste', - onPressed: () { - _handlePaste(); - editableTextState.hideToolbar(); - }, - )); - - buttonItems.add(ContextMenuButtonItem( - label: 'Select All', - onPressed: () { - editableTextState.selectAll(SelectionChangedCause.toolbar); - }, - )); + buttonItems.add( + ContextMenuButtonItem( + label: 'Select All', + onPressed: () { + editableTextState.selectAll(SelectionChangedCause.toolbar); + }, + ), + ); return AdaptiveTextSelectionToolbar.buttonItems( anchors: editableTextState.contextMenuAnchors, diff --git a/lib/virtual_keypad.dart b/lib/virtual_keypad.dart index 323b8fb..6a1c644 100644 --- a/lib/virtual_keypad.dart +++ b/lib/virtual_keypad.dart @@ -45,6 +45,7 @@ /// /// - [VirtualKeypad] - Customizable on-screen keyboard widget /// - [VirtualKeypadScope] - Manages keyboard-to-textfield connections (scope mode) +/// - [VirtualKeypadStandaloneScope] - Restricts standalone keyboard to a widget subtree /// - [VirtualKeypadTextField] - Text field optimized for virtual keyboard input (scope mode) /// - [VirtualKeypadController] - Controller with text manipulation methods /// - [VirtualKeypadTheme] - Theming for keyboard appearance @@ -57,6 +58,7 @@ export 'src/enums.dart'; export 'src/models.dart'; export 'src/scope.dart'; export 'src/standalone_input_control.dart'; +export 'src/standalone_scope.dart'; export 'src/theme.dart'; export 'src/widgets/keyboard.dart'; export 'src/widgets/text_field.dart'; diff --git a/test/virtual_keypad_test.dart b/test/virtual_keypad_test.dart index 623e39c..3e15dc7 100644 --- a/test/virtual_keypad_test.dart +++ b/test/virtual_keypad_test.dart @@ -47,8 +47,10 @@ void main() { test('insertText replaces selection', () { final controller = VirtualKeypadController(text: 'Hello World'); // Select "World" - controller.selection = - const TextSelection(baseOffset: 6, extentOffset: 11); + controller.selection = const TextSelection( + baseOffset: 6, + extentOffset: 11, + ); controller.insertText('Flutter'); expect(controller.text, 'Hello Flutter'); expect(controller.cursorPosition, 13); @@ -57,8 +59,10 @@ void main() { test('deleteBackward removes selection', () { final controller = VirtualKeypadController(text: 'Hello World'); // Select "World" - controller.selection = - const TextSelection(baseOffset: 6, extentOffset: 11); + controller.selection = const TextSelection( + baseOffset: 6, + extentOffset: 11, + ); controller.deleteBackward(); expect(controller.text, 'Hello '); expect(controller.cursorPosition, 6); @@ -69,9 +73,11 @@ void main() { test('insertText modifies current value', () { final control = StandaloneInputControl(); // Simulate attach with initial empty value - control.setEditingState(TextEditingValue.empty.copyWith( - selection: const TextSelection.collapsed(offset: 0), - )); + control.setEditingState( + TextEditingValue.empty.copyWith( + selection: const TextSelection.collapsed(offset: 0), + ), + ); control.insertText('Hi'); expect(control.currentValue.text, 'Hi'); expect(control.currentValue.selection.baseOffset, 2); @@ -79,10 +85,12 @@ void main() { test('deleteBackward removes character', () { final control = StandaloneInputControl(); - control.setEditingState(const TextEditingValue( - text: 'Hello', - selection: TextSelection.collapsed(offset: 5), - )); + control.setEditingState( + const TextEditingValue( + text: 'Hello', + selection: TextSelection.collapsed(offset: 5), + ), + ); control.deleteBackward(); expect(control.currentValue.text, 'Hell'); expect(control.currentValue.selection.baseOffset, 4); @@ -90,10 +98,12 @@ void main() { test('deleteBackward removes selection', () { final control = StandaloneInputControl(); - control.setEditingState(const TextEditingValue( - text: 'Hello World', - selection: TextSelection(baseOffset: 5, extentOffset: 11), - )); + control.setEditingState( + const TextEditingValue( + text: 'Hello World', + selection: TextSelection(baseOffset: 5, extentOffset: 11), + ), + ); control.deleteBackward(); expect(control.currentValue.text, 'Hello'); expect(control.currentValue.selection.baseOffset, 5); @@ -101,10 +111,12 @@ void main() { test('insertText replaces selection', () { final control = StandaloneInputControl(); - control.setEditingState(const TextEditingValue( - text: 'Hello World', - selection: TextSelection(baseOffset: 6, extentOffset: 11), - )); + control.setEditingState( + const TextEditingValue( + text: 'Hello World', + selection: TextSelection(baseOffset: 6, extentOffset: 11), + ), + ); control.insertText('Flutter'); expect(control.currentValue.text, 'Hello Flutter'); expect(control.currentValue.selection.baseOffset, 13); @@ -135,4 +147,77 @@ void main() { expect(key.isCharacter, false); }); }); + + group('VirtualKeypadStandaloneScope', () { + testWidgets('maybeOf returns null when no scope in tree', (tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (ctx) { + capturedContext = ctx; + return const SizedBox.shrink(); + }, + ), + ), + ); + expect(VirtualKeypadStandaloneScope.maybeOf(capturedContext), isNull); + }); + + testWidgets('maybeOf returns state when scope is ancestor', (tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: VirtualKeypadStandaloneScope( + child: Builder( + builder: (ctx) { + capturedContext = ctx; + return const SizedBox.shrink(); + }, + ), + ), + ), + ); + expect( + VirtualKeypadStandaloneScope.maybeOf(capturedContext), + isA(), + ); + }); + + testWidgets('two sibling scopes return different state instances', ( + tester, + ) async { + late BuildContext contextA; + late BuildContext contextB; + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + VirtualKeypadStandaloneScope( + child: Builder( + builder: (ctx) { + contextA = ctx; + return const SizedBox.shrink(); + }, + ), + ), + VirtualKeypadStandaloneScope( + child: Builder( + builder: (ctx) { + contextB = ctx; + return const SizedBox.shrink(); + }, + ), + ), + ], + ), + ), + ); + final scopeA = VirtualKeypadStandaloneScope.maybeOf(contextA); + final scopeB = VirtualKeypadStandaloneScope.maybeOf(contextB); + expect(scopeA, isNotNull); + expect(scopeB, isNotNull); + expect(scopeA, isNot(same(scopeB))); + }); + }); }