ImGui measured every span, highlight and the cursor from column 0 of the line (and rescanned the line for each column conversion), which is quadratic in line length on minified files. Columns now come from one table per line and pixel offsets from the nearest position already measured. Output is pixel-identical to before (compared under Xvfb, with syntax colouring, tabs, UTF-8 and search highlights). kte::kTabWidth replaces the literal 8s, and Qt's 4: Qt expanded tabs to 4 columns while the command layer scrolled by 8, misplacing the cursor and horizontal scroll on lines with tabs. MeasureChar and DisplayColumns (TermWidth.h) are the command layer's column model, now shared, and Qt draws by it instead of one column per byte, which shifted everything after a multi-byte character. Qt also: - drew operators in the dark-mode background colour, so they vanished; - computed the editor's column count three ways (different glyph, padding and status-row accounting) from painting, and positioned text with a rounded cell width, so the cursor drifted off the glyphs on long lines. One TextLayout now serves painting, resizing, font changes and wheel scrolling, with a fractional cell width; runs are drawn only where visible; - drew each non-ASCII character at its cell, since a fallback-font glyph wider than a cell pushed the rest of the run out of place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
979 lines
36 KiB
C++
979 lines
36 KiB
C++
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <string>
|
|
|
|
#include <imgui.h>
|
|
|
|
#include "ImGuiRenderer.h"
|
|
#include "SearchHighlight.h"
|
|
#include "TermWidth.h"
|
|
#include "Highlight.h"
|
|
#include "GUITheme.h"
|
|
#include "Buffer.h"
|
|
#include "Command.h"
|
|
#include "Editor.h"
|
|
|
|
|
|
// Version string expected to be provided by build system as KTE_VERSION_STR
|
|
#ifndef KTE_VERSION_STR
|
|
# define KTE_VERSION_STR "dev"
|
|
#endif
|
|
|
|
// ImGui compatibility: some bundled ImGui versions (or builds without docking)
|
|
// don't define ImGuiWindowFlags_NoDocking. Treat it as 0 in that case.
|
|
#ifndef ImGuiWindowFlags_NoDocking
|
|
# define ImGuiWindowFlags_NoDocking 0
|
|
#endif
|
|
|
|
|
|
void
|
|
ImGuiRenderer::Draw(Editor &ed)
|
|
{
|
|
// Make the editor window occupy the entire GUI container/viewport
|
|
ImGuiViewport *vp = ImGui::GetMainViewport();
|
|
// On HiDPI/Retina, snap to integer pixels to prevent any draw vs hit-test
|
|
// mismatches that can appear on the very first maximized frame.
|
|
ImVec2 main_pos = vp->Pos;
|
|
ImVec2 main_sz = vp->Size;
|
|
main_pos.x = std::floor(main_pos.x + 0.5f);
|
|
main_pos.y = std::floor(main_pos.y + 0.5f);
|
|
main_sz.x = std::floor(main_sz.x + 0.5f);
|
|
main_sz.y = std::floor(main_sz.y + 0.5f);
|
|
ImGui::SetNextWindowPos(main_pos);
|
|
ImGui::SetNextWindowSize(main_sz);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
|
| ImGuiWindowFlags_NoScrollbar
|
|
| ImGuiWindowFlags_NoScrollWithMouse
|
|
| ImGuiWindowFlags_NoResize
|
|
| ImGuiWindowFlags_NoMove
|
|
| ImGuiWindowFlags_NoCollapse
|
|
| ImGuiWindowFlags_NoSavedSettings
|
|
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
|
| ImGuiWindowFlags_NoNavFocus;
|
|
|
|
// Reduce padding so the buffer content uses the whole area
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 6.f));
|
|
|
|
ImGui::Begin("kte", nullptr, flags);
|
|
|
|
const Buffer *buf = ed.CurrentBuffer();
|
|
if (!buf) {
|
|
ImGui::TextUnformatted("[no buffer]");
|
|
} else {
|
|
// Per-row access only: Rows() rebuilds a string for every line of the
|
|
// file after each edit, which made every frame O(file size).
|
|
const std::size_t nlines = buf->Nrows();
|
|
std::size_t cy = buf->Cury();
|
|
std::size_t cx = buf->Curx();
|
|
const float line_h = ImGui::GetTextLineHeight();
|
|
const float row_h = ImGui::GetTextLineHeightWithSpacing();
|
|
const float space_w = ImGui::CalcTextSize(" ").x;
|
|
|
|
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
|
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
|
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
|
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
|
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
|
|
|
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
|
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
|
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
|
}
|
|
// Horizontal scroll is handled purely in pixel space (see
|
|
// cursor-visibility block after the line loop) so we don't
|
|
// convert the character-based coloffs to an ImGui scroll here.
|
|
|
|
// Reserve space for status bar at bottom.
|
|
// We calculate a height that is an exact multiple of the line height
|
|
// to avoid partial lines and "scroll past end" jitter.
|
|
float total_avail_h = ImGui::GetContentRegionAvail().y;
|
|
float wanted_bar_h = ImGui::GetFrameHeight();
|
|
float child_h_plan = std::max(0.0f, std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h);
|
|
float real_bar_h = total_avail_h - child_h_plan;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
|
ImGui::BeginChild("scroll", ImVec2(0, child_h_plan), false,
|
|
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
|
|
// Get child window position and scroll for click handling
|
|
ImVec2 child_window_pos = ImGui::GetWindowPos();
|
|
float scroll_y = ImGui::GetScrollY();
|
|
float scroll_x = ImGui::GetScrollX();
|
|
|
|
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
|
bool forced_scroll = false;
|
|
{
|
|
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
|
|
|
// Check if rowoffs was programmatically changed this frame
|
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
|
forced_scroll = true;
|
|
}
|
|
|
|
// If user scrolled vertically (not programmatic), update buffer row offset
|
|
if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) {
|
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
|
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
|
mbuf->Coloffs());
|
|
}
|
|
}
|
|
// Horizontal scroll is pixel-based and managed by the cursor
|
|
// visibility block below; we don't sync it back to coloffs.
|
|
|
|
// Update trackers for next frame
|
|
prev_scroll_y_ = scroll_y;
|
|
prev_scroll_x_ = scroll_x;
|
|
}
|
|
prev_buf_rowoffs_ = buf_rowoffs;
|
|
prev_buf_coloffs_ = buf_coloffs;
|
|
|
|
// Max-line-width cache: reset when the buffer or font changes. We only
|
|
// measure visible lines per frame and maintain a running max, so the
|
|
// scrollbar may be slightly conservative on first view of a file until
|
|
// the user scrolls, but idle CPU drops dramatically on large files.
|
|
// Edits bump the buffer version but we intentionally do NOT reset the
|
|
// max on version changes — keeping a conservative (possibly too-wide)
|
|
// scrollbar is preferable to per-keystroke jitter.
|
|
{
|
|
ImFont *cur_font = ImGui::GetFont();
|
|
float cur_fsize = ImGui::GetFontSize();
|
|
if (buf != max_width_buf_
|
|
|| cur_font != max_width_font_
|
|
|| cur_fsize != max_width_font_size_) {
|
|
max_width_buf_ = buf;
|
|
max_width_font_ = cur_font;
|
|
max_width_font_size_ = cur_fsize;
|
|
max_width_px_ = 0.0f;
|
|
}
|
|
max_width_version_ = buf->Version();
|
|
}
|
|
|
|
// Search matches: the pattern is compiled once per frame (compiling it
|
|
// per line per frame was a large source of idle CPU on macOS).
|
|
const kte::SearchHighlight search_hl(ed);
|
|
const bool search_mode = search_hl.Active();
|
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
|
|
|
// Compute the visible row range and skip rendering work for off-screen
|
|
// lines. ImGui clips drawing, but string allocation, tab expansion,
|
|
// syntax-highlight lookups, and width measurement are not free.
|
|
const std::size_t total_rows = nlines;
|
|
std::size_t first_vis = 0;
|
|
std::size_t last_vis = total_rows; // exclusive
|
|
if (row_h > 0.0f && total_rows > 0) {
|
|
const long margin = 4; // render a few extra rows above/below for smoother scrolling
|
|
long fv = static_cast<long>(std::floor(scroll_y / row_h)) - margin;
|
|
long lv = static_cast<long>(
|
|
std::ceil((scroll_y + child_h_plan) / row_h)) + margin;
|
|
if (fv < 0) fv = 0;
|
|
if (lv < 0) lv = 0;
|
|
if (static_cast<std::size_t>(fv) > total_rows)
|
|
fv = static_cast<long>(total_rows);
|
|
if (static_cast<std::size_t>(lv) > total_rows)
|
|
lv = static_cast<long>(total_rows);
|
|
first_vis = static_cast<std::size_t>(fv);
|
|
last_vis = static_cast<std::size_t>(lv);
|
|
}
|
|
|
|
// Mark selection state (mark -> cursor), in source coordinates
|
|
bool sel_active = false;
|
|
std::size_t sel_sy = 0, sel_sx = 0, sel_ey = 0, sel_ex = 0;
|
|
if (buf->MarkSet()) {
|
|
sel_sy = buf->MarkCury();
|
|
sel_sx = buf->MarkCurx();
|
|
sel_ey = buf->Cury();
|
|
sel_ex = buf->Curx();
|
|
if (sel_sy > sel_ey || (sel_sy == sel_ey && sel_sx > sel_ex)) {
|
|
std::swap(sel_sy, sel_ey);
|
|
std::swap(sel_sx, sel_ex);
|
|
}
|
|
sel_active = !(sel_sy == sel_ey && sel_sx == sel_ex);
|
|
}
|
|
// Visual-line selection: full-line highlight range
|
|
const bool vsel_active = buf->VisualLineActive();
|
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
|
|
|
// (mouse_selecting__ is a member variable)
|
|
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
|
// Convert mouse pos to buffer row
|
|
float content_y = (mp.y - child_window_pos.y) + scroll_y;
|
|
long by_l = static_cast<long>(content_y / row_h);
|
|
if (by_l < 0)
|
|
by_l = 0;
|
|
std::size_t by = static_cast<std::size_t>(by_l);
|
|
if (by >= nlines)
|
|
by = (nlines == 0) ? 0 : (nlines - 1);
|
|
|
|
if (nlines == 0)
|
|
return {0, 0};
|
|
|
|
// Expand tabs for the clicked line
|
|
std::string line_clicked = buf->GetLineString(by);
|
|
const std::size_t tabw = kte::kTabWidth;
|
|
std::string click_expanded;
|
|
click_expanded.reserve(line_clicked.size() + 16);
|
|
std::size_t click_rx = 0;
|
|
// Map: source column -> expanded column
|
|
std::vector<std::size_t> src_to_exp;
|
|
src_to_exp.reserve(line_clicked.size() + 1);
|
|
for (std::size_t ci = 0; ci < line_clicked.size(); ++ci) {
|
|
src_to_exp.push_back(click_rx);
|
|
if (line_clicked[ci] == '\t') {
|
|
std::size_t adv = (tabw - (click_rx % tabw));
|
|
click_expanded.append(adv, ' ');
|
|
click_rx += adv;
|
|
} else {
|
|
click_expanded.push_back(line_clicked[ci]);
|
|
click_rx += 1;
|
|
}
|
|
}
|
|
src_to_exp.push_back(click_rx); // past-end position
|
|
|
|
// Pixel x relative to the line start (accounting for scroll)
|
|
float visual_x = mp.x - child_window_pos.x;
|
|
if (visual_x < 0.0f)
|
|
visual_x = 0.0f;
|
|
// Add scroll offset in pixels
|
|
visual_x += scroll_x;
|
|
|
|
// Find the source column whose expanded position is closest
|
|
// to the click pixel, using actual text measurement.
|
|
std::size_t best_col = 0;
|
|
float best_dist = std::numeric_limits<float>::infinity();
|
|
for (std::size_t ci = 0; ci <= line_clicked.size(); ++ci) {
|
|
std::size_t exp_col = src_to_exp[ci];
|
|
float px = 0.0f;
|
|
if (exp_col > 0 && !click_expanded.empty()) {
|
|
std::size_t end = std::min(click_expanded.size(), exp_col);
|
|
px = ImGui::CalcTextSize(click_expanded.c_str(),
|
|
click_expanded.c_str() + end).x;
|
|
}
|
|
float dist = std::fabs(visual_x - px);
|
|
if (dist < best_dist) {
|
|
best_dist = dist;
|
|
best_col = ci;
|
|
}
|
|
}
|
|
return {by, best_col};
|
|
};
|
|
|
|
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
mouse_selecting_ = true;
|
|
auto [by, bx] = mouse_pos_to_buf();
|
|
char tmp[64];
|
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
|
|
|
// Only set mark on double click.
|
|
// Dragging will also set the mark if not already set (handled below).
|
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
|
mbuf->SetMark(bx, by);
|
|
}
|
|
}
|
|
}
|
|
if (mouse_selecting_ && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
|
auto [by, bx] = mouse_pos_to_buf();
|
|
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
|
if (!mbuf->MarkSet()) {
|
|
// We'd need to convert click_pos to buf coords, but it's complex here.
|
|
// Setting it to where the cursor was *before* we started moving it
|
|
// in this frame is a good approximation, or just using current.
|
|
mbuf->SetMark(mbuf->Curx(), mbuf->Cury());
|
|
}
|
|
}
|
|
}
|
|
char tmp[64];
|
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
|
}
|
|
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
mouse_selecting_ = false;
|
|
}
|
|
// Advance the cursor Y so the first visible line draws at its correct
|
|
// scroll position. Skipped rows simply leave the layout cursor untouched.
|
|
if (first_vis > 0) {
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
|
static_cast<float>(first_vis) * row_h);
|
|
}
|
|
for (std::size_t i = first_vis; i < last_vis; ++i) {
|
|
// Capture the screen position before drawing the line
|
|
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
|
std::string line = buf->GetLineString(i);
|
|
|
|
// Expand tabs to spaces with width=8, recording each source
|
|
// byte's rendered column (rx_of[line.size()] is the line's width).
|
|
const std::size_t tabw = kte::kTabWidth;
|
|
std::string expanded;
|
|
expanded.reserve(line.size() + 16);
|
|
std::vector<std::size_t> rx_of(line.size() + 1);
|
|
for (std::size_t src = 0; src < line.size(); ++src) {
|
|
rx_of[src] = expanded.size();
|
|
if (line[src] == '\t')
|
|
expanded.append(tabw - (expanded.size() % tabw), ' ');
|
|
else
|
|
expanded.push_back(line[src]);
|
|
}
|
|
rx_of[line.size()] = expanded.size();
|
|
auto src_to_rx = [&](std::size_t src) -> std::size_t {
|
|
return rx_of[std::min(src, line.size())];
|
|
};
|
|
|
|
// Rendered column -> pixel offset from the start of the line.
|
|
// Measuring from column 0 for every span was quadratic in line
|
|
// length; measure on from the nearest earlier position already
|
|
// measured (only character boundaries are kept, so a multi-byte
|
|
// character is never measured in pieces).
|
|
std::vector<std::pair<std::size_t, float> > px_marks{{0, 0.0f}}; // unrounded
|
|
auto rx_to_px = [&](std::size_t rx_col) -> float {
|
|
const std::size_t end = std::min(expanded.size(), rx_col);
|
|
auto it = std::upper_bound(px_marks.begin(), px_marks.end(), end,
|
|
[](std::size_t v, const std::pair<std::size_t, float> &m) {
|
|
return v < m.first;
|
|
});
|
|
--it; // px_marks[0] is column 0
|
|
float px = it->second;
|
|
if (it->first != end) {
|
|
px += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f,
|
|
expanded.c_str() + it->first,
|
|
expanded.c_str() + end).x;
|
|
if (end == expanded.size() || (static_cast<unsigned char>(expanded[end]) & 0xC0) != 0x80)
|
|
px_marks.insert(it + 1, {end, px});
|
|
}
|
|
// Rounded up as ImGui::CalcTextSize does (sums of rounded
|
|
// pieces would drift).
|
|
return end == 0 ? 0.0f : static_cast<float>(static_cast<int>(px + 0.99999f));
|
|
};
|
|
|
|
// Search highlight ranges for this line in source indices
|
|
search_hl.Ranges(line, hl_src_ranges);
|
|
// Draw background highlights (under text)
|
|
if (search_mode && !hl_src_ranges.empty()) {
|
|
// Current match emphasis
|
|
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
|
|
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
|
|
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
|
for (const auto &rg: hl_src_ranges) {
|
|
std::size_t sx = rg.first, ex = rg.second;
|
|
std::size_t rx_start = src_to_rx(sx);
|
|
std::size_t rx_end = src_to_rx(ex);
|
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start), line_pos.y);
|
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
|
line_pos.y + line_h);
|
|
// Choose color: current match stronger
|
|
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
|
ImU32 col = is_current
|
|
? IM_COL32(255, 220, 120, 140)
|
|
: IM_COL32(200, 200, 0, 90);
|
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
|
}
|
|
}
|
|
|
|
// Draw selection background (over search highlight; under text)
|
|
if (sel_active) {
|
|
bool line_has = false;
|
|
std::size_t sx = 0, ex = 0;
|
|
if (i < sel_sy || i > sel_ey) {
|
|
line_has = false;
|
|
} else if (sel_sy == sel_ey) {
|
|
sx = sel_sx;
|
|
ex = sel_ex;
|
|
line_has = ex > sx;
|
|
} else if (i == sel_sy) {
|
|
sx = sel_sx;
|
|
ex = line.size();
|
|
line_has = ex > sx;
|
|
} else if (i == sel_ey) {
|
|
sx = 0;
|
|
ex = std::min(sel_ex, line.size());
|
|
line_has = ex > sx;
|
|
} else {
|
|
sx = 0;
|
|
ex = line.size();
|
|
line_has = ex > sx;
|
|
}
|
|
if (line_has) {
|
|
std::size_t rx_start = src_to_rx(sx);
|
|
std::size_t rx_end = src_to_rx(ex);
|
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
|
line_pos.y);
|
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
|
line_pos.y + line_h);
|
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
|
}
|
|
}
|
|
if (vsel_active && i >= vsel_sy && i <= vsel_ey) {
|
|
// Visual-line (multi-cursor) mode: highlight only the per-line cursor spot.
|
|
const std::size_t spot_sx = std::min(buf->Curx(), line.size());
|
|
const std::size_t rx_start = src_to_rx(spot_sx);
|
|
std::size_t rx_end = rx_start;
|
|
if (spot_sx < line.size()) {
|
|
rx_end = src_to_rx(spot_sx + 1);
|
|
} else {
|
|
// EOL spot: draw a 1-cell highlight just past the last character.
|
|
rx_end = rx_start + 1;
|
|
}
|
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
|
line_pos.y);
|
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
|
line_pos.y + line_h);
|
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
|
}
|
|
// Draw syntax-colored runs (text above background highlights)
|
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
|
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
|
*buf, static_cast<int>(i), buf->Version());
|
|
// Sanitize spans defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
|
struct SSpan {
|
|
std::size_t s;
|
|
std::size_t e;
|
|
kte::TokenKind k;
|
|
};
|
|
std::vector<SSpan> spans;
|
|
spans.reserve(lh.spans.size());
|
|
const std::size_t line_len = line.size();
|
|
for (const auto &sp: lh.spans) {
|
|
int s_raw = sp.col_start;
|
|
int e_raw = sp.col_end;
|
|
if (e_raw < s_raw)
|
|
std::swap(e_raw, s_raw);
|
|
std::size_t s = static_cast<std::size_t>(std::max(
|
|
0, std::min(s_raw, static_cast<int>(line_len))));
|
|
std::size_t e = static_cast<std::size_t>(std::max(
|
|
static_cast<int>(s), std::min(e_raw, static_cast<int>(line_len))));
|
|
if (e <= s)
|
|
continue;
|
|
spans.push_back(SSpan{s, e, sp.kind});
|
|
}
|
|
std::sort(spans.begin(), spans.end(), [](const SSpan &a, const SSpan &b) {
|
|
return a.s < b.s;
|
|
});
|
|
|
|
for (const auto &sp: spans) {
|
|
std::size_t rx_s = src_to_rx(sp.s);
|
|
std::size_t rx_e = src_to_rx(sp.e);
|
|
std::size_t draw_start = rx_s;
|
|
if (draw_start >= expanded.size())
|
|
continue;
|
|
std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size());
|
|
if (draw_end <= draw_start)
|
|
continue;
|
|
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
|
|
ImVec2 p = ImVec2(line_pos.x + rx_to_px(draw_start),
|
|
line_pos.y);
|
|
ImGui::GetWindowDrawList()->AddText(
|
|
p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end);
|
|
}
|
|
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
|
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
|
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
|
} else {
|
|
// No syntax: draw the full line; ImGui scroll handles clipping.
|
|
if (!expanded.empty()) {
|
|
ImVec2 p = ImVec2(line_pos.x, line_pos.y);
|
|
ImGui::GetWindowDrawList()->AddText(
|
|
p, ImGui::GetColorU32(ImGuiCol_Text),
|
|
expanded.c_str());
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
|
}
|
|
|
|
// Draw a visible cursor indicator on the current line
|
|
if (i == cy) {
|
|
std::size_t rx_abs = src_to_rx(cx);
|
|
float cursor_px = rx_to_px(rx_abs);
|
|
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
|
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
|
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
|
}
|
|
|
|
// Track widest line for content width reporting. We only measure
|
|
// visible lines and fold the result into a monotonic max cached on
|
|
// the renderer (see max_width_* members). Off-screen lines are not
|
|
// measured per frame; if the user scrolls or edits, the cache is
|
|
// refreshed accordingly.
|
|
if (!expanded.empty()) {
|
|
const float line_w = rx_to_px(expanded.size());
|
|
if (line_w > max_width_px_)
|
|
max_width_px_ = line_w;
|
|
}
|
|
}
|
|
// After the visible-range loop, advance the layout cursor to the end of
|
|
// the (virtual) content so ImGui sees the correct total size. Vertical
|
|
// height comes from total_rows; horizontal width comes from the cached
|
|
// max line width. A Dummy at the final position records both.
|
|
if (total_rows > last_vis) {
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
|
static_cast<float>(total_rows - last_vis) * row_h);
|
|
}
|
|
if (max_width_px_ > 0.0f) {
|
|
ImGui::SetCursorPosX(max_width_px_);
|
|
}
|
|
ImGui::Dummy(ImVec2(0, 0));
|
|
// Synchronize cursor and scrolling after rendering all lines so content size is known.
|
|
{
|
|
float child_h_actual = ImGui::GetWindowHeight();
|
|
float child_w_actual = ImGui::GetWindowWidth();
|
|
float scroll_y_now = ImGui::GetScrollY();
|
|
float scroll_x_now = ImGui::GetScrollX();
|
|
|
|
long first_row = static_cast<long>(scroll_y_now / row_h);
|
|
long vis_rows = static_cast<long>(std::round(child_h_actual / row_h));
|
|
if (vis_rows < 1)
|
|
vis_rows = 1;
|
|
long last_row = first_row + vis_rows - 1;
|
|
|
|
long cyr = static_cast<long>(cy);
|
|
if (cyr < first_row) {
|
|
float target = static_cast<float>(cyr) * row_h;
|
|
if (target < 0.f)
|
|
target = 0.f;
|
|
float max_y = ImGui::GetScrollMaxY();
|
|
if (max_y >= 0.f && target > max_y)
|
|
target = max_y;
|
|
ImGui::SetScrollY(target);
|
|
first_row = static_cast<long>(target / row_h);
|
|
last_row = first_row + vis_rows - 1;
|
|
} else if (cyr > last_row) {
|
|
long new_first = cyr - vis_rows + 1;
|
|
if (new_first < 0)
|
|
new_first = 0;
|
|
float target = static_cast<float>(new_first) * row_h;
|
|
float max_y = ImGui::GetScrollMaxY();
|
|
if (target < 0.f)
|
|
target = 0.f;
|
|
if (max_y >= 0.f && target > max_y)
|
|
target = max_y;
|
|
ImGui::SetScrollY(target);
|
|
first_row = static_cast<long>(target / row_h);
|
|
last_row = first_row + vis_rows - 1;
|
|
}
|
|
|
|
// Horizontal scroll: ensure cursor is visible (pixel-based for proportional fonts)
|
|
float cursor_px_abs = 0.0f;
|
|
if (cy < nlines) {
|
|
std::string cur_line = buf->GetLineString(cy);
|
|
const std::size_t tabw = kte::kTabWidth;
|
|
// Expand tabs for cursor line to measure pixel position
|
|
std::string cur_expanded;
|
|
cur_expanded.reserve(cur_line.size() + 16);
|
|
std::size_t cur_rx = 0;
|
|
for (std::size_t ci = 0; ci < cur_line.size(); ++ci) {
|
|
if (cur_line[ci] == '\t') {
|
|
std::size_t adv = tabw - (cur_rx % tabw);
|
|
cur_expanded.append(adv, ' ');
|
|
cur_rx += adv;
|
|
} else {
|
|
cur_expanded.push_back(cur_line[ci]);
|
|
cur_rx += 1;
|
|
}
|
|
}
|
|
// Compute rendered column of cursor
|
|
std::size_t cursor_rx = 0;
|
|
for (std::size_t ci = 0; ci < cx && ci < cur_line.size(); ++ci) {
|
|
if (cur_line[ci] == '\t')
|
|
cursor_rx += tabw - (cursor_rx % tabw);
|
|
else
|
|
cursor_rx += 1;
|
|
}
|
|
std::size_t exp_end = std::min(cur_expanded.size(), cursor_rx);
|
|
if (exp_end > 0)
|
|
cursor_px_abs = ImGui::CalcTextSize(cur_expanded.c_str(),
|
|
cur_expanded.c_str() + exp_end).x;
|
|
}
|
|
if (cursor_px_abs < scroll_x_now || cursor_px_abs > scroll_x_now + child_w_actual) {
|
|
float target_x = cursor_px_abs - (child_w_actual / 2.0f);
|
|
if (target_x < 0.f)
|
|
target_x = 0.f;
|
|
float max_x = ImGui::GetScrollMaxX();
|
|
if (max_x >= 0.f && target_x > max_x)
|
|
target_x = max_x;
|
|
ImGui::SetScrollX(target_x);
|
|
}
|
|
|
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
|
int fr = static_cast<int>(std::max(0L, first_row));
|
|
int rc = static_cast<int>(std::max(1L, vis_rows));
|
|
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleVar(2); // WindowPadding, ItemSpacing
|
|
|
|
// Status bar area starting right after the scroll child
|
|
ImVec2 win_pos = ImGui::GetWindowPos();
|
|
ImVec2 win_sz = ImGui::GetWindowSize();
|
|
float x0 = win_pos.x;
|
|
float x1 = win_pos.x + win_sz.x;
|
|
float y0 = ImGui::GetCursorScreenPos().y;
|
|
float bar_h = real_bar_h;
|
|
|
|
ImVec2 p0(x0, y0);
|
|
ImVec2 p1(x1, y0 + bar_h);
|
|
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
|
|
|
// If a prompt is active, replace the entire status bar with the prompt text
|
|
if (ed.PromptActive()) {
|
|
std::string label = ed.PromptLabel();
|
|
std::string ptext = ed.PromptText();
|
|
auto kind = ed.CurrentPromptKind();
|
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
|
kind == Editor::PromptKind::Chdir) {
|
|
const char *home_c = std::getenv("HOME");
|
|
if (home_c && *home_c) {
|
|
std::string home(home_c);
|
|
if (ptext.rfind(home, 0) == 0) {
|
|
std::string rest = ptext.substr(home.size());
|
|
if (rest.empty())
|
|
ptext = "~";
|
|
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
|
ptext = std::string("~") + rest;
|
|
}
|
|
}
|
|
}
|
|
|
|
float pad = 6.f;
|
|
float left_x = p0.x + pad;
|
|
float right_x = p1.x - pad;
|
|
float max_px = std::max(0.0f, right_x - left_x);
|
|
|
|
std::string prefix;
|
|
if (kind == Editor::PromptKind::Command) {
|
|
prefix = ": ";
|
|
} else if (!label.empty()) {
|
|
prefix = label + ": ";
|
|
}
|
|
|
|
// Compose showing right-end of filename portion when too long for space
|
|
std::string final_msg;
|
|
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
|
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
|
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
|
// Trim from left until it fits by pixel width
|
|
std::string tail = ptext;
|
|
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
|
if (tail_sz.x > avail_px) {
|
|
// Remove leading chars until it fits
|
|
// Use a simple loop; text lengths are small here
|
|
size_t start = 0;
|
|
// To avoid O(n^2) worst-case, remove chunks
|
|
while (start < tail.size()) {
|
|
// Estimate how many chars to skip based on ratio
|
|
float ratio = tail_sz.x / avail_px;
|
|
size_t skip = ratio > 1.5f
|
|
? std::min(tail.size() - start,
|
|
(size_t) std::max<size_t>(
|
|
1, (size_t) (tail.size() / 4)))
|
|
: 1;
|
|
start += skip;
|
|
std::string candidate = tail.substr(start);
|
|
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
|
if (cand_sz.x <= avail_px) {
|
|
tail = candidate;
|
|
tail_sz = cand_sz;
|
|
break;
|
|
}
|
|
}
|
|
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
|
// As a last resort, ensure fit by chopping exactly
|
|
// binary reduce
|
|
size_t lo = 0, hi = tail.size();
|
|
while (lo < hi) {
|
|
size_t mid = (lo + hi) / 2;
|
|
std::string cand = tail.substr(mid);
|
|
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
|
|
hi = mid;
|
|
else
|
|
lo = mid + 1;
|
|
}
|
|
tail = tail.substr(lo);
|
|
}
|
|
}
|
|
final_msg = prefix + tail;
|
|
} else {
|
|
final_msg = prefix + ptext;
|
|
}
|
|
|
|
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
|
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
|
ImGui::SetCursorScreenPos(ImVec2(left_x, y0 + (bar_h - msg_sz.y) * 0.5f));
|
|
ImGui::TextUnformatted(final_msg.c_str());
|
|
ImGui::PopClipRect();
|
|
} else {
|
|
// Build left text
|
|
std::string left;
|
|
left.reserve(256);
|
|
left += "kge"; // GUI app name
|
|
left += " ";
|
|
left += KTE_VERSION_STR;
|
|
std::string fname;
|
|
try {
|
|
fname = ed.DisplayNameFor(*buf);
|
|
} catch (...) {
|
|
fname = buf->Filename();
|
|
try {
|
|
fname = std::filesystem::path(fname).filename().string();
|
|
} catch (...) {}
|
|
}
|
|
left += " ";
|
|
// Insert buffer position prefix "[x/N] " before filename
|
|
{
|
|
std::size_t total = ed.BufferCount();
|
|
if (total > 0) {
|
|
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
|
left += "[";
|
|
left += std::to_string(static_cast<unsigned long long>(idx1));
|
|
left += "/";
|
|
left += std::to_string(static_cast<unsigned long long>(total));
|
|
left += "] ";
|
|
}
|
|
}
|
|
left += fname;
|
|
if (buf->Dirty())
|
|
left += " *";
|
|
// Append total line count as "<n>L"
|
|
{
|
|
unsigned long lcount = static_cast<unsigned long>(buf->Nrows());
|
|
left += " ";
|
|
left += std::to_string(lcount);
|
|
left += "L";
|
|
}
|
|
|
|
// Build right text (cursor/mark)
|
|
int row1 = static_cast<int>(buf->Cury()) + 1;
|
|
int col1 = static_cast<int>(buf->Curx()) + 1;
|
|
bool have_mark = buf->MarkSet();
|
|
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
|
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
|
char rbuf[128];
|
|
if (have_mark)
|
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
|
else
|
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
|
std::string right = rbuf;
|
|
|
|
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
|
std::string msg;
|
|
if (ed.PromptActive()) {
|
|
msg = ed.PromptLabel();
|
|
if (!msg.empty())
|
|
msg += ": ";
|
|
msg += ed.PromptText();
|
|
} else {
|
|
msg = ed.Status();
|
|
}
|
|
|
|
// Measurements
|
|
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
|
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
|
float pad = 6.f;
|
|
float left_x = p0.x + pad;
|
|
float right_x = p1.x - pad - right_sz.x;
|
|
if (right_x < left_x + left_sz.x + pad) {
|
|
// Not enough room; clip left to fit
|
|
float max_left = std::max(0.0f, right_x - left_x - pad);
|
|
if (max_left < left_sz.x && max_left > 10.0f) {
|
|
// Render a clipped left using a child region
|
|
ImGui::SetCursorScreenPos(ImVec2(left_x, y0 + (bar_h - left_sz.y) * 0.5f));
|
|
ImGui::PushClipRect(ImVec2(left_x, y0), ImVec2(right_x - pad, y0 + bar_h),
|
|
true);
|
|
ImGui::TextUnformatted(left.c_str());
|
|
ImGui::PopClipRect();
|
|
}
|
|
} else {
|
|
// Draw left normally
|
|
ImGui::SetCursorScreenPos(ImVec2(left_x, y0 + (bar_h - left_sz.y) * 0.5f));
|
|
ImGui::TextUnformatted(left.c_str());
|
|
}
|
|
|
|
// Draw right
|
|
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
|
|
y0 + (bar_h - right_sz.y) * 0.5f));
|
|
ImGui::TextUnformatted(right.c_str());
|
|
|
|
// Draw middle message centered in remaining space
|
|
if (!msg.empty()) {
|
|
float mid_left = left_x + left_sz.x + pad;
|
|
float mid_right = std::max(right_x - pad, mid_left);
|
|
float mid_w = std::max(0.0f, mid_right - mid_left);
|
|
if (mid_w > 1.0f) {
|
|
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
|
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
|
// Clip to middle region
|
|
ImGui::PushClipRect(ImVec2(mid_left, y0), ImVec2(mid_right, y0 + bar_h), true);
|
|
ImGui::SetCursorScreenPos(ImVec2(msg_x, y0 + (bar_h - msg_sz.y) * 0.5f));
|
|
ImGui::TextUnformatted(msg.c_str());
|
|
ImGui::PopClipRect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(3);
|
|
|
|
// --- Visual File Picker overlay (GUI only) ---
|
|
if (ed.FilePickerVisible()) {
|
|
// Centered popup-style window that always fits within the current viewport
|
|
ImGuiViewport *vp2 = ImGui::GetMainViewport();
|
|
|
|
// Desired size, min size, and margins
|
|
const ImVec2 want(800.0f, 500.0f);
|
|
const ImVec2 min_sz(240.0f, 160.0f);
|
|
const float margin = 20.0f; // space from viewport edges
|
|
|
|
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
|
|
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),
|
|
std::max(32.0f, vp2->Size.y - 2.0f * margin));
|
|
|
|
// Clamp desired size to [min_sz, max_sz]
|
|
ImVec2 size(std::min(want.x, max_sz.x), std::min(want.y, max_sz.y));
|
|
size.x = std::max(size.x, std::min(min_sz.x, max_sz.x));
|
|
size.y = std::max(size.y, std::min(min_sz.y, max_sz.y));
|
|
|
|
// Center within the viewport using the final size
|
|
ImVec2 pos(vp2->Pos.x + std::max(margin, (vp2->Size.x - size.x) * 0.5f),
|
|
vp2->Pos.y + std::max(margin, (vp2->Size.y - size.y) * 0.5f));
|
|
|
|
// On HiDPI displays (macOS Retina), ensure integer pixel alignment to avoid
|
|
// potential hit-test vs draw mismatches from sub-pixel positions.
|
|
pos.x = std::floor(pos.x + 0.5f);
|
|
pos.y = std::floor(pos.y + 0.5f);
|
|
size.x = std::floor(size.x + 0.5f);
|
|
size.y = std::floor(size.y + 0.5f);
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
ImGuiWindowFlags wflags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoDocking;
|
|
bool open = true;
|
|
if (ImGui::Begin("File Picker", &open, wflags)) {
|
|
// Current directory
|
|
std::string curdir = ed.FilePickerDir();
|
|
if (curdir.empty()) {
|
|
try {
|
|
curdir = std::filesystem::current_path().string();
|
|
} catch (...) {
|
|
curdir = ".";
|
|
}
|
|
ed.SetFilePickerDir(curdir);
|
|
}
|
|
ImGui::TextUnformatted(curdir.c_str());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Up")) {
|
|
try {
|
|
std::filesystem::path p(curdir);
|
|
if (p.has_parent_path()) {
|
|
ed.SetFilePickerDir(p.parent_path().string());
|
|
}
|
|
} catch (...) {}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Close")) {
|
|
ed.SetFilePickerVisible(false);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Header
|
|
ImGui::TextUnformatted("Name");
|
|
ImGui::Separator();
|
|
|
|
// Scrollable list
|
|
ImGui::BeginChild("picker-list", ImVec2(0, 0), true);
|
|
|
|
// Build entries: directories first then files, alphabetical
|
|
struct Entry {
|
|
std::string name;
|
|
std::filesystem::path path;
|
|
bool is_dir;
|
|
};
|
|
std::vector<Entry> entries;
|
|
entries.reserve(256);
|
|
// Optional parent entry
|
|
try {
|
|
std::filesystem::path base(curdir);
|
|
std::error_code ec;
|
|
for (auto it = std::filesystem::directory_iterator(base, ec);
|
|
!ec && it != std::filesystem::directory_iterator(); it.increment(ec)) {
|
|
const auto &p = it->path();
|
|
std::string nm;
|
|
try {
|
|
nm = p.filename().string();
|
|
} catch (...) {
|
|
continue;
|
|
}
|
|
if (nm == "." || nm == "..")
|
|
continue;
|
|
bool is_dir = false;
|
|
std::error_code ec2;
|
|
is_dir = it->is_directory(ec2);
|
|
entries.push_back({nm, p, is_dir});
|
|
}
|
|
} catch (...) {
|
|
// ignore listing errors; show empty
|
|
}
|
|
std::sort(entries.begin(), entries.end(), [](const Entry &a, const Entry &b) {
|
|
if (a.is_dir != b.is_dir)
|
|
return a.is_dir && !b.is_dir;
|
|
return a.name < b.name;
|
|
});
|
|
|
|
// Draw rows
|
|
int idx = 0;
|
|
for (const auto &e: entries) {
|
|
ImGui::PushID(idx++); // ensure unique/stable IDs even if names repeat
|
|
std::string label;
|
|
label.reserve(e.name.size() + 4);
|
|
if (e.is_dir)
|
|
label += "[";
|
|
label += e.name;
|
|
if (e.is_dir)
|
|
label += "]";
|
|
|
|
// Render selectable row
|
|
ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick);
|
|
|
|
// Activate based strictly on hover + mouse, to avoid any off-by-one due to click routing
|
|
if (ImGui::IsItemHovered()) {
|
|
if (e.is_dir && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
// Enter directory on double-click
|
|
ed.SetFilePickerDir(e.path.string());
|
|
} else if (!e.is_dir && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
// Open file on single click
|
|
ed.RequestOpenFile(e.path.string());
|
|
(void) ed.ProcessPendingOpens();
|
|
ed.SetFilePickerVisible(false);
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
ImGui::End();
|
|
if (!open) {
|
|
ed.SetFilePickerVisible(false);
|
|
}
|
|
}
|
|
} |