#include "ImGui.hpp" #include #include #include #include #include #include #include #include #include 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(s[0]); if (b0 < 0x80) return b0; if ((b0 & 0xE0) == 0xC0) return ((b0 & 0x1F) << 6) | (static_cast(s[1]) & 0x3F); if ((b0 & 0xF0) == 0xE0) return ((b0 & 0x0F) << 12) | ((static_cast(s[1]) & 0x3F) << 6) | (static_cast(s[2]) & 0x3F); if ((b0 & 0xF8) == 0xF0) return ((b0 & 0x07) << 18) | ((static_cast(s[1]) & 0x3F) << 12) | ((static_cast(s[2]) & 0x3F) << 6) | (static_cast(s[3]) & 0x3F); return 0xFFFD; } constexpr auto decode_utf8(std::string_view text) -> std::vector { std::vector spans; usize i = 0; spans.reserve(text.size()); while (i < text.size()) { u8 b = static_cast(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(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); } 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(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(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 text_renderer) : m_text_renderer(text_renderer) { } void ImGui::begin(u32 const rune, bool ctrl, bool shift, std::string_view const clipboard, std::function 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 { 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 { 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(info.caret_byte); info.anchor = static_cast(info.caret_byte); return info; } auto ImGui::text_input_cursor(usize id) const -> std::optional { 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(begin_clamped); state.preedit_cursor_end = static_cast(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(cp)) != 0; return false; }; auto clamp_cursor = [&]() -> usize { 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[(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 { 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( (submitted ? 1 : 0) | (changed ? 2 : 0)) }; } auto ImGui::list_view(usize id, Rectangle bounds, usize elements, std::function 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