Bug with multiple text format ranges in TextField


#1

When there are multiple TextFormatRanges in a TextField, there appears to be an indexing issue. The end index of the first format will equal the beginning format of the second range. For example, if I have the word “three” with the “th” set as one TextFormat and “ree” as a second, the indexes will be as follows:

“th”
start:0
end:2
“ree”
start:2
end:5

Somehow the TextField parses this and displays it correctly, the problem I’m encountering is when I call TextField.getTextFormat(2,2);

I want to know what the correct TextFormat is for the character at index 2 (the letter “r”). I would expect to get the format information from the second TextFormat range (that encompasses “ree”), however getTextFormat thinks BOTH ranges apply to it, since the end index of the first range is 2, which is the same as the start index of the second range. getTextFormat resolves this conflict by nulling out conflicting values in the return TextFormat.

I’m not sure what the correct solution here is–I would expect range one (the “th”) to have a start index of 0 and an end index of 1 (instead of 2), and range two (the “ree”) to have a start index of 2 and end of 4. So it seems like modifying TextField.setTextFormat makes the most sense–to decrement the end index by 1. However I’m not all that familiar with the underlying TextEngine code so I’m not sure if there’s a valid reason to not do this.

I have a workaround that modifies TextField.getTextFormat, where I modify the range check from this:

if ((group.start <= beginIndex && group.end >= beginIndex) || (group.start <= endIndex && group.end >= endIndex))

to this:

if ((group.start <= beginIndex && group.end > beginIndex) || (group.start <= endIndex && group.end > endIndex))

I changed the “group.end” checks from >= to >. This solves my immediate problem, but I’m not sure if the indexes are supposed to be overlapping to begin with or not.


#2

I think the indices are meant to be start -> end - 1 characters. So start == 0 and end == 2 means that it’s two characters in length, the characters at index 0 and 1.

Thanks for the feedback, I just changed it to this:

