Help with TextBox!

kungfoomasta

28-05-2008 19:37:02

I'm trying to implement the textbox using the new render system, and running into some logic problems. Hopefully somebody can shed some light. To be more precise, my problem lies with shifting the text left and right when the text grows and shrinks.

Here is my function for setting the text cursor position, it is obviously not correct or I wouldn't be posting. Its just a jumble of ideas right now:


void TextBox::setCursorIndex(int index)
{
if((index < -1) || (index > (mText->getLength() - 1)))
throw Exception(Exception::ERR_TEXT,"Index out of bounds! index=" + Ogre::StringConverter::toString(index) + " length=" + Ogre::StringConverter::toString(mText->getLength()),"TextBox::setCursorIndex");

mCursorIndex = index;

// Determine the position of the text cursor relative to the text.
mCursorPosition = Point::ZERO;

// First thing to do: If Text width is (or has become) smaller than the TextBox dimensions, re-align the text
if(mText->getTextWidth() < (mDesc->dimensions.size.width - mDesc->padding[PADDING_LEFT] - mDesc->padding[PADDING_RIGHT]))
{
mDesc->textPosition.x = 0;
}
// Second thing to do: If Text width is (or has become) larger than the TextBox dimensions, and index

if(index == -1)
{
// Cursor is at end of text
mCursorPosition.x += mDesc->padding[PADDING_LEFT] + mDesc->textPosition.x + mText->getTextWidth();
}
else
{
// Cursor is before character at given index
Character* c = mText->getCharacter(mCursorIndex);
mCursorPosition.x += mDesc->padding[PADDING_LEFT] + mDesc->textPosition.x + c->dimensions.position.x;
}

// Determine if both the text and cursor position need to shift left or right

// If cursor is past end of TextBox
if(mCursorPosition.x > (mWidgetDesc->dimensions.size.width - mDesc->padding[PADDING_RIGHT]))
{
// Shift text and text cursor left by difference
float distance = (mWidgetDesc->dimensions.size.width - mDesc->padding[PADDING_RIGHT]) - mCursorPosition.x;
mCursorPosition.translate(Point(distance,0));
mDesc->textPosition.translate(Point(distance,0));
}
// Else if cursor is before beginning of TextBox
else if(mCursorPosition.x < mDesc->padding[PADDING_LEFT])
{
// Shift text and text cursor right by difference
float distance = mDesc->padding[PADDING_LEFT] - mCursorPosition.x;
mCursorPosition.translate(Point(distance * -1,0));
mDesc->textPosition.translate(Point(distance * -1,0));
}

Point p = getScreenPosition();
p.translate(mCursorPosition);
mTextCursor->setPosition(p);
redraw();
}


The Cursor Index is the position to the left of the Text Index. For example, Cursor Index 0 will be to the left of Text index 0. -1 index means to the right of the text, at the very end.

The goal is to have this function handle shifting the text appropriately. I haven't even tried to address different text alignments (left/right/center), that would only make things more complicated.

The Text position is the relative offset in relation to the TextBox. A Text position.x of 0 will have the text drawn at the beginning of the TextBox (offset by the left padding). Likewise, a negative position.x will cause the text to be drawn to the left of the TextBox's left edge. (It gets clipped automatically, no worries)

If anybody knows a good reference on implementing a TextBox that would be helpful. My approach may be different; after each addition or removal of text, I set the cursor index. Ideally if this function shifted the text properly, everything would work well.

kungfoomasta

28-05-2008 19:39:02

An alternative approach may be to shift the text as I add/remove characters. I'll have to play around and see what is the easiest solution for this.

Zini

29-05-2008 08:58:39

I am not 100% sure, if I understand your problem. But here is how I would do it:

First split the algorithm up into two cases:

1. Text does fit into the TextBox.
2. Text does not fit into the TextBox.

Case 1 is simple. The text formatting is determined by the text alignment. I think I don't need to elaborate on this one.

