Download ide/native/editor/editor_view.cpp from Snapkitty/sovereign-engine-v2: direct link, hf CLI and curl.
- Browser
- Download file 23.1 kB
-
https://huggingface.co/Snapkitty/sovereign-engine-v2/resolve/main/ide/native/editor/editor_view.cpp
- Command line
-
hf download hf://Snapkitty/sovereign-engine-v2/ide/native/editor/editor_view.cpp
-
curl -L -o editor_view.cpp https://huggingface.co/Snapkitty/sovereign-engine-v2/resolve/main/ide/native/editor/editor_view.cpp
23.1 kB
| /* | |
| * Sovereign IDE — Editor View | |
| * DirectWrite text rendering, visible caret, selection, keyboard navigation. | |
| * C++ for COM vtable calls; all public functions are extern "C". | |
| */ | |
| extern "C" { | |
| } | |
| /* Colors (dark theme) */ | |
| static const D2D1_COLOR_F C_BG = { 0.122f, 0.122f, 0.141f, 1.0f }; | |
| static const D2D1_COLOR_F C_GUTTER = { 0.098f, 0.098f, 0.118f, 1.0f }; | |
| static const D2D1_COLOR_F C_GUTTER_FG = { 0.420f, 0.420f, 0.520f, 1.0f }; | |
| static const D2D1_COLOR_F C_TEXT = { 0.859f, 0.859f, 0.859f, 1.0f }; | |
| static const D2D1_COLOR_F C_CARET = { 1.000f, 1.000f, 1.000f, 1.0f }; | |
| static const D2D1_COLOR_F C_SEL = { 0.173f, 0.369f, 0.529f, 0.6f }; | |
| static const D2D1_COLOR_F C_ACTIVE_LINE= { 1.000f, 1.000f, 1.000f, 0.04f }; | |
| static const D2D1_COLOR_F C_SQUIGGLE_E = { 1.000f, 0.260f, 0.260f, 1.0f }; /* error */ | |
| static const D2D1_COLOR_F C_SQUIGGLE_W = { 1.000f, 0.780f, 0.200f, 1.0f }; /* warning */ | |
| struct EditorView { | |
| HWND hwnd; | |
| Document *doc; | |
| ID2D1Factory *d2d_factory; | |
| ID2D1HwndRenderTarget *rt; | |
| IDWriteFactory *dw_factory; | |
| IDWriteTextFormat *text_format; | |
| IDWriteTextFormat *gutter_format; | |
| ID2D1SolidColorBrush *br_text; | |
| ID2D1SolidColorBrush *br_gutter_fg; | |
| ID2D1SolidColorBrush *br_caret; | |
| ID2D1SolidColorBrush *br_sel; | |
| ID2D1SolidColorBrush *br_active_line; | |
| ID2D1SolidColorBrush *br_gutter_bg; | |
| ID2D1SolidColorBrush *br_squiggle_e; | |
| ID2D1SolidColorBrush *br_squiggle_w; | |
| uint32_t width; | |
| uint32_t height; | |
| float scroll_y; /* in lines */ | |
| size_t caret; /* byte offset in buffer */ | |
| size_t sel_anchor; /* selection start; equals caret = no selection */ | |
| bool has_sel; | |
| bool caret_visible; /* blink state */ | |
| DWORD caret_tick; | |
| /* LSP diagnostics for current file */ | |
| LspDiagList diags; | |
| bool has_diags; | |
| }; | |
| static HRESULT create_brushes(EditorView *ev) { | |
| HRESULT hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_TEXT, &ev->br_text); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_GUTTER_FG, &ev->br_gutter_fg); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_CARET, &ev->br_caret); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_SEL, &ev->br_sel); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_ACTIVE_LINE, &ev->br_active_line); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_GUTTER, &ev->br_gutter_bg); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_SQUIGGLE_E, &ev->br_squiggle_e); if (FAILED(hr)) return hr; | |
| hr = ev->rt->CreateSolidColorBrush(C_SQUIGGLE_W, &ev->br_squiggle_w); if (FAILED(hr)) return hr; | |
| return S_OK; | |
| } | |
| static void release_brushes(EditorView *ev) { | |
| auto safe = [](IUnknown *p) { if (p) p->Release(); }; | |
| safe(ev->br_text); ev->br_text = nullptr; | |
| safe(ev->br_gutter_fg); ev->br_gutter_fg = nullptr; | |
| safe(ev->br_caret); ev->br_caret = nullptr; | |
| safe(ev->br_sel); ev->br_sel = nullptr; | |
| safe(ev->br_active_line); ev->br_active_line = nullptr; | |
| safe(ev->br_gutter_bg); ev->br_gutter_bg = nullptr; | |
| safe(ev->br_squiggle_e); ev->br_squiggle_e = nullptr; | |
| safe(ev->br_squiggle_w); ev->br_squiggle_w = nullptr; | |
| } | |
| extern "C" SovResult editor_view_create(EditorView **out, HWND hwnd) { | |
| EditorView *ev = (EditorView *)calloc(1, sizeof(EditorView)); | |
| if (!ev) return SOV_ERR_ALLOC; | |
| ev->hwnd = hwnd; | |
| ev->caret_visible = true; | |
| ev->caret_tick = GetTickCount(); | |
| HRESULT hr; | |
| hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &ev->d2d_factory); | |
| if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; } | |
| hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, | |
| __uuidof(IDWriteFactory), | |
| reinterpret_cast<IUnknown **>(&ev->dw_factory)); | |
| if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; } | |
| hr = ev->dw_factory->CreateTextFormat( | |
| L"Consolas", nullptr, | |
| DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, | |
| FONT_SIZE, L"en-us", &ev->text_format); | |
| if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; } | |
| hr = ev->dw_factory->CreateTextFormat( | |
| L"Consolas", nullptr, | |
| DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, | |
| 11.0f, L"en-us", &ev->gutter_format); | |
| if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; } | |
| ev->gutter_format->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_TRAILING); | |
| RECT rc; | |
| GetClientRect(hwnd, &rc); | |
| ev->width = (uint32_t)(rc.right - rc.left); | |
| ev->height = (uint32_t)(rc.bottom - rc.top); | |
| D2D1_RENDER_TARGET_PROPERTIES rtp = D2D1::RenderTargetProperties(); | |
| D2D1_HWND_RENDER_TARGET_PROPERTIES hwp = D2D1::HwndRenderTargetProperties( | |
| hwnd, D2D1::SizeU(ev->width, ev->height)); | |
| hr = ev->d2d_factory->CreateHwndRenderTarget(rtp, hwp, &ev->rt); | |
| if (FAILED(hr)) { free(ev); return SOV_ERR_ALLOC; } | |
| if (FAILED(create_brushes(ev))) { free(ev); return SOV_ERR_ALLOC; } | |
| *out = ev; | |
| return SOV_OK; | |
| } | |
| extern "C" void editor_view_destroy(EditorView *ev) { | |
| if (!ev) return; | |
| release_brushes(ev); | |
| if (ev->text_format) { ev->text_format->Release(); } | |
| if (ev->gutter_format) { ev->gutter_format->Release(); } | |
| if (ev->rt) { ev->rt->Release(); } | |
| if (ev->d2d_factory) { ev->d2d_factory->Release(); } | |
| if (ev->dw_factory) { ev->dw_factory->Release(); } | |
| free(ev); | |
| } | |
| extern "C" void editor_view_set_document(EditorView *ev, Document *doc) { | |
| ev->doc = doc; | |
| ev->caret = 0; | |
| ev->sel_anchor = 0; | |
| ev->has_sel = false; | |
| ev->scroll_y = 0.0f; | |
| } | |
| extern "C" void editor_view_set_diagnostics(EditorView *ev, const LspDiagList *list) { | |
| if (!ev) return; | |
| if (list) { | |
| ev->diags = *list; | |
| ev->has_diags = (list->count > 0); | |
| } else { | |
| ev->has_diags = false; | |
| } | |
| InvalidateRect(ev->hwnd, nullptr, FALSE); | |
| } | |
| extern "C" void editor_view_resize(EditorView *ev, uint32_t w, uint32_t h) { | |
| ev->width = w; | |
| ev->height = h; | |
| if (ev->rt) ev->rt->Resize(D2D1::SizeU(w, h)); | |
| } | |
| /* Convert byte offset → (line, col) */ | |
| static void offset_to_line_col(Document *doc, size_t offset, size_t *line, size_t *col) { | |
| char tmp[4096]; | |
| size_t pos = 0; | |
| *line = 0; | |
| *col = 0; | |
| size_t buf_len = buffer_length(doc->buffer); | |
| while (pos < offset && pos < buf_len) { | |
| size_t chunk = offset - pos; | |
| if (chunk > sizeof(tmp)) chunk = sizeof(tmp); | |
| size_t n = buffer_read(doc->buffer, pos, tmp, chunk); | |
| if (n == 0) break; | |
| for (size_t i = 0; i < n && pos < offset; i++, pos++) { | |
| if (tmp[i] == '\n') { (*line)++; *col = 0; } | |
| else { (*col)++; } | |
| } | |
| } | |
| } | |
| /* Render one line of text using DirectWrite */ | |
| static void draw_line(EditorView *ev, size_t line_idx, float y, | |
| size_t caret_line, size_t sel_start, size_t sel_end, bool has_sel) { | |
| if (!ev->doc) return; | |
| float x0 = GUTTER_WIDTH + PADDING_LEFT; | |
| float view_w = (float)ev->width - x0; | |
| if (view_w <= 0) return; | |
| /* Active line highlight */ | |
| if (line_idx == caret_line) { | |
| D2D1_RECT_F lr = D2D1::RectF(GUTTER_WIDTH, y, (float)ev->width, y + LINE_HEIGHT); | |
| ev->rt->FillRectangle(lr, ev->br_active_line); | |
| } | |
| /* Gutter line number */ | |
| wchar_t num[16]; | |
| swprintf_s(num, 16, L"%zu", line_idx + 1); | |
| D2D1_RECT_F gr = D2D1::RectF(0, y + 2.0f, GUTTER_WIDTH - 6.0f, y + LINE_HEIGHT); | |
| ev->rt->DrawText(num, (UINT32)wcslen(num), ev->gutter_format, gr, ev->br_gutter_fg); | |
| /* Read line text */ | |
| size_t line_start = buffer_line_start(ev->doc->buffer, line_idx); | |
| size_t line_len = buffer_line_length(ev->doc->buffer, line_idx); | |
| if (line_len == 0) return; | |
| char utf8[4096]; | |
| size_t n = buffer_read(ev->doc->buffer, line_start, utf8, line_len < 4095 ? line_len : 4095); | |
| /* UTF-8 → UTF-16 */ | |
| wchar_t wide[4096]; | |
| int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 4095); | |
| if (wlen <= 0) return; | |
| /* Selection highlight */ | |
| if (has_sel && sel_start < line_start + line_len && sel_end > line_start) { | |
| size_t s = sel_start > line_start ? sel_start - line_start : 0; | |
| size_t e = sel_end < line_start + line_len ? sel_end - line_start : line_len; | |
| IDWriteTextLayout *layout = nullptr; | |
| ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen, | |
| ev->text_format, view_w, LINE_HEIGHT, &layout); | |
| if (layout) { | |
| DWRITE_HIT_TEST_METRICS m; | |
| float xs, dummy; | |
| layout->HitTestTextPosition((UINT32)s, FALSE, &xs, &dummy, &m); | |
| float xe; | |
| layout->HitTestTextPosition((UINT32)(e < (size_t)wlen ? e : wlen), FALSE, &xe, &dummy, &m); | |
| D2D1_RECT_F sr = D2D1::RectF(x0 + xs, y, x0 + xe, y + LINE_HEIGHT); | |
| ev->rt->FillRectangle(sr, ev->br_sel); | |
| layout->Release(); | |
| } | |
| } | |
| /* Draw text */ | |
| D2D1_RECT_F tr = D2D1::RectF(x0, y, x0 + view_w, y + LINE_HEIGHT); | |
| ev->rt->DrawText(wide, (UINT32)wlen, ev->text_format, tr, ev->br_text); | |
| /* LSP squiggles */ | |
| if (ev->has_diags) { | |
| for (int di = 0; di < ev->diags.count; di++) { | |
| const LspDiagnostic *d = &ev->diags.entries[di]; | |
| if (d->line != (uint32_t)line_idx) continue; | |
| ID2D1SolidColorBrush *br = (d->severity == 1) ? ev->br_squiggle_e : ev->br_squiggle_w; | |
| /* Compute x start from col via layout hit-test */ | |
| float sq_x = x0; | |
| if (d->col > 0) { | |
| IDWriteTextLayout *lay = nullptr; | |
| ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen, | |
| ev->text_format, view_w, LINE_HEIGHT, &lay); | |
| if (lay) { | |
| DWRITE_HIT_TEST_METRICS m; | |
| float dummy; | |
| UINT32 col_clamped = (d->col < (UINT32)wlen) ? d->col : (UINT32)wlen; | |
| lay->HitTestTextPosition(col_clamped, FALSE, &sq_x, &dummy, &m); | |
| sq_x += x0; | |
| lay->Release(); | |
| } | |
| } | |
| float sq_y = y + LINE_HEIGHT - 3.0f; | |
| float sq_end = x0 + view_w; | |
| float step = 4.0f; | |
| bool up = true; | |
| for (float sx = sq_x; sx < sq_end - step; sx += step, up = !up) { | |
| float sy0 = up ? sq_y : sq_y + 2.0f; | |
| float sy1 = up ? sq_y + 2.0f : sq_y; | |
| ev->rt->DrawLine(D2D1::Point2F(sx, sy0), | |
| D2D1::Point2F(sx + step, sy1), br, 1.2f); | |
| } | |
| } | |
| } | |
| } | |
| extern "C" void editor_view_paint(EditorView *ev, RECT client) { | |
| if (!ev || !ev->rt) return; | |
| /* Caret blink */ | |
| DWORD now = GetTickCount(); | |
| if (now - ev->caret_tick > 530) { | |
| ev->caret_visible = !ev->caret_visible; | |
| ev->caret_tick = now; | |
| } | |
| ev->rt->BeginDraw(); | |
| ev->rt->Clear(&C_BG); | |
| /* Gutter background */ | |
| D2D1_RECT_F gutter_rect = D2D1::RectF(0, 0, GUTTER_WIDTH, (float)client.bottom); | |
| ev->rt->FillRectangle(gutter_rect, ev->br_gutter_bg); | |
| if (!ev->doc) { | |
| ev->rt->EndDraw(nullptr, nullptr); | |
| return; | |
| } | |
| size_t caret_line, caret_col; | |
| offset_to_line_col(ev->doc, ev->caret, &caret_line, &caret_col); | |
| size_t sel_start = ev->has_sel ? (ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret) : 0; | |
| size_t sel_end = ev->has_sel ? (ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret) : 0; | |
| size_t total_lines = buffer_line_count(ev->doc->buffer); | |
| size_t first_line = (size_t)ev->scroll_y; | |
| size_t visible = (size_t)((float)ev->height / LINE_HEIGHT) + 2; | |
| for (size_t i = first_line; i < total_lines && i < first_line + visible; i++) { | |
| float y = (float)(i - first_line) * LINE_HEIGHT; | |
| draw_line(ev, i, y, caret_line, sel_start, sel_end, ev->has_sel); | |
| } | |
| /* Draw caret */ | |
| if (ev->caret_visible) { | |
| size_t line_start = buffer_line_start(ev->doc->buffer, caret_line); | |
| size_t char_in_line = ev->caret - line_start; | |
| float caret_x = GUTTER_WIDTH + PADDING_LEFT; | |
| if (char_in_line > 0) { | |
| char utf8[1024]; | |
| size_t n = buffer_read(ev->doc->buffer, line_start, utf8, char_in_line < 1023 ? char_in_line : 1023); | |
| wchar_t wide[1024]; | |
| int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 1023); | |
| if (wlen > 0) { | |
| IDWriteTextLayout *layout = nullptr; | |
| ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen, | |
| ev->text_format, 9999.0f, LINE_HEIGHT, &layout); | |
| if (layout) { | |
| DWRITE_TEXT_METRICS tm; | |
| layout->GetMetrics(&tm); | |
| caret_x += tm.width; | |
| layout->Release(); | |
| } | |
| } | |
| } | |
| float caret_y = (float)(caret_line - first_line) * LINE_HEIGHT; | |
| D2D1_RECT_F cr = D2D1::RectF(caret_x, caret_y + 2.0f, caret_x + CARET_WIDTH, caret_y + LINE_HEIGHT - 2.0f); | |
| ev->rt->FillRectangle(cr, ev->br_caret); | |
| } | |
| HRESULT hr = ev->rt->EndDraw(nullptr, nullptr); | |
| if (hr == (HRESULT)D2DERR_RECREATE_TARGET) { | |
| /* Device lost — recreate */ | |
| release_brushes(ev); | |
| ev->rt->Release(); ev->rt = nullptr; | |
| RECT rc; GetClientRect(ev->hwnd, &rc); | |
| D2D1_RENDER_TARGET_PROPERTIES rtp = D2D1::RenderTargetProperties(); | |
| D2D1_HWND_RENDER_TARGET_PROPERTIES hwp = D2D1::HwndRenderTargetProperties( | |
| ev->hwnd, D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top)); | |
| ev->d2d_factory->CreateHwndRenderTarget(rtp, hwp, &ev->rt); | |
| create_brushes(ev); | |
| } | |
| } | |
| /* Clamp caret into buffer */ | |
| static void clamp_caret(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t len = buffer_length(ev->doc->buffer); | |
| if (ev->caret > len) ev->caret = len; | |
| } | |
| /* Move caret left/right by one byte (UTF-8 aware would need more work) */ | |
| static void caret_move_left(EditorView *ev) { | |
| if (ev->caret > 0) ev->caret--; | |
| } | |
| static void caret_move_right(EditorView *ev) { | |
| if (!ev->doc) return; | |
| if (ev->caret < buffer_length(ev->doc->buffer)) ev->caret++; | |
| } | |
| static void caret_move_up(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t line, col; | |
| offset_to_line_col(ev->doc, ev->caret, &line, &col); | |
| if (line == 0) { ev->caret = 0; return; } | |
| size_t prev_start = buffer_line_start(ev->doc->buffer, line - 1); | |
| size_t prev_len = buffer_line_length(ev->doc->buffer, line - 1); | |
| ev->caret = prev_start + (col < prev_len ? col : prev_len); | |
| } | |
| static void caret_move_down(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t line, col; | |
| offset_to_line_col(ev->doc, ev->caret, &line, &col); | |
| size_t total = buffer_line_count(ev->doc->buffer); | |
| if (line + 1 >= total) return; | |
| size_t next_start = buffer_line_start(ev->doc->buffer, line + 1); | |
| size_t next_len = buffer_line_length(ev->doc->buffer, line + 1); | |
| ev->caret = next_start + (col < next_len ? col : next_len); | |
| } | |
| static void caret_home(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t line, col; (void)col; | |
| offset_to_line_col(ev->doc, ev->caret, &line, &col); | |
| ev->caret = buffer_line_start(ev->doc->buffer, line); | |
| } | |
| static void caret_end(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t line, col; (void)col; | |
| offset_to_line_col(ev->doc, ev->caret, &line, &col); | |
| ev->caret = buffer_line_start(ev->doc->buffer, line) | |
| + buffer_line_length(ev->doc->buffer, line); | |
| } | |
| static void ensure_caret_visible(EditorView *ev) { | |
| if (!ev->doc) return; | |
| size_t line, col; (void)col; | |
| offset_to_line_col(ev->doc, ev->caret, &line, &col); | |
| float visible_lines = (float)ev->height / LINE_HEIGHT; | |
| if ((float)line < ev->scroll_y) { | |
| ev->scroll_y = (float)line; | |
| } else if ((float)line >= ev->scroll_y + visible_lines - 1) { | |
| ev->scroll_y = (float)line - visible_lines + 2.0f; | |
| if (ev->scroll_y < 0) ev->scroll_y = 0; | |
| } | |
| } | |
| extern "C" void editor_view_key_down(EditorView *ev, UINT vk, UINT mods) { | |
| if (!ev->doc) return; | |
| bool shift = (mods & 1) != 0; | |
| bool ctrl = (mods & 2) != 0; | |
| if (shift && !ev->has_sel) { | |
| ev->sel_anchor = ev->caret; | |
| ev->has_sel = true; | |
| } | |
| switch (vk) { | |
| case VK_LEFT: | |
| if (!shift && ev->has_sel) { ev->caret = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret; ev->has_sel = false; } | |
| else caret_move_left(ev); | |
| break; | |
| case VK_RIGHT: | |
| if (!shift && ev->has_sel) { ev->caret = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret; ev->has_sel = false; } | |
| else caret_move_right(ev); | |
| break; | |
| case VK_UP: caret_move_up(ev); break; | |
| case VK_DOWN: caret_move_down(ev); break; | |
| case VK_HOME: caret_home(ev); break; | |
| case VK_END: caret_end(ev); break; | |
| case VK_PRIOR: /* Page Up */ | |
| for (int i = 0; i < 20; i++) caret_move_up(ev); | |
| break; | |
| case VK_NEXT: /* Page Down */ | |
| for (int i = 0; i < 20; i++) caret_move_down(ev); | |
| break; | |
| case VK_DELETE: { | |
| if (ev->has_sel) { | |
| size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret; | |
| size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret; | |
| buffer_delete(ev->doc->buffer, s, e - s); | |
| ev->caret = s; ev->has_sel = false; | |
| } else { | |
| buffer_delete(ev->doc->buffer, ev->caret, 1); | |
| } | |
| break; | |
| } | |
| case VK_BACK: { | |
| if (ev->has_sel) { | |
| size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret; | |
| size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret; | |
| buffer_delete(ev->doc->buffer, s, e - s); | |
| ev->caret = s; ev->has_sel = false; | |
| } else if (ev->caret > 0) { | |
| ev->caret--; | |
| buffer_delete(ev->doc->buffer, ev->caret, 1); | |
| } | |
| break; | |
| } | |
| case 'Z': | |
| if (ctrl) { | |
| if (buffer_can_undo(ev->doc->buffer)) { | |
| buffer_undo(ev->doc->buffer); | |
| clamp_caret(ev); | |
| } | |
| } | |
| break; | |
| case 'Y': | |
| if (ctrl) { | |
| if (buffer_can_redo(ev->doc->buffer)) { | |
| buffer_redo(ev->doc->buffer); | |
| clamp_caret(ev); | |
| } | |
| } | |
| break; | |
| case 'A': | |
| if (ctrl) { | |
| ev->sel_anchor = 0; | |
| ev->caret = buffer_length(ev->doc->buffer); | |
| ev->has_sel = true; | |
| } | |
| break; | |
| case 'S': | |
| if (ctrl) document_save(ev->doc); | |
| break; | |
| } | |
| if (!shift) ev->has_sel = false; | |
| clamp_caret(ev); | |
| ensure_caret_visible(ev); | |
| ev->caret_visible = true; | |
| ev->caret_tick = GetTickCount(); | |
| InvalidateRect(ev->hwnd, NULL, FALSE); | |
| } | |
| extern "C" void editor_view_char(EditorView *ev, wchar_t ch) { | |
| if (!ev->doc) return; | |
| if (ch < 0x20 && ch != '\t' && ch != '\n' && ch != '\r') return; | |
| if (ch == '\r') ch = '\n'; | |
| if (ev->has_sel) { | |
| size_t s = ev->sel_anchor < ev->caret ? ev->sel_anchor : ev->caret; | |
| size_t e = ev->sel_anchor > ev->caret ? ev->sel_anchor : ev->caret; | |
| buffer_delete(ev->doc->buffer, s, e - s); | |
| ev->caret = s; ev->has_sel = false; | |
| } | |
| /* UTF-16 → UTF-8 */ | |
| char utf8[4]; | |
| int utf8_len = WideCharToMultiByte(CP_UTF8, 0, &ch, 1, utf8, 4, NULL, NULL); | |
| if (utf8_len > 0) { | |
| buffer_insert(ev->doc->buffer, ev->caret, utf8, (size_t)utf8_len); | |
| ev->caret += (size_t)utf8_len; | |
| } | |
| ensure_caret_visible(ev); | |
| ev->caret_visible = true; | |
| ev->caret_tick = GetTickCount(); | |
| InvalidateRect(ev->hwnd, NULL, FALSE); | |
| } | |
| /* Hit test: pixel (x, y) → buffer offset */ | |
| static size_t hit_test(EditorView *ev, int px, int py) { | |
| if (!ev->doc) return 0; | |
| size_t line = (size_t)(ev->scroll_y + (float)py / LINE_HEIGHT); | |
| size_t total = buffer_line_count(ev->doc->buffer); | |
| if (line >= total) line = total - 1; | |
| size_t line_start = buffer_line_start(ev->doc->buffer, line); | |
| size_t line_len = buffer_line_length(ev->doc->buffer, line); | |
| if (line_len == 0) return line_start; | |
| float text_x = (float)px - (GUTTER_WIDTH + PADDING_LEFT); | |
| if (text_x <= 0) return line_start; | |
| char utf8[4096]; | |
| size_t n = buffer_read(ev->doc->buffer, line_start, utf8, line_len < 4095 ? line_len : 4095); | |
| wchar_t wide[4096]; | |
| int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8, (int)n, wide, 4095); | |
| if (wlen <= 0) return line_start; | |
| IDWriteTextLayout *layout = nullptr; | |
| ev->dw_factory->CreateTextLayout(wide, (UINT32)wlen, | |
| ev->text_format, 9999.0f, LINE_HEIGHT, &layout); | |
| if (!layout) return line_start; | |
| BOOL trailing, inside; | |
| DWRITE_HIT_TEST_METRICS m; | |
| layout->HitTestPoint(text_x, LINE_HEIGHT / 2.0f, &trailing, &inside, &m); | |
| layout->Release(); | |
| size_t char_pos = m.textPosition + (trailing ? 1 : 0); | |
| return line_start + (char_pos < line_len ? char_pos : line_len); | |
| } | |
| extern "C" void editor_view_mouse_down(EditorView *ev, int x, int y) { | |
| ev->caret = hit_test(ev, x, y); | |
| ev->sel_anchor = ev->caret; | |
| ev->has_sel = false; | |
| ev->caret_visible = true; | |
| ev->caret_tick = GetTickCount(); | |
| InvalidateRect(ev->hwnd, NULL, FALSE); | |
| } | |
| extern "C" void editor_view_mouse_move(EditorView *ev, int x, int y, bool btn) { | |
| if (!btn) return; | |
| size_t new_pos = hit_test(ev, x, y); | |
| if (new_pos != ev->caret) { | |
| ev->caret = new_pos; | |
| ev->has_sel = (ev->caret != ev->sel_anchor); | |
| InvalidateRect(ev->hwnd, NULL, FALSE); | |
| } | |
| } | |
| extern "C" size_t editor_view_caret_offset(const EditorView *ev) { | |
| return ev->caret; | |
| } | |