vkdoom_m/libraries/ZWidget/src/widgets/textedit/textedit.cpp
2025-08-26 22:09:24 -04:00

1049 lines
22 KiB
C++

#include "widgets/textedit/textedit.h"
#include "widgets/scrollbar/scrollbar.h"
#include "core/utf8reader.h"
#include "core/colorf.h"
#include <algorithm>
#ifdef _MSC_VER
#pragma warning(disable: 4267) // warning C4267: 'initializing': conversion from 'size_t' to 'int', possible loss of data
#endif
TextEdit::TextEdit(Widget* parent) : Widget(parent)
{
SetStyleClass("textedit");
selectionBG = GetStyleColor("selection-color");
selectionFG = GetStyleColor("color");
timer = new Timer(this);
timer->FuncExpired = [this]() { OnTimerExpired(); };
scroll_timer = new Timer(this);
scroll_timer->FuncExpired = [this]() { OnScrollTimerExpired(); };
SetCursor(StandardCursor::ibeam);
CreateComponents();
}
TextEdit::~TextEdit()
{
}
bool TextEdit::IsReadOnly() const
{
return readonly;
}
bool TextEdit::IsLowercase() const
{
return lowercase;
}
bool TextEdit::IsUppercase() const
{
return uppercase;
}
int TextEdit::GetMaxLength() const
{
return max_length;
}
std::string TextEdit::GetLineText(int line) const
{
if (line >= 0 && line < (int)lines.size())
return lines[line].text;
else
return std::string();
}
std::string TextEdit::GetText() const
{
std::string::size_type size = 0;
for (size_t i = 0; i < lines.size(); i++)
size += lines[i].text.size();
size += lines.size() - 1;
std::string text;
text.reserve(size);
for (size_t i = 0; i < lines.size(); i++)
{
if (i > 0)
text.push_back('\n');
text += lines[i].text;
}
return text;
}
int TextEdit::GetLineCount() const
{
return (int)lines.size();
}
std::string TextEdit::GetSelection() const
{
std::string::size_type offset = ToOffset(selection_start);
int start = (int)std::min(offset, offset + selection_length);
return GetText().substr(start, abs(selection_length));
}
int TextEdit::GetSelectionStart() const
{
return (int)ToOffset(selection_start);
}
int TextEdit::GetSelectionLength() const
{
return selection_length;
}
int TextEdit::GetCursorPos() const
{
return (int)ToOffset(cursor_pos);
}
int TextEdit::GetCursorLineNumber() const
{
return cursor_pos.y;
}
void TextEdit::SelectAll()
{
std::string::size_type size = 0;
for (size_t i = 0; i < lines.size(); i++)
size += lines[i].text.size();
size += lines.size() - 1;
SetSelection(0, (int)size);
}
void TextEdit::SetReadOnly(bool enable)
{
if (readonly != enable)
{
readonly = enable;
Update();
}
}
void TextEdit::SetLowercase(bool enable)
{
if (lowercase != enable)
{
lowercase = enable;
Update();
}
}
void TextEdit::SetUppercase(bool enable)
{
if (uppercase != enable)
{
uppercase = enable;
Update();
}
}
void TextEdit::SetMaxLength(int length)
{
if (max_length != length)
{
max_length = length;
std::string::size_type size = 0;
for (size_t i = 0; i < lines.size(); i++)
size += lines[i].text.size();
size += lines.size() - 1;
if ((int)size > length)
{
if (FuncBeforeEditChanged)
FuncBeforeEditChanged();
SetSelection(length, (int)size - length);
DeleteSelectedText();
if (FuncAfterEditChanged)
FuncAfterEditChanged();
}
Update();
}
}
void TextEdit::SetText(const std::string& text)
{
lines.clear();
std::string::size_type start = 0;
std::string::size_type end = text.find('\n');
while (end != std::string::npos)
{
TextEdit::Line line{this};
line.text = text.substr(start, end - start);
lines.push_back(line);
start = end + 1;
end = text.find('\n', start);
}
TextEdit::Line line{this};
line.text = text.substr(start);
lines.push_back(line);
clip_start_offset = 0;
SetCursorPos(0);
ClearSelection();
Update();
}
void TextEdit::AddText(const std::string& text)
{
std::string::size_type start = 0;
std::string::size_type end = text.find('\n');
while (end != std::string::npos)
{
TextEdit::Line line{this};
line.text = text.substr(start, end - start);
lines.push_back(line);
start = end + 1;
end = text.find('\n', start);
}
TextEdit::Line line{this};
line.text = text.substr(start);
lines.push_back(line);
// clip_start_offset = 0;
// SetCursorPos(0);
ClearSelection();
Update();
}
void TextEdit::SetSelection(int pos, int length)
{
selection_start = ivec2(pos, 0);
selection_length = length;
Update();
}
void TextEdit::ClearSelection()
{
SetSelection(0, 0);
Update();
}
void TextEdit::DeleteSelectedText()
{
if (GetSelectionLength() == 0)
return;
std::string::size_type offset = ToOffset(selection_start);
int start = (int)std::min(offset, offset + selection_length);
int length = std::abs(selection_length);
ClearSelection();
std::string text = GetText();
SetText(text.substr(0, start) + text.substr(start + length));
SetCursorPos(start);
}
void TextEdit::SetCursorPos(int pos)
{
cursor_pos = FromOffset(pos);
Update();
}
void TextEdit::SetInputMask(const std::string& mask)
{
input_mask = mask;
}
void TextEdit::SetCursorDrawingEnabled(bool enable)
{
if (!readonly)
timer->Start(500);
}
void TextEdit::SetSelectAllOnFocusGain(bool enable)
{
select_all_on_focus_gain = enable;
}
void TextEdit::OnMouseMove(const Point& pos)
{
if (mouse_selecting && !ignore_mouse_events)
{
if (pos.x < 0.0 || pos.x > GetWidth())
{
if (pos.x < 0.0)
mouse_moves_left = true;
else
mouse_moves_left = false;
if (!readonly)
scroll_timer->Start(50, true);
}
else
{
scroll_timer->Stop();
cursor_pos = GetCharacterIndex(pos);
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
Update();
}
}
}
bool TextEdit::OnMouseDown(const Point& pos, InputKey key)
{
if (key == InputKey::LeftMouse)
{
SetPointerCapture();
mouse_selecting = true;
cursor_pos = GetCharacterIndex(pos);
selection_start = cursor_pos;
selection_length = 0;
Update();
}
return true;
}
bool TextEdit::OnMouseDoubleclick(const Point& pos, InputKey key)
{
return true;
}
bool TextEdit::OnMouseUp(const Point& pos, InputKey key)
{
if (mouse_selecting && key == InputKey::LeftMouse)
{
if (ignore_mouse_events) // This prevents text selection from changing from what was set when focus was gained.
{
ReleasePointerCapture();
ignore_mouse_events = false;
mouse_selecting = false;
}
else
{
scroll_timer->Stop();
ReleasePointerCapture();
mouse_selecting = false;
ivec2 sel_end = GetCharacterIndex(pos);
selection_length = ToOffset(sel_end) - ToOffset(selection_start);
cursor_pos = sel_end;
SetFocus();
Update();
}
}
return true;
}
void TextEdit::OnKeyChar(std::string chars)
{
if (!chars.empty() && !(chars[0] >= 0 && chars[0] < 32))
{
if (FuncBeforeEditChanged)
FuncBeforeEditChanged();
DeleteSelectedText();
ClearSelection();
if (input_mask.empty())
{
// not in any special mode, just insert the string.
InsertText(cursor_pos, chars);
cursor_pos.x += chars.size();
}
else
{
if (InputMaskAcceptsInput(cursor_pos, chars))
{
InsertText(cursor_pos, chars);
cursor_pos.x += chars.size();
}
}
if (FuncAfterEditChanged)
FuncAfterEditChanged();
}
}
void TextEdit::OnKeyDown(InputKey key)
{
if (!readonly && key == InputKey::Enter)
{
if (FuncEnterPressed)
{
FuncEnterPressed();
}
else
{
ClearSelection();
InsertText(cursor_pos, "\n");
SetCursorPos(GetCursorPos() + 1);
}
return;
}
if (!readonly) // Do not flash cursor when readonly
{
cursor_blink_visible = true;
timer->Start(500); // don't blink cursor when moving or typing.
}
if (key == InputKey::Enter || key == InputKey::Escape || key == InputKey::Tab)
{
// Do not consume these.
return;
}
else if (key == InputKey::A && GetKeyState(InputKey::Ctrl))
{
// select all
SelectAll();
}
else if (key == InputKey::C && GetKeyState(InputKey::Ctrl))
{
std::string str = GetSelection();
SetClipboardText(str);
}
else if (readonly)
{
// Do not consume messages on read only component (only allow CTRL-A and CTRL-C)
return;
}
else if (key == InputKey::Up)
{
if (GetKeyState(InputKey::Shift) && selection_length == 0)
selection_start = cursor_pos;
if (cursor_pos.y > 0)
{
cursor_pos.y--;
cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x);
}
if (GetKeyState(InputKey::Shift))
{
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
}
else
{
// Clear the selection if a cursor key is pressed but shift isn't down.
selection_start = ivec2(0, 0);
selection_length = 0;
}
MoveVerticalScroll();
Update();
undo_info.first_text_insert = true;
}
else if (key == InputKey::Down)
{
if (GetKeyState(InputKey::Shift) && selection_length == 0)
selection_start = cursor_pos;
if (cursor_pos.y < (int)lines.size() - 1)
{
cursor_pos.y++;
cursor_pos.x = std::min(lines[cursor_pos.y].text.size(), (size_t)cursor_pos.x);
}
if (GetKeyState(InputKey::Shift))
{
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
}
else
{
// Clear the selection if a cursor key is pressed but shift isn't down.
selection_start = ivec2(0, 0);
selection_length = 0;
}
MoveVerticalScroll();
Update();
undo_info.first_text_insert = true;
}
else if (key == InputKey::Left)
{
Move(-1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl));
}
else if (key == InputKey::Right)
{
Move(1, GetKeyState(InputKey::Shift), GetKeyState(InputKey::Ctrl));
}
else if (key == InputKey::Backspace)
{
Backspace();
}
else if (key == InputKey::Delete)
{
Del();
}
else if (key == InputKey::Home)
{
if (GetKeyState(InputKey::Ctrl))
cursor_pos = ivec2(0, 0);
else
cursor_pos.x = 0;
if (GetKeyState(InputKey::Shift))
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
else
ClearSelection();
Update();
MoveVerticalScroll();
}
else if (key == InputKey::End)
{
if (GetKeyState(InputKey::Ctrl))
cursor_pos = ivec2(lines.back().text.length(), lines.size() - 1);
else
cursor_pos.x = lines[cursor_pos.y].text.size();
if (GetKeyState(InputKey::Shift))
selection_length = ToOffset(cursor_pos) - ToOffset(selection_start);
else
ClearSelection();
Update();
}
else if (key == InputKey::X && GetKeyState(InputKey::Ctrl))
{
std::string str = GetSelection();
DeleteSelectedText();
SetClipboardText(str);
}
else if (key == InputKey::V && GetKeyState(InputKey::Ctrl))
{
std::string str = GetClipboardText();
std::string::const_iterator end_str = std::remove(str.begin(), str.end(), '\r');
str.resize(end_str - str.begin());
DeleteSelectedText();
if (input_mask.empty())
{
InsertText(cursor_pos, str);
SetCursorPos(GetCursorPos() + str.length());
}
else
{
if (InputMaskAcceptsInput(cursor_pos, str))
{
InsertText(cursor_pos, str);
SetCursorPos(GetCursorPos() + str.length());
}
}
MoveVerticalScroll();
}
else if (GetKeyState(InputKey::Ctrl) && key == InputKey::Z)
{
if (!readonly)
{
std::string tmp = undo_info.undo_text;
undo_info.undo_text = GetText();
SetText(tmp);
}
}
else if (key == InputKey::Shift)
{
if (selection_length == 0)
selection_start = cursor_pos;
}
if (FuncAfterEditChanged)
FuncAfterEditChanged();
}
void TextEdit::OnKeyUp(InputKey key)
{
}
void TextEdit::OnSetFocus()
{
if (!readonly)
timer->Start(500);
if (select_all_on_focus_gain)
SelectAll();
ignore_mouse_events = true;
cursor_pos.y = lines.size() - 1;
cursor_pos.x = lines[cursor_pos.y].text.length();
Update();
if (FuncFocusGained)
FuncFocusGained();
}
void TextEdit::OnLostFocus()
{
timer->Stop();
ClearSelection();
Update();
if (FuncFocusLost)
FuncFocusLost();
}
void TextEdit::CreateComponents()
{
vert_scrollbar = new Scrollbar(this);
vert_scrollbar->FuncScroll = [this]() { OnVerticalScroll(); };
vert_scrollbar->SetVisible(false);
vert_scrollbar->SetVertical();
}
void TextEdit::OnVerticalScroll()
{
}
void TextEdit::UpdateVerticalScroll()
{
Rect rect(
GetWidth() - 16.0/*vert_scrollbar->GetWidth()*/,
0.0,
16.0/*vert_scrollbar->GetWidth()*/,
GetHeight());
vert_scrollbar->SetFrameGeometry(rect);
double total_height = GetTotalLineHeight();
double height_per_line = std::max(1.0, total_height / std::max(1.0, (double)lines.size()));
bool visible = total_height > GetHeight();
vert_scrollbar->SetRanges((int)std::round(GetHeight() / height_per_line), (int)std::round(total_height / height_per_line));
vert_scrollbar->SetLineStep(1);
vert_scrollbar->SetVisible(visible);
if (visible == false)
vert_scrollbar->SetPosition(0);
}
void TextEdit::MoveVerticalScroll()
{
double total_height = GetTotalLineHeight();
double height_per_line = std::max(1.0, total_height / std::max((size_t)1, lines.size()));
int lines_fit = (int)(GetHeight() / height_per_line);
if (cursor_pos.y >= vert_scrollbar->GetPosition() + lines_fit)
{
vert_scrollbar->SetPosition(cursor_pos.y - lines_fit + 1);
}
else if (cursor_pos.y < vert_scrollbar->GetPosition())
{
vert_scrollbar->SetPosition(cursor_pos.y);
}
}
double TextEdit::GetTotalLineHeight()
{
double total = 0;
for (std::vector<Line>::const_iterator iter = lines.begin(); iter != lines.end(); iter++)
{
total += iter->layout.GetSize().height;
}
return total;
}
void TextEdit::Move(int steps, bool shift, bool ctrl)
{
if (shift && selection_length == 0)
selection_start = cursor_pos;
// Jump over words if control is pressed.
if (ctrl)
{
if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0)
{
cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size();
cursor_pos.y--;
}
else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size())
{
cursor_pos.x = 0;
cursor_pos.y++;
}
ivec2 new_pos;
if (steps < 0)
new_pos = FindPreviousBreakCharacter(cursor_pos);
else
new_pos = FindNextBreakCharacter(cursor_pos);
cursor_pos = new_pos;
}
else if (steps < 0 && cursor_pos.x == 0 && cursor_pos.y > 0)
{
cursor_pos.x = (int)lines[cursor_pos.y - 1].text.size();
cursor_pos.y--;
}
else if (steps > 0 && cursor_pos.x == (int)lines[cursor_pos.y].text.size() && cursor_pos.y + 1 < (int)lines.size())
{
cursor_pos.x = 0;
cursor_pos.y++;
}
else
{
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
utf8_reader.set_position(cursor_pos.x);
if (steps > 0)
{
for (int i = 0; i < steps; i++)
utf8_reader.next();
}
else if (steps < 0)
{
for (int i = 0; i < -steps; i++)
utf8_reader.prev();
}
cursor_pos.x = (int)utf8_reader.position();
}
if (shift)
{
selection_length = (int)ToOffset(cursor_pos) - (int)ToOffset(selection_start);
}
else
{
// Clear the selection if a cursor key is pressed but shift isn't down.
selection_start = ivec2(0, 0);
selection_length = 0;
}
MoveVerticalScroll();
Update();
undo_info.first_text_insert = true;
}
std::string TextEdit::break_characters = " ::;,.-";
TextEdit::ivec2 TextEdit::FindNextBreakCharacter(ivec2 search_start)
{
search_start.x++;
if (search_start.x >= int(lines[search_start.y].text.size()) - 1)
return ivec2(lines[search_start.y].text.size(), search_start.y);
size_t pos = lines[search_start.y].text.find_first_of(break_characters, search_start.x);
if (pos == std::string::npos)
return ivec2(lines[search_start.y].text.size(), search_start.y);
return ivec2((int)pos, search_start.y);
}
TextEdit::ivec2 TextEdit::FindPreviousBreakCharacter(ivec2 search_start)
{
search_start.x--;
if (search_start.x <= 0)
return ivec2(0, search_start.y);
size_t pos = lines[search_start.y].text.find_last_of(break_characters, search_start.x);
if (pos == std::string::npos)
return ivec2(0, search_start.y);
return ivec2((int)pos, search_start.y);
}
void TextEdit::InsertText(ivec2 pos, const std::string& str)
{
undo_info.first_erase = false;
if (undo_info.first_text_insert)
{
undo_info.undo_text = GetText();
undo_info.first_text_insert = false;
}
// checking if insert exceeds max length
if (ToOffset(ivec2((int)lines[lines.size() - 1].text.size(), (int)lines.size() - 1)) + str.length() > (size_t)max_length)
{
return;
}
std::string::size_type start = 0;
while (true)
{
std::string::size_type next_newline = str.find('\n', start);
lines[pos.y].text.insert(pos.x, str.substr(start, next_newline - start));
lines[pos.y].invalidated = true;
if (next_newline == std::string::npos)
break;
pos.x += next_newline - start;
Line line{this};
line.text = lines[pos.y].text.substr(pos.x);
lines.insert(lines.begin() + pos.y + 1, line);
lines[pos.y].text = lines[pos.y].text.substr(0, pos.x);
lines[pos.y].invalidated = true;
pos = ivec2(0, pos.y + 1);
start = next_newline + 1;
}
MoveVerticalScroll();
Update();
}
void TextEdit::Backspace()
{
if (undo_info.first_erase)
{
undo_info.first_erase = false;
undo_info.undo_text = GetText();
}
if (GetSelectionLength() != 0)
{
DeleteSelectedText();
ClearSelection();
Update();
}
else
{
if (cursor_pos.x > 0)
{
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
utf8_reader.set_position(cursor_pos.x);
utf8_reader.prev();
int length = utf8_reader.char_length();
lines[cursor_pos.y].text.erase(cursor_pos.x - length, length);
lines[cursor_pos.y].invalidated = true;
cursor_pos.x -= length;
Update();
}
else if (cursor_pos.y > 0)
{
selection_start = ivec2(lines[cursor_pos.y - 1].text.length(), cursor_pos.y - 1);
selection_length = 1;
DeleteSelectedText();
}
}
MoveVerticalScroll();
}
void TextEdit::Del()
{
if (undo_info.first_erase)
{
undo_info.first_erase = false;
undo_info.undo_text = GetText();
}
if (GetSelectionLength() != 0)
{
DeleteSelectedText();
ClearSelection();
Update();
}
else
{
if (cursor_pos.x < (int)lines[cursor_pos.y].text.size())
{
UTF8Reader utf8_reader(lines[cursor_pos.y].text.data(), lines[cursor_pos.y].text.length());
utf8_reader.set_position(cursor_pos.x);
int length = utf8_reader.char_length();
lines[cursor_pos.y].text.erase(cursor_pos.x, length);
lines[cursor_pos.y].invalidated = true;
Update();
}
else if (cursor_pos.y + 1 < (int)lines.size())
{
selection_start = ivec2((int)lines[cursor_pos.y].text.length(), cursor_pos.y);
selection_length = 1;
DeleteSelectedText();
}
}
MoveVerticalScroll();
}
void TextEdit::OnTimerExpired()
{
if (IsVisible() == false)
{
timer->Stop();
return;
}
if (cursor_blink_visible)
timer->Start(500);
else
timer->Start(500);
cursor_blink_visible = !cursor_blink_visible;
Update();
}
void TextEdit::OnGeometryChanged()
{
Canvas* canvas = GetCanvas();
for (auto& line : lines)
{
line.invalidated = true;
}
vertical_text_align = canvas->verticalTextAlign();
clip_start_offset = 0;
UpdateVerticalScroll();
}
void TextEdit::OnScrollTimerExpired()
{
if (mouse_moves_left)
Move(-1, false, false);
else
Move(1, false, false);
}
void TextEdit::OnEnableChanged()
{
bool enabled = IsEnabled();
if (!enabled)
{
cursor_blink_visible = false;
timer->Stop();
}
Update();
}
bool TextEdit::InputMaskAcceptsInput(ivec2 cursor_pos, const std::string& str)
{
return str.find_first_not_of(input_mask) == std::string::npos;
}
std::string::size_type TextEdit::ToOffset(ivec2 pos) const
{
if (pos.y < (int)lines.size())
{
std::string::size_type offset = 0;
for (int line = 0; line < pos.y; line++)
{
offset += lines[line].text.size() + 1;
}
return offset + std::min((size_t)pos.x, lines[pos.y].text.size());
}
else
{
std::string::size_type offset = 0;
for (size_t line = 0; line < lines.size(); line++)
{
offset += lines[line].text.size() + 1;
}
return offset - 1;
}
}
TextEdit::ivec2 TextEdit::FromOffset(std::string::size_type offset) const
{
int line_offset = 0;
for (int line = 0; line < (int)lines.size(); line++)
{
if (offset <= line_offset + lines[line].text.size())
{
return ivec2(offset - line_offset, line);
}
line_offset += lines[line].text.size() + 1;
}
return ivec2(lines.back().text.size(), lines.size() - 1);
}
double TextEdit::GetTotalHeight()
{
Canvas* canvas = GetCanvas();
LayoutLines(canvas);
if (!lines.empty())
{
return lines.back().box.bottom();
}
else
{
return GetHeight();
}
}
void TextEdit::LayoutLines(Canvas* canvas)
{
ivec2 sel_start;
ivec2 sel_end;
if (selection_length > 0)
{
sel_start = selection_start;
sel_end = FromOffset(ToOffset(selection_start) + selection_length);
}
else if (selection_length < 0)
{
sel_start = FromOffset(ToOffset(selection_start) + selection_length);
sel_end = selection_start;
}
Colorf textColor = GetStyleColor("color");
Point draw_pos;
for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++)
{
Line& line = lines[i];
if (line.invalidated)
{
line.layout.Clear();
if (!line.text.empty())
line.layout.AddText(line.text, font, textColor);
else
line.layout.AddText(" ", font, textColor); // Draw one space character to get the correct height
line.layout.Layout(canvas, GetWidth());
line.box = Rect(draw_pos, line.layout.GetSize());
line.invalidated = false;
}
if (sel_start != sel_end && sel_start.y <= (int)i && sel_end.y >= (int)i)
{
line.layout.SetSelectionRange(sel_start.y < (int)i ? 0 : sel_start.x, sel_end.y > (int)i ? line.text.size() : sel_end.x);
}
else
{
line.layout.SetSelectionRange(0, 0);
}
line.layout.HideCursor();
if (HasFocus())
{
if (cursor_blink_visible && cursor_pos.y == (int)i)
{
line.layout.SetCursorPos(cursor_pos.x);
line.layout.SetCursorColor(textColor);
line.layout.ShowCursor();
}
}
line.box.x = draw_pos.x;
line.box.y = draw_pos.y;
line.layout.SetPosition(line.box.topLeft());
draw_pos = line.box.bottomLeft();
}
UpdateVerticalScroll();
}
void TextEdit::OnPaint(Canvas* canvas)
{
LayoutLines(canvas);
for (size_t i = (size_t)vert_scrollbar->GetPosition(); i < lines.size(); i++)
lines[i].layout.DrawLayout(canvas);
}
TextEdit::ivec2 TextEdit::GetCharacterIndex(Point mouse_wincoords)
{
Canvas* canvas = GetCanvas();
for (size_t i = 0; i < lines.size(); i++)
{
Line& line = lines[i];
if (line.box.top() <= mouse_wincoords.y && line.box.bottom() > mouse_wincoords.y)
{
SpanLayout::HitTestResult result = line.layout.HitTest(canvas, mouse_wincoords);
switch (result.type)
{
case SpanLayout::HitTestResult::inside:
return ivec2(clamp(result.offset, (size_t)0, line.text.size()), i);
case SpanLayout::HitTestResult::outside_left:
return ivec2(0, i);
default:
case SpanLayout::HitTestResult::outside_right:
return ivec2(line.text.size(), i);
}
}
}
return ivec2(lines.back().text.size(), lines.size() - 1);
}