Case 2: Here you must ignore the text alignment, while the text box has a cursor.Obviously you want the part of the text to be shown that is currently edited, so the position is determined by the cursor, not by the alignment.

(Note: if the text box does not have an active cursor, this should be implemented as case 1 instead, so that the alignment can still be respected)

The general idea here should be to keep the cursor in the middle of the text box, so you can see what is before and what is after it.

Case 2a: The text between the cursor and the end (of the text) is fitting into half the text box.
Here you should simply right-align the text, so that the maximum amount of text around the cursor is visible.

Case 2b: The text between the cursor and the beginning is fitting into half the text box.
Here you should simply left-align the text, again so that maximum amount of text around the cursor is visible.

Case 2c: Neither 2a nor 2b is true. Simply put the cursor in the middle of the text box and position the text accordingly.

kungfoomasta

29-05-2008 17:43:44

Wow, simple approach! I definately agree with 1 and 2.

I was typing up a post to try and understand your cases and behavior more, but I think I got it. :D I will implement this tonight! Thanks for the help on this.

kungfoomasta

30-05-2008 02:50:10

Awesome, it seems to be working! I have 5 pixel padding on the left and right side, so it looks a little odd with characters getting clipped on the ends, since the text centers on the cursor. But if the padding size was 0, it would look normal, because the characters would be clipped on the edge of the TextBox. The padding is configurable, like everything I try to implement, but either way its not a big deal.

Here is the code, if you want a peek:


void TextBox::setCursorIndex(int index)
{
if((index < -1) || (index > (mText->getLength() - 1)))
throw Exception(Exception::ERR_TEXT,"Index out of bounds! index=" + Ogre::StringConverter::toString(index) + " length=" + Ogre::StringConverter::toString(mText->getLength()),"TextBox::setCursorIndex");

mCursorIndex = index;

if(mText->getLength() == 0)
{
mDesc->textPosition.x = mDesc->padding[PADDING_LEFT];
mCursorPosition.x = mDesc->textPosition.x;

Point p = getScreenPosition();
p.translate(mCursorPosition);
mTextCursor->setPosition(p);
redraw();

return;
}

float relativeCursorPosition;
if(mCursorIndex == -1)
{
Character* c = mText->getCharacter(mText->getLength() - 1);
relativeCursorPosition = c->dimensions.position.x + c->dimensions.size.width;
}
else
relativeCursorPosition = mText->getCharacter(mCursorIndex)->dimensions.position.x;

float textBoxWidth = (mDesc->dimensions.size.width - mDesc->padding[PADDING_LEFT] - mDesc->padding[PADDING_RIGHT]);

// If text fits within TextBox, align text
if(mText->getTextWidth() < textBoxWidth)
{
switch(mDesc->textAlignment)
{
case TEXT_ALIGNMENT_CENTER:
mDesc->textPosition.x = (textBoxWidth - mText->getTextWidth()) / 2.0;
break;
case TEXT_ALIGNMENT_LEFT:
mDesc->textPosition.x = mDesc->padding[PADDING_LEFT];
break;
case TEXT_ALIGNMENT_RIGHT:
mDesc->textPosition.x = (mDesc->dimensions.size.width - mDesc->padding[PADDING_RIGHT]) - mText->getTextWidth();
break;
}
}
// Else text is larger than TextBox bounds. Ignore Text alignment property.
else
{
// Case 1: if the distance between the cursor and beggining of text is less than
// half the text box size, left align the text.
if(relativeCursorPosition < (textBoxWidth / 2.0))
{
mDesc->textPosition.x = mDesc->padding[PADDING_LEFT];
}
// Case 2: if the distance between the cursor and end of text is less than
// half the text box size, right align the text.
else if((mText->getTextWidth() - relativeCursorPosition) < (textBoxWidth / 2.0))
{
mDesc->textPosition.x = (mDesc->dimensions.size.width - mDesc->padding[PADDING_RIGHT]) - mText->getTextWidth();
}
// Case 3: Center the cursor with the TextBox and position the Text accordingly
else
{
mDesc->textPosition.x = (textBoxWidth / 2.0) - relativeCursorPosition;
}
}

mCursorPosition.x = mDesc->textPosition.x + relativeCursorPosition;

Point p = getScreenPosition();
p.translate(mCursorPosition);
mTextCursor->setPosition(p);
redraw();
}


