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