From de2ffd3aaddbcb4d93960a1a1f4b28386e129d18 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 18 Jun 2026 19:08:45 +0200 Subject: [PATCH 01/11] New: implement baseline offsets and superscript/subscript support --- AppKit/CPFont.j | 16 +++ AppKit/CPTextView/CPLayoutManager.j | 50 ++++++- AppKit/CPTextView/CPTypesetter.j | 55 ++++++-- Tests/Manual/CPTextView/AppController.j | 173 ++++++++++++++++++++++++ 4 files changed, 280 insertions(+), 14 deletions(-) diff --git a/AppKit/CPFont.j b/AppKit/CPFont.j index fa8884278..0bbd3d448 100644 --- a/AppKit/CPFont.j +++ b/AppKit/CPFont.j @@ -464,6 +464,22 @@ following: return _CPRealFontSize(_size); } +/*! + Returns the font size. Cocoa/AppKit compatibility alias for -size. +*/ +- (float)pointSize +{ + return [self size]; +} + +/*! + Returns the font name. Cocoa/AppKit compatibility alias for -familyName. +*/ +- (CPString)fontName +{ + return [self familyName]; +} + /*! Returns the font as a CSS string */ diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index 32810373b..b1d22ea80 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -31,8 +31,9 @@ @import "CPFont.j" @global _MakeRangeFromAbs - @global document +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName @class CPTextContainer @class CPTextView @@ -1225,7 +1226,7 @@ var _objectsInRange = function(aList, aRange) if (![attributes objectForKey:_CPAttachmentInvisible]) { var view = [attributes objectForKey:_CPAttachmentView]; - var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle}; + var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle, underline:underline, baselineOffset:0.0}; _runs.push(run); } } @@ -1235,6 +1236,34 @@ var _objectsInRange = function(aList, aRange) bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName], font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0]; + var baselineOffset = [attributes objectForKey:CPBaselineOffsetAttributeName], + superscript = [attributes objectForKey:CPSuperscriptAttributeName]; + + if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number") + baselineOffset = 0.0; + + if (superscript === nil || superscript === undefined || typeof superscript !== "number") + superscript = 0; + + if (superscript !== 0) + { + var size = [font size], + scaledSize = size * 0.65, + fontName = [font familyName], + isBold = [font isBold], + isItalic = [font isItalic]; + + font = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic]; + + if (baselineOffset === 0.0) + { + if (superscript > 0) + baselineOffset = size * 0.35; + else + baselineOffset = -size * 0.15; + } + } + var currentLoc = effectiveRange.location, strLen = string.length, startIdx = 0; @@ -1254,7 +1283,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: subString, bgcolor: bgcolor, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: baselineOffset }; _runs.push(run); } @@ -1267,7 +1298,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: nil, bgcolor: nil, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: 0.0 }; _runs.push(tabRun); @@ -1286,7 +1319,9 @@ var _objectsInRange = function(aList, aRange) elem: nil, string: subString, bgcolor: bgcolor, - paragraphStyle: paragraphStyle + paragraphStyle: paragraphStyle, + underline: underline, + baselineOffset: baselineOffset }; _runs.push(run); } @@ -1458,7 +1493,10 @@ var _objectsInRange = function(aList, aRange) if (!_RectEqualToRectHorizontally(newLineFragment._fragmentRect, _fragmentRect)) return NO; - if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor || newFragmentRuns[i].font !== oldFragmentRuns[i].font) + if (newFragmentRuns[i].color !== oldFragmentRuns[i].color || + newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor || + newFragmentRuns[i].font !== oldFragmentRuns[i].font || + newFragmentRuns[i].baselineOffset !== oldFragmentRuns[i].baselineOffset) return NO; var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle], diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 06c8a368f..074c3b175 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -30,6 +30,9 @@ @import "CPTextStorage.j" @import "CPFont.j" +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName + // forward declare these classes for type matching @class CPLayoutManager @class CPTextContainer @@ -353,9 +356,41 @@ var CPSystemTypesetterFactory, if (!currentFont) currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0]; - ascent = [currentFont ascender]; - descent = [currentFont descender]; - leading = (ascent - descent) * 0.2; // FAKE leading + // Safely retrieve and validate CPBaselineOffsetAttributeName + var baselineOffset = [_currentAttributes objectForKey:CPBaselineOffsetAttributeName]; + if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number") + baselineOffset = 0.0; + + // Safely retrieve and validate CPSuperscriptAttributeName + var superscript = [_currentAttributes objectForKey:CPSuperscriptAttributeName]; + if (superscript === nil || superscript === undefined || typeof superscript !== "number") + superscript = 0; + + if (superscript !== 0) + { + var size = [currentFont size], + scaledSize = size * 0.65, + fontName = [currentFont familyName], + isBold = [currentFont isBold], + isItalic = [currentFont isItalic]; + + currentFont = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic]; + + if (baselineOffset === 0.0) + { + if (superscript > 0) + baselineOffset = size * 0.35; + else + baselineOffset = -size * 0.15; + } + } + + var fontAscent = [currentFont ascender] || 0.0, + fontDescent = [currentFont descender] || 0.0; + + ascent = fontAscent + baselineOffset; + descent = fontDescent + baselineOffset; + leading = (fontAscent - fontDescent) * 0.2; // FAKE leading currentFontLineHeight = ascent - descent + leading; @@ -368,11 +403,15 @@ var CPSystemTypesetterFactory, } - if (currentFontLineHeight > _lineHeight) - _lineHeight = currentFontLineHeight; + // Clean bounds logic to prevent NaN and layout calculation overhead + var currentAscent = (ascent === undefined || isNaN(ascent)) ? 0.0 : ascent, + currentLineHeight = (currentFontLineHeight === undefined || isNaN(currentFontLineHeight)) ? 12.0 : currentFontLineHeight; - if (ascent > _lineBase) - _lineBase = ascent; + if (currentLineHeight > _lineHeight) + _lineHeight = currentLineHeight; + + if (currentAscent > _lineBase) + _lineBase = currentAscent; lineRange.length++; measuringRange.length++; @@ -546,7 +585,7 @@ var CPSystemTypesetterFactory, isNewline = NO; _lineFragments = []; _lineHeight = 0; - _lineBase = ascent; + _lineBase = 0; isStartOfPhysicalLine = YES; } diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 3fab7a14c..dd6513a89 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -17,6 +17,9 @@ @import @import +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName + @implementation AppController : CPObject { CPTextView _textView; @@ -77,6 +80,82 @@ [_textView alignJustified:self]; } +- (void)makeSuperscript:(id)sender +{ + var range = [_textView selectedRange]; + if (range.length > 0) + { + var textStorage = [_textView textStorage]; + [textStorage beginEditing]; + [textStorage removeAttribute:CPBaselineOffsetAttributeName range:range]; + [textStorage addAttribute:CPSuperscriptAttributeName value:1 range:range]; + [textStorage endEditing]; + [_textView setNeedsDisplay:YES]; + } +} + +- (void)makeSubscript:(id)sender +{ + var range = [_textView selectedRange]; + if (range.length > 0) + { + var textStorage = [_textView textStorage]; + [textStorage beginEditing]; + [textStorage removeAttribute:CPBaselineOffsetAttributeName range:range]; + [textStorage addAttribute:CPSuperscriptAttributeName value:-1 range:range]; + [textStorage endEditing]; + [_textView setNeedsDisplay:YES]; + } +} + +- (void)raiseBaseline:(id)sender +{ + var range = [_textView selectedRange]; + if (range.length > 0) + { + var textStorage = [_textView textStorage]; + [textStorage beginEditing]; + + var currentOffset = [textStorage attribute:CPBaselineOffsetAttributeName atIndex:range.location effectiveRange:nil] || 0.0; + var newOffset = currentOffset + 2.0; + + [textStorage addAttribute:CPBaselineOffsetAttributeName value:newOffset range:range]; + [textStorage endEditing]; + [_textView setNeedsDisplay:YES]; + } +} + +- (void)lowerBaseline:(id)sender +{ + var range = [_textView selectedRange]; + if (range.length > 0) + { + var textStorage = [_textView textStorage]; + [textStorage beginEditing]; + + var currentOffset = [textStorage attribute:CPBaselineOffsetAttributeName atIndex:range.location effectiveRange:nil] || 0.0; + var newOffset = currentOffset - 2.0; + + [textStorage addAttribute:CPBaselineOffsetAttributeName value:newOffset range:range]; + [textStorage endEditing]; + [_textView setNeedsDisplay:YES]; + } +} + +- (void)resetBaseline:(id)sender +{ + var range = [_textView selectedRange]; + if (range.length > 0) + { + var textStorage = [_textView textStorage]; + [textStorage beginEditing]; + [textStorage removeAttribute:CPBaselineOffsetAttributeName range:range]; + [textStorage removeAttribute:CPSuperscriptAttributeName range:range]; + [textStorage endEditing]; + [_textView setNeedsDisplay:YES]; + } +} + - (void)insertAttachment:(id)sender { // Insert modern spinner image attachment @@ -215,6 +294,48 @@ [alignJustifyBtn setTarget:self]; [alignJustifyBtn setAction:@selector(alignJustified:)]; [toolbarView addSubview:alignJustifyBtn]; + currentX += 80; + + // Baseline & Script Testing Group + var labelBaseline = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 85, 20)]; + [labelBaseline setStringValue:@"Baseline:"]; + [labelBaseline setFont:[CPFont systemFontOfSize:12]]; + [toolbarView addSubview:labelBaseline]; + currentX += 85; + + var superBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 45, 30)]; + [superBtn setTitle:@"x²"]; + [superBtn setTarget:self]; + [superBtn setAction:@selector(makeSuperscript:)]; + [toolbarView addSubview:superBtn]; + currentX += 50; + + var subBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 45, 30)]; + [subBtn setTitle:@"x₂"]; + [subBtn setTarget:self]; + [subBtn setAction:@selector(makeSubscript:)]; + [toolbarView addSubview:subBtn]; + currentX += 50; + + var raiseBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)]; + [raiseBtn setTitle:@"Base+"]; + [raiseBtn setTarget:self]; + [raiseBtn setAction:@selector(raiseBaseline:)]; + [toolbarView addSubview:raiseBtn]; + currentX += 55; + + var lowerBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)]; + [lowerBtn setTitle:@"Base-"]; + [lowerBtn setTarget:self]; + [lowerBtn setAction:@selector(lowerBaseline:)]; + [toolbarView addSubview:lowerBtn]; + currentX += 55; + + var normalBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 60, 30)]; + [normalBtn setTitle:@"Normal"]; + [normalBtn setTarget:self]; + [normalBtn setAction:@selector(resetBaseline:)]; + [toolbarView addSubview:normalBtn]; // Default return key target test var returnButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth([contentView bounds]) - 270, 15, 250, 30)]; @@ -305,6 +426,13 @@ [formatMenu addItemWithTitle:@"Italic" action:@selector(italic:) keyEquivalent:@"i"]; [formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"]; [formatMenu addItem:[CPMenuItem separatorItem]]; + // Baseline & Scripts + [formatMenu addItemWithTitle:@"Superscript" action:@selector(makeSuperscript:) keyEquivalent:@"="]; + [formatMenu addItemWithTitle:@"Subscript" action:@selector(makeSubscript:) keyEquivalent:@"-"]; + [formatMenu addItemWithTitle:@"Raise Baseline" action:@selector(raiseBaseline:) keyEquivalent:@"+"]; + [formatMenu addItemWithTitle:@"Lower Baseline" action:@selector(lowerBaseline:) keyEquivalent:@"_"]; + [formatMenu addItemWithTitle:@"Reset Baseline" action:@selector(resetBaseline:) keyEquivalent:@"0"]; + [formatMenu addItem:[CPMenuItem separatorItem]]; // Alignment [formatMenu addItemWithTitle:@"Align Left" action:@selector(alignLeft:) keyEquivalent:@"{"]; [formatMenu addItemWithTitle:@"Center" action:@selector(alignCenter:) keyEquivalent:@"|"]; @@ -327,6 +455,51 @@ attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], elegantForeground, elegantBackground] forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]]; + // VISUAL TEST CASES: Baseline, Superscript, and Subscript Features + [_textView insertText:@"\n"]; + var sectionHeaderColor = [CPColor colorWithRed:0.5 green:0.25 blue:0.1 alpha:1.0]; + [_textView insertText:[[CPAttributedString alloc] initWithString:@"Baseline Shift & Superscript/Subscript Showcase\n" + attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:16], sectionHeaderColor] + forKeys:[CPFontAttributeName, CPForegroundColorAttributeName]]]]; + + var normalFont = [CPFont systemFontOfSize:14.0]; + + // Test Case: E = mc² (Superscript) + var formulaEnergy = [[CPAttributedString alloc] initWithString:@" • Energy mass equivalence: E = mc" attributes:@{ CPFontAttributeName: normalFont }]; + var scriptTwo = [[CPAttributedString alloc] initWithString:@"2" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }]; + [_textView insertText:formulaEnergy]; + [_textView insertText:scriptTwo]; + [_textView insertText:@"\n"]; + + // Test Case: H₂O (Subscript) + var formulaWater = [[CPAttributedString alloc] initWithString:@" • Chemical formula: H" attributes:@{ CPFontAttributeName: normalFont }]; + var scriptSubTwo = [[CPAttributedString alloc] initWithString:@"2" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: -1 }]; + var formulaWaterEnd = [[CPAttributedString alloc] initWithString:@"O\n" attributes:@{ CPFontAttributeName: normalFont }]; + [_textView insertText:formulaWater]; + [_textView insertText:scriptSubTwo]; + [_textView insertText:formulaWaterEnd]; + + // Test Case: Ordinals + var ordText = [[CPAttributedString alloc] initWithString:@" • Ordinals: 1" attributes:@{ CPFontAttributeName: normalFont }]; + var st = [[CPAttributedString alloc] initWithString:@"st" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }]; + var rdText = [[CPAttributedString alloc] initWithString:@", 3" attributes:@{ CPFontAttributeName: normalFont }]; + var rd = [[CPAttributedString alloc] initWithString:@"rd" attributes:@{ CPFontAttributeName: normalFont, CPSuperscriptAttributeName: 1 }]; + [_textView insertText:ordText]; + [_textView insertText:st]; + [_textView insertText:rdText]; + [_textView insertText:rd]; + [_textView insertText:@"\n"]; + + // Test Case: Custom Baseline Offsets + var offsetLead = [[CPAttributedString alloc] initWithString:@" • Custom Offsets: " attributes:@{ CPFontAttributeName: normalFont }]; + var offsetUp = [[CPAttributedString alloc] initWithString:@"Raised " attributes:@{ CPFontAttributeName: normalFont, CPBaselineOffsetAttributeName: 4.0 }]; + var offsetDown = [[CPAttributedString alloc] initWithString:@"Lowered " attributes:@{ CPFontAttributeName: normalFont, CPBaselineOffsetAttributeName: -4.0 }]; + var offsetNormal = [[CPAttributedString alloc] initWithString:@"Standard\n" attributes:@{ CPFontAttributeName: normalFont }]; + [_textView insertText:offsetLead]; + [_textView insertText:offsetUp]; + [_textView insertText:offsetDown]; + [_textView insertText:offsetNormal]; + // Highlighted Heading - Pine & Sage Green tones [_textView insertText:@"\n"]; var showcaseForeground = [CPColor colorWithRed:0.15 green:0.25 blue:0.15 alpha:1.0]; // Forest Green From e67c1419212779362b10f0e5cc62e6525e0c9034 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 18 Jun 2026 22:21:50 +0200 Subject: [PATCH 02/11] new: color panel --- Tests/Manual/CPTextView/AppController.j | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index dd6513a89..c8cb009df 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -8,6 +8,7 @@ @import @import @import +@import @import @import @import @@ -55,6 +56,11 @@ [[CPFontManager sharedFontManager] orderFrontFontPanel:self]; } +- (void)orderFrontColorPanel:(id)sender +{ + [[CPColorPanel sharedColorPanel] orderFront:self]; +} + - (void)toggleRuler:(id)sender { [_scrollView setRulersVisible:![_scrollView rulersVisible]]; @@ -420,6 +426,7 @@ var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"]; [formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"t"]; + [formatMenu addItemWithTitle:@"Color panel" action:@selector(orderFrontColorPanel:) keyEquivalent:@"C"]; [formatMenu addItem:[CPMenuItem separatorItem]]; // Styles [formatMenu addItemWithTitle:@"Bold" action:@selector(bold:) keyEquivalent:@"b"]; From d40a151e056eb843784d66f1079332d4165b44f8 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 07:18:52 +0200 Subject: [PATCH 03/11] new: colorpanel sync --- AppKit/CPTextView/CPTextView.j | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index ddba66801..0398d8f46 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1005,6 +1005,25 @@ Sets the selection to a range of characters in response to user action. if (doOverwrite && _placeholderString == nil && isNewSelection) [self setTypingAttributes:[_textStorage attributesAtIndex:CPMaxRange(range) effectiveRange:nil]]; + // Update the shared CPColorPanel with the active selection color + if ([self _isFirstResponder] && [_textStorage length] > 0) + { + var currentTextColor = [self textColor] || [CPColor blackColor]; + + if ([self isRichText]) + { + var charIndex = _selectionRange.location; + if (charIndex >= [_textStorage length]) + charIndex = MAX(0, charIndex - 1); + + var attributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + if ([attributes objectForKey:CPForegroundColorAttributeName]) + currentTextColor = [attributes objectForKey:CPForegroundColorAttributeName]; + } + + [[CPColorPanel sharedColorPanel] setColor:currentTextColor]; + } + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeSelectionNotification object:self]; } @@ -1799,6 +1818,13 @@ Sets the selection to a range of characters in response to user action. // SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON TYPING ATTRIBUTES CHANGE [self updateRuler]; + // Synchronize CPColorPanel if text view is active + if ([self _isFirstResponder]) + { + var currentTextColor = [_typingAttributes objectForKey:CPForegroundColorAttributeName] || [self textColor] || [CPColor blackColor]; + [[CPColorPanel sharedColorPanel] setColor:currentTextColor]; + } + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; // We always clear the saved selection range from the last mouse down event here. From c187f5de1d77fc3d306208ebe50aa145c2cae3e3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 08:48:17 +0200 Subject: [PATCH 04/11] fixed: rtf roundtrip with baseline stuff --- AppKit/CPTextView/_CPRTFParser.j | 50 ++++++++++++++++++++++++++++++ AppKit/CPTextView/_CPRTFProducer.j | 22 +++++++------ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 6a0ba91ea..69bd0e224 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -38,6 +38,8 @@ @global CPParagraphStyleAttributeName @global CPAttachmentAttributeName @global CPUnderlineStyleAttributeName +@global CPBaselineOffsetAttributeName +@global CPSuperscriptAttributeName @global CPLeftTabStopType @global CPRightTabStopType @@ -72,6 +74,8 @@ var cp1252Map = { BOOL script; BOOL _tabChanged; CPTabStopType _nextTabType; + int superscript; + float baselineOffset; } - (id)init @@ -104,6 +108,8 @@ var cp1252Map = { mynew.ulColour = ulColour; mynew._tabChanged = _tabChanged; mynew._nextTabType = _nextTabType; + mynew.superscript = superscript; + mynew.baselineOffset = baselineOffset; return mynew; } @@ -169,6 +175,8 @@ var cp1252Map = { underline = 0; strikethrough = 0; script = 0; + superscript = 0; + baselineOffset = 0.0; } - (void)addTab:(float)location type:(CPTextTabType)type @@ -212,6 +220,12 @@ var cp1252Map = { if (underline) [ret setObject:[CPNumber numberWithInt:1] forKey:CPUnderlineStyleAttributeName]; + if (superscript !== 0) + [ret setObject:[CPNumber numberWithInt:superscript] forKey:CPSuperscriptAttributeName]; + + if (baselineOffset !== 0.0) + [ret setObject:[CPNumber numberWithFloat:baselineOffset] forKey:CPBaselineOffsetAttributeName]; + return ret; } @end @@ -226,6 +240,12 @@ var kRgsymRtf = { "b" : [ "b", 1, false, kRTFParserType_prop, "propBold"], "ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"], "i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"], + "super" : [ "super", 1, true, kRTFParserType_prop, "propSuper"], + "sub" : [ "sub", 1, true, kRTFParserType_prop, "propSub"], + "nosupersub" : [ "nosupersub",1, true, kRTFParserType_prop, "propNoSuperSub"], + "up" : [ "up", 6, false, kRTFParserType_prop, "propUp"], + "dn" : [ "dn", 6, false, kRTFParserType_prop, "propDn"], + "plain" : [ "plain", 0, false, kRTFParserType_prop, "propPlain"], "pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"], "pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"], "qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"], @@ -621,6 +641,36 @@ var kRgsymRtf = { } break; + case "super": + [self _flushCurrentRun]; + _currentRun.superscript = 1; + break; + + case "sub": + [self _flushCurrentRun]; + _currentRun.superscript = -1; + break; + + case "nosupersub": + [self _flushCurrentRun]; + _currentRun.superscript = 0; + break; + + case "up": + [self _flushCurrentRun]; + _currentRun.baselineOffset = parseFloat(param) / 2.0; + break; + + case "dn": + [self _flushCurrentRun]; + _currentRun.baselineOffset = -parseFloat(param) / 2.0; + break; + + case "plain": + [self _flushCurrentRun]; + [_currentRun resetFont]; + break; + case "qc": // paragraph center [_currentRun.paragraph setAlignment:CPCenterTextAlignment]; break; diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 1e48afda8..116ebf997 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -646,28 +646,29 @@ function _points2twips(a) { return (a) * 20.0; } else if ([currAttrib isEqualToString:CPUnderlineStyleAttributeName]) { headerString += @"\\ul"; - trailerString += @"\\ulnone"; + trailerString += @"\\ulnone "; // trailing space important! } else if ([currAttrib isEqualToString:CPSuperscriptAttributeName]) { var value = [attributes objectForKey:CPSuperscriptAttributeName], - svalue = [value intValue] * 6; + ivalue = [value intValue]; - if (svalue > 0) + if (ivalue > 0) { - headerString += [CPString stringWithFormat:@"\\up%d", svalue]; - trailerString += @"\\up0"; + headerString += @"\\super"; + trailerString += @"\\nosupersub "; // trailing space important! } - else if (svalue < 0) + else if (ivalue < 0) { - headerString += [CPString stringWithFormat:@"\\dn-%d", svalue]; - trailerString += @"\\dn0"; + headerString += @"\\sub"; + trailerString += @"\\nosupersub "; // trailing space important! } } else if ([currAttrib isEqualToString:CPBaselineOffsetAttributeName]) { var value = [attributes objectForKey:CPBaselineOffsetAttributeName], - svalue = [value floatValue] * 2; + fvalue = [value floatValue], + svalue = Math.round(fvalue * 2.0); // Convert standard points to RTF half-points if (svalue > 0) { @@ -676,7 +677,8 @@ function _points2twips(a) { return (a) * 20.0; } } else if (svalue < 0) { - headerString += [CPString stringWithFormat:@"\\dn-%d", svalue]; + // Correct negative formatting using safe positive boundary + headerString += [CPString stringWithFormat:@"\\dn%d", Math.abs(svalue)]; trailerString += @"\\dn0"; } } From beb4aef42422ca161379e8b269f399887176b1df Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 09:47:39 +0200 Subject: [PATCH 05/11] new: heightTracksTextView support in CPTextContainer --- AppKit/CPTextView/CPTextContainer.j | 59 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/AppKit/CPTextView/CPTextContainer.j b/AppKit/CPTextView/CPTextContainer.j index 48490579e..e851de4dd 100644 --- a/AppKit/CPTextView/CPTextContainer.j +++ b/AppKit/CPTextView/CPTextContainer.j @@ -148,27 +148,58 @@ CPLineMovesUp = 4; - (void)setWidthTracksTextView:(BOOL)flag { - _widthTracksTextView = flag; - [_textView setPostsFrameChangedNotifications:flag]; + if (_widthTracksTextView === flag) + return; - if (flag && _textView) - { - [[CPNotificationCenter defaultCenter] addObserver:self - selector:@selector(textViewFrameChanged:) - name:CPViewFrameDidChangeNotification - object:_textView]; - } - else + _widthTracksTextView = flag; + [self _updateFrameObserver]; +} + +// Controls whether the receiver adjusts the height of its bounding rectangle when its text view is resized. +- (BOOL)heightTracksTextView +{ + return _heightTracksTextView; +} + +- (void)setHeightTracksTextView:(BOOL)flag +{ + if (_heightTracksTextView === flag) + return; + + _heightTracksTextView = flag; + [self _updateFrameObserver]; +} + +- (void)_updateFrameObserver +{ + if (_textView) { [[CPNotificationCenter defaultCenter] removeObserver:self name:CPViewFrameDidChangeNotification object:_textView]; + + var flag = _widthTracksTextView || _heightTracksTextView; + [_textView setPostsFrameChangedNotifications:flag]; + + if (flag) + { + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(textViewFrameChanged:) + name:CPViewFrameDidChangeNotification + object:_textView]; + } } } - (void)textViewFrameChanged:(CPNotification)aNotification { - var newSize = CGSizeMake([_textView frame].size.width, _size.height); + var newSize = CGSizeMake(_size.width, _size.height); + + if (_widthTracksTextView) + newSize.width = [_textView frame].size.width; + + if (_heightTracksTextView) + newSize.height = [_textView frame].size.height; [self setContainerSize:newSize]; } @@ -177,7 +208,9 @@ CPLineMovesUp = 4; { if (_textView) { - [self setWidthTracksTextView:NO]; // We only support width + [[CPNotificationCenter defaultCenter] removeObserver:self + name:CPViewFrameDidChangeNotification + object:_textView]; [_textView setTextContainer:nil]; } @@ -185,7 +218,7 @@ CPLineMovesUp = 4; if (_textView) { - [self setWidthTracksTextView:_widthTracksTextView]; // We only support width + [self _updateFrameObserver]; [_textView setTextContainer:self]; } From 89445f94a2b145e4f32dc27199f6df10e2d19c41 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 22:26:52 +0200 Subject: [PATCH 06/11] fixed: whitespace before tab --- AppKit/CPTextView/_CPRTFParser.j | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 69bd0e224..f04654a05 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -334,6 +334,7 @@ var kRgsymRtf = { CPArray _fontArray; CPString _freename; BOOL _parsingFontTable; + BOOL _keywordIsControlWord; // Table parsing state BOOL _inTableActive; @@ -353,6 +354,7 @@ var kRgsymRtf = { _states = []; _currentParseIndex = 0; _hexreturn = NO; + _keywordIsControlWord = NO; _result = [CPAttributedString new]; _colorArray = []; _fontArray = ['Arial']; // FIXME: should be name of system font @@ -889,7 +891,12 @@ var kRgsymRtf = { ch = rtf.charAt(_currentParseIndex); if (!/[a-zA-Z]/.test(ch)) + { + _keywordIsControlWord = NO; return [self _translateKeyword:ch parameter:nil fParameter:fParam]; + } + + _keywordIsControlWord = YES; while (new RegExp("[a-zA-Z]").test(ch)) { @@ -978,6 +985,7 @@ var kRgsymRtf = { break; case "{": + lastchar = 0; if (_waitingForNextRow) [self _flushTableIfAny]; @@ -986,6 +994,7 @@ var kRgsymRtf = { break; case "}": + lastchar = 0; if (_waitingForNextRow) [self _flushTableIfAny]; @@ -1009,7 +1018,7 @@ var kRgsymRtf = { _freename = ''; ch = [self _parseKeyword:rtf length:len]; - if (!_hexreturn && ch.length == 0) + if (!_hexreturn && _keywordIsControlWord) lastchar = 1; else lastchar = 0; @@ -1040,6 +1049,7 @@ var kRgsymRtf = { case 0x0a: case '\n': case '\r': + lastchar = 0; break; default: From 319bcdaba92dacb1b615a431ddf691f03049986b Mon Sep 17 00:00:00 2001 From: daboe01 Date: Fri, 19 Jun 2026 22:56:38 +0200 Subject: [PATCH 07/11] fixed: table roundtrip issue --- AppKit/CPTextView/_CPRTFProducer.j | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/AppKit/CPTextView/_CPRTFProducer.j b/AppKit/CPTextView/_CPRTFProducer.j index 116ebf997..a0625f30b 100644 --- a/AppKit/CPTextView/_CPRTFProducer.j +++ b/AppKit/CPTextView/_CPRTFProducer.j @@ -542,6 +542,17 @@ function _points2twips(a) { return (a) * 20.0; } cellText = ""; } + // Safely extract text representation from CPAttributedString / CPTextStorage if present + if (cellText && typeof cellText === "object") { + if (typeof cellText.string === "function") { + cellText = [cellText string]; + } else if (cellText._string !== undefined) { + cellText = cellText._string; + } else if (cellText.string !== undefined) { + cellText = cellText.string; + } + } + cellText = String(cellText); cellText = cellText.replace(/\\/g, '\\\\'); cellText = cellText.replace(/{/g, '\\{'); @@ -715,7 +726,7 @@ function _points2twips(a) { return (a) * 20.0; } var nobraces; if ([headerString length]) - nobraces = [CPString stringWithFormat:@"%@ %@}", headerString, substring]; + nobraces = [CPString stringWithFormat:@"%@ %@", headerString, substring]; else nobraces = substring; From 15bbca8d776f67fa2d1ce9380467aabf5ce027f6 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 10:36:07 +0200 Subject: [PATCH 08/11] fixed: whitespace issue --- AppKit/CPTextView/_CPRTFParser.j | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index f04654a05..9028e49f5 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -979,7 +979,11 @@ var kRgsymRtf = { else { _freename += tmp; - [self _appendPlainString:tmp]; + // Only append literal spaces to the document if we are in the active body state + if (_curState == 0) + { + [self _appendPlainString:tmp]; + } } break; @@ -1025,7 +1029,8 @@ var kRgsymRtf = { if (_hexreturn) { - if (ch.length > 0) + // Only append decoded characters if we are in the active body state + if (ch.length > 0 && _curState === 0) { var byteVal = parseInt(ch, 16); var unicodeVal = byteVal; From 26b2280fc2946dde0867b624460706029b9090f3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 10:40:46 +0200 Subject: [PATCH 09/11] fixed: color spill issue --- AppKit/CPTextView/_CPRTFParser.j | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 9028e49f5..b2e80b295 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -797,10 +797,15 @@ var kRgsymRtf = { [self _flushCurrentRun]; var fontIndex = parseInt(param) - 1; - if (_currentRun && fontIndex >= 0) - _currentRun.fgColour = _colorArray[fontIndex]; + if (_currentRun) + { + if (fontIndex >= 0 && fontIndex < _colorArray.length) + _currentRun.fgColour = _colorArray[fontIndex]; + else + _currentRun.fgColour = nil; + } - break; + break; case "cb": // change background color case "highlight": From 766035f4b71aa64ec9fdfe1426c4a72424c0382a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 12:06:24 +0200 Subject: [PATCH 10/11] Fixed: baseline alignment in _CPLineFragment to prevent overlapping text descenders --- AppKit/CPTextView/CPLayoutManager.j | 10 ++++------ AppKit/CPTextView/CPTextView.j | 28 +++++++++++++--------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index b1d22ea80..4bd345dc4 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -965,16 +965,11 @@ _oncontextmenuhandler = function () { return false; }; if (frame) { var correctedRect = CGRectCreateCopy(frame); - correctedRect.size.height -= frame._descent; - correctedRect.origin.y -= frame._descent; if (!rect) rect = CGRectCreateCopy(correctedRect); else rect = CGRectUnion(rect, correctedRect); - - if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)])) - rect.size.width = containerSize.width - rect.origin.x; } } } @@ -1348,7 +1343,10 @@ var _objectsInRange = function(aList, aRange) { _glyphsFrames[i] = CGRectMake(origin.x, origin.y, someAdvancements[i].width, height); _glyphsFrames[i]._descent = someAdvancements[i].descent; - _glyphsOffsets[i] = height - someAdvancements[i].height; + + // Align the run's baseline with the common line baseline (_location.y) + _glyphsOffsets[i] = _location.y - someAdvancements[i].height; + origin.x += someAdvancements[i].width; } } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 0398d8f46..71f1b2c46 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2333,30 +2333,28 @@ Sets the selection to a range of characters in response to user action. if (_selectionRange.location == numberOfGlyphs && _isNewlineCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1])) return CGRectCreateCopy([_layoutManager extraLineFragmentRect]); - var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; + var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; - var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, - caretOffset = [_layoutManager _characterOffsetAtLocation:loc], - oldYPosition = CGRectGetMaxY(caretRect), - caretDescend = [_layoutManager _descentAtLocation:loc]; + var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, + caretOffset = [_layoutManager _characterOffsetAtLocation:loc], + font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font]; - if (caretOffset > 0) - { - caretRect.origin.y += caretOffset; - caretRect.size.height = oldYPosition - caretRect.origin.y; - } + if (caretOffset > 0) + { + caretRect.origin.y += caretOffset; + } - if (caretDescend < 0) - caretRect.size.height -= caretDescend; + // Set the caret height to match the size of the active font + caretRect.size.height = [font size]; - if (_selectionRange.location == numberOfGlyphs) - caretRect.origin.x += caretRect.size.width; + if (_selectionRange.location == numberOfGlyphs) + caretRect.origin.x += caretRect.size.width; caretRect.origin.x += _textContainerOrigin.x; caretRect.origin.y += _textContainerOrigin.y; caretRect.size.width = MAX(1.0, caretRect.size.width); - caretRect.size.height = MAX(1.0, caretRect.size.height); + caretRect.size.height = MAX(1.0, caretRect.size.height) + 2; return caretRect; } From c8789c5450db0efd441b748bb770399a70b50e38 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Sat, 20 Jun 2026 12:32:10 +0200 Subject: [PATCH 11/11] formatting --- AppKit/CPTextView/CPTextView.j | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 71f1b2c46..b4260e217 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2333,22 +2333,22 @@ Sets the selection to a range of characters in response to user action. if (_selectionRange.location == numberOfGlyphs && _isNewlineCharacter([[_textStorage string] characterAtIndex:_selectionRange.location - 1])) return CGRectCreateCopy([_layoutManager extraLineFragmentRect]); - var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; + var caretRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(_selectionRange.location, 1) inTextContainer:_textContainer]; - var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, - caretOffset = [_layoutManager _characterOffsetAtLocation:loc], - font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font]; + var loc = (_selectionRange.location == numberOfGlyphs) ? _selectionRange.location - 1 : _selectionRange.location, + caretOffset = [_layoutManager _characterOffsetAtLocation:loc], + font = [_textStorage attribute:CPFontAttributeName atIndex:loc effectiveRange:nil] || [self font]; - if (caretOffset > 0) - { - caretRect.origin.y += caretOffset; - } + if (caretOffset > 0) + { + caretRect.origin.y += caretOffset; + } - // Set the caret height to match the size of the active font - caretRect.size.height = [font size]; + // Set the caret height to match the size of the active font + caretRect.size.height = [font size]; - if (_selectionRange.location == numberOfGlyphs) - caretRect.origin.x += caretRect.size.width; + if (_selectionRange.location == numberOfGlyphs) + caretRect.origin.x += caretRect.size.width; caretRect.origin.x += _textContainerOrigin.x; caretRect.origin.y += _textContainerOrigin.y;