Skip to content

Commit d5849d1

Browse files
#5756 Improved tapping on links when the font is really small.
1 parent b8cbdca commit d5849d1

File tree

1 file changed

+39
-95
lines changed

1 file changed

+39
-95
lines changed

lib/src/render_paragraph.dart

Lines changed: 39 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -491,101 +491,41 @@ class ClickableRenderParagraph extends RenderBox
491491
@override
492492
@protected
493493
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
494-
final children = _getClickableChildren();
495-
print('${this.size} => $children');
496-
InlineSpan? midSpanHit = _getHitTarget(position);
497-
if (midSpanHit is HitTestTarget) {
498-
print('hit: $midSpanHit');
499-
result.add(HitTestEntry(midSpanHit as HitTestTarget));
494+
final preferredLineHeight = _textPainter.preferredLineHeight;
495+
int lines = position.dy ~/ preferredLineHeight;
496+
final normalizedDy = lines * preferredLineHeight + (preferredLineHeight / 2);
497+
final normalizedPosition = Offset(position.dx, normalizedDy);
498+
final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(normalizedPosition);
499+
// The hit-test can't fall through the horizontal gaps between visually
500+
// adjacent characters on the same line, even with a large letter-spacing or
501+
// text justification, as graphemeClusterLayoutBounds.width is the advance
502+
// width to the next character, so there's no gap between their
503+
// graphemeClusterLayoutBounds rects.
504+
final InlineSpan? spanHit = glyph != null && glyph.graphemeClusterLayoutBounds.contains(normalizedPosition)
505+
? _textPainter.text!.getSpanForPosition(TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start))
506+
: null;
507+
if (spanHit is HitTestTarget) {
508+
result.add(HitTestEntry(spanHit as HitTestTarget));
500509
return true;
501510
}
502-
503-
if (hitTestInlineChildren(result, position)) {
504-
print('testling inline');
511+
bool hit = hitTestInlineChildren(result, position);
512+
if (hit && wasHit(result)) {
505513
return true;
506514
}
507515

508-
return false;
509-
}
510-
511-
List<ClickableRenderParagraph> _getClickableChildren() {
512-
return _getChildren(this, '');
513-
}
514-
515-
List<ClickableRenderParagraph> _getChildren(ContainerRenderObjectMixin parent, String indent) {
516-
print('${indent}#childCount: ${parent.childCount}');
517-
final children = <ClickableRenderParagraph>[];
518-
if (parent.firstChild == null) return children;
519-
var child = parent.firstChild;
520-
while (child != null) {
521-
children.addAll(_printChild(child, '$indent\t'));
522-
child = parent.childAfter(child);
523-
}
524-
return children;
525-
}
526-
527-
List<ClickableRenderParagraph> _printChild(RenderObject child, String indent) {
528-
if (child is ClickableRenderParagraph) {
529-
print('$indent size: ${child.size} -> ${child.parentData}');
530-
if (child.childCount == 0) {
531-
return [child];
532-
}
533-
}
534-
final children = <ClickableRenderParagraph>[];
535-
536-
final parentData = child.parentData;
537-
print('${indent} ${child.runtimeType} -> PARENT: $parentData');
538-
if (child is ContainerRenderObjectMixin) {
539-
print('${indent}child count: ${child.childCount}');
540-
children.addAll(_getChildren(child, '$indent\t'));
541-
} else if (child is RenderProxyBox) {
542-
children.addAll(_printChild(child.child!, '$indent\t'));
543-
} else if (child is RenderObjectWithChildMixin) {
544-
children.addAll(_printChild(child.child!, '$indent\t'));
545-
} else {
546-
print('$indent #############');
516+
if (preferredLineHeight >= 24) return false;
517+
final hitTryOffset = math.min(24 - _textPainter.preferredLineHeight, preferredLineHeight / 2);
518+
final dec = Offset(position.dx, position.dy + hitTryOffset);
519+
hit = (hitTestInlineChildren(result, dec));
520+
if (hit && result.path.any((entry) => entry is TextSpan)) {
521+
return true;
547522
}
548-
return children;
549-
}
550523

551-
InlineSpan? _getHitTarget(Offset position) {
552-
final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position);
553-
if (glyph != null) {
554-
var bounds = glyph.graphemeClusterLayoutBounds;
555-
final padding = (30 - bounds.height) / 2;
556-
if (padding > 0) {
557-
bounds = Rect.fromLTRB(bounds.left, bounds.top - padding, bounds.right, bounds.bottom + padding);
558-
}
559-
final InlineSpan? spanHit = bounds.contains(position) ? text.getSpanForPosition(TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start)) : null;
560-
if (spanHit is HitTestTarget) {
561-
return spanHit;
562-
}
563-
}
564-
return null;
524+
final inc = Offset(position.dx, position.dy - hitTryOffset);
525+
return hitTestInlineChildren(result, inc);
565526
}
566527