Thanks for the break down of logic, you saved me a lot of trial and error. :D

Zini

30-05-2008 14:07:22

I am always glad, when I can help. Especially if that gives me an opportunity to sneak in another RISC OS-feature, so I don't have to put up with Windows' way of doing it.

kungfoomasta

30-05-2008 18:08:48

Now that you mention it, there was something about the behavior of the textbox that wasn't familiar to me. I don't really analyze how TextBoxes work on Windows, but with the implementation we have now, if we're at case 2.3, where the cursor is always centered in the textbox, when you add characters, the right portion of text stays where it is and the left portion goes back left more. I wonder how this compares to the behavior I'm used to, I don't really know the behavior, but it felt different to me somehow.. :lol:

Does Risc TextBox behavior support Home/End/Ctrl keys?

kungfoomasta

30-05-2008 19:43:24

The last difficult part of the TextBox is highlighting. The actual implementation of highlighting characters is easy, its fitting in the behavior to allow left mouse click, shift, ctrl, left/right arrow, home, and end to highlight text.

Here is the interface I have planned for highlighting:


- Text::highlight(code_point cp, bool allInstances)
- Text::highlight(UTFString s, bool allInstances)
- Text::highlight(int index)
- Text::highlight(int startIndex, int endIndex)
- find characters and highlight them
- Text::clearHighlighting
- iterate through all characters and setHighlight(false)

- TextBox::highlight: 4 overloads (match Text)
- TextBox::clearHighlighting

- Text::getIndexOfNextWord
- returns -1 if end of text
- Text::getIndexOfPreviousWord
- returns 0 if beginning of text


Just to clarify, Text is a class separate from TextBox. The TextBox wraps around the Text, and signals the need to be redrawn when the Text is modified.

I'm thinking I'll need a boolean flag to know if I'm in Select mode. For example, if you hold down shift and left click the text, text in between the previous cursor position and the current cursor position (placed from LMB down) will be highlighted. Alternatively, I can left click down and drag and text will be highlighted. (pressing/releasing shift during this time does nothing to the state, from what I observe) I need to know the start and end of highlighting. Anybody have suggestions on how to organize this?

Zini

31-05-2008 14:17:29


I wonder how this compares to the behavior I'm used to, I don't really know the behavior,


I bit of experimenting shows that on Windows the text jumps by half the text box width, once you move beyond the left or the right border. A bit disruptive IMHO. And a lot harder to implement.


Does Risc TextBox behavior support Home/End/Ctrl keys?


Bound to different key-combinations, but the functionality is there, yes.

Zini

31-05-2008 14:25:01


I'm thinking I'll need a boolean flag to know if I'm in Select mode. For example, if you hold down shift and left click the text, text in between the previous cursor position and the current cursor position (placed from LMB down) will be highlighted. Alternatively, I can left click down and drag and text will be highlighted. (pressing/releasing shift during this time does nothing to the state, from what I observe) I need to know the start and end of highlighting. Anybody have suggestions on how to organize this?


The selection is obviously bound by two cursor positions. One of them is the original position, where the cursor was at, when the selection operation was started. You need to store this one somewhere. Note, that it can either be the beginning or the end and this can even change, while modifying the current selection.

The second cursor position is the current cursor position, which is modified in the usual way (mouse, cursor keys ,...). Drawing the selection simply means determining, which of the two positions is the beginning and then drawing it up to the other position.

kungfoomasta

02-06-2008 19:54:57

