From 0dc7ed81e3f6c031df28660c839b97834ce1df35 Mon Sep 17 00:00:00 2001 From: Muzammil Hussain <58479501+Muzammil-Bit@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:06:58 +0500 Subject: [PATCH] fix(ios): tall code blocks with long lines no longer draw an empty pane The code pane drew through drawRect: on one view sized to the whole block, so a tall block with long lines needed a backing store larger than iOS would allocate and showed an empty pane until wrapping narrowed it. The pane now draws through a canvas child that switches to a CATiledLayer once it is too big for one store; smaller blocks still draw in their first frame. Fixes pingdotgg/t3code#32928 --- .../ios/segments/ENRMCodeBlockContainerView.m | 176 +++++++++++++++--- 1 file changed, 145 insertions(+), 31 deletions(-) diff --git a/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m b/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m index ad6a4e010..94c49fb11 100644 --- a/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m +++ b/packages/react-native-enriched-markdown/ios/segments/ENRMCodeBlockContainerView.m @@ -166,7 +166,7 @@ static BOOL ENRMColorIsDark(RCTUIColor *color) // fixed origin; the hosting ENRMCodeBlockContainerView draws everything else. // // The TextKit stack (storage/layout manager/container) is built and laid out -// once, when attributedCode is set, then cached: drawRect: only paints the +// once, when attributedCode is set, then cached: drawing only paints the // already-computed glyphs, so scrolling and redraws never re-lay-out. This is // the same rendering path drawAtPoint used internally, so output is identical, // but the layout is no longer thrown away on every draw. The container is @@ -183,12 +183,82 @@ @interface ENRMCodeBlockContentView : RCTUIView #if TARGET_OS_OSX @property (nonatomic, copy, nullable) NSMenu * (^menuProvider)(void); #endif +- (void)drawCodeInRect:(CGRect)rect; @end +#if !TARGET_OS_OSX +// iOS gives a drawRect: view one backing store for its whole bounds, and a +// tall block with long lines needs more than it will allocate, so the pane +// stays blank. The content view does not draw; a canvas child does, through a +// CATiledLayer once the pane is too big for one store. +static const CGFloat kENRMCodeBlockMaxUntiledPixels = 2048 * 2048; + +@interface ENRMCodeBlockCanvasView : UIView +@property (nonatomic, weak) ENRMCodeBlockContentView *contentView; +@end + +@implementation ENRMCodeBlockCanvasView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [UIColor clearColor]; + self.contentMode = UIViewContentModeRedraw; + self.userInteractionEnabled = NO; + } + return self; +} + +- (void)drawRect:(CGRect)rect +{ + [self.contentView drawCodeInRect:rect]; +} + +@end + +@interface ENRMCodeBlockTiledLayer : CATiledLayer +@end + +@implementation ENRMCodeBlockTiledLayer + +// Tiles are drawn off the main thread; show them as they land, without a fade. ++ (CFTimeInterval)fadeDuration +{ + return 0; +} + +@end + +@interface ENRMCodeBlockTiledCanvasView : ENRMCodeBlockCanvasView +@end + +@implementation ENRMCodeBlockTiledCanvasView + ++ (Class)layerClass +{ + return [ENRMCodeBlockTiledLayer class]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + ((CATiledLayer *)self.layer).tileSize = CGSizeMake(1024, 1024); + } + return self; +} + +@end +#endif + @implementation ENRMCodeBlockContentView { NSTextStorage *_textStorage; NSLayoutManager *_layoutManager; NSTextContainer *_textContainer; +#if !TARGET_OS_OSX + ENRMCodeBlockCanvasView *_canvas; +#endif } #if TARGET_OS_OSX @@ -201,26 +271,57 @@ - (NSMenu *)menuForEvent:(NSEvent *)event { return self.menuProvider ? self.menuProvider() : [super menuForEvent:event]; } + +- (void)drawRect:(CGRect)rect +{ + [self drawCodeInRect:rect]; +} +#else +- (void)layoutSubviews +{ + [super layoutSubviews]; + CGFloat scale = self.traitCollection.displayScale > 0 ? self.traitCollection.displayScale : 3; + CGFloat pixels = self.bounds.size.width * self.bounds.size.height * scale * scale; + Class canvasClass = + pixels > kENRMCodeBlockMaxUntiledPixels ? [ENRMCodeBlockTiledCanvasView class] : [ENRMCodeBlockCanvasView class]; + if (![_canvas isMemberOfClass:canvasClass]) { + [_canvas removeFromSuperview]; + _canvas = [[canvasClass alloc] initWithFrame:self.bounds]; + _canvas.contentView = self; + [self addSubview:_canvas]; + } + _canvas.frame = self.bounds; +} + +- (void)setNeedsDisplay +{ + [super setNeedsDisplay]; + [_canvas setNeedsDisplay]; +} #endif +// Tiled canvases draw on background threads, so the TextKit stack and the +// origin change and draw under this lock. - (void)setAttributedCode:(nullable NSAttributedString *)attributedCode { - _attributedCode = attributedCode; - if (attributedCode.length == 0) { - _textStorage = nil; - _layoutManager = nil; - _textContainer = nil; - return; + @synchronized(self) { + _attributedCode = attributedCode; + if (attributedCode.length == 0) { + _textStorage = nil; + _layoutManager = nil; + _textContainer = nil; + return; + } + _textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedCode]; + _layoutManager = [[NSLayoutManager alloc] init]; + _layoutManager.usesFontLeading = ENRMLayoutManagerUsesFontLeading; + _textContainer = [[NSTextContainer alloc] initWithSize:[self containerSize]]; + _textContainer.lineFragmentPadding = 0; + _textContainer.maximumNumberOfLines = 0; + [_layoutManager addTextContainer:_textContainer]; + [_textStorage addLayoutManager:_layoutManager]; + [_layoutManager ensureLayoutForTextContainer:_textContainer]; } - _textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedCode]; - _layoutManager = [[NSLayoutManager alloc] init]; - _layoutManager.usesFontLeading = ENRMLayoutManagerUsesFontLeading; - _textContainer = [[NSTextContainer alloc] initWithSize:[self containerSize]]; - _textContainer.lineFragmentPadding = 0; - _textContainer.maximumNumberOfLines = 0; - [_layoutManager addTextContainer:_textContainer]; - [_textStorage addLayoutManager:_layoutManager]; - [_layoutManager ensureLayoutForTextContainer:_textContainer]; } - (CGSize)containerSize @@ -228,15 +329,24 @@ - (CGSize)containerSize return CGSizeMake(_wrapWidth > 0 ? _wrapWidth : CGFLOAT_MAX, CGFLOAT_MAX); } +- (void)setTextOrigin:(CGPoint)textOrigin +{ + @synchronized(self) { + _textOrigin = textOrigin; + } +} + - (void)setWrapWidth:(CGFloat)wrapWidth { if (_wrapWidth == wrapWidth) { return; } - _wrapWidth = wrapWidth; - if (_textContainer) { - _textContainer.size = [self containerSize]; - [_layoutManager ensureLayoutForTextContainer:_textContainer]; + @synchronized(self) { + _wrapWidth = wrapWidth; + if (_textContainer) { + _textContainer.size = [self containerSize]; + [_layoutManager ensureLayoutForTextContainer:_textContainer]; + } } #if !TARGET_OS_OSX [self setNeedsDisplay]; @@ -247,22 +357,26 @@ - (void)setWrapWidth:(CGFloat)wrapWidth - (CGFloat)usedWidth { - if (!_layoutManager) { - return 0; + @synchronized(self) { + if (!_layoutManager) { + return 0; + } + return ceil([_layoutManager usedRectForTextContainer:_textContainer].size.width); } - return ceil([_layoutManager usedRectForTextContainer:_textContainer].size.width); } -- (void)drawRect:(CGRect)rect +- (void)drawCodeInRect:(CGRect)rect { - if (!_layoutManager) { - return; + @synchronized(self) { + if (!_layoutManager) { + return; + } + // Only paint the glyphs intersecting the dirty rect; tall blocks skip their + // off-screen lines. The rect is in view space, the layout in container space. + CGRect boundingRect = CGRectOffset(rect, -_textOrigin.x, -_textOrigin.y); + NSRange glyphRange = [_layoutManager glyphRangeForBoundingRect:boundingRect inTextContainer:_textContainer]; + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_textOrigin]; } - // Only paint the glyphs intersecting the dirty rect; tall blocks skip their - // off-screen lines. The rect is in view space, the layout in container space. - CGRect boundingRect = CGRectOffset(rect, -self.textOrigin.x, -self.textOrigin.y); - NSRange glyphRange = [_layoutManager glyphRangeForBoundingRect:boundingRect inTextContainer:_textContainer]; - [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:self.textOrigin]; } @end