567-
// bool _hitTestInlineChildren(BoxHitTestResult result, Offset position) {
568-
// RenderBox? child = firstChild;
569-
// print('testing child: $child ($position) => ${parent.runtimeType}');
570-
// while (child != null) {
571-
// final TextParentData childParentData = child.parentData! as TextParentData;
572-
// print('parentData: $childParentData');
573-
// final Offset? childOffset = childParentData.offset;
574-
// if (childOffset == null) {
575-
// return false;
576-
// }
577-
// final bool isHit = result.addWithPaintOffset(
578-
// offset: childOffset,
579-
// position: position,
580-
// hitTest: (BoxHitTestResult result, Offset transformed) => child!.hitTest(result, position: transformed),
581-
// );
582-
// if (isHit) {
583-
// return true;
584-
// }
585-
// child = childAfter(child);
586-
// }
587-
// return false;
588-
// }
528+
bool wasHit(HitTestResult result) => result.path.any((entry) => entry.target is TextSpan);
589529

590530
bool _needsClipping = false;
591531
ui.Shader? _overflowShader;
@@ -603,11 +543,11 @@ class ClickableRenderParagraph extends RenderBox
603543
_textPainter.markNeedsLayout();
604544
}
605545

606-
// Placeholder dimensions representing the sizes of child inline widgets.
607-
//
608-
// These need to be cached because the text painter's placeholder dimensions
609-
// will be overwritten during intrinsic width/height calculations and must be
610-
// restored to the original values before final layout and painting.
546+
// Placeholder dimensions representing the sizes of child inline widgets.
547+
//
548+
// These need to be cached because the text painter's placeholder dimensions
549+
// will be overwritten during intrinsic width/height calculations and must be
550+
// restored to the original values before final layout and painting.
611551
List<PlaceholderDimensions>? _placeholderDimensions;
612552

613553
double _adjustMaxWidth(double maxWidth) {
@@ -991,10 +931,10 @@ class ClickableRenderParagraph extends RenderBox
991931
..attributedLabel = attributedLabel;
992932
}
993933

994-
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
995-
// can be re-used when [assembleSemanticsNode] is called again. This ensures
996-
// stable ids for the [SemanticsNode]s of [TextSpan]s across
997-
// [assembleSemanticsNode] invocations.
934+
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
935+
// can be re-used when [assembleSemanticsNode] is called again. This ensures
936+
// stable ids for the [SemanticsNode]s of [TextSpan]s across
937+
// [assembleSemanticsNode] invocations.
998938
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
999939

1000940
@override
@@ -2971,3 +2911,7 @@ class _SelectableFragment with Selectable, Diagnosticable, ChangeNotifier implem
29712911
properties.add(DiagnosticsProperty<String>('fullText', fullText));
29722912
}
29732913
}
2914+
2915+
class ExtraOffset extends Offset {
2916+
ExtraOffset(super.dx, super.dy);
2917+
}

0 commit comments

Comments
 (0)