793 lines
22 KiB
C++
793 lines
22 KiB
C++
#include "ImGui.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include <raylib.h>
|
|
|
|
namespace Waylight {
|
|
|
|
namespace {
|
|
|
|
struct CodepointSpan {
|
|
u32 codepoint {};
|
|
usize start {};
|
|
usize end {};
|
|
};
|
|
|
|
constexpr inline float px_pos(float x) { return std::floor(x + 0.5f); }
|
|
constexpr inline float px_w(float w) { return std::ceil(w); }
|
|
|
|
constexpr auto utf8_rune_from_first(char const *s) -> u32
|
|
{
|
|
u8 b0 = static_cast<u8>(s[0]);
|
|
if (b0 < 0x80)
|
|
return b0;
|
|
|
|
if ((b0 & 0xE0) == 0xC0)
|
|
return ((b0 & 0x1F) << 6) | (static_cast<u8>(s[1]) & 0x3F);
|
|
|
|
if ((b0 & 0xF0) == 0xE0)
|
|
return ((b0 & 0x0F) << 12) | ((static_cast<u8>(s[1]) & 0x3F) << 6)
|
|
| (static_cast<u8>(s[2]) & 0x3F);
|
|
|
|
if ((b0 & 0xF8) == 0xF0)
|
|
return ((b0 & 0x07) << 18) | ((static_cast<u8>(s[1]) & 0x3F) << 12)
|
|
| ((static_cast<u8>(s[2]) & 0x3F) << 6)
|
|
| (static_cast<u8>(s[3]) & 0x3F);
|
|
|
|
return 0xFFFD;
|
|
}
|
|
|
|
constexpr auto decode_utf8(std::string_view text) -> std::vector<CodepointSpan>
|
|
{
|
|
std::vector<CodepointSpan> spans;
|
|
usize i = 0;
|
|
spans.reserve(text.size());
|
|
|
|
while (i < text.size()) {
|
|
u8 b = static_cast<u8>(text[i]);
|
|
usize len = 1;
|
|
|
|
if (b < 0x80)
|
|
len = 1;
|
|
else if ((b & 0xE0) == 0xC0)
|
|
len = 2;
|
|
else if ((b & 0xF0) == 0xE0)
|
|
len = 3;
|
|
else if ((b & 0xF8) == 0xF0)
|
|
len = 4;
|
|
|
|
if (i + len > text.size())
|
|
len = 1;
|
|
|
|
u32 cp = utf8_rune_from_first(text.data() + i);
|
|
spans.push_back({ cp, i, i + len });
|
|
i += len;
|
|
}
|
|
|
|
return spans;
|
|
}
|
|
|
|
auto encode_utf8(u32 cp) -> std::string
|
|
{
|
|
char buf[5] = { 0, 0, 0, 0, 0 };
|
|
int len = 0;
|
|
if (cp <= 0x7F) {
|
|
buf[len++] = static_cast<char>(cp);
|
|
} else if (cp <= 0x7FF) {
|
|
buf[len++] = static_cast<char>(0xC0 | (cp >> 6));
|
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp <= 0xFFFF) {
|
|
if (cp >= 0xD800 && cp <= 0xDFFF)
|
|
return {};
|
|
buf[len++] = static_cast<char>(0xE0 | (cp >> 12));
|
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp <= 0x10FFFF) {
|
|
buf[len++] = static_cast<char>(0xF0 | (cp >> 18));
|
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
|
buf[len++] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
buf[len++] = static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else {
|
|
return {};
|
|
}
|
|
return std::string(buf, len);
|
|
}
|
|
|
|
auto rune_index_for_byte(std::string_view text, usize byte_offset) -> int
|
|
{
|
|
auto spans { decode_utf8(text) };
|
|
int idx = 0;
|
|
for (auto const &span : spans) {
|
|
if (span.start >= byte_offset)
|
|
break;
|
|
idx++;
|
|
}
|
|
if (byte_offset >= text.size())
|
|
idx = static_cast<int>(spans.size());
|
|
return idx;
|
|
}
|
|
|
|
auto clamp_preedit_index(int value, usize text_size) -> usize
|
|
{
|
|
if (value < 0)
|
|
return 0;
|
|
auto const as_size { static_cast<usize>(value) };
|
|
return std::min(as_size, text_size);
|
|
}
|
|
|
|
constexpr float HORIZONTAL_PADDING = 6.0f;
|
|
constexpr float VERTICAL_PADDING = 4.0f;
|
|
constexpr double CARET_BLINK_INTERVAL = 0.5;
|
|
constexpr float CARET_DESCENT_FRACTION = 0.25f;
|
|
|
|
} // namespace
|
|
|
|
ImGui::ImGui(std::shared_ptr<TextRenderer> text_renderer)
|
|
: m_text_renderer(text_renderer)
|
|
{
|
|
}
|
|
|
|
void ImGui::begin(u32 const rune, bool ctrl, bool shift,
|
|
std::string_view const clipboard,
|
|
std::function<void(std::string_view const &)> clipboard_set)
|
|
{
|
|
m_rune = rune;
|
|
m_ctrl = ctrl;
|
|
m_shift = shift;
|
|
m_clipboard = clipboard;
|
|
m_clipboard_set = clipboard_set;
|
|
}
|
|
|
|
void ImGui::end()
|
|
{
|
|
m_rune = false;
|
|
m_ctrl = false;
|
|
m_shift = false;
|
|
m_clipboard = {};
|
|
m_clipboard_set = nullptr;
|
|
}
|
|
|
|
void ImGui::set_font(FontHandle font) { m_font = font; }
|
|
|
|
auto ImGui::focused_text_input() const -> std::optional<usize>
|
|
{
|
|
if (m_focused_id == 0)
|
|
return std::nullopt;
|
|
return m_focused_id;
|
|
}
|
|
|
|
auto ImGui::text_input_surrounding(usize id, std::pmr::string const &str) const
|
|
-> std::optional<TextInputSurrounding>
|
|
{
|
|
auto it { m_ti_states.find(id) };
|
|
if (it == m_ti_states.end())
|
|
return std::nullopt;
|
|
TextInputSurrounding info;
|
|
info.text.assign(str.data(), str.size());
|
|
info.caret_byte = std::min(it->second.caret_byte, str.size());
|
|
info.cursor = static_cast<int>(info.caret_byte);
|
|
info.anchor = static_cast<int>(info.caret_byte);
|
|
return info;
|
|
}
|
|
|
|
auto ImGui::text_input_cursor(usize id) const -> std::optional<TextInputCursor>
|
|
{
|
|
auto it { m_ti_states.find(id) };
|
|
if (it == m_ti_states.end())
|
|
return std::nullopt;
|
|
TextInputCursor cursor;
|
|
cursor.rect = it->second.caret_rect;
|
|
cursor.visible
|
|
= it->second.caret_visible && !it->second.preedit_cursor_hidden;
|
|
return cursor;
|
|
}
|
|
|
|
void ImGui::ime_commit_text(std::pmr::string &str, std::string_view text)
|
|
{
|
|
if (m_focused_id == 0)
|
|
return;
|
|
auto it { m_ti_states.find(m_focused_id) };
|
|
if (it == m_ti_states.end())
|
|
return;
|
|
auto &state { it->second };
|
|
usize insert_pos = std::min(state.caret_byte, str.size());
|
|
if (!text.empty())
|
|
str.insert(insert_pos, text);
|
|
state.caret_byte = insert_pos + text.size();
|
|
std::string_view const view(str.data(), str.size());
|
|
state.current_rune_idx = rune_index_for_byte(view, state.caret_byte);
|
|
state.caret_timer = 0.0;
|
|
state.caret_visible = true;
|
|
state.external_change = true;
|
|
}
|
|
|
|
void ImGui::ime_delete_surrounding(
|
|
std::pmr::string &str, usize before, usize after)
|
|
{
|
|
if (m_focused_id == 0)
|
|
return;
|
|
auto it { m_ti_states.find(m_focused_id) };
|
|
if (it == m_ti_states.end())
|
|
return;
|
|
auto &state { it->second };
|
|
usize caret_byte { std::min(state.caret_byte, str.size()) };
|
|
usize start { before > caret_byte ? 0 : caret_byte - before };
|
|
usize end { std::min(caret_byte + after, str.size()) };
|
|
if (end > start) {
|
|
str.erase(start, end - start);
|
|
state.caret_byte = start;
|
|
std::string_view const view(str.data(), str.size());
|
|
state.current_rune_idx = rune_index_for_byte(view, state.caret_byte);
|
|
state.caret_timer = 0.0;
|
|
state.caret_visible = true;
|
|
state.external_change = true;
|
|
}
|
|
}
|
|
|
|
void ImGui::ime_set_preedit(std::string text, int cursor_begin, int cursor_end)
|
|
{
|
|
if (m_focused_id == 0)
|
|
return;
|
|
auto it { m_ti_states.find(m_focused_id) };
|
|
if (it == m_ti_states.end())
|
|
return;
|
|
auto &state { it->second };
|
|
state.preedit_text = std::move(text);
|
|
state.preedit_cursor_hidden = (cursor_begin == -1 && cursor_end == -1);
|
|
usize const size = state.preedit_text.size();
|
|
if (state.preedit_cursor_hidden) {
|
|
state.preedit_cursor_begin = 0;
|
|
state.preedit_cursor_end = 0;
|
|
} else {
|
|
auto begin_clamped { clamp_preedit_index(cursor_begin, size) };
|
|
auto end_clamped { clamp_preedit_index(cursor_end, size) };
|
|
state.preedit_cursor_begin = static_cast<int>(begin_clamped);
|
|
state.preedit_cursor_end = static_cast<int>(end_clamped);
|
|
}
|
|
state.preedit_active
|
|
= !state.preedit_text.empty() || !state.preedit_cursor_hidden;
|
|
if (state.preedit_active) {
|
|
state.caret_timer = 0.0;
|
|
state.caret_visible = !state.preedit_cursor_hidden;
|
|
}
|
|
}
|
|
|
|
void ImGui::ime_clear_preedit()
|
|
{
|
|
if (m_focused_id == 0)
|
|
return;
|
|
auto it { m_ti_states.find(m_focused_id) };
|
|
if (it == m_ti_states.end())
|
|
return;
|
|
auto &state { it->second };
|
|
state.preedit_text.clear();
|
|
state.preedit_cursor_begin = 0;
|
|
state.preedit_cursor_end = 0;
|
|
state.preedit_active = false;
|
|
state.preedit_cursor_hidden = false;
|
|
state.caret_visible = true;
|
|
state.caret_timer = 0.0;
|
|
}
|
|
|
|
size_t utf8_length(std::string_view const &s)
|
|
{
|
|
size_t count = std::count_if(
|
|
s.begin(), s.end(), [](auto const &c) { return (c & 0xC0) != 0x80; });
|
|
return count;
|
|
}
|
|
|
|
auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec,
|
|
TextInputOptions options) -> std::bitset<2>
|
|
{
|
|
assert(id != 0);
|
|
assert(
|
|
m_font.has_value() && "ImGui font must be set before using text input");
|
|
|
|
bool submitted { false };
|
|
bool changed { false };
|
|
|
|
auto &state { m_ti_states[id] };
|
|
assert(!options.multiline && "Multiline not yet implemented.");
|
|
|
|
if (m_focused_id == 0)
|
|
m_focused_id = id;
|
|
|
|
if (style().font_size > rec.height) {
|
|
TraceLog(LOG_WARNING,
|
|
std::format("Text size for text input {} is bigger than height ({} "
|
|
"> {}). Clipping will occur.",
|
|
id, style().font_size, rec.height)
|
|
.c_str());
|
|
}
|
|
|
|
std::string_view str_view(str.data(), str.size());
|
|
auto spans { decode_utf8(str_view) };
|
|
|
|
auto is_space = [](u32 cp) -> bool {
|
|
if (cp == '\n' || cp == '\r' || cp == '\t' || cp == '\v' || cp == '\f')
|
|
return true;
|
|
if (cp <= 0x7F)
|
|
return std::isspace(static_cast<unsigned char>(cp)) != 0;
|
|
return false;
|
|
};
|
|
|
|
auto clamp_cursor = [&]() -> usize {
|
|
int const max_idx = static_cast<int>(spans.size());
|
|
state.current_rune_idx = std::clamp(state.current_rune_idx, 0, max_idx);
|
|
if (state.current_rune_idx == max_idx)
|
|
return str.size();
|
|
return spans[(usize)state.current_rune_idx].start;
|
|
};
|
|
|
|
usize caret_byte = clamp_cursor();
|
|
|
|
auto refresh_spans = [&]() {
|
|
str_view = std::string_view(str.data(), str.size());
|
|
spans = decode_utf8(str_view);
|
|
caret_byte = clamp_cursor();
|
|
};
|
|
|
|
auto erase_range = [&](usize byte_begin, usize byte_end) {
|
|
if (byte_end > byte_begin && byte_begin < str.size()) {
|
|
str.erase(byte_begin, byte_end - byte_begin);
|
|
changed = true;
|
|
}
|
|
};
|
|
|
|
auto selection_range_bytes
|
|
= [&](int a_idx, int b_idx) -> std::pair<usize, usize> {
|
|
int lo = std::max(0, std::min(a_idx, b_idx));
|
|
int hi = std::max(0, std::max(a_idx, b_idx));
|
|
usize byte_begin
|
|
= (lo >= (int)spans.size()) ? str.size() : spans[(usize)lo].start;
|
|
usize byte_end
|
|
= (hi >= (int)spans.size()) ? str.size() : spans[(usize)hi].start;
|
|
return { byte_begin, byte_end };
|
|
};
|
|
|
|
auto erase_selection_if_any = [&]() -> bool {
|
|
if (!state.has_selection(state.current_rune_idx))
|
|
return false;
|
|
auto [b, e] = selection_range_bytes(
|
|
state.sel_anchor_idx, state.current_rune_idx);
|
|
if (e > b) {
|
|
str.erase(b, e - b);
|
|
changed = true;
|
|
refresh_spans();
|
|
state.current_rune_idx = rune_index_for_byte(str_view, b);
|
|
state.clear_selection();
|
|
}
|
|
return true;
|
|
};
|
|
|
|
auto move_left_word = [&]() {
|
|
while (state.current_rune_idx > 0
|
|
&& is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint))
|
|
state.current_rune_idx--;
|
|
while (state.current_rune_idx > 0
|
|
&& !is_space(spans[(usize)(state.current_rune_idx - 1)].codepoint))
|
|
state.current_rune_idx--;
|
|
};
|
|
auto move_right_word = [&]() {
|
|
while (state.current_rune_idx < (int)spans.size()
|
|
&& is_space(spans[(usize)state.current_rune_idx].codepoint))
|
|
state.current_rune_idx++;
|
|
while (state.current_rune_idx < (int)spans.size()
|
|
&& !is_space(spans[(usize)state.current_rune_idx].codepoint))
|
|
state.current_rune_idx++;
|
|
};
|
|
|
|
bool caret_activity = false;
|
|
|
|
if (m_focused_id == id && m_rune != 0) {
|
|
bool request_refresh = false;
|
|
|
|
auto handle_backspace = [&]() {
|
|
if (state.current_rune_idx <= 0
|
|
|| state.current_rune_idx > (int)spans.size())
|
|
return;
|
|
if (m_ctrl) {
|
|
int idx = state.current_rune_idx, scan = idx - 1;
|
|
while (scan >= 0 && is_space(spans[(usize)scan].codepoint))
|
|
scan--;
|
|
while (scan >= 0 && !is_space(spans[(usize)scan].codepoint))
|
|
scan--;
|
|
int start_idx = std::max(scan + 1, 0);
|
|
usize b = spans[(usize)start_idx].start;
|
|
usize e = (idx >= (int)spans.size()) ? str.size()
|
|
: spans[(usize)idx].start;
|
|
erase_range(b, e);
|
|
state.current_rune_idx = start_idx;
|
|
} else {
|
|
auto const &prev = spans[(usize)(state.current_rune_idx - 1)];
|
|
erase_range(prev.start, prev.end);
|
|
state.current_rune_idx--;
|
|
}
|
|
request_refresh = true;
|
|
};
|
|
|
|
auto handle_delete = [&]() {
|
|
if (state.current_rune_idx < 0
|
|
|| state.current_rune_idx >= (int)spans.size()) {
|
|
if (!m_ctrl)
|
|
return;
|
|
}
|
|
int idx = state.current_rune_idx;
|
|
if (m_ctrl) {
|
|
int scan = idx;
|
|
while (scan < (int)spans.size()
|
|
&& is_space(spans[(usize)scan].codepoint))
|
|
scan++;
|
|
while (scan < (int)spans.size()
|
|
&& !is_space(spans[(usize)scan].codepoint))
|
|
scan++;
|
|
usize b = (idx < (int)spans.size()) ? spans[(usize)idx].start
|
|
: str.size();
|
|
usize e = (scan < (int)spans.size()) ? spans[(usize)scan].start
|
|
: str.size();
|
|
erase_range(b, e);
|
|
} else if (idx < (int)spans.size()) {
|
|
auto const &curr = spans[(usize)idx];
|
|
erase_range(curr.start, curr.end);
|
|
}
|
|
request_refresh = true;
|
|
};
|
|
|
|
bool extend = m_shift;
|
|
|
|
switch (m_rune) {
|
|
case 1: // Left
|
|
if (!extend)
|
|
state.clear_selection();
|
|
if (state.current_rune_idx > 0) {
|
|
if (extend && state.sel_anchor_idx == -1)
|
|
state.sel_anchor_idx = state.current_rune_idx;
|
|
if (m_ctrl)
|
|
move_left_word();
|
|
else
|
|
state.current_rune_idx--;
|
|
caret_byte = clamp_cursor();
|
|
}
|
|
break;
|
|
case 4: // Right
|
|
if (!extend)
|
|
state.clear_selection();
|
|
if (state.current_rune_idx < (int)spans.size()) {
|
|
if (extend && state.sel_anchor_idx == -1)
|
|
state.sel_anchor_idx = state.current_rune_idx;
|
|
if (m_ctrl)
|
|
move_right_word();
|
|
else
|
|
state.current_rune_idx++;
|
|
caret_byte = clamp_cursor();
|
|
}
|
|
break;
|
|
case 3: // Up -> home
|
|
if (!extend)
|
|
state.clear_selection();
|
|
if (extend && state.sel_anchor_idx == -1)
|
|
state.sel_anchor_idx = state.current_rune_idx;
|
|
state.current_rune_idx = 0;
|
|
caret_byte = clamp_cursor();
|
|
break;
|
|
case 2: // Down -> end
|
|
if (!extend)
|
|
state.clear_selection();
|
|
if (extend && state.sel_anchor_idx == -1)
|
|
state.sel_anchor_idx = state.current_rune_idx;
|
|
state.current_rune_idx = (int)spans.size();
|
|
caret_byte = clamp_cursor();
|
|
break;
|
|
case 8: // Backspace
|
|
if (erase_selection_if_any()) {
|
|
request_refresh = true;
|
|
break;
|
|
}
|
|
handle_backspace();
|
|
break;
|
|
case 0x7F: // Delete
|
|
if (erase_selection_if_any()) {
|
|
request_refresh = true;
|
|
break;
|
|
}
|
|
handle_delete();
|
|
break;
|
|
case 'a':
|
|
if (m_ctrl) {
|
|
state.sel_anchor_idx = 0;
|
|
state.current_rune_idx = (int)spans.size();
|
|
request_refresh = true;
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case 'c':
|
|
if (m_ctrl) {
|
|
if (state.has_selection(state.current_rune_idx)
|
|
&& m_clipboard_set) {
|
|
auto [b, e] = selection_range_bytes(
|
|
state.sel_anchor_idx, state.current_rune_idx);
|
|
m_clipboard_set(std::string_view(str.data() + b, e - b));
|
|
}
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case 'x':
|
|
if (m_ctrl) {
|
|
if (state.has_selection(state.current_rune_idx)
|
|
&& m_clipboard_set) {
|
|
auto [b, e] = selection_range_bytes(
|
|
state.sel_anchor_idx, state.current_rune_idx);
|
|
m_clipboard_set(std::string_view(str.data() + b, e - b));
|
|
str.erase(b, e - b);
|
|
changed = true;
|
|
request_refresh = true;
|
|
refresh_spans();
|
|
state.current_rune_idx = rune_index_for_byte(str_view, b);
|
|
state.clear_selection();
|
|
}
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case 'v':
|
|
if (m_ctrl && !m_clipboard.empty()) {
|
|
erase_selection_if_any();
|
|
if (!options.multiline) {
|
|
std::string clip2;
|
|
clip2.reserve(m_clipboard.size());
|
|
std::copy_if(m_clipboard.begin(), m_clipboard.end(),
|
|
clip2.begin(),
|
|
[](char ch) { return ch != '\n' && ch != '\r'; });
|
|
str.insert(caret_byte, clip2);
|
|
state.current_rune_idx += (int)utf8_length(clip2);
|
|
} else {
|
|
str.insert(caret_byte, m_clipboard);
|
|
state.current_rune_idx += (int)utf8_length(m_clipboard);
|
|
}
|
|
changed = true;
|
|
request_refresh = true;
|
|
break;
|
|
} else {
|
|
goto insert_printable;
|
|
}
|
|
break;
|
|
case '\r':
|
|
case '\n':
|
|
if (options.multiline) {
|
|
erase_selection_if_any();
|
|
auto encoded { encode_utf8('\n') };
|
|
if (!encoded.empty()) {
|
|
str.insert(caret_byte, encoded);
|
|
state.current_rune_idx++;
|
|
changed = true;
|
|
request_refresh = true;
|
|
}
|
|
} else {
|
|
submitted = true;
|
|
}
|
|
break;
|
|
default:
|
|
insert_printable:
|
|
if (m_rune >= 0x20) {
|
|
erase_selection_if_any();
|
|
auto encoded { encode_utf8(m_rune) };
|
|
if (!encoded.empty()) {
|
|
str.insert(caret_byte, encoded);
|
|
state.current_rune_idx++;
|
|
changed = true;
|
|
request_refresh = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (request_refresh)
|
|
refresh_spans();
|
|
else
|
|
caret_byte = clamp_cursor();
|
|
caret_activity = true;
|
|
}
|
|
|
|
state.caret_byte = caret_byte;
|
|
|
|
double const dt = (double)GetFrameTime();
|
|
if (m_focused_id == id) {
|
|
if (state.preedit_active && state.preedit_cursor_hidden) {
|
|
state.caret_visible = false;
|
|
state.caret_timer = 0.0;
|
|
} else if (caret_activity) {
|
|
state.caret_timer = 0.0;
|
|
state.caret_visible = true;
|
|
} else {
|
|
if (state.caret_timer == 0.0)
|
|
state.caret_visible = true;
|
|
state.caret_timer += dt;
|
|
if (state.caret_timer >= CARET_BLINK_INTERVAL) {
|
|
int toggles = (int)(state.caret_timer / CARET_BLINK_INTERVAL);
|
|
state.caret_timer
|
|
= std::fmod(state.caret_timer, CARET_BLINK_INTERVAL);
|
|
if (toggles % 2 == 1)
|
|
state.caret_visible = !state.caret_visible;
|
|
}
|
|
}
|
|
} else {
|
|
state.caret_visible = false;
|
|
state.caret_timer = 0.0;
|
|
}
|
|
|
|
Color const bg_col { 16, 16, 16, 100 };
|
|
Color const border_col { 220, 220, 220, 180 };
|
|
DrawRectangleRec(rec, bg_col);
|
|
DrawRectangleLinesEx(rec, 1.0f, border_col);
|
|
|
|
float const text_top = rec.y + VERTICAL_PADDING;
|
|
float const baseline_y = text_top + style().font_size;
|
|
float const max_caret_h
|
|
= std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING);
|
|
float caret_height = style().font_size * (1.0f + CARET_DESCENT_FRACTION);
|
|
caret_height = std::min(
|
|
caret_height, max_caret_h > 0.0f ? max_caret_h : caret_height);
|
|
if (caret_height <= 0.0f)
|
|
caret_height = style().font_size;
|
|
float caret_top = baseline_y - style().font_size;
|
|
float const min_top = rec.y + VERTICAL_PADDING;
|
|
float const max_top = rec.y + rec.height - VERTICAL_PADDING - caret_height;
|
|
caret_top = std::clamp(caret_top, min_top, max_top);
|
|
float caret_bottom = caret_top + caret_height;
|
|
float const desired_bottom
|
|
= baseline_y + style().font_size * CARET_DESCENT_FRACTION;
|
|
if (caret_bottom < desired_bottom) {
|
|
float const adjust = desired_bottom - caret_bottom;
|
|
caret_top = std::min(caret_top + adjust, max_top);
|
|
caret_bottom = caret_top + caret_height;
|
|
}
|
|
state.cursor_position.y = caret_top;
|
|
|
|
float const available_width
|
|
= std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING);
|
|
float const base_y = px_pos(baseline_y);
|
|
|
|
int const font_px = (int)style().font_size;
|
|
|
|
Vector2 prefix_metrics { 0.0f, 0.0f };
|
|
{
|
|
std::string_view prefix(str.data(), caret_byte);
|
|
if (m_text_renderer)
|
|
prefix_metrics
|
|
= m_text_renderer->measure_text(*m_font, prefix, font_px);
|
|
}
|
|
|
|
Vector2 caret_preedit_metrics { 0.0f, 0.0f };
|
|
bool const has_preedit = state.preedit_active
|
|
&& (!state.preedit_text.empty() || !state.preedit_cursor_hidden);
|
|
if (has_preedit && m_text_renderer) {
|
|
auto pe_end = clamp_preedit_index(
|
|
state.preedit_cursor_end, state.preedit_text.size());
|
|
caret_preedit_metrics = m_text_renderer->measure_text(*m_font,
|
|
std::string_view(state.preedit_text.data(), (usize)pe_end),
|
|
font_px);
|
|
}
|
|
|
|
Vector2 full_metrics { 0.0f, 0.0f };
|
|
{
|
|
std::string display;
|
|
display.reserve(
|
|
str.size() + (has_preedit ? state.preedit_text.size() : 0));
|
|
display.append(std::string_view(str.data(), caret_byte));
|
|
if (has_preedit)
|
|
display.append(state.preedit_text);
|
|
display.append(
|
|
std::string_view(str.data() + caret_byte, str.size() - caret_byte));
|
|
if (m_text_renderer)
|
|
full_metrics = m_text_renderer->measure_text(*m_font,
|
|
std::string_view(display.data(), display.size()), font_px);
|
|
}
|
|
|
|
float caret_offset = prefix_metrics.x + caret_preedit_metrics.x;
|
|
state.cursor_position.x = caret_offset;
|
|
|
|
if (full_metrics.x <= available_width) {
|
|
state.scroll_offset.x = 0.0f;
|
|
} else {
|
|
float &scroll = state.scroll_offset.x;
|
|
float caret_local = caret_offset - scroll;
|
|
float const pad = 8.0f;
|
|
if (caret_local > available_width - pad)
|
|
scroll = caret_offset - (available_width - pad);
|
|
else if (caret_local < pad)
|
|
scroll = caret_offset - pad;
|
|
scroll = std::clamp(
|
|
scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width));
|
|
}
|
|
state.scroll_offset.y = 0.0f;
|
|
|
|
float const origin = rec.x + HORIZONTAL_PADDING - state.scroll_offset.x;
|
|
|
|
BeginScissorMode(rec.x, rec.y, rec.width, rec.height);
|
|
{
|
|
if (m_font.has_value() && m_text_renderer) {
|
|
Color const &text_color = style().text_color;
|
|
|
|
std::string display;
|
|
display.reserve(
|
|
str.size() + (has_preedit ? state.preedit_text.size() : 0));
|
|
display.append(std::string_view(str.data(), caret_byte));
|
|
if (has_preedit)
|
|
display.append(state.preedit_text);
|
|
display.append(std::string_view(
|
|
str.data() + caret_byte, str.size() - caret_byte));
|
|
|
|
m_text_renderer->draw_text(*m_font,
|
|
std::string_view(display.data(), display.size()),
|
|
{ origin, base_y }, font_px, text_color);
|
|
|
|
if (state.has_selection(state.current_rune_idx)) {
|
|
auto [sb, se] = selection_range_bytes(
|
|
state.sel_anchor_idx, state.current_rune_idx);
|
|
|
|
Vector2 sel_prefix = m_text_renderer->measure_text(
|
|
*m_font, std::string_view(str.data(), sb), font_px);
|
|
Vector2 sel_width = m_text_renderer->measure_text(*m_font,
|
|
std::string_view(str.data() + sb, se - sb), font_px);
|
|
|
|
Rectangle sel_rect { std::floor(origin + sel_prefix.x + 0.5f),
|
|
std::floor(caret_top + 0.5f),
|
|
std::max(1.0f, sel_width.x) + 1,
|
|
std::max(1.0f, std::round(caret_height)) };
|
|
DrawRectangleRec(sel_rect, style().selection_color);
|
|
|
|
m_text_renderer->draw_text(*m_font,
|
|
std::string_view(str.data() + sb, se - sb),
|
|
{ origin + sel_prefix.x, base_y }, font_px,
|
|
style().selection_text_color);
|
|
}
|
|
|
|
if (m_focused_id == id && state.caret_visible) {
|
|
float const caret_x = std::floor(origin + caret_offset + 0.5f);
|
|
Vector2 const p0 { caret_x, std::floor(caret_top + 0.5f) };
|
|
Vector2 const p1 { caret_x,
|
|
std::floor((caret_top + caret_height) + 0.5f) };
|
|
DrawLineV(p0, p1, text_color);
|
|
}
|
|
}
|
|
}
|
|
EndScissorMode();
|
|
|
|
if (state.external_change) {
|
|
changed = true;
|
|
state.external_change = false;
|
|
}
|
|
|
|
return std::bitset<2> { static_cast<unsigned long long>(
|
|
(submitted ? 1 : 0) | (changed ? 2 : 0)) };
|
|
}
|
|
|
|
auto ImGui::list_view(usize id, Rectangle bounds, usize elements,
|
|
std::function<Vector2(usize i)> draw_cb, ListViewOptions options) -> bool
|
|
{
|
|
auto const &state { m_lv_states[id] };
|
|
|
|
bool submitted { false };
|
|
|
|
m_next_lv_next = false;
|
|
m_next_lv_previous = false;
|
|
m_next_lv_clear = false;
|
|
|
|
BeginScissorMode(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
|
|
EndScissorMode();
|
|
|
|
m_prev_lv_selected_item = state.selected_item;
|
|
|
|
return submitted;
|
|
}
|
|
|
|
} // namespace Waylight
|