Add horizontal scrolling support and refactor mouse click handling in GUI.

- Introduce horizontal scrolling with column offset synchronization in GUI.
- Refactor mouse click handling for improved accuracy and viewport alignment.
- Enhance tab expansion and cursor rendering logic for better user experience.
- Replace redundant variable declarations in `Buffer` for cleaner code.
This commit is contained in:
2025-11-30 17:19:46 -08:00
parent 38ba8c9871
commit fcb2e9a7ed
5 changed files with 189 additions and 83 deletions

27
.idea/workspace.xml generated
View File

@@ -33,18 +33,12 @@
</configurations> </configurations>
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling."> <list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines.">
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.junie/guidelines.md" beforeDir="false" afterPath="$PROJECT_DIR$/.junie/guidelines.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Editor.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/main.cc" beforeDir="false" afterPath="$PROJECT_DIR$/main.cc" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -145,7 +139,7 @@
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
</configuration> </configuration>
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge"> <configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
@@ -176,7 +170,7 @@
<workItem from="1764539556448" duration="156000" /> <workItem from="1764539556448" duration="156000" />
<workItem from="1764539725338" duration="1075000" /> <workItem from="1764539725338" duration="1075000" />
<workItem from="1764542392763" duration="3512000" /> <workItem from="1764542392763" duration="3512000" />
<workItem from="1764548345516" duration="1529000" /> <workItem from="1764548345516" duration="3453000" />
</task> </task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions."> <task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -250,7 +244,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1764505723411</updated> <updated>1764505723411</updated>
</task> </task>
<option name="localTasksCounter" value="10" /> <task id="LOCAL-00010" summary="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines.">
<option name="closed" value="true" />
<created>1764550164829</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1764550164829</updated>
</task>
<option name="localTasksCounter" value="11" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -273,7 +275,8 @@
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." /> <MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." />
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." /> <MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." />
<MESSAGE value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." /> <MESSAGE value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." />
<option name="LAST_COMMIT_MESSAGE" value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." /> <MESSAGE value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
<option name="LAST_COMMIT_MESSAGE" value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@@ -347,8 +347,8 @@ Buffer::insert_text(int row, int col, std::string_view text)
if (static_cast<std::size_t>(row) >= rows_.size()) if (static_cast<std::size_t>(row) >= rows_.size())
rows_.emplace_back(""); rows_.emplace_back("");
std::size_t y = static_cast<std::size_t>(row); auto y = static_cast<std::size_t>(row);
std::size_t x = static_cast<std::size_t>(col); auto x = static_cast<std::size_t>(col);
if (x > rows_[y].size()) if (x > rows_[y].size())
x = rows_[y].size(); x = rows_[y].size();
@@ -384,13 +384,13 @@ Buffer::delete_text(int row, int col, std::size_t len)
row = 0; row = 0;
if (static_cast<std::size_t>(row) >= rows_.size()) if (static_cast<std::size_t>(row) >= rows_.size())
return; return;
std::size_t y = static_cast<std::size_t>(row); const auto y = static_cast<std::size_t>(row);
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size()); const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
std::size_t remaining = len; std::size_t remaining = len;
while (remaining > 0 && y < rows_.size()) { while (remaining > 0 && y < rows_.size()) {
auto &line = rows_[y]; auto &line = rows_[y];
std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size())); const std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
if (x < line.size() && in_line > 0) { if (x < line.size() && in_line > 0) {
line.erase(x, in_line); line.erase(x, in_line);
remaining -= in_line; remaining -= in_line;
@@ -414,15 +414,18 @@ Buffer::delete_text(int row, int col, std::size_t len)
void void
Buffer::split_line(int row, int col) Buffer::split_line(int row, const int col)
{ {
if (row < 0) if (row < 0) {
row = 0; row = 0;
if (static_cast<std::size_t>(row) >= rows_.size()) }
if (static_cast<std::size_t>(row) >= rows_.size()) {
rows_.resize(static_cast<std::size_t>(row) + 1); rows_.resize(static_cast<std::size_t>(row) + 1);
std::size_t y = static_cast<std::size_t>(row); }
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size()); const auto y = static_cast<std::size_t>(row);
std::string tail = rows_[y].substr(x); const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
const auto tail = rows_[y].substr(x);
rows_[y].erase(x); rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail); rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
} }
@@ -431,24 +434,28 @@ Buffer::split_line(int row, int col)
void void
Buffer::join_lines(int row) Buffer::join_lines(int row)
{ {
if (row < 0) if (row < 0) {
row = 0; row = 0;
std::size_t y = static_cast<std::size_t>(row); }
if (y + 1 >= rows_.size())
const auto y = static_cast<std::size_t>(row);
if (y + 1 >= rows_.size()) {
return; return;
}
rows_[y] += rows_[y + 1]; rows_[y] += rows_[y + 1];
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1)); rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
} }
void void
Buffer::insert_row(int row, std::string_view text) Buffer::insert_row(int row, const std::string_view text)
{ {
if (row < 0) if (row < 0)
row = 0; row = 0;
if (static_cast<std::size_t>(row) > rows_.size()) if (static_cast<std::size_t>(row) > rows_.size())
row = static_cast<int>(rows_.size()); row = static_cast<int>(rows_.size());
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(row), std::string(text)); rows_.insert(rows_.begin() + row, std::string(text));
} }
@@ -459,7 +466,7 @@ Buffer::delete_row(int row)
row = 0; row = 0;
if (static_cast<std::size_t>(row) >= rows_.size()) if (static_cast<std::size_t>(row) >= rows_.size())
return; return;
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(row)); rows_.erase(rows_.begin() + row);
} }

