fix: add synchronized scrolling between RichText and TextField in MentionHighlightTextField to fix scroll issues

Changed files
+57 -31
lib
+57 -31
lib/widgets/faceted_text_field.dart
··· 348 348 } 349 349 350 350 class _MentionHighlightTextFieldState extends State<_MentionHighlightTextField> { 351 + final ScrollController _richTextScrollController = ScrollController(); 352 + final ScrollController _textFieldScrollController = ScrollController(); 353 + 351 354 void _onMentionTap(String did) { 352 355 // Show overlay for this mention (simulate as if user is typing @mention) 353 356 final parent = context.findAncestorStateOfType<_FacetedTextFieldState>(); ··· 364 367 super.initState(); 365 368 _parseFacets(); 366 369 widget.controller.addListener(_parseFacets); 370 + 371 + // Sync scroll controllers 372 + _textFieldScrollController.addListener(() { 373 + if (_richTextScrollController.hasClients && _textFieldScrollController.hasClients) { 374 + _richTextScrollController.jumpTo(_textFieldScrollController.offset); 375 + } 376 + }); 367 377 } 368 378 369 379 @override 370 380 void dispose() { 371 381 widget.controller.removeListener(_parseFacets); 372 382 _facetDebounce?.cancel(); 383 + _richTextScrollController.dispose(); 384 + _textFieldScrollController.dispose(); 373 385 super.dispose(); 374 386 } 375 387 ··· 466 478 } 467 479 return LayoutBuilder( 468 480 builder: (context, constraints) { 469 - return Stack( 470 - children: [ 471 - // RichText for highlight 472 - Padding( 473 - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 474 - child: RichText( 475 - text: TextSpan(children: spans), 476 - maxLines: widget.maxLines, 477 - overflow: TextOverflow.visible, 481 + return SizedBox( 482 + width: double.infinity, // Make it full width 483 + height: widget.maxLines == 1 484 + ? null 485 + : (baseStyle?.fontSize ?? 15) * 1.4 * widget.maxLines + 486 + 24, // Line height * maxLines + padding 487 + child: Stack( 488 + children: [ 489 + // RichText for highlight wrapped in SingleChildScrollView 490 + SingleChildScrollView( 491 + controller: _richTextScrollController, 492 + physics: const NeverScrollableScrollPhysics(), // Disable direct interaction 493 + child: Padding( 494 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 495 + child: RichText( 496 + text: TextSpan(children: spans), 497 + maxLines: null, // Allow unlimited lines for scrolling 498 + overflow: TextOverflow.visible, 499 + ), 500 + ), 478 501 ), 479 - ), 480 - // Editable TextField for input, but with transparent text so only RichText is visible 481 - TextField( 482 - controller: widget.controller, 483 - maxLines: widget.maxLines, 484 - enabled: widget.enabled, 485 - keyboardType: widget.keyboardType, 486 - onChanged: widget.onChanged, 487 - style: baseStyle?.copyWith(color: const Color(0x01000000)), 488 - cursorColor: theme.colorScheme.primary, 489 - showCursor: true, 490 - enableInteractiveSelection: true, 491 - decoration: InputDecoration( 492 - hintText: widget.hintText, 493 - hintStyle: baseStyle?.copyWith(color: theme.hintColor), 494 - border: InputBorder.none, 495 - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 496 - isDense: true, 497 - prefixIcon: widget.prefixIcon, 498 - suffixIcon: widget.suffixIcon, 502 + // Editable TextField for input, but with transparent text so only RichText is visible 503 + Positioned.fill( 504 + child: TextField( 505 + controller: widget.controller, 506 + scrollController: _textFieldScrollController, 507 + maxLines: null, // Allow unlimited lines for scrolling 508 + enabled: widget.enabled, 509 + keyboardType: widget.keyboardType, 510 + onChanged: widget.onChanged, 511 + style: baseStyle?.copyWith(color: const Color(0x01000000)), 512 + cursorColor: theme.colorScheme.primary, 513 + showCursor: true, 514 + enableInteractiveSelection: true, 515 + decoration: InputDecoration( 516 + hintText: widget.hintText, 517 + hintStyle: baseStyle?.copyWith(color: theme.hintColor), 518 + border: InputBorder.none, 519 + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), 520 + isDense: true, 521 + prefixIcon: widget.prefixIcon, 522 + suffixIcon: widget.suffixIcon, 523 + ), 524 + ), 499 525 ), 500 - ), 501 - ], 526 + ], 527 + ), 502 528 ); 503 529 }, 504 530 );