new: constrain tab stop marker dragging to preceding text boundary

This commit is contained in:
daboe01
2026-08-26 19:01:32 +02:00
parent 378f7bbda1
commit 874792fa6e
3 changed files with 118 additions and 32 deletions
+11 -2
View File
@@ -377,13 +377,17 @@ CPRulerOrientationVertical = 1;
}
else
{
// Allow client view to constrain initial placement
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)])
rulerLocation = [client rulerView:self willAddMarker:nil atLocation:rulerLocation];
var newMarker = [[CPRulerMarker alloc] initWithRulerView:self
markerLocation:rulerLocation
imageValue:rulerLocation
representedObject:nil];
[self addMarker:newMarker];
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)])
[client rulerView:self didAddMarker:newMarker];
@@ -405,6 +409,12 @@ CPRulerOrientationVertical = 1;
var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y),
newLocation = Math.max(0.0, _dragStartLocation + delta);
// Constrain marker location to last possible position
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)])
newLocation = [client rulerView:self willMoveMarker:_draggingMarker toLocation:newLocation];
[_draggingMarker setImageValue:newLocation];
[self _positionMarker:_draggingMarker];
@@ -426,7 +436,6 @@ CPRulerOrientationVertical = 1;
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
}
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)])
[client rulerView:self didMoveMarker:_draggingMarker];
}
+90
View File
@@ -2729,6 +2729,96 @@ var compareTabStops = function(obj1, obj2, context) {
[self setNeedsDisplay:YES];
}
- (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)proposedLocation
{
var rep = [aMarker representedObject];
// Indent markers can be dragged anywhere >= 0
if (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent")
return Math.max(0.0, proposedLocation);
var textLength = [_textStorage length];
if (textLength === 0)
return Math.max(0.0, proposedLocation);
var selectedRange = [self selectedRange],
targetRange = selectedRange;
if (targetRange.length === 0)
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
if (targetRange.length === 0)
return Math.max(0.0, proposedLocation);
[_layoutManager _validateLayoutAndGlyphs];
var theString = [_textStorage string],
minLocation = 0.0,
currentParagraphStyle = [_textStorage attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:nil] || [CPParagraphStyle defaultParagraphStyle],
tabStops = [currentParagraphStyle tabStops] || [];
// Find which tab stop index this marker represents
var targetTabIndex = -1;
for (var i = 0; i < [tabStops count]; i++)
{
var tab = [tabStops objectAtIndex:i];
if (tab === rep || ([tab location] === [rep location] && [tab alignment] === [rep alignment]))
{
targetTabIndex = i;
break;
}
}
var start = targetRange.location,
end = CPMaxRange(targetRange),
tabCountInLine = 0,
lineStart = start;
for (var i = start; i < end; i++)
{
var charCode = theString.charCodeAt(i);
if (charCode === 10 || charCode === 13)
{
tabCountInLine = 0;
lineStart = i + 1;
continue;
}
if (charCode === 9) // '\t'
{
// If this tab corresponds to the marker being dragged
if (tabCountInLine === targetTabIndex || targetTabIndex === -1)
{
var precedingX = 0.0;
if (i === lineStart)
{
var isFirstLine = (lineStart === 0 || theString.charCodeAt(lineStart - 1) === 10 || theString.charCodeAt(lineStart - 1) === 13);
precedingX = isFirstLine ? [currentParagraphStyle firstLineHeadIndent] : [currentParagraphStyle headIndent];
}
else
{
// Find right edge of glyph immediately preceding the tab
var glyphRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(i - 1, 1) inTextContainer:_textContainer];
precedingX = CGRectGetMaxX(glyphRect);
}
// Minimum safety spacer (e.g. + 2px after the preceding glyph)
var limit = precedingX + 2.0;
if (limit > minLocation)
minLocation = limit;
}
tabCountInLine++;
}
}
return Math.max(minLocation, proposedLocation);
}
- (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)proposedLocation
{
return [self rulerView:aRulerView willMoveMarker:aMarker toLocation:proposedLocation];
}
@end
@implementation CPTextView (CPTextViewDelegate)
+17 -30
View File
@@ -138,38 +138,30 @@ var CPSystemTypesetterFactory,
return [_layoutManager textContainers];
}
// Retrieves correct CPTextTab stop accounting for CPArray properties
// Retrieves correct CPTextTab stop accounting for custom stops and default intervals
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
{
var tabStops = [_currentParagraph tabStops],
defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
// Calculate the immediate next standard tab stop location based on defaultInterval
var nextDefaultLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval;
var l = tabStops ? [tabStops count] : 0;
// Search for the first custom tab stop strictly greater than aWidth
var nextCustomTab = nil;
if (tabStops)
// 1. If custom tab stops exist ahead of current position, use the first one encountered
if (l > 0)
{
var l = [tabStops count];
for (var i = 0; i < l; i++)
{
var tab = [tabStops objectAtIndex:i];
if ([tab location] > aWidth)
{
nextCustomTab = tab;
break;
}
return tab;
}
}
// If a custom tab stop exists and is closer than (or at) the next default interval, use it
if (nextCustomTab && [nextCustomTab location] <= nextDefaultLocation)
return nextCustomTab;
// 2. Otherwise (or when all custom tab stops are behind the text), advance to the next default interval
var nextLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval;
// Otherwise, snap to the immediate next regular tab interval
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextDefaultLocation];
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
}
- (BOOL)_flushRange:(CPRange)lineRange
@@ -209,7 +201,7 @@ var CPSystemTypesetterFactory,
[_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange];
[_layoutManager _setAdvancements:advancements forGlyphRange:lineRange];
//fix the _lineFragments when fontsizes differ
// fix the _lineFragments when fontsizes differ
var l = _lineFragments.length;
for (var i = 0 ; i < l ; i++)
@@ -255,7 +247,7 @@ var CPSystemTypesetterFactory,
isTabStop = NO,
isAttachment = NO,
isWordWrapped = NO,
numberOfGlyphs= [_textStorage length],
numberOfGlyphs = [_textStorage length],
leading,
numLines = 0,
theString = [_textStorage string],
@@ -297,7 +289,7 @@ var CPSystemTypesetterFactory,
for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++)
{
// check whether there any change in the attributes from here on
// check whether there is any change in the attributes from here on
if (!CPLocationInRange(glyphIndex, _attributesRange))
{
_currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange];
@@ -414,10 +406,10 @@ var CPSystemTypesetterFactory,
// We are processing characters, so we are no longer at the start of a physical line
isStartOfPhysicalLine = NO;
var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
var currentCharCode = theString.charCodeAt(glyphIndex),
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char.
switch (currentCharCode)
{
case CPAttachmentCharacter:
{
@@ -449,6 +441,7 @@ var CPSystemTypesetterFactory,
}
case 9: // '\t'
{
// Measure against the actual text position before the tab stop
var nextTab = [self textTabForWidth:prevRangeWidth + lineOrigin.x writingDirection:0];
isTabStop = YES;
@@ -506,10 +499,6 @@ var CPSystemTypesetterFactory,
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;
@@ -539,7 +528,7 @@ var CPSystemTypesetterFactory,
isNewline = YES;
isWordWrapped = YES;
glyphIndex = CPMaxRange(lineRange) - 1; // start the line starts directly at current character
glyphIndex = CPMaxRange(lineRange) - 1;
}
if (isNewline || isTabStop || isAttachment)
@@ -571,8 +560,6 @@ var CPSystemTypesetterFactory,
containerSizeHeight = containerSize.height;
}
// If this is a soft wrap (isWordWrapped), next line gets headIndent.
// If it was a paragraph return, it gets firstLineHeadIndent.
isFirstLineOfParagraph = !isWordWrapped;
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
@@ -598,7 +585,7 @@ var CPSystemTypesetterFactory,
}
}
// this is to "flush" the remaining characters
// Flush remaining characters
if (lineRange.length)
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];