View File

@@ -8,6 +8,8 @@
#include <cstdio> #include <cstdio>
#include <string> #include <string>
#include <filesystem> #include <filesystem>
#include <cmath>
#include <limits>
// Version string expected to be provided by build system as KTE_VERSION_STR // Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR #ifndef KTE_VERSION_STR
@@ -63,30 +65,45 @@ GUIRenderer::Draw(Editor &ed)
bool forced_scroll = false; bool forced_scroll = false;
{ {
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels 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 buf_rowoffs = static_cast<long>(buf->Rowoffs()); const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
const long buf_coloffs = static_cast<long>(buf->Coloffs());
const long scroll_top = static_cast<long>(scroll_y / row_h); const long scroll_top = static_cast<long>(scroll_y / row_h);
const long scroll_left = static_cast<long>(scroll_x / space_w);
// Detect programmatic change (e.g., keyboard navigation ensured visibility) // Detect programmatic change (e.g., keyboard navigation ensured visibility)
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) { if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h); ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
scroll_y = ImGui::GetScrollY(); scroll_y = ImGui::GetScrollY();
forced_scroll = true; forced_scroll = true;
} else { }
// If user scrolled (scroll_y changed), update buffer row offset accordingly if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { ImGui::SetScrollX(static_cast<float>(buf_coloffs) * space_w);
if (Buffer *mbuf = const_cast<Buffer *>(buf)) { scroll_x = ImGui::GetScrollX();
// Keep horizontal offset owned by GUI; only update vertical offset here forced_scroll = true;
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)), }
mbuf->Coloffs()); // If user scrolled, update buffer offsets accordingly
} if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
mbuf->Coloffs());
}
}
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left)));
} }
} }
// Update trackers for next frame // Update trackers for next frame
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs()); prev_buf_rowoffs = static_cast<long>(buf->Rowoffs());
prev_buf_coloffs = static_cast<long>(buf->Coloffs());
prev_scroll_y = ImGui::GetScrollY(); prev_scroll_y = ImGui::GetScrollY();
prev_scroll_x = ImGui::GetScrollX();
} }
// Synchronize cursor and scrolling. // Synchronize cursor and scrolling.
// Ensure the cursor is visible even on the first frame or when it didn't move, // Ensure the cursor is visible even on the first frame or when it didn't move,
@@ -120,61 +137,127 @@ GUIRenderer::Draw(Editor &ed)
// Handle mouse click before rendering to avoid dependent on drawn items // Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos; ImVec2 mp = ImGui::GetIO().MousePos;
// Map Y to row // Compute viewport-relative row so (0) is top row of the visible area
float rel_y = scroll_y + (mp.y - list_origin.y); float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
long row = static_cast<long>(rel_y / row_h); long vy = static_cast<long>(vy_f);
if (row < 0) if (vy < 0)
row = 0; vy = 0;
if (row >= static_cast<long>(lines.size()))
row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1)); // Clamp vy within visible content height to avoid huge jumps
// Map X to column by measuring text width ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
std::size_t col = 0; ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
if (!lines.empty()) { float child_h = (cr_max.y - cr_min.y);
const std::string &line = lines[static_cast<std::size_t>(row)]; long vis_rows = static_cast<long>(child_h / row_h);
float rel_x = scroll_x + (mp.x - list_origin.x); if (vis_rows < 1)
if (rel_x <= 0.0f) { vis_rows = 1;
col = 0; if (vy >= vis_rows)
} else { vy = vis_rows - 1;
float prev_w = 0.0f;
for (std::size_t i = 1; i <= line.size(); ++i) { // Translate viewport row to buffer row using Buffer::Rowoffs
ImVec2 sz = ImGui::CalcTextSize( std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
line.c_str(), line.c_str() + static_cast<long>(i)); if (by >= lines.size()) {
if (sz.x >= rel_x) { if (!lines.empty())
// Pick closer between i-1 and i by = lines.size() - 1;
float d_prev = rel_x - prev_w; else
float d_curr = sz.x - rel_x; by = 0;
col = (d_prev <= d_curr) ? (i - 1) : i; }
break;
} // Compute desired pixel X inside the viewport content (subtract horizontal scroll)
prev_w = sz.x; float px = (mp.x - list_origin.x - scroll_x);
if (i == line.size()) { if (px < 0.0f)
// clicked beyond EOL px = 0.0f;
float eol_w = sz.x;
col = (rel_x > eol_w + space_w * 0.5f) // Convert pixel X to a render-column target including horizontal col offset
? line.size() // Use our own tab expansion of width 8 to match command layer logic.
: line.size(); const std::string &line_clicked = lines[by];
} const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
if (!line_clicked.empty() && coloffs > 0) {
while (i < line_clicked.size() && rx_abs < coloffs) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
} }
++i;
} }
} }
// Dispatch command to move cursor
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
float best_dist = std::numeric_limits<float>::infinity();
while (true) {
// For i in [current..size], evaluate candidate including the implicit end position
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
float rx_px = static_cast<float>(rx_view) * space_w;
float dist = std::fabs(px - rx_px);
if (dist <= best_dist) {
best_dist = dist;
best_col = i;
}
if (i == line_clicked.size())
break;
// advance to next source column
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
// Dispatch absolute buffer coordinates (row:col)
char tmp[64]; char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%ld:%zu", row, col); std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp)); Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
} }
// Cache current horizontal offset in rendered columns
const std::size_t coloffs_now = buf->Coloffs();
for (std::size_t i = rowoffs; i < lines.size(); ++i) { for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line // Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos(); ImVec2 line_pos = ImGui::GetCursorScreenPos();
const std::string &line = lines[i]; const std::string &line = lines[i];
ImGui::TextUnformatted(line.c_str());
// 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
// Emit entire line (ImGui child scrolling will handle clipping)
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));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
ImGui::TextUnformatted(expanded.c_str());
// Draw a visible cursor indicator on the current line // Draw a visible cursor indicator on the current line
if (i == cy) { if (i == cy) {
// Compute X offset by measuring text width up to cursor column // Compute rendered X (rx) from source column with tab expansion
std::size_t px_count = std::min(cx, line.size()); std::size_t rx_abs = 0;
ImVec2 pre_sz = ImGui::CalcTextSize(line.c_str(), for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) {
line.c_str() + static_cast<long>(px_count)); if (line[k] == '\t')
ImVec2 p0 = ImVec2(line_pos.x + pre_sz.x, line_pos.y); 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;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(rx_viewport) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h); ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);

