#include #include #include #include #include #include #include #include #include #include "ImGuiRenderer.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 { const auto &lines = buf->Rows(); 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. static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs const long buf_rowoffs = static_cast(buf->Rowoffs()); const long buf_coloffs = static_cast(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(buf_rowoffs) * row_h; ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y)); } if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) { float target_x = static_cast(buf_coloffs) * space_w; float target_y = static_cast(buf_rowoffs) * row_h; ImGui::SetNextWindowScroll(ImVec2(target_x, target_y)); } // Reserve space for status bar at bottom ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), 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(); std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui // Synchronize buffer offsets from ImGui scroll if user scrolled manually bool forced_scroll = false; { static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels const long scroll_top = static_cast(scroll_y / row_h); const long scroll_left = static_cast(scroll_x / space_w); // Check if rowoffs was programmatically changed this frame if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) { forced_scroll = true; } // If user scrolled (not programmatic), update buffer offsets accordingly if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) { if (Buffer *mbuf = const_cast(buf)) { mbuf->SetOffsets(static_cast(std::max(0L, scroll_top)), mbuf->Coloffs()); } } if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) { if (Buffer *mbuf = const_cast(buf)) { mbuf->SetOffsets(mbuf->Rowoffs(), static_cast(std::max(0L, scroll_left))); } } // 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; // Synchronize cursor and scrolling. // Ensure the cursor is visible, but avoid aggressive centering so that // the same lines remain visible until the cursor actually goes off-screen. { // Compute visible row range using the child window height float child_h = ImGui::GetWindowHeight(); long first_row = static_cast(scroll_y / row_h); long vis_rows = static_cast(child_h / row_h); if (vis_rows < 1) vis_rows = 1; long last_row = first_row + vis_rows - 1; long cyr = static_cast(cy); if (cyr < first_row) { // Scroll just enough to bring the cursor line to the top float target = static_cast(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); scroll_y = ImGui::GetScrollY(); first_row = static_cast(scroll_y / row_h); last_row = first_row + vis_rows - 1; } else if (cyr > last_row) { // Scroll just enough to bring the cursor line to the bottom long new_first = cyr - vis_rows + 1; if (new_first < 0) new_first = 0; float target = static_cast(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); scroll_y = ImGui::GetScrollY(); first_row = static_cast(scroll_y / row_h); last_row = first_row + vis_rows - 1; } // Horizontal scroll: ensure cursor column is visible float child_w = ImGui::GetWindowWidth(); long vis_cols = static_cast(child_w / space_w); if (vis_cols < 1) vis_cols = 1; long first_col = static_cast(scroll_x / space_w); long last_col = first_col + vis_cols - 1; // Compute cursor's rendered X position (accounting for tabs) std::size_t cursor_rx = 0; if (cy < lines.size()) { std::string cur_line = static_cast(lines[cy]); const std::size_t tabw = 8; for (std::size_t i = 0; i < cx && i < cur_line.size(); ++i) { if (cur_line[i] == '\t') { cursor_rx += tabw - (cursor_rx % tabw); } else { cursor_rx += 1; } } } long cxr = static_cast(cursor_rx); if (cxr < first_col || cxr > last_col) { float target_x = static_cast(cxr) * space_w; // Center horizontally if possible target_x -= (child_w / 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); scroll_x = ImGui::GetScrollX(); } // Phase 3: prefetch visible viewport highlights and warm around in background if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { int fr = static_cast(std::max(0L, first_row)); int rc = static_cast(std::max(1L, vis_rows)); buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version()); } } // Cache current horizontal offset in rendered columns for click handling const std::size_t coloffs_now = buf->Coloffs(); // Handle mouse click before rendering to avoid dependent on drawn items if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { ImVec2 mp = ImGui::GetIO().MousePos; // Compute content-relative position accounting for scroll // mp.y - child_window_pos.y gives us pixels from top of child window // Adding scroll_y gives us pixels from top of content (buffer row 0) float content_y = (mp.y - child_window_pos.y) + scroll_y; long by_l = static_cast(content_y / row_h); if (by_l < 0) by_l = 0; // Convert to buffer row std::size_t by = static_cast(by_l); if (by >= lines.size()) { if (!lines.empty()) by = lines.size() - 1; else by = 0; } // Compute click X position relative to left edge of child window (in pixels) // This gives us the visual offset from the start of displayed content float visual_x = mp.x - child_window_pos.x; if (visual_x < 0.0f) visual_x = 0.0f; // Convert visual pixel offset to rendered column, then add coloffs_now // to get the absolute rendered column in the buffer std::size_t clicked_rx = static_cast(visual_x / space_w) + coloffs_now; // Empty buffer guard: if there are no lines yet, just move to 0:0 if (lines.empty()) { Execute(ed, CommandId::MoveCursorTo, std::string("0:0")); } else { // Convert rendered column (clicked_rx) to source column accounting for tabs std::string line_clicked = static_cast(lines[by]); const std::size_t tabw = 8; // Iterate through source columns, computing rendered position, to find closest match std::size_t rx = 0; // rendered column position std::size_t best_col = 0; float best_dist = std::numeric_limits::infinity(); float clicked_rx_f = static_cast(clicked_rx); for (std::size_t i = 0; i <= line_clicked.size(); ++i) { // Check current position float dist = std::fabs(clicked_rx_f - static_cast(rx)); if (dist < best_dist) { best_dist = dist; best_col = i; } // Advance to next position if not at end if (i < line_clicked.size()) { if (line_clicked[i] == '\t') { rx += (tabw - (rx % tabw)); } else { rx += 1; } } } // Dispatch absolute buffer coordinates (row:col) char tmp[64]; std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col); Execute(ed, CommandId::MoveCursorTo, std::string(tmp)); } } for (std::size_t i = rowoffs; i < lines.size(); ++i) { // Capture the screen position before drawing the line ImVec2 line_pos = ImGui::GetCursorScreenPos(); std::string line = static_cast(lines[i]); // Expand tabs to spaces with width=8 and apply horizontal scroll offset const std::size_t tabw = 8; std::string expanded; expanded.reserve(line.size() + 16); std::size_t rx_abs_draw = 0; // rendered column for drawing // Compute search highlight ranges for this line in source indices bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); std::vector > hl_src_ranges; if (search_mode) { // If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring if (ed.PromptActive() && ( ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed. CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) { try { std::regex rx(ed.SearchQuery()); for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); it != std::sregex_iterator(); ++it) { const auto &m = *it; std::size_t sx = static_cast(m.position()); std::size_t ex = sx + static_cast(m.length()); hl_src_ranges.emplace_back(sx, ex); } } catch (const std::regex_error &) { // ignore invalid patterns here; status line already shows the error } } else { const std::string &q = ed.SearchQuery(); std::size_t pos = 0; while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) { hl_src_ranges.emplace_back(pos, pos + q.size()); pos += q.size(); } } } auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t { std::size_t rx = 0; std::size_t s = 0; while (s < upto_src_exclusive && s < line.size()) { if (line[s] == '\t') rx += (tabw - (rx % tabw)); else rx += 1; ++s; } return rx; }; // 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); // Apply horizontal scroll offset if (rx_end <= coloffs_now) continue; // fully left of view std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0; std::size_t vx1 = rx_end - coloffs_now; ImVec2 p0 = ImVec2(line_pos.x + static_cast(vx0) * space_w, line_pos.y); ImVec2 p1 = ImVec2(line_pos.x + static_cast(vx1) * space_w, 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); } } // Emit entire line to an expanded buffer (tabs -> spaces) for (std::size_t src = 0; src < line.size(); ++src) { char c = line[src]; if (c == '\t') { std::size_t adv = (tabw - (rx_abs_draw % tabw)); expanded.append(adv, ' '); rx_abs_draw += adv; } else { expanded.push_back(c); rx_abs_draw += 1; } } // 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(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 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::max( 0, std::min(s_raw, static_cast(line_len)))); std::size_t e = static_cast(std::max( static_cast(s), std::min(e_raw, static_cast(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; }); // Helper to convert a src column to expanded rx position auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t { std::size_t rx = 0; for (std::size_t k = 0; k < sidx && k < line.size(); ++k) { rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1; } return rx; }; for (const auto &sp: spans) { std::size_t rx_s = src_to_rx_full(sp.s); std::size_t rx_e = src_to_rx_full(sp.e); if (rx_e <= coloffs_now) continue; // fully left of viewport // Clamp to visible portion and expanded length std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now; if (draw_start >= expanded.size()) continue; // fully right of expanded text std::size_t draw_end = std::min(rx_e, expanded.size()); if (draw_end <= draw_start) continue; // Screen position is relative to coloffs_now std::size_t screen_x = draw_start - coloffs_now; ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k)); ImVec2 p = ImVec2(line_pos.x + static_cast(screen_x) * space_w, 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 as one run, accounting for horizontal scroll offset if (coloffs_now < expanded.size()) { ImVec2 p = ImVec2(line_pos.x, line_pos.y); ImGui::GetWindowDrawList()->AddText( p, ImGui::GetColorU32(ImGuiCol_Text), expanded.c_str() + coloffs_now); ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h)); } else { // Line is fully scrolled out of view horizontally ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h)); } } // Draw a visible cursor indicator on the current line if (i == cy) { // Compute rendered X (rx) from source column with tab expansion std::size_t rx_abs = 0; for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) { if (line[k] == '\t') rx_abs += (tabw - (rx_abs % tabw)); else rx_abs += 1; } // Convert to viewport x by subtracting horizontal col offset std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0; // For proportional fonts (Linux GUI), avoid accumulating drift by computing // the exact pixel width of the expanded substring up to the cursor. // expanded contains the line with tabs expanded to spaces and is what we draw. float cursor_px = 0.0f; if (rx_viewport > 0 && coloffs_now < expanded.size()) { std::size_t start = coloffs_now; std::size_t end = std::min(expanded.size(), start + rx_viewport); // Measure substring width in pixels ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start, expanded.c_str() + end); cursor_px = sz.x; } 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); } } ImGui::EndChild(); // Status bar spanning full width ImGui::Separator(); // Compute full content width and draw a filled background rectangle ImVec2 win_pos = ImGui::GetWindowPos(); ImVec2 cr_min = ImGui::GetWindowContentRegionMin(); ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); float x0 = win_pos.x + cr_min.x; float x1 = win_pos.x + cr_max.x; ImVec2 cursor = ImGui::GetCursorScreenPos(); float bar_h = ImGui::GetFrameHeight(); ImVec2 p0(x0, cursor.y); ImVec2 p1(x1, cursor.y + 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( 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, p0.y + (bar_h - msg_sz.y) * 0.5f)); ImGui::TextUnformatted(final_msg.c_str()); ImGui::PopClipRect(); // Advance cursor to after the bar to keep layout consistent ImGui::Dummy(ImVec2(x1 - x0, bar_h)); } 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(idx1)); left += "/"; left += std::to_string(static_cast(total)); left += "] "; } } left += fname; if (buf->Dirty()) left += " *"; // Append total line count as "L" { unsigned long lcount = static_cast(buf->Rows().size()); left += " "; left += std::to_string(lcount); left += "L"; } // Build right text (cursor/mark) int row1 = static_cast(buf->Cury()) + 1; int col1 = static_cast(buf->Curx()) + 1; bool have_mark = buf->MarkSet(); int mrow1 = have_mark ? static_cast(buf->MarkCury()) + 1 : 0; int mcol1 = have_mark ? static_cast(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, p0.y + (bar_h - left_sz.y) * 0.5f)); ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true); ImGui::TextUnformatted(left.c_str()); ImGui::PopClipRect(); } } else { // Draw left normally ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f)); ImGui::TextUnformatted(left.c_str()); } // Draw right ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (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, p0.y), ImVec2(mid_right, p1.y), true); ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f)); ImGui::TextUnformatted(msg.c_str()); ImGui::PopClipRect(); } } // Advance cursor to after the bar to keep layout consistent ImGui::Dummy(ImVec2(x1 - x0, bar_h)); } } 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 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 std::string err; if (!ed.OpenFile(e.path.string(), err)) { ed.SetStatus(std::string("open: ") + err); } else { ed.SetStatus(std::string("Opened: ") + e.name); } ed.SetFilePickerVisible(false); } } ImGui::PopID(); } ImGui::EndChild(); } ImGui::End(); if (!open) { ed.SetFilePickerVisible(false); } } }