New: implement baseline offsets and superscript/subscript support

This commit is contained in:
daboe01
2026-06-18 19:08:45 +02:00
parent bc0380e1cb
commit de2ffd3aad
4 changed files with 280 additions and 14 deletions
+16
View File
@@ -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
*/
+44 -6
View File
@@ -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],
+47 -8
View File
@@ -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;
}
+173
View File
@@ -17,6 +17,9 @@
@import <AppKit/CPTextStorage.j>
@import <AppKit/_CPTableTextAttachment.j>
@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