#include "ImGui.hpp" #include #include #include #include #include #include #include #include #include namespace { struct CodepointSpan { u32 codepoint {}; std::size_t start {}; std::size_t end {}; }; auto decode_utf8(std::string_view text) -> std::vector { std::vector spans; std::size_t i = 0; spans.reserve(text.size()); while (i < text.size()) { u8 const byte = static_cast(text[i]); std::size_t const start = i; std::size_t length = 1; u32 cp = 0xFFFD; if (byte < 0x80) { cp = byte; } else if ((byte & 0xE0) == 0xC0) { if (i + 1 < text.size()) { u8 const b1 = static_cast(text[i + 1]); if ((b1 & 0xC0) == 0x80) { u32 const t = ((static_cast(byte) & 0x1F) << 6) | (static_cast(b1) & 0x3F); if (t >= 0x80) { cp = t; length = 2; } } } } else if ((byte & 0xF0) == 0xE0) { if (i + 2 < text.size()) { u8 const b1 = static_cast(text[i + 1]); u8 const b2 = static_cast(text[i + 2]); if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80) { u32 const t = ((static_cast(byte) & 0x0F) << 12) | ((static_cast(b1) & 0x3F) << 6) | (static_cast(b2) & 0x3F); if (t >= 0x800 && (t < 0xD800 || t > 0xDFFF)) { cp = t; length = 3; } } } } else if ((byte & 0xF8) == 0xF0) { if (i + 3 < text.size()) { u8 const b1 = static_cast(text[i + 1]); u8 const b2 = static_cast(text[i + 2]); u8 const b3 = static_cast(text[i + 3]); if ((b1 & 0xC0) == 0x80 && (b2 & 0xC0) == 0x80 && (b3 & 0xC0) == 0x80) { u32 const t = ((static_cast(byte) & 0x07) << 18) | ((static_cast(b1) & 0x3F) << 12) | ((static_cast(b2) & 0x3F) << 6) | (static_cast(b3) & 0x3F); if (t >= 0x10000 && t <= 0x10FFFF) { cp = t; length = 4; } } } } spans.push_back(CodepointSpan { cp, start, start + length }); i += length; } 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(cp); } else if (cp <= 0x7FF) { buf[len++] = static_cast(0xC0 | (cp >> 6)); buf[len++] = static_cast(0x80 | (cp & 0x3F)); } else if (cp <= 0xFFFF) { if (cp >= 0xD800 && cp <= 0xDFFF) return {}; buf[len++] = static_cast(0xE0 | (cp >> 12)); buf[len++] = static_cast(0x80 | ((cp >> 6) & 0x3F)); buf[len++] = static_cast(0x80 | (cp & 0x3F)); } else if (cp <= 0x10FFFF) { buf[len++] = static_cast(0xF0 | (cp >> 18)); buf[len++] = static_cast(0x80 | ((cp >> 12) & 0x3F)); buf[len++] = static_cast(0x80 | ((cp >> 6) & 0x3F)); buf[len++] = static_cast(0x80 | (cp & 0x3F)); } else { return {}; } return std::string(buf, len); } constexpr float HORIZONTAL_PADDING = 6.0f; constexpr float VERTICAL_PADDING = 4.0f; constexpr float CARET_WIDTH = 2.0f; constexpr float CARET_INSET = 1.0f; constexpr double CARET_BLINK_INTERVAL = 0.5; constexpr float CARET_DESCENT_FRACTION = 0.25f; } // namespace ImGui::ImGui(std::shared_ptr text_renderer) : m_text_renderer(text_renderer) { } void ImGui::begin(u32 const rune, bool ctrl, bool shift) { m_rune = rune; m_ctrl = ctrl; m_shift = shift; } void ImGui::end() { } void ImGui::set_font(FontHandle font) { m_font = font; } auto ImGui::text_input(std::size_t 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 (options.font_size > rec.height) { TraceLog(LOG_WARNING, std::format("Text size for text input {} is bigger than height ({} " "> {}). Clipping will occur.", id, options.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(cp)) != 0; return false; }; auto clamp_cursor = [&]() -> std::size_t { int const max_idx = static_cast(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[state.current_rune_idx].start; }; std::size_t 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 = [&](std::size_t byte_begin, std::size_t byte_end) { if (byte_end > byte_begin && byte_begin < str.size()) { str.erase(byte_begin, byte_end - byte_begin); changed = true; } }; 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 > static_cast(spans.size())) return; if (m_ctrl) { int idx = state.current_rune_idx; int scan = idx - 1; while (scan >= 0 && is_space( spans[static_cast(scan)].codepoint)) scan--; while (scan >= 0 && !is_space( spans[static_cast(scan)].codepoint)) scan--; int start_idx = std::max(scan + 1, 0); std::size_t byte_begin = spans[static_cast(start_idx)].start; std::size_t byte_end = (idx >= static_cast(spans.size())) ? str.size() : spans[static_cast(idx)].start; erase_range(byte_begin, byte_end); state.current_rune_idx = start_idx; } else { auto const &prev = spans[static_cast( 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 >= static_cast(spans.size())) { if (!m_ctrl) return; } int idx = state.current_rune_idx; if (m_ctrl) { int scan = idx; while (scan < static_cast(spans.size()) && is_space( spans[static_cast(scan)].codepoint)) scan++; while (scan < static_cast(spans.size()) && !is_space( spans[static_cast(scan)].codepoint)) scan++; std::size_t byte_begin = (idx < static_cast(spans.size())) ? spans[static_cast(idx)].start : str.size(); std::size_t byte_end = (scan < static_cast(spans.size())) ? spans[static_cast(scan)].start : str.size(); erase_range(byte_begin, byte_end); } else if (idx < static_cast(spans.size())) { auto const &curr = spans[static_cast(idx)]; erase_range(curr.start, curr.end); } request_refresh = true; }; switch (m_rune) { case 1: // Left (H) if (state.current_rune_idx > 0) { state.current_rune_idx--; if (m_ctrl) { while (state.current_rune_idx > 0 && is_space(spans[static_cast( state.current_rune_idx)] .codepoint)) state.current_rune_idx--; while (state.current_rune_idx > 0 && !is_space(spans[static_cast( state.current_rune_idx)] .codepoint)) state.current_rune_idx--; } caret_byte = clamp_cursor(); } break; case 4: // Right (L) if (state.current_rune_idx < static_cast(spans.size())) { state.current_rune_idx++; if (m_ctrl) { while ( state.current_rune_idx < static_cast(spans.size()) && is_space(spans[static_cast( state.current_rune_idx - 1)] .codepoint)) state.current_rune_idx++; while ( state.current_rune_idx < static_cast(spans.size()) && !is_space(spans[static_cast( state.current_rune_idx - 1)] .codepoint)) state.current_rune_idx++; } caret_byte = clamp_cursor(); } break; case 3: // Up (K) state.current_rune_idx = 0; caret_byte = clamp_cursor(); break; case 2: // Down (J) state.current_rune_idx = static_cast(spans.size()); caret_byte = clamp_cursor(); break; case 8: // Backspace handle_backspace(); break; case 0x7F: // Delete handle_delete(); break; case '\r': case '\n': if (options.multiline) { 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: if (m_rune >= 0x20) { 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; } double const dt = static_cast(GetFrameTime()); if (m_focused_id == id) { 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 = static_cast(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; } Vector2 prefix_metrics { 0.0f, 0.0f }; Vector2 full_metrics { 0.0f, 0.0f }; if (m_font.has_value() && m_text_renderer) { std::string_view const prefix_view(str.data(), caret_byte); prefix_metrics = m_text_renderer->measure_text( *m_font, prefix_view, static_cast(options.font_size)); full_metrics = m_text_renderer->measure_text( *m_font, str_view, static_cast(options.font_size)); } state.cursor_position.x = prefix_metrics.x; float const available_width = std::max(0.0f, rec.width - 2.0f * HORIZONTAL_PADDING); if (full_metrics.x <= available_width) { state.scroll_offset.x = 0.0f; } else { float &scroll = state.scroll_offset.x; float caret_local = state.cursor_position.x - scroll; if (caret_local > available_width) { scroll = state.cursor_position.x - available_width; } else if (caret_local < 0.0f) { scroll = state.cursor_position.x; } scroll = std::clamp( scroll, 0.0f, std::max(0.0f, full_metrics.x - available_width)); } state.scroll_offset.y = 0.0f; 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 + options.font_size; float const max_caret_height = std::max(0.0f, rec.height - 2.0f * VERTICAL_PADDING); float caret_height = options.font_size * (1.0f + CARET_DESCENT_FRACTION); caret_height = std::min(caret_height, max_caret_height > 0.0f ? max_caret_height : caret_height); if (caret_height <= 0.0f) caret_height = options.font_size; float caret_top = baseline_y - options.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 + options.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; BeginScissorMode(rec.x, rec.y, rec.width, rec.height); { if (m_font.has_value() && m_text_renderer) { Vector2 const text_pos { rec.x + HORIZONTAL_PADDING - state.scroll_offset.x, baseline_y, }; Color const text_color { 255, 255, 255, 255 }; m_text_renderer->draw_text(*m_font, str_view, text_pos, static_cast(options.font_size), text_color); if (m_focused_id == id && state.caret_visible) { float const caret_x = std::round(rec.x + HORIZONTAL_PADDING + state.cursor_position.x - state.scroll_offset.x + CARET_INSET); Rectangle caret_rect { caret_x, caret_top, CARET_WIDTH, caret_height, }; Color const caret_color { 255, 255, 255, 200 }; DrawRectangleRec(caret_rect, caret_color); } } } EndScissorMode(); return std::bitset<2> { static_cast( (submitted ? 1 : 0) | (changed ? 2 : 0)) }; }