CTFrameGetLineOrigin unbelievably strange bug
I'm working on text selection with core text. The selection mechanism itself is working, except for one very strange thing. I can select fine on the final line of the text view only . All previous lines snap to either all selected or not selected at all. Here is the logic (taken from Apple's sample code):
NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
for (int i = 0; i < [lines count]; i++) {
CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
CFRange lineRange = CTLineGetStringRange(line);
NSRange range = NSMakeRange(lineRange.location, lineRange.length);
NSRange intersection = [self RangeIntersection:range withSecond:selectionRange];
if (intersection.location != NSNotFound && intersection.length > 0) {
// The text range for this line intersects our selection range
CGFloat xStart = CTLineGetOffsetForStringIndex(line, intersection.location, NULL);
CGFloat xEnd = CTLineGetOffsetForStringIndex(line, intersection.location + intersection.length, NULL);
CGPoint origin;
// Get coordinate and bounds information for the intersection text range
CTFrameGetLineOrigins(_frame, CFRangeMake(i, 0), &origin);
CGFloat ascent, descent;
CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
// Create a rect for the intersection and draw it with selection color
CGRect selectionRect = CGRectMake(xStart, origin.y - descent, xEnd - xStart, ascent + descent);
UIRectFill(selectionRect);
}
}
I noticed one very strange thing. Calls to CTFrameGetLineOrigin
seem to destroy the values inside of xStart and xEnd. I inserted logs as in the following:
NSLog(@"BEFORE: xStart (%p) = %f, xEnd (%p) = %f, origin (%p) = %@", &xStart, xStart, &xEnd, xEnd, &origin, NSStringFromCGPoint(origin));
CTFrameGetLineOrigins(_frame, CFRangeMake(i, 0), &origin);
NSLog(@"AFTER: xStart (%p) = %f, xEnd (%p) = %f, origin (%p) = %@", &xStart, xStart, &xEnd, xEnd, &origin, NSStringFromCGPoint(origin));
The output for the lines that don't work is as follows
2012-09-19 12:08:39.831 SimpleTextInput[1172:11603] BEFORE: xStart (0xbfffcefc) = 18.000000, xEnd (0xbfffcef8) = 306.540009, origin (0xbfffcef0) = {0, -0}
2012-09-19 12:08:39.831 SimpleTextInput[1172:11603] AFTER: xStart (0xbfffcefc) = 370.000000, xEnd (0xbfffcef8) = 0.000000, origin (0xbfffcef0) = {0, 397}
If I do the following, it will fix itself...but I have no idea why:
CGFloat xStart = CTLineGetOffsetForStringIndex(line, intersection.location, NULL);
CGFloat xEnd = CTLineGetOffsetForStringIndex(line, intersection.location + intersection.length, NULL);
CGFloat xStart2 = 0.f; //HACK, not used at all except to pad memory
CGPoint origin;
It seems like CTFrameGetLineOrigin
doesn't respect the memory boundaries of origin
(Logging xStart2
shows that its value gets corrupted in the same way), but if that is the case then why would the last line of text work as expected? Can someone explain this to me?
No strange bug in CTFrameGetLineOrigins.
In CTFrameGetLineOrigins 2nd parameter: CFRange
is range of line origins you wish to copy. If the range length of the range is 0, then the copy operation continues from the start index of the range to the last line origin.
You are passing CFRangeMake(i, 0)
. Here in first loop iteration (i=0), it will try to fill &origin with all lines origins (0 to end line) and override some other memory.
Try following code, this will solve the problem.
NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
CGPoint origins[lines.count]; //allocate enough space for buffer.
// Get all line origins...
CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), origins);
for (int i = 0; i < [lines count]; i++) {
CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
CFRange lineRange = CTLineGetStringRange(line);
NSRange range = NSMakeRange(lineRange.location, lineRange.length);
NSRange intersection = [self RangeIntersection:range withSecond:selectionRange];
if (intersection.location != NSNotFound && intersection.length > 0) {
// The text range for this line intersects our selection range
CGFloat xStart = CTLineGetOffsetForStringIndex(line, intersection.location, NULL);
CGFloat xEnd = CTLineGetOffsetForStringIndex(line, intersection.location + intersection.length, NULL);
CGFloat ascent, descent;
CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
// Create a rect for the intersection and draw it with selection color
CGRect selectionRect = CGRectMake(xStart + origins[i].x, origins[i].y - descent, xEnd - xStart, ascent + descent);
UIRectFill(selectionRect);
}
}
This looks like it's the drawRangeAsSelection
from the SimpleTextInput
example. If that's the case you can do:
CTFrameGetLineOrigins(_frame, CFRangeMake(i, 1), &origin);
Which will simply retrieve the origin for just the one line that you need (which is what is intended in this method). I also noticed they made the same mistake in: cursorRectForIndex
and firstRectForNSRange
. Credit goes to @RobNapier for helping me figure this one out.