I just ran into a problem with the way I'm setting the cursor's position. Currently the Cursor's Index represents the Index of the Character the cursor is placed in front of. I use -1 to represent the position to the right of the last character in a line of text. The problem comes in when you consider multiple lines of text:

12345
67890


There is no way to position the cursor after the '5', because setting the cursor index to 5 will result in the cursor positioned before the '6'.

It seems my setCursorIndex(int index) will not work well for the TextArea widget. Does anybody have any ideas on a cursor placement system that will work for multiple lines of text? (this includes 1 line of text)

kungfoomasta

02-06-2008 20:36:08

I think I will look into making a structure/class called a CursorPosition:

class CursorPosition
{
public:
enum Side { LEFT, RIGHT };
CursorPosition();

Side side;
unsigned int characterIndex;
};


I would like a more intuitive name to replace "Side", but I can't think of anything at the moment.

If there are better ideas please post them, but I think this idea will work.

Zini

02-06-2008 22:01:32

This


12345
67890


actually should be


12345<CR>
67890


If you break the line, you have an (at least) implicit return character at this position. So if you have index 5 it would be at the end of the first line, while 6 would be the beginning of the second.

But if your text data structure does not implement this return character explicitly, the only option is a more complex index type as you proposed.

kungfoomasta

02-06-2008 22:24:23

In the current implementation newline characters create new TextLine objects within the Text. They don't have a Character instance associated with them.

I could create a Character instance for newlines and give it a width of 0. (and update draw calls to ignore this character) I will try this method and see how it works. Simple sounds good to me. :)

kungfoomasta

15-10-2008 06:22:17

Hello all!

I figured out how to implement the TextBox so that it behaves like the windows TextBox. Zini I read above you might dislike this.. the only problem I had with the way implemented above (Risc-like) is with selecting text via mouse cursor and highlighting. For example, we implemented the behavior such that the cursor is always in the center of the text box, when moving through the middle of a long segment of text. Whenever the mouse clicks on a character to reposition the text, the text jumps to center the cursor in the middle of the text box. So when you intially click the mouse, the text jumps, and for each character you move the mouse over (highlighting) the text jumps. Maybe the Risc way could work out.. I'm just more comfortable with windows since I use the platform most. So I'm open for opinions.

Here is the new code, it was surprisingly very simple and easy to implement:


// Update CursorIndex
mCursorIndex = index;

// If text fits within TextBox, align text
if(mText->getTextWidth() < mClientDimensions.size.width)
{
switch(mDesc->textAlignment)
{
case TEXT_ALIGNMENT_CENTER:
mDesc->textPosition.x = (mClientDimensions.size.width - mText->getTextWidth()) / 2.0;
break;
case TEXT_ALIGNMENT_LEFT:
mDesc->textPosition.x = 0;
break;
case TEXT_ALIGNMENT_RIGHT:
mDesc->textPosition.x = mClientDimensions.size.width - mText->getTextWidth();
break;
}
}
// Else text is larger than TextBox bounds. Ignore Text alignment property.
else
{
// calculate the position of the desired index in relation to the textbox dimensions

// X Position of cursor index relative to start of text
Point relativeCursorPosition = mText->getPositionAtCharacterIndex(mCursorIndex);
// X Position of cursor index relative to TextBox's top left client corner
float indexPosition = (relativeCursorPosition.x + mDesc->textPosition.x);

if(indexPosition < 0)
{
mDesc->textPosition.x -= indexPosition;
}
else if(indexPosition > mClientDimensions.size.width)
{
mDesc->textPosition.x -= (indexPosition - mClientDimensions.size.width);
}
}

mCursorPosition.x = mDesc->textPosition.x + mText->getPositionAtCharacterIndex(mCursorIndex).x;

// Position Cursor
Point p = getScreenPosition();
p.translate(mClientDimensions.position);
p.translate(mCursorPosition);
mTextCursor->setPosition(p);
redraw();