diff --git a/src/App.cpp b/src/App.cpp index 1b1525b..a9a608d 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -319,6 +319,11 @@ auto App::init_wayland() -> void app->m_kbd.typing.push_back('v'); handled = true; break; + case XKB_KEY_c: + case XKB_KEY_C: + app->m_kbd.typing.push_back('c'); + handled = true; + break; case XKB_KEY_w: case XKB_KEY_W: if (ctrl) { @@ -530,7 +535,6 @@ auto App::init_wayland() -> void #endif }; wl_data_offer_add_listener(offer, &offer_l, app); - // swap old if (app->m_wayland.curr_offer && app->m_wayland.curr_offer != offer) wl_data_offer_destroy( @@ -551,45 +555,54 @@ auto App::init_wayland() -> void app->m_clipboard_cache.clear(); return; } - char const *mime_utf8 = "text/plain;charset=utf-8"; - char const *mime_plain = "text/plain"; + + char const *mime = "text/plain;charset=utf-8"; int fds[2]; if (pipe(fds) != 0) return; - wl_data_offer_receive(offer, mime_utf8, fds[1]); + + wl_data_offer_receive(offer, mime, fds[1]); wl_display_flush(app->m_wayland.display); close(fds[1]); - std::string data_utf8; - char buf[4096]; - for (;;) { - ssize_t n = read(fds[0], buf, sizeof(buf)); - if (n > 0) - data_utf8.append(buf, buf + n); - else - break; - } - close(fds[0]); - if (data_utf8.empty()) { - if (pipe(fds) != 0) - return; - wl_data_offer_receive( - offer, mime_plain, fds[1]); - wl_display_flush(app->m_wayland.display); - close(fds[1]); - std::string data_plain; + int rfd = fds[0]; + + std::thread([app, rfd, offer]() { + std::string data; + char buf[4096]; for (;;) { - ssize_t n = read(fds[0], buf, sizeof(buf)); - if (n > 0) - data_plain.append(buf, buf + n); - else - break; + ssize_t n = read(rfd, buf, sizeof buf); + if (n > 0) { + data.append(buf, buf + n); + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; } - close(fds[0]); - app->m_clipboard_cache = std::move(data_plain); - } else { - app->m_clipboard_cache = std::move(data_utf8); - } + close(rfd); + + struct Ctx { + App *app; + wl_data_offer *offer; + std::string data; + }; + auto *ctx + = new Ctx { app, offer, std::move(data) }; + + g_main_context_invoke( + nullptr, + +[](gpointer p) -> gboolean { + auto *ctx = static_cast(p); + if (!ctx->data.empty()) + ctx->app->m_clipboard_cache + = std::move(ctx->data); + wl_data_offer_destroy(ctx->offer); + delete ctx; + return G_SOURCE_REMOVE; + }, + ctx); + }).detach(); }, }; wl_data_device_add_listener(app->m_wayland.ddev, &ddev_l, app); @@ -1103,18 +1116,33 @@ auto App::clipboard(std::string_view const &str) -> void static wl_data_source_listener const src_l = { .target = [](void *, wl_data_source *, char const *) {}, .send = - [](void *data, wl_data_source *, char const *mime, int32_t fd) { + [](void *data, wl_data_source *, char const *, int32_t fd) { auto *app = static_cast(data); - (void)mime; - size_t off = 0; - while (off < app->m_clipboard_cache.size()) { - ssize_t n = write(fd, app->m_clipboard_cache.data() + off, - app->m_clipboard_cache.size() - off); - if (n <= 0) - break; - off += static_cast(n); - } + + int wfd = dup(fd); close(fd); + + std::pmr::string payload = app->m_clipboard_cache; + std::thread([wfd, payload = std::move(payload)]() { + size_t off = 0; + while (off < payload.size()) { + ssize_t n = write(wfd, payload.data() + off, + std::min(64 * 1024, payload.size() - off)); + if (n > 0) { + off += (size_t)n; + continue; + } + if (n < 0 && (errno == EINTR)) + continue; + if (n < 0 && (errno == EAGAIN)) { + std::this_thread::sleep_for( + std::chrono::milliseconds(1)); + continue; + } + break; + } + close(wfd); + }).detach(); }, .cancelled = [](void *data, wl_data_source *src) { diff --git a/src/ImGui.cpp b/src/ImGui.cpp index e023e72..dc0e845 100644 --- a/src/ImGui.cpp +++ b/src/ImGui.cpp @@ -19,6 +19,9 @@ struct CodepointSpan { 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]); @@ -60,7 +63,7 @@ constexpr auto decode_utf8(std::string_view text) -> std::vector len = 4; if (i + len > text.size()) - len = 1; // avoid overflow + len = 1; u32 cp = utf8_rune_from_first(text.data() + i); spans.push_back({ cp, i, i + len }); @@ -132,7 +135,6 @@ auto slice_bytes(std::string_view text, usize begin, usize end) constexpr float HORIZONTAL_PADDING = 6.0f; constexpr float VERTICAL_PADDING = 4.0f; -constexpr float CARET_WIDTH = 2.0f; constexpr double CARET_BLINK_INTERVAL = 0.5; constexpr float CARET_DESCENT_FRACTION = 0.25f; @@ -289,7 +291,7 @@ size_t utf8_length(std::string_view const &s) { size_t count = 0; for (unsigned char c : s) - if ((c & 0xC0) != 0x80) // continuation bytes + if ((c & 0xC0) != 0x80) ++count; return count; } @@ -305,7 +307,6 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, bool changed { false }; auto &state { m_ti_states[id] }; - assert(!options.multiline && "Multiline not yet implemented."); if (m_focused_id == 0) @@ -322,157 +323,258 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, std::string_view str_view(str.data(), str.size()); auto spans { decode_utf8(str_view) }; - auto is_space { [](u32 cp) -> bool { + 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 { + 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[state.current_rune_idx].start; - } }; + return spans[(usize)state.current_rune_idx].start; + }; usize caret_byte = clamp_cursor(); - auto refresh_spans { [&]() { + 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) { + 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 > static_cast(spans.size())) - return; + 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; - int scan = idx - 1; - while (scan >= 0 - && is_space(spans[static_cast(scan)].codepoint)) + 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[static_cast(scan)].codepoint)) + while (scan >= 0 && !is_space(spans[(usize)scan].codepoint)) scan--; int start_idx = std::max(scan + 1, 0); - usize byte_begin = spans[static_cast(start_idx)].start; - usize byte_end = (idx >= static_cast(spans.size())) - ? str.size() - : spans[static_cast(idx)].start; - erase_range(byte_begin, byte_end); + 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[static_cast(state.current_rune_idx - 1)]; + 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 { [&]() { + auto handle_delete = [&]() { if (state.current_rune_idx < 0 - || state.current_rune_idx >= static_cast(spans.size())) { + || 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 < static_cast(spans.size()) - && is_space(spans[static_cast(scan)].codepoint)) + while (scan < (int)spans.size() + && is_space(spans[(usize)scan].codepoint)) scan++; - while (scan < static_cast(spans.size()) - && !is_space(spans[static_cast(scan)].codepoint)) + while (scan < (int)spans.size() + && !is_space(spans[(usize)scan].codepoint)) scan++; - usize byte_begin = (idx < static_cast(spans.size())) - ? spans[static_cast(idx)].start - : str.size(); - usize 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)] }; + 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 (H) + case 1: // Left + if (!extend) + state.clear_selection(); 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--; - } + 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 (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++; - } + 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 (K) + 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 (J) - state.current_rune_idx = static_cast(spans.size()); + 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(); + state.selecting_all = true; + 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()); + for (auto ch : m_clipboard) + if (ch != '\n' && ch != '\r') + clip2.push_back(ch); + 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); @@ -484,28 +586,10 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, submitted = true; } break; - case 'v': - if (m_ctrl && !m_clipboard.empty()) { - if (!options.multiline) { - std::string clipboard_no_newlines; - for (auto const &ch : m_clipboard) { - if (ch == '\n' || ch == '\r') - continue; - clipboard_no_newlines.push_back(ch); - } - str.insert(caret_byte, clipboard_no_newlines); - state.current_rune_idx - += utf8_length(clipboard_no_newlines); - } else { - str.insert(caret_byte, m_clipboard); - state.current_rune_idx += utf8_length(m_clipboard); - } - changed = true; - request_refresh = 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); @@ -517,17 +601,18 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, break; } - if (request_refresh) { + if (request_refresh) refresh_spans(); - } else { + else caret_byte = clamp_cursor(); - } caret_activity = true; + if (!m_shift) + state.clear_selection(); } state.caret_byte = caret_byte; - double const dt = static_cast(GetFrameTime()); + double const dt = (double)GetFrameTime(); if (m_focused_id == id) { if (state.preedit_active && state.preedit_cursor_hidden) { state.caret_visible = false; @@ -540,8 +625,7 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, 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); + int toggles = (int)(state.caret_timer / CARET_BLINK_INTERVAL); state.caret_timer = std::fmod(state.caret_timer, CARET_BLINK_INTERVAL); if (toggles % 2 == 1) @@ -553,82 +637,6 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, state.caret_timer = 0.0; } - Vector2 prefix_metrics { 0.0f, 0.0f }; - Vector2 full_metrics { 0.0f, 0.0f }; - Vector2 preedit_metrics { 0.0f, 0.0f }; - Vector2 caret_preedit_metrics { 0.0f, 0.0f }; - Vector2 selection_prefix_metrics { 0.0f, 0.0f }; - Vector2 selection_metrics { 0.0f, 0.0f }; - std::string display_buffer; - bool const has_preedit = state.preedit_active - && (!state.preedit_text.empty() || !state.preedit_cursor_hidden); - - if (m_font.has_value() && m_text_renderer) { - int const font_px = static_cast(style().font_size); - std::string_view const prefix_view(str.data(), caret_byte); - prefix_metrics - = m_text_renderer->measure_text(*m_font, prefix_view, font_px); - - if (has_preedit) { - std::string_view const preedit_view( - state.preedit_text.data(), state.preedit_text.size()); - preedit_metrics - = m_text_renderer->measure_text(*m_font, preedit_view, font_px); - - auto caret_idx { clamp_preedit_index( - state.preedit_cursor_end, preedit_view.size()) }; - caret_preedit_metrics = m_text_renderer->measure_text( - *m_font, slice_bytes(preedit_view, 0, caret_idx), font_px); - - if (!state.preedit_cursor_hidden - && state.preedit_cursor_begin != state.preedit_cursor_end) { - auto sel_begin - = clamp_preedit_index(std::min(state.preedit_cursor_begin, - state.preedit_cursor_end), - preedit_view.size()); - auto sel_end - = clamp_preedit_index(std::max(state.preedit_cursor_begin, - state.preedit_cursor_end), - preedit_view.size()); - selection_prefix_metrics = m_text_renderer->measure_text( - *m_font, slice_bytes(preedit_view, 0, sel_begin), font_px); - selection_metrics = m_text_renderer->measure_text(*m_font, - slice_bytes(preedit_view, sel_begin, sel_end), font_px); - } - - display_buffer.reserve(str.size() + state.preedit_text.size()); - display_buffer.append(prefix_view); - display_buffer.append(preedit_view); - display_buffer.append(str_view.substr(caret_byte)); - full_metrics = m_text_renderer->measure_text(*m_font, - std::string_view(display_buffer.data(), display_buffer.size()), - font_px); - } else { - full_metrics - = m_text_renderer->measure_text(*m_font, str_view, font_px); - } - } - - float caret_offset = prefix_metrics.x + caret_preedit_metrics.x; - state.cursor_position.x = caret_offset; - - 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 = caret_offset - scroll; - if (caret_local > available_width) { - scroll = caret_offset - available_width; - } else if (caret_local < 0.0f) { - scroll = caret_offset; - } - 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); @@ -636,11 +644,11 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, float const text_top = rec.y + VERTICAL_PADDING; float const baseline_y = text_top + style().font_size; - float const max_caret_height + 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_height > 0.0f ? max_caret_height : caret_height); + 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; @@ -657,72 +665,111 @@ auto ImGui::text_input(usize id, std::pmr::string &str, Rectangle rec, } state.cursor_position.y = caret_top; - float const caret_draw_x - = rec.x + HORIZONTAL_PADDING + caret_offset - state.scroll_offset.x; - state.caret_rect = Rectangle { - caret_draw_x, - caret_top, - CARET_WIDTH, - caret_height, - }; + 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) { - int const font_px = static_cast(style().font_size); - Vector2 const base_pos { - rec.x + HORIZONTAL_PADDING - state.scroll_offset.x, - baseline_y, - }; Color const &text_color = style().text_color; - std::string_view const left_view(str.data(), caret_byte); - std::string_view const right_view( - str.data() + caret_byte, str.size() - caret_byte); - m_text_renderer->draw_text( - *m_font, left_view, base_pos, font_px, 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); - float advance = prefix_metrics.x; - if (has_preedit) { - Vector2 const preedit_pos { - base_pos.x + advance, - base_pos.y, - }; - if (selection_metrics.x > 0.0f) { - float const sel_offset - = prefix_metrics.x + selection_prefix_metrics.x; - Rectangle const sel_rect { - rec.x + HORIZONTAL_PADDING + sel_offset - - state.scroll_offset.x, - caret_top, - selection_metrics.x, - caret_height, - }; - Color const highlight { style().selection_color }; - DrawRectangleRec(sel_rect, highlight); - } - Color const &preedit_color { selection_metrics.x > 0.0f - ? style().selection_text_color - : style().preedit_color }; m_text_renderer->draw_text(*m_font, - std::string_view( - state.preedit_text.data(), state.preedit_text.size()), - preedit_pos, font_px, preedit_color); - advance += preedit_metrics.x; + std::string_view(str.data() + sb, se - sb), + { origin + sel_prefix.x, base_y }, font_px, + style().selection_text_color); } - Vector2 const right_pos { - base_pos.x + advance, - base_pos.y, - }; - m_text_renderer->draw_text( - *m_font, right_view, right_pos, font_px, text_color); - if (m_focused_id == id && state.caret_visible) { - Rectangle caret_rect = state.caret_rect; - caret_rect.x = std::round(caret_rect.x); - Color const caret_color { text_color }; - DrawRectangleRec(caret_rect, caret_color); + float const caret_x = std::floor(origin + caret_offset + 0.5f); + Vector2 p0 { caret_x, std::floor(caret_top + 0.5f) }; + Vector2 p1 { caret_x, + std::floor((caret_top + caret_height) + 0.5f) }; + DrawLineV(p0, p1, text_color); } } } diff --git a/src/ImGui.hpp b/src/ImGui.hpp index 8a760a2..b822e64 100644 --- a/src/ImGui.hpp +++ b/src/ImGui.hpp @@ -112,6 +112,18 @@ private: usize caret_byte { 0 }; Rectangle caret_rect {}; bool external_change { false }; + int sel_anchor_idx = -1; + bool selecting_all = false; + + bool has_selection(int curr_idx) const + { + return sel_anchor_idx != -1 && sel_anchor_idx != curr_idx; + } + void clear_selection() + { + sel_anchor_idx = -1; + selecting_all = false; + } }; struct ListViewState { diff --git a/src/TextRenderer.cpp b/src/TextRenderer.cpp index f7312cf..59d6e37 100644 --- a/src/TextRenderer.cpp +++ b/src/TextRenderer.cpp @@ -253,6 +253,9 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, // } //} + auto c1 { (int)std::round(msdf_bitmap(0, 0)[3]) }; + auto c4 { (int)std::round(msdf_bitmap(bmp_w - 1, bmp_h - 1)[3]) }; + auto sum_white = 0; auto sum_black = 0; for (int y = 0; y < bmp_h; ++y) { @@ -266,6 +269,11 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, } } } + bool flip { sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6 }; + if (c1 == c4) { + flip = false; + } + // This really isn't the most accurate thing in the world but should work // for now. Things like commas might be fucked. for (int y = 0; y < bmp_h; ++y) { @@ -273,7 +281,7 @@ auto TextRenderer::generate_glyph(FontRuntime &rt, FontData &fd, for (int x = 0; x < bmp_w; ++x) { float const *px = msdf_bitmap(x, y); auto const r = msdfgen::pixelFloatToByte(px[0]); - if (sum_white > sum_black && (float)bmp_w / (float)bmp_h > 0.6) { + if (flip) { buffer[static_cast(dst_y) * bmp_w + x] = Color { 255, 255, 255, static_cast(255 - r) }; } else { diff --git a/todo.txt b/todo.txt index a1e3d6f..41dea04 100644 --- a/todo.txt +++ b/todo.txt @@ -1,5 +1,4 @@ - [ ] Fix text moving left and right based on cursor position (right side of cursor is too left) -- [ ] Integrate IME directly into ImGui - [ ] Implement selection in ImGui::text_edit -- [ ] Implement clipboard copy/paste +- [-] Implement clipboard copy/paste