if ((group.start <= beginIndex && group.end > beginIndex) || (group.start <= endIndex && group.end >= endIndex)) {

…only I’m unsure how the API should behave if the beginIndex and endIndex values are equal. For example, if you called textField.getTextFormat (2, 2);, I assume it should give you the format for the second format range you shared, but I think with this (new) logic, you’d get no format


#3

In the example I gave, my expectation for the returned format is to get the second format range when calling

textField.getTextFormat (2,2);

When I changed both

group.end >= beginIndex
group.end >= endIndex

to

group.end > beginIndex
group.end > endIndex

I started seeing the result I expected. I’m just not sure if that’s accidentally opening up trouble for some other edge-case.


#4

I ran across an edge-case for my previous modification:
If the TextField doesn’t have any text in it, group.start and group.end both equal 0, and neither of the conditionals can be met, and I get a null format returned (when what I was hoping to get was the defaultTextFormat that I had set).


#5

Here’s the current behavior on dev:

I wonder if adding something like this would help

if (endIndex == beginIndex) endIndex++;

or adding

|| (beginIndex == endIndex && group.start <= beginIndex && group.end >= beginIndex)

#6

I tested those changes, and it doesn’t completely fix the problem. I’m still seeing an index overlap on the (group.start <= endIndex && group.end >= endIndex) part of the conditional.

I’ve been testing this statement, and I think it covers all use cases:

if ((group.start <= beginIndex && group.end > beginIndex) ||
(group.start <= endIndex && group.end > endIndex) || 
(group.start==group.end && (group.start>=beginIndex && group.start<=endIndex)))

While testing I also stumbled upon a crash situation in setTextFormat when you try call textField.setTextFormat with a beginIndex greater than text.length

Changing

if (endIndex < beginIndex) return;

to

if (endIndex < beginIndex || beginIndex > max) return;

seems to resolve the issue.

EDIT: I’m realizing that I have some blinders on because I’ve been focused on retrieving the TextFormat for a single character. With the changes I posted above, the beginIndex and endIndex of getTextFormat suddenly start meaning different things than the beginIndex and endIndex of setTextFormat. I guess it comes down to me still not understanding why there is overlap between beginIndex and endIndex in setTextFormat in the first place. Calling

textField.setTextFormat(format,0,2);

and only having that affect the first and second characters (position 0 and 1), as opposed to position 0, 1 and 2, just doesn’t make sense.


#7

Pretend there are markers between the characters

   H   E   L   L   O
 ^   ^   ^   ^   ^   ^
 0   1   2   3   4   5

If you wanted to include “HE”, you need to start at 0, and continue on through 2.


#8

Here’s what I have now:

	public function getTextFormat (beginIndex:Int = -1, endIndex:Int = -1):TextFormat {
		
		var format = null;
		if (beginIndex >= text.length) return null;
		
		if (beginIndex == -1) beginIndex = 0;
		if (endIndex == -1) endIndex = text.length;
		
		for (group in __textEngine.textFormatRanges) {
			
			if ((group.start <= beginIndex && group.end > beginIndex) || (group.start <= endIndex && group.end >= endIndex) || (beginIndex == endIndex && group.start <= beginIndex && group.end >= beginIndex)) {
				...

#9

(Apologies in advance for the length of this post…typing out these examples line by line was the only way for me to make sure I was actually looking at the problem correctly)

Okay, using the HELLO example I’m still running into issues with the new code.

  H   E   L   L   O
^   ^   ^   ^   ^   ^
0   1   2   3   4   5

Let’s say the H and E are one format, and LLO a second format. So we have HELLO. I want to find out the text format for the first L. So I call getTextFormat(2,2). The two text format groups are

group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO

My expectation is to meet the conditions for group[1], and not meet the conditions for group[0]. But what’s happening is that both groups meet the conditional:

beginIndex = 2;
endIndex = 2;
group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO
if (
//This check works, because group[0].end is not > 2
(group.start <= beginIndex && group.end > beginIndex)
//This check *fails* because group[0].end is >= endIndex
|| (group.start <= endIndex && group.end >= endIndex) 

But with the way the indexes are organized, it makes more sense to not call with beginIndex and endIndex being the same, so what happens if I call getTextFormat(2,3)?

beginIndex = 2;
endIndex = 3;
group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO
if (
//This check works, because group[0].end is not > 2
(group.start <= beginIndex && group.end > beginIndex)
//This check works because group[0].end is < 3
|| (group.start <= endIndex && group.end >= endIndex) 

That call works! But the problem now is that if I try to get the format for the E using getTextFormat(1,2), I run into a similar problem as before:

beginIndex = 1;
endIndex = 2;
group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO
if (
//This check works, because group[1].start is > 1
(group.start <= beginIndex && group.end > beginIndex)
//This check *fails* because both group[0].start and group[1].start are <= 2 
//AND both group[0].end and group[1].end are <= 2
|| (group.start <= endIndex && group.end >= endIndex) 

In this example it seems like the second conditional should have group.start be < endIndex instead of <=. But even if we make that change, my original call, where both beginIndex and endIndex are identical: getTextFormat(2,2) will still have both format groups meet the conditional.

beginIndex = 2;
endIndex = 2;
group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO
if (
//This check works, because group[0].end equals beginIndex, and is not > 2
(group.start <= beginIndex && group.end > beginIndex) 
//This check *still fails* because group[0].end is >= 2
|| (group.start < endIndex && group.end >= endIndex) 

So it seems like we should also check for group.end > endIndex instead of >= endIndex.

beginIndex = 2;
endIndex = 2;
group[0] = {start:0,end:2} //the HE
group[1] = {start:2,end:5} //the LLO
if (
//This check works, because group[0].end equals beginIndex, and is not > 2
(group.start <= beginIndex && group.end > beginIndex) 
//This check works now because group[0].end is not > 2
|| (group.start < endIndex && group.end > endIndex) 

After running through my tests, I think this will actually work, and it doesn’t require the 3rd check for if beginIndex == endIndex:

if ((group.start <= beginIndex && group.end > beginIndex) || (group.start < endIndex && group.end > endIndex)) {

#10

Okay! I think this makes sense. The only case it doesn’t handle is if group.end is less than group.start, but that would be a bug further up in our code. Thank you so much for all your help into looking into this!