View File

@@ -1,5 +1,8 @@
#include "KKeymap.h" #include "KKeymap.h"
#include <iostream>
#include <ncurses.h> #include <ncurses.h>
#include <ostream>
auto auto
@@ -21,9 +24,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
out = CommandId::SaveAndQuit; out = CommandId::SaveAndQuit;
return true; return true;
default: default:
// Important: do not return here — fall through to non-ctrl table return false;
// so that C-k u/U still work even if Ctrl is (incorrectly) held
break;
} }
} }
@@ -49,6 +50,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'e': case 'e':
out = CommandId::OpenFileStart; out = CommandId::OpenFileStart;
return true; return true;
case 'E':
std::cerr << "E is not a valid command" << std::endl;
return false;
case 'f': case 'f':
out = CommandId::FlushKillRing; out = CommandId::FlushKillRing;
return true; return true;

View File

@@ -189,6 +189,15 @@ main(int argc, const char *argv[])
fe = std::make_unique<TerminalFrontend>(); fe = std::make_unique<TerminalFrontend>();
} }
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
if (use_gui) {
/* likely using the .app, so need to cd */
if (chdir(getenv("HOME")) != 0) {
std::cerr << "kge.app: failed to chdir to HOME" << std::endl;
}
}
#endif
if (!fe->Init(editor)) { if (!fe->Init(editor)) {
std::cerr << "kte: failed to initialize frontend" << std::endl; std::cerr << "kte: failed to initialize frontend" << std::endl;
return 1; return 1;