From 78e1ea43c81dd84f59b976dd86f13ac951ea920c Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 15:58:16 +0200 Subject: [PATCH 1/3] new: layouting speedup in CPTextView --- AppKit/CPTextView/CPLayoutManager.j | 66 ++++++++++++++++++++++++++++- AppKit/CPTextView/CPTypesetter.j | 6 ++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index f64d8d6a4..103e19921 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -286,6 +286,8 @@ _oncontextmenuhandler = function () { return false; }; if (removeRange.length) _removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange); + else + _removeInvalidLineFragmentsRange = nil; // We erased all lines if (!startIndex) @@ -716,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return lineFragment._glyphsFrames[index]._descent; + return [lineFragment glyphFrames][index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1074,6 +1076,7 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; + BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1161,6 +1164,7 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; + _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1207,6 +1211,8 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { + _exactFramesCalculated = NO; + var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1223,6 +1229,62 @@ var _objectsInRange = function(aList, aRange) } } +- (CPArray)glyphFrames +{ + if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) + { + _exactFramesCalculated = YES; + var originX = _fragmentRect.origin.x + _location.x, + currentX = originX, + frameIndex = 0, + l = _runs.length; + + // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch + for (var r = 0; r < l; r++) + { + var run = _runs[r], + runStr = run.string; + + if (!runStr) + { + // Attachments / Unsichtbares überspringen + if (frameIndex < _glyphsFrames.length) + { + var frame = _glyphsFrames[frameIndex]; + frame.origin.x = currentX; + currentX += frame.size.width; + frameIndex++; + } + continue; + } + + var runLen = runStr.length, + runFont = run.font; + + // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen + for (var i = 0; i < runLen; i++) + { + if (frameIndex >= _glyphsFrames.length) break; + + var frame = _glyphsFrames[frameIndex], + prefix = runStr.substr(0, i), + prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, + prefixWithChar = runStr.substr(0, i + 1), + prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; + + frame.origin.x = currentX + prefixWidth; + frame.size.width = prefixWithCharWidth - prefixWidth; + + frameIndex++; + } + + currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; + } + } + + return _glyphsFrames; +} + - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1281,6 +1343,8 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { + [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! + var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 49169206f..e8946fcfc 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,8 +316,10 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons - rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; + var currentCharCode = theString.charCodeAt(glyphIndex), + charStr = theString.charAt(glyphIndex), + charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, + rangeWidth = prevRangeWidth + charWidth; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { From d6c5d8b4f2bdc25bb670b0aaf32b33a0204d1bd1 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 16:47:12 +0200 Subject: [PATCH 2/3] formatting --- AppKit/CPTextView/CPLayoutManager.j | 4 ++++ AppKit/CPTextView/CPTypesetter.j | 3 +++ 2 files changed, 7 insertions(+) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 103e19921..6179243b9 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -1229,6 +1229,10 @@ var _objectsInRange = function(aList, aRange) } } +// The spacings with kerning/ligatures are recalculated here for the visible part +// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter +// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) + - (CPArray)glyphFrames { if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index e8946fcfc..418bfbb4e 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -318,6 +318,9 @@ var CPSystemTypesetterFactory, var currentCharCode = theString.charCodeAt(glyphIndex), charStr = theString.charAt(glyphIndex), + // this is ignoring kerning and ligatures but 100x faster than calculating the full string + // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) + // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, rangeWidth = prevRangeWidth + charWidth; From 7f25de0fad1a3dea879ad6d74dde4683960d6bb9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sun, 17 May 2026 21:30:11 +0200 Subject: [PATCH 3/3] improved: kerning safe optimization --- AppKit/CPTextView/CPLayoutManager.j | 68 +---------------------------- AppKit/CPTextView/CPTypesetter.j | 20 ++++++--- 2 files changed, 14 insertions(+), 74 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 6179243b9..926e2b993 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -718,7 +718,7 @@ _oncontextmenuhandler = function () { return false; }; var index = location - lineFragment._range.location; - return [lineFragment glyphFrames][index]._descent; + return lineFragment._glyphsFrames[index]._descent; } - (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect @@ -1076,7 +1076,6 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; - BOOL _exactFramesCalculated; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1164,7 +1163,6 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; - _exactFramesCalculated = NO; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1211,8 +1209,6 @@ var _objectsInRange = function(aList, aRange) - (void)setAdvancements:(CPArray)someAdvancements { - _exactFramesCalculated = NO; - var count = someAdvancements.length, origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y), height = _usedRect.size.height; @@ -1229,66 +1225,6 @@ var _objectsInRange = function(aList, aRange) } } -// The spacings with kerning/ligatures are recalculated here for the visible part -// See comment in layoutGlyphsInLayoutManager: of CPSimpleTypesetter -// This gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - -- (CPArray)glyphFrames -{ - if (!_exactFramesCalculated && _glyphsFrames && _runs && _runs.length > 0) - { - _exactFramesCalculated = YES; - var originX = _fragmentRect.origin.x + _location.x, - currentX = originX, - frameIndex = 0, - l = _runs.length; - - // Wir gehen alle Runs (Wörter/Textblöcke in dieser Zeile) durch - for (var r = 0; r < l; r++) - { - var run = _runs[r], - runStr = run.string; - - if (!runStr) - { - // Attachments / Unsichtbares überspringen - if (frameIndex < _glyphsFrames.length) - { - var frame = _glyphsFrames[frameIndex]; - frame.origin.x = currentX; - currentX += frame.size.width; - frameIndex++; - } - continue; - } - - var runLen = runStr.length, - runFont = run.font; - - // Nun messen wir für diesen sichtbaren Run den exakten Substring inkl. Kerning/Ligaturen - for (var i = 0; i < runLen; i++) - { - if (frameIndex >= _glyphsFrames.length) break; - - var frame = _glyphsFrames[frameIndex], - prefix = runStr.substr(0, i), - prefixWidth = prefix.length > 0 ? [prefix sizeWithFont:runFont inWidth:NULL].width : 0.0, - prefixWithChar = runStr.substr(0, i + 1), - prefixWithCharWidth = [prefixWithChar sizeWithFont:runFont inWidth:NULL].width; - - frame.origin.x = currentX + prefixWidth; - frame.size.width = prefixWithCharWidth - prefixWidth; - - frameIndex++; - } - - currentX += [runStr sizeWithFont:runFont inWidth:NULL].width; - } - } - - return _glyphsFrames; -} - - (void)_adjustForHeight:(double)height { var count = _glyphsFrames.length; @@ -1347,8 +1283,6 @@ var _objectsInRange = function(aList, aRange) - (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange { - [self glyphFrames]; // Erzwingt die exakte Berechnung, bevor gezeichnet wird! - var runs = _objectsInRange(_runs, aRange), c = runs.length, orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 418bfbb4e..ef793cb86 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -316,13 +316,8 @@ var CPSystemTypesetterFactory, lineRange.length++; measuringRange.length++; - var currentCharCode = theString.charCodeAt(glyphIndex), - charStr = theString.charAt(glyphIndex), - // this is ignoring kerning and ligatures but 100x faster than calculating the full string - // we cut corners here and do the exact calculation in - glyphFrames of _CPLineFragment (CPLayoutManager.j) - // this gives an overall speed imrovement of ~20% in my testing (depending on text size / view size of course) - charWidth = [charStr sizeWithFont:currentFont inWidth:NULL].width, - rangeWidth = prevRangeWidth + charWidth; + var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons + rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. { @@ -370,6 +365,17 @@ var CPSystemTypesetterFactory, wrapWidth = rangeWidth; wrapRange._height = _lineHeight; wrapRange._base = _lineBase; + + // Optimization: Start measuring from the next character to avoid O(n^2) + // string width calculation within a line since spaces do not carry ligatures or kerning. + // Only reset the measuring range if the next character is NOT another space. + // This prevents compounded subpixel rounding errors with contiguous spaces. + if (theString.charCodeAt(glyphIndex + 1) !== 32) + { + currentAnchor = rangeWidth; + measuringRange = CPMakeRange(glyphIndex + 1, 0); + } + break; case 10: