Creating simple text editor is easy task.
It will be obvious when you started. Very intuitive.
First, draw the text. Just simple loop.
Then add `text cursor`, it's that thing | when you edit text.
Text cursor is variable, from 0 to TextLen. This is index where to put
character. In drawing, inside loop, add if(i == textCursor){} and draw
rectangle | inside this if (you must have current character coordinate).
Add functions Left and Right, and call it when you press left and right on keyboard.
In Left() add --textCursor; In Right() add ++textCursor;
and you will see how cursor will be move when you press left or right.
You can add typing, deleting. You can implement scrolling and text selection.
Easy, but, it require so much code. My version is little bit complicated,
and it has 1500 lines.
Better way to create normal text editor is create classes for many things,
for line, for text cursor and other.
This is `endless` list box, so I need to draw only visible+4 lines.
Next, code from final version:
Lets start from Set Text
void slGUITextEditor::SetText(const slString& text)
{
size_t len = text.size();
if (len)
{
if (len > m_textBufferAllocated)
reallocate(len);
Clear(false);
for (uint32_t i = 0; i < len; ++i)
{
m_textBuffer[i] = text.c_str()[i];
}
m_textBufferLen = len;
m_textBuffer[len] = 0;
}
else
{
m_textBufferLen = 0;
m_textBuffer[0] = 0;
}
findNumberOfLines();
findTextCursorRect();
findHScroll();
findVScroll();
}
Remove old text and assign new. After this, I need to find number of lines
void slGUITextEditor::findNumberOfLines()
{
m_numberOfLines = 1; // minimum 1 line even if there is no text
m_lines.clear();
m_lines.push_back(LineInfo(0, 1, 0));
LineInfo* li = &m_lines.m_data[0];
++li->m_size;
size_t ln = 1;
for (uint32_t i = 0; i < m_textBufferLen; ++i)
{
// if we have \n then next character is first in new line
if (m_textBuffer[i] == U'\n')
{
++m_numberOfLines;
m_lines.push_back(LineInfo(i + 1, ++ln, 0));
li = &m_lines.m_data[m_lines.m_size - 1];
}
++li->m_size;
}
}
LineInfo has index for first character in this line, number of characters (m_size).
m_line I not used...remove it later
For scrolling I need to know text cursor rectangle. If rectangle is outside
of buildRect, then scroll view.
if (m_textBufferLen)
{
uint32_t index = m_firstItemIndexForDraw;
auto input = slInput::GetData();
auto& mp = input->mousePosition;
bool mouseMove = false;
if (input->mouseMoveDelta.x != 0.f || input->mouseMoveDelta.y != 0.f)
mouseMove = true;
slVec2f pos;
pos.x = m_buildRect.x - m_h_scroll;
pos.y = m_buildRect.y - m_v_scroll;
pos.y += (m_lineHeight * (float)index);
uint32_t itemsSize = m_numberOfLines;
// or not under. if cursor is out of last char in the line (cursor.x > charRect.z)
// then index must be last char in this line
// if cursor is not in the line (below text), then index is m_textBufferLen
size_t charIndexUnderCursor = 0;
size_t nearestChar = 0;
bool isCharIndexUnderCursor = false;
auto s1 = m_selectionStart;
auto s2 = m_selectionEnd;
if (s1 > s2)
{
s1 = s2;
s2 = m_selectionStart;
}
float distance = 99999.f;
Start loop for lines.
for (uint32_t i = 0; i < m_numberOfVisibleLines + 4; ++i)
{
slVec2f textPosition = pos;
slVec4f r;
r.x = pos.x;
r.y = pos.y;
r.z = m_buildRect.z;
r.w = r.y + m_lineHeight;
if (IsDrawBG() && !m_skipDraw)
{
slColor cc;
cc = GetStyle()->m_textEditorLine1BGColor;
if (index)
{
if ((index % 2) != 0)
cc = GetStyle()->m_textEditorLine2BGColor;
}
gs->DrawGUIRectangle(r, cc, cc, 0, 0);
}
float fontMaxSizeY = 0.f;
Start loop for each line. Get font, glyph, calculate character rectangle.
Also find nearest character.
size_t last = m_textBufferLen + 1;
for (size_t o = m_lines.m_data[index].m_index; o < last; ++o)
{
char32_t ch = m_textBuffer[o];
slGUIFont* font = m_textDrawCallback->OnFont(0, ch);
slGUIFontGlyph* g = font->GetGlyphMap()[ch];
if (font->GetMaxSize().y > fontMaxSizeY)
fontMaxSizeY = (float)font->GetMaxSize().y;
slVec4f chrct;
chrct.x = textPosition.x;
chrct.y = textPosition.y;
chrct.z = chrct.x + g->m_width + g->m_overhang + g->m_underhang + font->m_characterSpacing;
chrct.w = chrct.y + m_lineHeight;
auto d = slMath::distance(slVec4f(chrct.x, chrct.y, 0.f, 0.f),
slVec4f(mp.x, mp.y, 0.f, 0.f));
if (d < distance)
{
distance = d;
nearestChar = o;
}
Because I don't want to duplicate drawing code, I add
mouse interactions right in Draw()
I need to find right character. If mouse cursor in line,
then find it. If cursor outside of right border, charIndexUnderCursor
will be last in this line. If cursor outside of left border, then
charIndexUnderCursor will be first index in this line.
if ((mp.y >= chrct.y) && (mp.y <= chrct.w))
{
if (mp.x >= chrct.x)
{
if (mp.x > chrct.x + ((float)g->m_width * 0.5f))
{
if (ch == U'\n')
charIndexUnderCursor = o;
else
charIndexUnderCursor = o + 1;
}
else
charIndexUnderCursor = o;
isCharIndexUnderCursor = true;
}
else if (mp.x < m_buildRect.x)
{
charIndexUnderCursor = m_lines.m_data[index].m_index;
isCharIndexUnderCursor = true;
}
}
Draw character and selection rectangle
if (ch && !m_skipDraw)
{
if (IsTextSelected())
{
if (o >= s1 && o < s2)
{
slColor cc = GetStyle()->m_textEditorSelectedTextBGColor;
auto slrcht = chrct;
slrcht.x -= 1.f;
slrcht.z += 1.f;
gs->DrawGUIRectangle(slrcht, cc, cc, 0, 0);
}
}
gs->DrawGUICharacter(
m_textBuffer[o],
font,
textPosition,
*m_textDrawCallback->OnColor(0, m_textBuffer[o]));
}
Draw text cursor
if (m_drawTextCursor)
{
if (o == m_textCursor)
{
if (!m_skipDraw)
{
auto fnl = m_textCursorRect;
fnl.y -= m_v_scroll;
fnl.w -= m_v_scroll;
fnl.x -= m_h_scroll;
fnl.z -= m_h_scroll;
gs->DrawGUIRectangle(fnl, GetStyle()->m_textEditorCursorColor, GetStyle()->m_textEditorCursorColor, 0, 0);
}
}
}
Finish this loops
textPosition.x += g->m_width + g->m_overhang + g->m_underhang + font->m_characterSpacing;
switch (m_textBuffer[o])
{
case U' ':
textPosition.x += font->m_spaceSize;
break;
case U'\t':
textPosition.x += font->m_tabSize;
break;
case U'\n':
textPosition.x = pos.x;
o = m_textBufferLen;
break;
}
}
pos.y = pos.y + m_lineHeight;
++index;
if (index >= itemsSize)
break;
}
Mouse interactions
If no characters under cursor, then set nearest character
if (!isCharIndexUnderCursor)
{
if (mouseMove && m_isLMBHit && slInput::IsLMBHold())
{
isCharIndexUnderCursor = true;
charIndexUnderCursor = nearestChar;
}
}
Continue. First click
if (isCharIndexUnderCursor)
{
if (charIndexUnderCursor > m_textBufferLen)
charIndexUnderCursor = m_textBufferLen;
bool needupdate = false;
if (IsCursorInRect())
{
if (slInput::IsLMBHit())
{
m_textCursor = charIndexUnderCursor;
needupdate = true;
}
}
After this, if move mouse, select characters, and scroll view