Files
ghostgram/submodules/TelegramUI/Components/RichTextEditor/Sources/RichTextInputDecUpstream/TextView/IDTextLayoutCanvasView.m
T
ichmagmaus111andClaude Opus 5.5 534cabe35b bug fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:55:08 +01:00

836 lines
31 KiB
Objective-C

#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import "IDTextLayoutCanvasView.h"
#import "IDTextLayoutCanvasViewInternal.h"
#import "IDTextCanvasAttachmentCoordinator.h"
#import "IDTextCanvasAttachmentLifecycle.h"
#import "IDTextRenderingSurfaceView.h"
#import "IDPrivateUIKit+TextLayoutCanvas.h"
#include <math.h>
@interface IDTextCustomRenderingHostingView : IDTextRenderingSurfaceView
@property(nonatomic, strong, nullable) id hostedSurface;
@property(nonatomic, strong, nullable) CALayer *hostedLayer;
- (void)updateWithSurface:(id)hostedSurface;
- (void)teardown;
@end
@implementation IDTextCustomRenderingHostingView
- (void)updateWithSurface:(id)hostedSurface {
CALayer *hostedLayer = IDPrivateCanvasCustomSurfaceLayer(hostedSurface);
if (_hostedSurface == hostedSurface && _hostedLayer == hostedLayer) {
[self id_updateHostedLayerGeometry];
return;
}
if (_hostedLayer.superlayer == self.layer) {
[_hostedLayer removeFromSuperlayer];
}
_hostedSurface = hostedSurface;
_hostedLayer = hostedLayer;
hostedLayer.anchorPoint = CGPointZero;
hostedLayer.position = self.bounds.origin;
[self id_updateHostedLayerGeometry];
[self.layer addSublayer:hostedLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self id_updateHostedLayerGeometry];
}
- (void)setNeedsDisplay {
[self.hostedLayer setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
- (void)teardown {
if (self.hostedLayer.superlayer == self.layer) {
[self.hostedLayer removeFromSuperlayer];
}
self.hostedLayer = nil;
self.hostedSurface = nil;
[super teardown];
}
- (void)id_updateHostedLayerGeometry {
self.hostedLayer.bounds = self.bounds;
self.hostedLayer.anchorPoint = CGPointZero;
self.hostedLayer.position = self.bounds.origin;
}
@end
static CGFloat IDTextCanvasDisplayScale(UIView *view) {
CGFloat scale = view.traitCollection.displayScale;
return scale > 0 && isfinite(scale) ? scale : 1.0;
}
static BOOL IDTextCanvasContentSizeForUsageBounds(
CGRect usageBounds,
CGSize *contentSizeOut
) {
if (contentSizeOut == NULL ||
!isfinite(usageBounds.origin.x) ||
!isfinite(usageBounds.origin.y) ||
!isfinite(usageBounds.size.width) ||
!isfinite(usageBounds.size.height)) {
return NO;
}
CGFloat maxX = CGRectGetMaxX(usageBounds);
CGFloat maxY = CGRectGetMaxY(usageBounds);
if (!isfinite(maxX) || !isfinite(maxY)) {
return NO;
}
*contentSizeOut = CGSizeMake(MAX(maxX, 0), MAX(maxY, 0));
return YES;
}
@class IDTextLayoutCanvasView;
@interface IDTextParagraphAnchoredAttachmentSurface : UIView
@property(nonatomic, weak, nullable) IDTextLayoutCanvasView *canvas;
@property(nonatomic, weak, nullable) id attachment;
@property(nonatomic, weak, nullable) NSTextLayoutFragment *layoutFragment;
@property(nonatomic, strong, nullable) id attachmentViewProvider;
- (instancetype)initWithCanvas:(IDTextLayoutCanvasView *)canvas attachment:(id)attachment;
- (void)teardown;
@end
@implementation IDTextParagraphAnchoredAttachmentSurface
- (void)setLayoutFragment:(NSTextLayoutFragment *)layoutFragment {
if (_layoutFragment == layoutFragment) {
return;
}
_layoutFragment = layoutFragment;
[self setNeedsLayout];
[self setNeedsDisplay];
}
- (UIViewAutoresizing)autoresizingMask {
return UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
- (instancetype)initWithCanvas:(IDTextLayoutCanvasView *)canvas attachment:(id)attachment {
self = [super initWithFrame:canvas.bounds];
if (self != nil) {
_canvas = canvas;
_attachment = attachment;
self.opaque = NO;
self.clipsToBounds = YES;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
IDPrivateCanvasSetHostsLayoutEngine(self, YES);
}
return self;
}
- (void)layoutSubviews {
IDTextLayoutCanvasView *canvas = self.canvas;
NSTextLayoutFragment *fragment = self.layoutFragment;
id attachment = self.attachment;
if (canvas == nil || fragment == nil || attachment == nil) {
self.attachmentViewProvider = nil;
[super layoutSubviews];
return;
}
self.frame = [canvas id_frameForParagraphAnchoredAttachment:attachment
layoutFragment:fragment];
[super layoutSubviews];
if (self.attachmentViewProvider == nil) {
self.attachmentViewProvider =
IDPrivateCanvasParagraphAnchoredProvider(fragment, attachment);
}
UIView *providerView =
IDPrivateCanvasAttachmentProviderView(self.attachmentViewProvider);
if (providerView != nil && providerView.superview != self) {
[self addSubview:providerView];
}
providerView.frame = self.bounds;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
if (self.layoutFragment == nil || self.attachment == nil || context == NULL) {
return;
}
if (IDPrivateCanvasAttachmentProviderView(self.attachmentViewProvider) != nil) {
return;
}
IDPrivateCanvasDrawParagraphAnchoredAttachment(self.layoutFragment,
self.attachment,
self.bounds,
context);
}
- (void)teardown {
[IDPrivateCanvasAttachmentProviderView(self.attachmentViewProvider) removeFromSuperview];
self.attachmentViewProvider = nil;
self.layoutFragment = nil;
self.attachment = nil;
}
@end
@interface IDTextLayoutCanvasView ()
@property(nonatomic, strong, readwrite) NSTextLayoutManager *textLayoutManager;
@property(nonatomic, strong, readwrite) NSTextContainer *textContainer;
@property(nonatomic, strong, readwrite) NSTextViewportLayoutController *textViewportLayoutController;
@end
// `IDTextLayoutCanvasView+PrivateCallbacks.m`: adds the callbacks UIKit sends under private names.
FOUNDATION_EXTERN void IDTextLayoutCanvasViewInstallPrivateCallbacks(Class cls);
@implementation IDTextLayoutCanvasView
+ (void)initialize {
IDTextLayoutCanvasViewInstallPrivateCallbacks(self);
}
@synthesize layoutFragmentViewsNeedDisplay = _layoutFragmentViewsNeedDisplay;
- (instancetype)initWithTextLayoutManager:(NSTextLayoutManager *)layoutManager
textContainer:(NSTextContainer *)textContainer {
self = [super initWithFrame:CGRectZero];
if (self == nil) {
return nil;
}
_textLayoutManager = layoutManager;
_textContainer = textContainer;
_textViewportLayoutController = layoutManager.textViewportLayoutController;
_textViewportLayoutController.delegate =
(id<NSTextViewportLayoutControllerDelegate>)self;
_fragmentSurfaces = [NSMapTable weakToStrongObjectsMapTable];
_unusedFragmentKeys = [NSHashTable weakObjectsHashTable];
_attachmentCoordinator = [IDTextCanvasAttachmentCoordinator new];
_externalSurfaces = [NSMutableSet set];
_unusedExternalSurfaces = [NSMutableSet set];
_anchoredAttachmentSurfaces = [NSMapTable strongToStrongObjectsMapTable];
_nativeSurfaceViewClass = IDTextRenderingSurfaceView.class;
_viewportOverride = CGRectNull;
_cachedContentSize = CGSizeMake(NAN, NAN);
self.contentMode = UIViewContentModeRedraw;
self.opaque = NO;
return self;
}
- (void)setContext:(id)context {
_context = context;
self.attachmentCoordinator.lifecycleSink =
[context conformsToProtocol:@protocol(IDTextCanvasAttachmentLifecycleSink)]
? context
: nil;
}
- (CGPoint)id_textContainerOrigin {
return IDPrivateCanvasContextTextContainerOrigin(self.context);
}
- (id)id_textLayoutController {
return self.textLayoutManager.delegate;
}
- (id)id_layoutManager {
return nil;
}
- (id)id_customRenderController {
return IDPrivateCanvasCustomRenderController(self.context);
}
- (UIColor *)id_textColor {
return IDPrivateCanvasTextColor(self.context);
}
- (BOOL)id_isEditable {
return IDPrivateCanvasContextIsEditable(self.context);
}
- (BOOL)id_isEditing {
return IDPrivateCanvasContextIsEditing(self.context);
}
- (void)id_textKit2CanvasDidLayout {
IDPrivateCanvasNotifyContextDidLayout(self.context);
}
- (void)id_updateInteractionGeometry {
IDPrivateCanvasUpdateContextInteractionGeometry(self.context);
}
- (void)setFrame:(CGRect)frame {
BOOL changed = !CGRectEqualToRect(self.frame, frame);
[super setFrame:frame];
if (changed) {
[self setNeedsLayout];
}
}
- (void)setBounds:(CGRect)bounds {
BOOL changed = !CGRectEqualToRect(self.bounds, bounds);
[super setBounds:bounds];
if (changed) {
[self setNeedsLayout];
}
}
- (void)setNeedsDisplay {
[super setNeedsDisplay];
self.layoutFragmentViewsNeedDisplay = YES;
[self setNeedsLayout];
}
- (void)setLayoutFragmentViewsNeedDisplay:(BOOL)needsDisplay {
_layoutFragmentViewsNeedDisplay = needsDisplay;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
return hitView == self ? nil : hitView;
}
- (void)layoutSubviews {
[super layoutSubviews];
[self performViewportLayout];
if (self.id_hoverEffectContainerLayer != nil) {
self.id_hoverEffectContainerLayer.frame = self.layer.bounds;
self.id_hoverEffectContainerLayer.hidden = IDPrivateCanvasContextIsEditing(self.context);
}
if (self.id_selectionContainerView.superview == self) {
self.id_selectionContainerView.frame = self.bounds;
}
}
- (void)setId_selectionContainerView:(UIView *)selectionContainerView {
if (_id_selectionContainerView == selectionContainerView) {
return;
}
[_id_selectionContainerView removeFromSuperview];
_id_selectionContainerView = selectionContainerView;
if (selectionContainerView != nil) {
[self addSubview:selectionContainerView];
}
[self setNeedsLayout];
}
- (void)performViewportLayout {
if (self.performingViewportLayout || CGRectIsEmpty(self.bounds)) {
return;
}
self.performingViewportLayout = YES;
@try {
[self id_viewportWillLayout];
if (self.viewportLayoutTestHook != nil) {
self.viewportLayoutTestHook();
}
[self.textViewportLayoutController layoutViewport];
[self id_viewportDidLayout];
self.completedViewportLayoutCount += 1;
} @finally {
self.performingViewportLayout = NO;
}
}
- (void)viewportBoundsDidChange {
[self setNeedsLayout];
}
- (CGRect)viewportBounds {
if (!CGRectIsNull(self.viewportOverride)) {
return self.viewportOverride;
}
UIView *host = self.superview;
if (host == nil) {
return self.bounds;
}
CGRect visibleBounds = [self convertRect:host.bounds fromView:host];
CGRect viewport = CGRectIntersection(self.bounds, visibleBounds);
return CGRectIsNull(viewport) ? CGRectZero : viewport;
}
- (NSUInteger)visibleSurfaceCount {
return self.fragmentSurfaces.count + self.externalSurfaces.count;
}
- (NSArray<NSTextLayoutFragment *> *)visibleFragments {
return self.fragmentSurfaces.keyEnumerator.allObjects;
}
- (NSArray<UIView *> *)visibleSurfaces {
return self.fragmentSurfaces.objectEnumerator.allObjects;
}
- (UIView *)surfaceForFragment:(NSTextLayoutFragment *)fragment {
return [self.fragmentSurfaces objectForKey:fragment];
}
- (void)setNativeSurfaceViewClass:(Class)nativeSurfaceViewClass {
if (![nativeSurfaceViewClass isSubclassOfClass:IDTextRenderingSurfaceView.class]) {
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:@"nativeSurfaceViewClass must subclass IDTextRenderingSurfaceView"
userInfo:nil];
}
_nativeSurfaceViewClass = nativeSurfaceViewClass;
}
- (void)id_notifyCustomRendererPreLayout {
id renderer = IDPrivateCanvasCustomRenderController(self.context);
self.currentCustomRenderController = renderer;
if (self.lastCustomRenderController != renderer) {
self.lastCustomRenderController = renderer;
for (NSTextLayoutFragment *fragment in self.fragmentSurfaces.keyEnumerator.allObjects) {
UIView *surface = [self.fragmentSurfaces objectForKey:fragment];
if ([surface respondsToSelector:@selector(teardown)]) {
[(id)surface teardown];
}
[surface removeFromSuperview];
[self.fragmentSurfaces removeObjectForKey:fragment];
}
[self.unusedFragmentKeys removeAllObjects];
}
if (renderer == nil) {
return;
}
IDPrivateCanvasPrepareCustomRenderer(renderer, IDTextCanvasDisplayScale(self));
IDPrivateCanvasCustomRendererBeginLayout(renderer);
IDPrivateCanvasCustomRendererPreLayoutNotify(renderer);
}
- (void)id_notifyCustomRendererPostLayout {
id renderer = self.currentCustomRenderController;
if (renderer == nil) {
return;
}
__weak IDTextLayoutCanvasView *weakSelf = self;
IDPrivateCanvasCustomRendererEndLayout(
renderer,
^(NSTextLayoutFragment *fragment, id customSurface) {
UIView *surface = [weakSelf.fragmentSurfaces objectForKey:fragment];
IDTextCustomRenderingHostingView *hostingView =
(IDTextCustomRenderingHostingView *)
IDPrivateCanvasRequireCustomEndConsumerHost(
surface,
IDTextCustomRenderingHostingView.class);
[hostingView updateWithSurface:customSurface];
});
if (self.layoutFragmentViewsNeedDisplay) {
IDPrivateCanvasSetNeedsDisplay(renderer);
self.layoutFragmentViewsNeedDisplay = NO;
}
IDPrivateCanvasCustomRendererPostLayoutNotify(renderer);
self.currentCustomRenderController = nil;
}
- (void)id_viewportWillLayout {
[self.attachmentCoordinator beginViewportLayout];
[self.unusedFragmentKeys removeAllObjects];
for (NSTextLayoutFragment *fragment in self.fragmentSurfaces.keyEnumerator.allObjects) {
[self.unusedFragmentKeys addObject:fragment];
}
[self.unusedExternalSurfaces setSet:self.externalSurfaces];
if (self.id_textEmphasisBackgroundView != nil) {
[self.unusedExternalSurfaces removeObject:self.id_textEmphasisBackgroundView];
}
[self id_notifyCustomRendererPreLayout];
}
- (UIView *)id_renderingSurfaceForTextLayoutFragment:(NSTextLayoutFragment *)fragment {
UIView *surface = [self.fragmentSurfaces objectForKey:fragment];
if (surface != nil) {
[surface setNeedsDisplay];
return surface;
}
CGRect clipRect = IDPrivateCanvasContextClipRectForFadedEdges(self.context);
if (self.currentCustomRenderController != nil) {
surface = [[IDTextCustomRenderingHostingView alloc]
initWithLayoutFragment:fragment
containerOrigin:CGPointZero
containerSize:self.textContainer.size
clipRect:clipRect];
} else {
Class surfaceClass = self.nativeSurfaceViewClass;
surface = [[surfaceClass alloc] initWithLayoutFragment:fragment
containerOrigin:CGPointZero
containerSize:self.textContainer.size
clipRect:clipRect];
}
[self.fragmentSurfaces setObject:surface forKey:fragment];
return surface;
}
- (void)id_configureRenderingSurfaceForTextLayoutFragment:(NSTextLayoutFragment *)fragment {
UIView *surface = [self.fragmentSurfaces objectForKey:fragment];
if (surface == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"configureRenderingSurfaceForTextLayoutFragment: "
"requires a keyed surface from "
"renderingSurfaceForTextLayoutFragment:"
userInfo:nil];
}
IDTextRenderingSurfaceView *fragmentSurface = (IDTextRenderingSurfaceView *)surface;
if (self.id_textEmphasisBackgroundView.superview == self) {
[self insertSubview:surface belowSubview:self.id_textEmphasisBackgroundView];
} else if (self.id_selectionContainerView.superview == self) {
[self insertSubview:surface belowSubview:self.id_selectionContainerView];
} else {
[self insertSubview:surface atIndex:0];
}
[fragmentSurface performChanges:^{
[fragmentSurface setContainerOrigin:CGPointZero];
[fragmentSurface setContainerSize:self.textContainer.size];
[fragmentSurface setClipRect:
IDPrivateCanvasContextClipRectForFadedEdges(self.context)];
}];
[self.unusedFragmentKeys removeObject:fragment];
if (self.currentCustomRenderController != nil) {
IDPrivateCanvasCustomRendererDidLayoutFragment(self.currentCustomRenderController,
fragment);
}
[surface layoutIfNeeded];
[fragmentSurface enumerateTextAttachmentViewsUsingBlock:^(UIView *view,
CGRect fragmentFrame) {
[self.attachmentCoordinator recordAttachmentView:view fragmentFrame:fragmentFrame];
}];
}
- (void)id_viewportDidLayout {
BOOL usedCustomRenderer = self.currentCustomRenderController != nil;
[self id_removeUnusedElementViews];
if (usedCustomRenderer) {
[self id_notifyCustomRendererPostLayout];
} else if (self.layoutFragmentViewsNeedDisplay) {
for (UIView *surface in self.fragmentSurfaces.objectEnumerator.allObjects) {
[surface setNeedsDisplay];
}
self.layoutFragmentViewsNeedDisplay = NO;
}
[self id_updateContentSizeIfNeeded];
[self.attachmentCoordinator finishViewportLayout];
IDPrivateCanvasNotifyContextDidLayout(self.context);
if (usedCustomRenderer) {
IDPrivateCanvasUpdateContextInteractionGeometry(self.context);
}
}
- (void)id_removeUnusedElementViews {
for (NSTextLayoutFragment *fragment in self.unusedFragmentKeys.allObjects) {
UIView *surface = [self.fragmentSurfaces objectForKey:fragment];
if ([surface respondsToSelector:@selector(teardown)]) {
[(id)surface teardown];
}
[surface removeFromSuperview];
[self.fragmentSurfaces removeObjectForKey:fragment];
}
[self.unusedFragmentKeys removeAllObjects];
for (UIView *surface in self.unusedExternalSurfaces.copy) {
if ([surface respondsToSelector:@selector(teardown)]) {
[(id)surface teardown];
}
[surface removeFromSuperview];
[self.externalSurfaces removeObject:surface];
for (id attachment in self.anchoredAttachmentSurfaces.keyEnumerator.allObjects) {
if ([self.anchoredAttachmentSurfaces objectForKey:attachment] == surface) {
[self.anchoredAttachmentSurfaces removeObjectForKey:attachment];
}
}
}
[self.unusedExternalSurfaces removeAllObjects];
}
- (void)id_updateContentSizeIfNeeded {
CGSize contentSize = CGSizeZero;
if (!IDTextCanvasContentSizeForUsageBounds(
self.textLayoutManager.usageBoundsForTextContainer,
&contentSize)) {
return;
}
if (CGSizeEqualToSize(self.cachedContentSize, contentSize)) {
return;
}
self.cachedContentSize = contentSize;
if (self.contentSizeDidChange != nil) {
self.contentSizeDidChange(contentSize);
}
}
- (void)id_flushElementViews {
[self.attachmentCoordinator flush];
[self.unusedFragmentKeys removeAllObjects];
for (NSTextLayoutFragment *fragment in self.fragmentSurfaces.keyEnumerator.allObjects) {
[self.unusedFragmentKeys addObject:fragment];
}
[self.unusedExternalSurfaces setSet:self.externalSurfaces];
[self id_removeUnusedElementViews];
for (UIView *surface in self.externalSurfaces.copy) {
[surface removeFromSuperview];
}
[self.externalSurfaces removeAllObjects];
[self.anchoredAttachmentSurfaces removeAllObjects];
self.id_uniqueWritingToolsAnimatedLayoutInfo = nil;
[self.unusedFragmentKeys removeAllObjects];
[self.unusedExternalSurfaces removeAllObjects];
}
- (BOOL)id_addRenderingSurface:(UIView *)surface
group:(NSInteger)group
placement:(NSInteger)placement {
if (surface == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"External rendering-surface nonnull stage failed"
userInfo:nil];
}
if (group != 0 && group != 1) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"External rendering-surface group stage supports only 0 and 1"
userInfo:nil];
}
if (placement != 0 && placement != 1) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"External rendering-surface placement stage supports only 0 and 1"
userInfo:nil];
}
UIView *selection = self.id_selectionContainerView.superview == self
? self.id_selectionContainerView
: nil;
if (group == 1) {
if (placement == 0 && selection != nil) {
[self insertSubview:surface belowSubview:selection];
} else {
[self insertSubview:surface atIndex:0];
}
} else if (placement == 0) {
[self addSubview:surface];
} else if (selection != nil) {
[self insertSubview:surface aboveSubview:selection];
} else {
[self addSubview:surface];
}
if (surface.translatesAutoresizingMaskIntoConstraints) {
if (CGRectIsEmpty(surface.frame)) {
surface.frame = self.bounds;
surface.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
}
IDPrivateCanvasSetHostsLayoutEngine(surface, YES);
}
[self.externalSurfaces addObject:surface];
[self.unusedExternalSurfaces removeObject:surface];
return YES;
}
- (UIView *)id_renderingSurfaceForTextParagraphAnchoredAttachment:(id)attachment
associatedLayoutFragment:(NSTextLayoutFragment *)fragment {
if (attachment == nil || fragment == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Paragraph-anchored surface creation requires attachment and fragment"
userInfo:nil];
}
IDTextParagraphAnchoredAttachmentSurface *surface =
(IDTextParagraphAnchoredAttachmentSurface *)
[self.anchoredAttachmentSurfaces objectForKey:attachment];
if (surface == nil) {
surface = [[IDTextParagraphAnchoredAttachmentSurface alloc]
initWithCanvas:self
attachment:attachment];
[self.anchoredAttachmentSurfaces setObject:surface forKey:attachment];
}
surface.layoutFragment = fragment;
[self id_addRenderingSurface:surface group:0 placement:1];
return surface;
}
- (CGRect)id_frameForParagraphAnchoredAttachment:(id)attachment
layoutFragment:(NSTextLayoutFragment *)fragment {
CGRect bounds = IDPrivateCanvasParagraphAnchoredBounds(fragment, attachment);
UIView *surface = [self.fragmentSurfaces objectForKey:fragment];
CGRect frame;
if (surface != nil) {
frame = [self convertRect:bounds fromView:surface];
} else {
CGPoint textContainerOrigin =
IDPrivateCanvasContextTextContainerOrigin(self.context);
frame = CGRectOffset(bounds,
CGRectGetMinX(fragment.layoutFragmentFrame) +
textContainerOrigin.x,
CGRectGetMinY(fragment.layoutFragmentFrame) +
textContainerOrigin.y);
}
if (!isfinite(frame.origin.x) || !isfinite(frame.origin.y) ||
!isfinite(frame.size.width) || !isfinite(frame.size.height)) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Paragraph-anchored attachment frame stage requires finite geometry"
userInfo:nil];
}
CGFloat width = MAX(CGRectGetWidth(self.bounds), 0);
CGFloat minimumX = MIN(MAX(CGRectGetMinX(frame), 0), width);
CGFloat maximumX = MIN(MAX(CGRectGetMaxX(frame), minimumX), width);
frame.origin.x = minimumX;
frame.size.width = maximumX - minimumX;
return frame;
}
- (void)id_setTextEmphasisBackgroundView:(UIView *)backgroundView
withTextView:(UIView *)textView {
if (self.id_textEmphasisBackgroundView != backgroundView) {
if (textView == nil) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Text emphasis setup requires a text view"
userInfo:nil];
}
UIView *oldBackgroundView = self.id_textEmphasisBackgroundView;
[oldBackgroundView removeFromSuperview];
if (oldBackgroundView != nil) {
[self.externalSurfaces removeObject:oldBackgroundView];
[self.unusedExternalSurfaces removeObject:oldBackgroundView];
}
self.id_textEmphasisBackgroundView = backgroundView;
[self setNeedsLayout];
}
id textContentManager = self.textLayoutManager.textContentManager;
if (backgroundView == nil &&
!IDPrivateCanvasSupportsAnchoredTextAttachments(textContentManager)) {
IDPrivateCanvasClearPlatformSurfaceUpdaterIfAvailable(
self.textViewportLayoutController);
return;
}
__weak IDTextLayoutCanvasView *weakSelf = self;
__weak UIView *weakTextView = textView;
IDPrivateCanvasInstallPlatformSurfaceUpdaterIfAvailable(
self.textViewportLayoutController,
^(id viewportLayoutController, id textRange, CGRect bounds) {
IDTextLayoutCanvasView *canvas = weakSelf;
UIView *retainedTextView = weakTextView;
if (canvas != nil && retainedTextView != nil && textRange != nil) {
[canvas id_updateRenderingSurfaceViewsForTextRange:textRange
inTextView:retainedTextView
bounds:bounds];
}
});
}
- (void)id_updateRenderingSurfaceViewsForTextRange:(id)textRange
inTextView:(UIView *)textView
bounds:(CGRect)bounds {
__block BOOL hasHighlight = NO;
if (!IDPrivateCanvasTextRangeIsEmpty(textRange)) {
NSRange range = IDPrivateCanvasNSRangeForTextKitRange(textView, textRange);
NSTextStorage *textStorage = IDPrivateCanvasTextStorage(textView);
if (range.location != NSNotFound && range.location <= textStorage.length &&
range.length <= textStorage.length - range.location) {
[textStorage enumerateAttribute:NSTextHighlightStyleAttributeName
inRange:range
options:0
usingBlock:^(id value, NSRange attributeRange, BOOL *stop) {
if (value != nil) {
hasHighlight = YES;
*stop = YES;
}
}];
}
}
UIView *emphasisView = self.id_textEmphasisBackgroundView;
if (hasHighlight && emphasisView != nil) {
[self id_addRenderingSurface:emphasisView group:1 placement:1];
if (self.id_selectionContainerView.superview == self) {
[self insertSubview:emphasisView belowSubview:self.id_selectionContainerView];
} else {
[self addSubview:emphasisView];
}
CGPoint textContainerOrigin =
IDPrivateCanvasContextTextContainerOrigin(self.context);
CGRect frame = CGRectOffset(bounds,
textContainerOrigin.x,
textContainerOrigin.y);
if (!CGRectEqualToRect(emphasisView.frame, frame)) {
emphasisView.frame = frame;
}
emphasisView.hidden = NO;
IDPrivateCanvasSetEmphasisTextRange(emphasisView, textRange);
IDPrivateCanvasSetEmphasisOrigin(
emphasisView,
CGPointMake(-CGRectGetMinX(bounds), -CGRectGetMinY(bounds)));
} else if (emphasisView.superview == self && !CGRectIsEmpty(emphasisView.frame)) {
emphasisView.hidden = YES;
}
id textContentManager = self.textLayoutManager.textContentManager;
if (!IDPrivateCanvasSupportsAnchoredTextAttachments(textContentManager)) {
return;
}
CGRect viewportBounds = self.viewportBounds;
__weak IDTextLayoutCanvasView *weakSelf = self;
[self.textLayoutManager enumerateTextLayoutFragmentsFromLocation:nil
options:0
usingBlock:^BOOL(
NSTextLayoutFragment *fragment) {
IDTextLayoutCanvasView *canvas = weakSelf;
if (canvas == nil) {
return NO;
}
CGRect fragmentFrame = fragment.layoutFragmentFrame;
for (id attachment in IDPrivateCanvasAnchoredTextAttachments(fragment)) {
CGRect attachmentFrame = CGRectOffset(
IDPrivateCanvasParagraphAnchoredBounds(fragment, attachment),
CGRectGetMinX(fragmentFrame),
CGRectGetMinY(fragmentFrame));
if (CGRectIntersectsRect(attachmentFrame, viewportBounds)) {
UIView *surface = [canvas
id_renderingSurfaceForTextParagraphAnchoredAttachment:attachment
associatedLayoutFragment:fragment];
[surface setNeedsLayout];
}
}
id<NSTextLocation> fragmentLocation = fragment.rangeInElement.location;
id<NSTextLocation> endLocation = [(NSTextRange *)textRange endLocation];
return [fragmentLocation compare:endLocation] == NSOrderedAscending;
}];
}
- (void)id_isEditingDidChange {
if (self.id_hoverEffectContainerLayer != nil) {
[self setNeedsLayout];
}
}
- (void)id_observeEnclosingScrollView {
[self viewportBoundsDidChange];
}
- (void)id_traitsChanged {
[self setNeedsLayout];
[self setNeedsDisplay];
}
- (void)id_invalidateTextContainerOrigin {
[self setNeedsLayout];
}
@end
#endif