Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f04e4357 | |||
| 0585edad9e | |||
| 8712ea673d | |||
| 3148e16cf8 | |||
| 34eaa72033 | |||
| f49f1698f4 | |||
| f4b3188069 |
47
Buffer.h
47
Buffer.h
@@ -35,9 +35,12 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
|
||||
@@ -48,6 +51,26 @@
|
||||
#include "Highlight.h"
|
||||
#include <mutex>
|
||||
|
||||
// Edit mode determines which font class is used for a buffer.
|
||||
enum class EditMode { Code, Writing };
|
||||
|
||||
// Detect edit mode from a filename's extension.
|
||||
inline EditMode
|
||||
DetectEditMode(const std::string &filename)
|
||||
{
|
||||
std::string ext = std::filesystem::path(filename).extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
static const std::unordered_set<std::string> writing_exts = {
|
||||
".txt", ".md", ".markdown", ".rst", ".org",
|
||||
".tex", ".adoc", ".asciidoc",
|
||||
};
|
||||
if (writing_exts.count(ext))
|
||||
return EditMode::Writing;
|
||||
return EditMode::Code;
|
||||
}
|
||||
|
||||
// Forward declaration for swap journal integration
|
||||
namespace kte {
|
||||
class SwapRecorder;
|
||||
@@ -484,6 +507,27 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Edit mode (code vs writing)
|
||||
[[nodiscard]] EditMode GetEditMode() const
|
||||
{
|
||||
return edit_mode_;
|
||||
}
|
||||
|
||||
|
||||
void SetEditMode(EditMode m)
|
||||
{
|
||||
edit_mode_ = m;
|
||||
}
|
||||
|
||||
|
||||
void ToggleEditMode()
|
||||
{
|
||||
edit_mode_ = (edit_mode_ == EditMode::Code)
|
||||
? EditMode::Writing
|
||||
: EditMode::Code;
|
||||
}
|
||||
|
||||
|
||||
void SetSyntaxEnabled(bool on)
|
||||
{
|
||||
syntax_enabled_ = on;
|
||||
@@ -614,6 +658,9 @@ private:
|
||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||
std::unique_ptr<UndoSystem> undo_sys_;
|
||||
|
||||
// Edit mode (code vs writing)
|
||||
EditMode edit_mode_ = EditMode::Code;
|
||||
|
||||
// Syntax/highlighting state
|
||||
std::uint64_t version_ = 0; // increment on edits
|
||||
bool syntax_enabled_ = true;
|
||||
|
||||
@@ -4,7 +4,7 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(KTE_VERSION "1.8.0")
|
||||
set(KTE_VERSION "1.10.0")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
@@ -205,6 +205,8 @@ set(FONT_HEADERS
|
||||
fonts/FontList.h
|
||||
fonts/B612Mono.h
|
||||
fonts/BrassMono.h
|
||||
fonts/CrimsonPro.h
|
||||
fonts/ETBook.h
|
||||
fonts/BrassMonoCode.h
|
||||
fonts/FiraCode.h
|
||||
fonts/Go.h
|
||||
@@ -216,6 +218,7 @@ set(FONT_HEADERS
|
||||
fonts/IosevkaExtended.h
|
||||
fonts/ShareTech.h
|
||||
fonts/SpaceMono.h
|
||||
fonts/Spectral.h
|
||||
fonts/Syne.h
|
||||
fonts/Triplicate.h
|
||||
fonts/Unispace.h
|
||||
@@ -327,6 +330,7 @@ if (BUILD_TESTS)
|
||||
tests/test_swap_edge_cases.cc
|
||||
tests/test_swap_recovery_prompt.cc
|
||||
tests/test_swap_cleanup.cc
|
||||
tests/test_swap_cleanup2.cc
|
||||
tests/test_swap_git_editor.cc
|
||||
tests/test_piece_table.cc
|
||||
tests/test_search.cc
|
||||
|
||||
116
CONFIG.md
Normal file
116
CONFIG.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# kge Configuration
|
||||
|
||||
kge loads configuration from `~/.config/kte/kge.toml`. If no TOML file is
|
||||
found, it falls back to the legacy `kge.ini` format.
|
||||
|
||||
## TOML Format
|
||||
|
||||
```toml
|
||||
[window]
|
||||
fullscreen = false
|
||||
columns = 80
|
||||
rows = 42
|
||||
|
||||
[font]
|
||||
# Default font and size
|
||||
name = "default"
|
||||
size = 18.0
|
||||
# Font used in code mode (monospace)
|
||||
code = "default"
|
||||
# Font used in writing mode (proportional)
|
||||
writing = "crimsonpro"
|
||||
|
||||
[appearance]
|
||||
theme = "nord"
|
||||
# "dark" or "light" for themes with variants
|
||||
background = "dark"
|
||||
|
||||
[editor]
|
||||
syntax = true
|
||||
```
|
||||
|
||||
## Sections
|
||||
|
||||
### `[window]`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|--------------|------|---------|---------------------------------|
|
||||
| `fullscreen` | bool | false | Start in fullscreen mode |
|
||||
| `columns` | int | 80 | Initial window width in columns |
|
||||
| `rows` | int | 42 | Initial window height in rows |
|
||||
|
||||
### `[font]`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----------|--------|--------------|------------------------------------------|
|
||||
| `name` | string | "default" | Default font loaded at startup |
|
||||
| `size` | float | 18.0 | Font size in pixels |
|
||||
| `code` | string | "default" | Font for code mode (monospace) |
|
||||
| `writing` | string | "crimsonpro" | Font for writing mode (proportional) |
|
||||
|
||||
### `[appearance]`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|--------------|--------|---------|-----------------------------------------|
|
||||
| `theme` | string | "nord" | Color theme |
|
||||
| `background` | string | "dark" | Background mode: "dark" or "light" |
|
||||
|
||||
### `[editor]`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|----------|------|---------|------------------------------|
|
||||
| `syntax` | bool | true | Enable syntax highlighting |
|
||||
|
||||
## Edit Modes
|
||||
|
||||
kge has two edit modes that control which font is used:
|
||||
|
||||
- **code** — Uses the monospace font (`font.code`). Default for source files.
|
||||
- **writing** — Uses the proportional font (`font.writing`). Auto-detected
|
||||
for `.txt`, `.md`, `.markdown`, `.rst`, `.org`, `.tex`, `.adoc`, and
|
||||
`.asciidoc` files.
|
||||
|
||||
Toggle with `C-k m` or `: mode [code|writing]`.
|
||||
|
||||
## Available Fonts
|
||||
|
||||
### Monospace
|
||||
|
||||
b612, berkeley, berkeley-bold, brassmono, brassmono-bold, brassmonocode,
|
||||
brassmonocode-bold, fira, go, ibm, idealist, inconsolata, inconsolataex,
|
||||
iosevka, iosevkaex, sharetech, space, syne, triplicate, unispace
|
||||
|
||||
### Proportional (Serif)
|
||||
|
||||
crimsonpro, etbook, spectral
|
||||
|
||||
## Available Themes
|
||||
|
||||
amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord,
|
||||
old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
|
||||
|
||||
Themes with light/dark variants: eink, gruvbox, leuchtturm, old-book,
|
||||
solarized. Set `background = "light"` or use `: background light`.
|
||||
|
||||
## Migrating from kge.ini
|
||||
|
||||
If you have an existing `kge.ini`, kge will still read it but prints a
|
||||
notice to stderr suggesting migration. To migrate, create `kge.toml` in the
|
||||
same directory (`~/.config/kte/`) using the format above. The TOML file
|
||||
takes priority when both exist.
|
||||
|
||||
The INI keys map to TOML as follows:
|
||||
|
||||
| INI key | TOML equivalent |
|
||||
|---------------|--------------------------|
|
||||
| `fullscreen` | `window.fullscreen` |
|
||||
| `columns` | `window.columns` |
|
||||
| `rows` | `window.rows` |
|
||||
| `font` | `font.name` |
|
||||
| `font_size` | `font.size` |
|
||||
| `theme` | `appearance.theme` |
|
||||
| `background` | `appearance.background` |
|
||||
| `syntax` | `editor.syntax` |
|
||||
|
||||
New keys `font.code` and `font.writing` have no INI equivalent (the INI
|
||||
parser accepts `code_font` and `writing_font` if needed).
|
||||
80
Command.cc
80
Command.cc
@@ -752,6 +752,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
||||
if (buf->IsFileBacked()) {
|
||||
if (buf->Save(err)) {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap())
|
||||
sm->ResetJournal(*buf);
|
||||
} else {
|
||||
ctx.editor.SetStatus(err);
|
||||
return false;
|
||||
@@ -759,6 +761,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
||||
} else if (!buf->Filename().empty()) {
|
||||
if (buf->SaveAs(buf->Filename(), err)) {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap())
|
||||
sm->ResetJournal(*buf);
|
||||
} else {
|
||||
ctx.editor.SetStatus(err);
|
||||
return false;
|
||||
@@ -1331,6 +1335,40 @@ cmd_font_set_size(CommandContext &ctx)
|
||||
#endif
|
||||
|
||||
|
||||
// Toggle edit mode (code/writing) for current buffer
|
||||
static bool
|
||||
cmd_toggle_edit_mode(const CommandContext &ctx)
|
||||
{
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b)
|
||||
return false;
|
||||
|
||||
std::string arg = ctx.arg;
|
||||
std::transform(arg.begin(), arg.end(), arg.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
// Trim whitespace
|
||||
auto start = arg.find_first_not_of(" \t");
|
||||
if (start != std::string::npos)
|
||||
arg = arg.substr(start);
|
||||
auto end = arg.find_last_not_of(" \t");
|
||||
if (end != std::string::npos)
|
||||
arg = arg.substr(0, end + 1);
|
||||
|
||||
if (arg == "code") {
|
||||
b->SetEditMode(EditMode::Code);
|
||||
} else if (arg == "writing") {
|
||||
b->SetEditMode(EditMode::Writing);
|
||||
} else {
|
||||
b->ToggleEditMode();
|
||||
}
|
||||
|
||||
const char *mode_str = (b->GetEditMode() == EditMode::Writing) ? "writing" : "code";
|
||||
ctx.editor.SetStatus(std::string("Mode: ") + mode_str);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Background set command (GUI, ImGui-only for now)
|
||||
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||
static bool
|
||||
@@ -1353,6 +1391,10 @@ cmd_background_set(const CommandContext &ctx)
|
||||
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (mode.empty()) {
|
||||
ctx.editor.SetStatus(std::string("Background: ") + kte::BackgroundModeName());
|
||||
return true;
|
||||
}
|
||||
if (mode != "light" && mode != "dark") {
|
||||
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
||||
return true;
|
||||
@@ -1880,15 +1922,15 @@ cmd_insert_text(CommandContext &ctx)
|
||||
#endif
|
||||
}
|
||||
if (cmd == "font") {
|
||||
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
||||
// Complete against installed font families (case-insensitive prefix)
|
||||
std::vector<std::string> cands;
|
||||
QStringList fams = QFontDatabase::families();
|
||||
std::string apfx_lower = argprefix;
|
||||
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
|
||||
[](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
||||
// Qt: complete against system font families
|
||||
QStringList fams = QFontDatabase::families();
|
||||
for (const auto &fam: fams) {
|
||||
std::string n = fam.toStdString();
|
||||
std::string nlower = n;
|
||||
@@ -1899,6 +1941,13 @@ cmd_insert_text(CommandContext &ctx)
|
||||
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
|
||||
cands.push_back(n);
|
||||
}
|
||||
#elif defined(KTE_BUILD_GUI)
|
||||
// ImGui: complete against embedded font registry
|
||||
for (const auto &n : kte::Fonts::FontRegistry::Instance().FontNames()) {
|
||||
if (apfx_lower.empty() || n.rfind(apfx_lower, 0) == 0)
|
||||
cands.push_back(n);
|
||||
}
|
||||
#endif
|
||||
if (cands.empty()) {
|
||||
// no change
|
||||
} else if (cands.size() == 1) {
|
||||
@@ -1919,9 +1968,19 @@ cmd_insert_text(CommandContext &ctx)
|
||||
}
|
||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||
return true;
|
||||
#else
|
||||
(void) argprefix;
|
||||
#endif
|
||||
}
|
||||
if (cmd == "mode") {
|
||||
std::vector<std::string> modes = {"code", "writing"};
|
||||
std::vector<std::string> cands;
|
||||
for (const auto &m : modes) {
|
||||
if (argprefix.empty() || m.rfind(argprefix, 0) == 0)
|
||||
cands.push_back(m);
|
||||
}
|
||||
if (cands.size() == 1) {
|
||||
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
|
||||
}
|
||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||
return true;
|
||||
}
|
||||
// default: no special arg completion
|
||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||
@@ -2568,6 +2627,10 @@ cmd_newline(CommandContext &ctx)
|
||||
ctx.editor.SetStatus(err);
|
||||
} else {
|
||||
buf->SetDirty(false);
|
||||
if (auto *sm = ctx.editor.Swap()) {
|
||||
sm->NotifyFilenameChanged(*buf);
|
||||
sm->ResetJournal(*buf);
|
||||
}
|
||||
ctx.editor.SetStatus("Saved as " + value);
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
@@ -5013,6 +5076,11 @@ InstallDefaultCommands()
|
||||
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
||||
false, false
|
||||
});
|
||||
// Edit mode toggle (public)
|
||||
CommandRegistry::Register({
|
||||
CommandId::ToggleEditMode, "mode", "Toggle or set edit mode: code|writing",
|
||||
cmd_toggle_edit_mode, true, false
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -113,6 +113,12 @@ enum class CommandId {
|
||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||
// GUI: open a new editor window sharing the same buffer list
|
||||
NewWindow,
|
||||
// GUI: font size controls
|
||||
FontZoomIn,
|
||||
FontZoomOut,
|
||||
FontZoomReset,
|
||||
// Edit mode (code/writing)
|
||||
ToggleEditMode,
|
||||
};
|
||||
|
||||
|
||||
|
||||
75
Editor.cc
75
Editor.cc
@@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message)
|
||||
Buffer *
|
||||
Editor::CurrentBuffer()
|
||||
{
|
||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &buffers_[curbuf_];
|
||||
return &bufs[curbuf_];
|
||||
}
|
||||
|
||||
|
||||
const Buffer *
|
||||
Editor::CurrentBuffer() const
|
||||
{
|
||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
||||
const auto &bufs = Buffers();
|
||||
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &buffers_[curbuf_];
|
||||
return &bufs[curbuf_];
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
||||
|
||||
// Prepare list of other buffer paths
|
||||
std::vector<std::vector<std::filesystem::path> > others;
|
||||
others.reserve(buffers_.size());
|
||||
for (const auto &b: buffers_) {
|
||||
const auto &bufs = Buffers();
|
||||
others.reserve(bufs.size());
|
||||
for (const auto &b: bufs) {
|
||||
if (&b == &buf)
|
||||
continue;
|
||||
if (b.Filename().empty())
|
||||
@@ -161,41 +164,44 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
||||
std::size_t
|
||||
Editor::AddBuffer(const Buffer &buf)
|
||||
{
|
||||
buffers_.push_back(buf);
|
||||
auto &bufs = Buffers();
|
||||
bufs.push_back(buf);
|
||||
// Attach swap recorder
|
||||
if (swap_) {
|
||||
swap_->Attach(&buffers_.back());
|
||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
||||
swap_->Attach(&bufs.back());
|
||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
if (bufs.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
return buffers_.size() - 1;
|
||||
return bufs.size() - 1;
|
||||
}
|
||||
|
||||
|
||||
std::size_t
|
||||
Editor::AddBuffer(Buffer &&buf)
|
||||
{
|
||||
buffers_.push_back(std::move(buf));
|
||||
auto &bufs = Buffers();
|
||||
bufs.push_back(std::move(buf));
|
||||
if (swap_) {
|
||||
swap_->Attach(&buffers_.back());
|
||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
||||
swap_->Attach(&bufs.back());
|
||||
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
if (bufs.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
return buffers_.size() - 1;
|
||||
return bufs.size() - 1;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Editor::OpenFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
// If the current buffer is an unnamed, empty, clean scratch buffer, reuse
|
||||
// it instead of creating a new one.
|
||||
auto &bufs_ref = Buffers();
|
||||
if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) {
|
||||
Buffer &cur = bufs_ref[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const std::size_t nrows = cur.Nrows();
|
||||
@@ -268,7 +274,7 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
if (swap_) {
|
||||
swap_->NotifyFilenameChanged(buffers_[idx]);
|
||||
swap_->NotifyFilenameChanged(Buffers()[idx]);
|
||||
}
|
||||
SwitchTo(idx);
|
||||
// Defensive: ensure any active prompt is closed after a successful open
|
||||
@@ -446,12 +452,13 @@ Editor::ProcessPendingOpens()
|
||||
bool
|
||||
Editor::SwitchTo(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (index >= bufs.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
Buffer &b = bufs[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
@@ -478,21 +485,22 @@ Editor::SwitchTo(std::size_t index)
|
||||
bool
|
||||
Editor::CloseBuffer(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
auto &bufs = Buffers();
|
||||
if (index >= bufs.size()) {
|
||||
return false;
|
||||
}
|
||||
if (swap_) {
|
||||
// Always remove swap file when closing a buffer on normal exit.
|
||||
// Swap files are for crash recovery; on clean close, we don't need them.
|
||||
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||
swap_->Detach(&buffers_[index], true);
|
||||
buffers_[index].SetSwapRecorder(nullptr);
|
||||
swap_->Detach(&bufs[index], true);
|
||||
bufs[index].SetSwapRecorder(nullptr);
|
||||
}
|
||||
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (buffers_.empty()) {
|
||||
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
if (bufs.empty()) {
|
||||
curbuf_ = 0;
|
||||
} else if (curbuf_ >= buffers_.size()) {
|
||||
curbuf_ = buffers_.size() - 1;
|
||||
} else if (curbuf_ >= bufs.size()) {
|
||||
curbuf_ = bufs.size() - 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -516,7 +524,12 @@ Editor::Reset()
|
||||
// Reset close-confirm/save state
|
||||
close_confirm_pending_ = false;
|
||||
close_after_save_ = false;
|
||||
buffers_.clear();
|
||||
auto &bufs = Buffers();
|
||||
if (swap_) {
|
||||
for (auto &buf : bufs)
|
||||
swap_->Detach(&buf, true);
|
||||
}
|
||||
bufs.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
|
||||
|
||||
15
Editor.h
15
Editor.h
@@ -521,7 +521,7 @@ public:
|
||||
// Buffers
|
||||
[[nodiscard]] std::size_t BufferCount() const
|
||||
{
|
||||
return buffers_.size();
|
||||
return Buffers().size();
|
||||
}
|
||||
|
||||
|
||||
@@ -531,6 +531,19 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Clamp curbuf_ to valid range. Call when the shared buffer list may
|
||||
// have been modified by another editor (e.g., buffer closed in another window).
|
||||
void ValidateBufferIndex()
|
||||
{
|
||||
const auto &bufs = Buffers();
|
||||
if (bufs.empty()) {
|
||||
curbuf_ = 0;
|
||||
} else if (curbuf_ >= bufs.size()) {
|
||||
curbuf_ = bufs.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Buffer *CurrentBuffer();
|
||||
|
||||
const Buffer *CurrentBuffer() const;
|
||||
|
||||
114
GUIConfig.cc
114
GUIConfig.cc
@@ -3,9 +3,29 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
|
||||
#include "GUIConfig.h"
|
||||
|
||||
// toml++ for TOML config parsing
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Weverything"
|
||||
#elif defined(__GNUC__)
|
||||
# pragma GCC diagnostic push
|
||||
# pragma GCC diagnostic ignored "-Wall"
|
||||
# pragma GCC diagnostic ignored "-Wextra"
|
||||
#endif
|
||||
|
||||
#include "ext/tomlplusplus/toml.hpp"
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
# pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
|
||||
static void
|
||||
trim(std::string &s)
|
||||
@@ -19,32 +39,104 @@ trim(std::string &s)
|
||||
|
||||
|
||||
static std::string
|
||||
default_config_path()
|
||||
config_dir()
|
||||
{
|
||||
const char *home = std::getenv("HOME");
|
||||
if (!home || !*home)
|
||||
return {};
|
||||
std::string path(home);
|
||||
path += "/.config/kte/kge.ini";
|
||||
return path;
|
||||
return std::string(home) + "/.config/kte";
|
||||
}
|
||||
|
||||
|
||||
GUIConfig
|
||||
GUIConfig::Load()
|
||||
{
|
||||
GUIConfig cfg; // defaults already set
|
||||
const std::string path = default_config_path();
|
||||
GUIConfig cfg;
|
||||
std::string dir = config_dir();
|
||||
if (dir.empty())
|
||||
return cfg;
|
||||
|
||||
if (!path.empty()) {
|
||||
cfg.LoadFromFile(path);
|
||||
// Try TOML first
|
||||
std::string toml_path = dir + "/kge.toml";
|
||||
if (cfg.LoadFromTOML(toml_path))
|
||||
return cfg;
|
||||
|
||||
// Fall back to legacy INI
|
||||
std::string ini_path = dir + "/kge.ini";
|
||||
if (cfg.LoadFromINI(ini_path)) {
|
||||
std::cerr << "kge: loaded legacy kge.ini; consider migrating to kge.toml\n";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
GUIConfig::LoadFromFile(const std::string &path)
|
||||
GUIConfig::LoadFromTOML(const std::string &path)
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
return false;
|
||||
|
||||
toml::table tbl;
|
||||
try {
|
||||
tbl = toml::parse_file(path);
|
||||
} catch (const toml::parse_error &err) {
|
||||
std::cerr << "kge: TOML parse error in " << path << ": " << err.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// [window]
|
||||
if (auto win = tbl["window"].as_table()) {
|
||||
if (auto v = (*win)["fullscreen"].value<bool>())
|
||||
fullscreen = *v;
|
||||
if (auto v = (*win)["columns"].value<int64_t>()) {
|
||||
if (*v > 0) columns = static_cast<int>(*v);
|
||||
}
|
||||
if (auto v = (*win)["rows"].value<int64_t>()) {
|
||||
if (*v > 0) rows = static_cast<int>(*v);
|
||||
}
|
||||
}
|
||||
|
||||
// [font]
|
||||
if (auto sec = tbl["font"].as_table()) {
|
||||
if (auto v = (*sec)["name"].value<std::string>())
|
||||
font = *v;
|
||||
if (auto v = (*sec)["size"].value<double>()) {
|
||||
if (*v > 0.0) font_size = static_cast<float>(*v);
|
||||
}
|
||||
if (auto v = (*sec)["code"].value<std::string>())
|
||||
code_font = *v;
|
||||
if (auto v = (*sec)["writing"].value<std::string>())
|
||||
writing_font = *v;
|
||||
}
|
||||
|
||||
// [appearance]
|
||||
if (auto sec = tbl["appearance"].as_table()) {
|
||||
if (auto v = (*sec)["theme"].value<std::string>())
|
||||
theme = *v;
|
||||
if (auto v = (*sec)["background"].value<std::string>()) {
|
||||
std::string bg = *v;
|
||||
std::transform(bg.begin(), bg.end(), bg.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (bg == "light" || bg == "dark")
|
||||
background = bg;
|
||||
}
|
||||
}
|
||||
|
||||
// [editor]
|
||||
if (auto sec = tbl["editor"].as_table()) {
|
||||
if (auto v = (*sec)["syntax"].value<bool>())
|
||||
syntax = *v;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
GUIConfig::LoadFromINI(const std::string &path)
|
||||
{
|
||||
std::ifstream in(path);
|
||||
if (!in.good())
|
||||
@@ -104,6 +196,10 @@ GUIConfig::LoadFromFile(const std::string &path)
|
||||
}
|
||||
} else if (key == "font") {
|
||||
font = val;
|
||||
} else if (key == "code_font") {
|
||||
code_font = val;
|
||||
} else if (key == "writing_font") {
|
||||
writing_font = val;
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
|
||||
20
GUIConfig.h
20
GUIConfig.h
@@ -1,5 +1,7 @@
|
||||
/*
|
||||
* GUIConfig - loads simple GUI configuration from $HOME/.config/kte/kge.ini
|
||||
* GUIConfig - loads GUI configuration from $HOME/.config/kte/kge.toml
|
||||
*
|
||||
* Falls back to legacy kge.ini if no TOML config is found.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
@@ -22,12 +24,18 @@ public:
|
||||
std::string background = "dark";
|
||||
|
||||
// Default syntax highlighting state for GUI (kge): on/off
|
||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||
bool syntax = true; // default: enabled
|
||||
bool syntax = true;
|
||||
|
||||
// Load from default path: $HOME/.config/kte/kge.ini
|
||||
// Per-mode font defaults
|
||||
std::string code_font = "default";
|
||||
std::string writing_font = "crimsonpro";
|
||||
|
||||
// Load from default paths: try kge.toml first, fall back to kge.ini
|
||||
static GUIConfig Load();
|
||||
|
||||
// Load from explicit path. Returns true if file existed and was parsed.
|
||||
bool LoadFromFile(const std::string &path);
|
||||
// Load from explicit TOML path. Returns true if file existed and was parsed.
|
||||
bool LoadFromTOML(const std::string &path);
|
||||
|
||||
// Load from explicit INI path (legacy). Returns true if file existed and was parsed.
|
||||
bool LoadFromINI(const std::string &path);
|
||||
};
|
||||
|
||||
206
GUITheme.h
206
GUITheme.h
@@ -312,7 +312,7 @@ namespace kte {
|
||||
enum class BackgroundMode { Light, Dark };
|
||||
|
||||
// Global background mode; default to Dark to match prior defaults
|
||||
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||
inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||
|
||||
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||
enum class ThemeId {
|
||||
@@ -330,11 +330,13 @@ enum class ThemeId {
|
||||
Amber = 10,
|
||||
WeylandYutani = 11,
|
||||
Orbital = 12,
|
||||
Tufte = 13,
|
||||
Leuchtturm = 14,
|
||||
};
|
||||
|
||||
// Current theme tracking
|
||||
static inline auto gCurrentTheme = ThemeId::Nord;
|
||||
static inline std::size_t gCurrentThemeIndex = 6; // Nord index
|
||||
inline auto gCurrentTheme = ThemeId::Nord;
|
||||
inline std::size_t gCurrentThemeIndex = 7; // Nord index
|
||||
|
||||
// Forward declarations for helpers used below
|
||||
static size_t ThemeIndexFromId(ThemeId id);
|
||||
@@ -372,11 +374,13 @@ BackgroundModeName()
|
||||
#include "themes/Everforest.h"
|
||||
#include "themes/KanagawaPaper.h"
|
||||
#include "themes/LCARS.h"
|
||||
#include "themes/Leuchtturm.h"
|
||||
#include "themes/OldBook.h"
|
||||
#include "themes/Amber.h"
|
||||
#include "themes/WeylandYutani.h"
|
||||
#include "themes/Zenburn.h"
|
||||
#include "themes/Orbital.h"
|
||||
#include "themes/Tufte.h"
|
||||
|
||||
|
||||
// Theme abstraction and registry (generalized theme system)
|
||||
@@ -409,6 +413,28 @@ struct LCARSTheme final : Theme {
|
||||
}
|
||||
};
|
||||
|
||||
struct LeuchtturmTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "leuchtturm";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Dark)
|
||||
ApplyLeuchtturmDarkTheme();
|
||||
else
|
||||
ApplyLeuchtturmLightTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Leuchtturm;
|
||||
}
|
||||
};
|
||||
|
||||
struct EverforestTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
@@ -488,6 +514,28 @@ struct OrbitalTheme final : Theme {
|
||||
}
|
||||
};
|
||||
|
||||
struct TufteTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "tufte";
|
||||
}
|
||||
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Dark)
|
||||
ApplyTufteDarkTheme();
|
||||
else
|
||||
ApplyTufteLightTheme();
|
||||
}
|
||||
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::Tufte;
|
||||
}
|
||||
};
|
||||
|
||||
struct ZenburnTheme final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
@@ -657,18 +705,20 @@ ThemeRegistry()
|
||||
static std::vector<std::unique_ptr<Theme> > reg;
|
||||
if (reg.empty()) {
|
||||
// Alphabetical by canonical name:
|
||||
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, weyland-yutani, zenburn
|
||||
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
|
||||
reg.emplace_back(std::make_unique<detail::AmberTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::KanagawaPaperTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::LCARSTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::LeuchtturmTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::OldBookTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::TufteTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::WeylandYutaniTheme>());
|
||||
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
||||
}
|
||||
@@ -845,20 +895,24 @@ ThemeIndexFromId(const ThemeId id)
|
||||
return 4;
|
||||
case ThemeId::LCARS:
|
||||
return 5;
|
||||
case ThemeId::Nord:
|
||||
case ThemeId::Leuchtturm:
|
||||
return 6;
|
||||
case ThemeId::OldBook:
|
||||
case ThemeId::Nord:
|
||||
return 7;
|
||||
case ThemeId::Orbital:
|
||||
case ThemeId::OldBook:
|
||||
return 8;
|
||||
case ThemeId::Plan9:
|
||||
case ThemeId::Orbital:
|
||||
return 9;
|
||||
case ThemeId::Solarized:
|
||||
case ThemeId::Plan9:
|
||||
return 10;
|
||||
case ThemeId::WeylandYutani:
|
||||
case ThemeId::Solarized:
|
||||
return 11;
|
||||
case ThemeId::Zenburn:
|
||||
case ThemeId::Tufte:
|
||||
return 12;
|
||||
case ThemeId::WeylandYutani:
|
||||
return 13;
|
||||
case ThemeId::Zenburn:
|
||||
return 14;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -882,30 +936,144 @@ ThemeIdFromIndex(const size_t idx)
|
||||
case 5:
|
||||
return ThemeId::LCARS;
|
||||
case 6:
|
||||
return ThemeId::Nord;
|
||||
return ThemeId::Leuchtturm;
|
||||
case 7:
|
||||
return ThemeId::OldBook;
|
||||
return ThemeId::Nord;
|
||||
case 8:
|
||||
return ThemeId::Orbital;
|
||||
return ThemeId::OldBook;
|
||||
case 9:
|
||||
return ThemeId::Plan9;
|
||||
return ThemeId::Orbital;
|
||||
case 10:
|
||||
return ThemeId::Solarized;
|
||||
return ThemeId::Plan9;
|
||||
case 11:
|
||||
return ThemeId::WeylandYutani;
|
||||
return ThemeId::Solarized;
|
||||
case 12:
|
||||
return ThemeId::Tufte;
|
||||
case 13:
|
||||
return ThemeId::WeylandYutani;
|
||||
case 14:
|
||||
return ThemeId::Zenburn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||
|
||||
// Tufte palette: high-contrast, restrained color. Body text is true black on
|
||||
// cream; only keywords and links get subtle color to avoid a "christmas tree."
|
||||
static ImVec4
|
||||
SyntaxInkTufte(const TokenKind k, const bool dark)
|
||||
{
|
||||
const ImVec4 ink = dark ? RGBA(0xEAE6DE) : RGBA(0x111111); // body text
|
||||
const ImVec4 dim = dark ? RGBA(0x8A8680) : RGBA(0x555555); // comments
|
||||
const ImVec4 red = dark ? RGBA(0xD06060) : RGBA(0x8B0000); // keywords/preproc
|
||||
const ImVec4 navy = dark ? RGBA(0x7098C0) : RGBA(0x1A3A5C); // functions/links
|
||||
const ImVec4 grn = dark ? RGBA(0x8AAA6E) : RGBA(0x2E5E2E); // strings
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
case TokenKind::Preproc:
|
||||
return red;
|
||||
case TokenKind::String:
|
||||
case TokenKind::Char:
|
||||
return grn;
|
||||
case TokenKind::Comment:
|
||||
return dim;
|
||||
case TokenKind::Function:
|
||||
return navy;
|
||||
case TokenKind::Number:
|
||||
case TokenKind::Constant:
|
||||
return dark ? RGBA(0xC8A85A) : RGBA(0x6B4C00);
|
||||
case TokenKind::Type:
|
||||
return dark ? RGBA(0xBBAA90) : RGBA(0x333333);
|
||||
case TokenKind::Error:
|
||||
return dark ? RGBA(0xD06060) : RGBA(0xCC0000);
|
||||
default:
|
||||
return ink;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Leuchtturm palette: blue-black fountain pen ink with brass and bronze accents.
|
||||
// Body text is ink-colored; accents drawn from the pen metals.
|
||||
static ImVec4
|
||||
SyntaxInkLeuchtturm(const TokenKind k, const bool dark)
|
||||
{
|
||||
const ImVec4 ink = dark ? RGBA(0xE5DDD0) : RGBA(0x040720); // fountain pen ink
|
||||
const ImVec4 dim = dark ? RGBA(0x7A7060) : RGBA(0x6A6558); // comments
|
||||
const ImVec4 brass = dark ? RGBA(0xB8A060) : RGBA(0x504518); // patinated brass
|
||||
const ImVec4 bronze= dark ? RGBA(0xC08050) : RGBA(0x5C3010); // dark bronze
|
||||
const ImVec4 navy = dark ? RGBA(0x8898B0) : RGBA(0x1C2E4A); // deep navy
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
case TokenKind::Preproc:
|
||||
return brass;
|
||||
case TokenKind::String:
|
||||
case TokenKind::Char:
|
||||
return bronze;
|
||||
case TokenKind::Comment:
|
||||
return dim;
|
||||
case TokenKind::Function:
|
||||
return navy;
|
||||
case TokenKind::Number:
|
||||
case TokenKind::Constant:
|
||||
return dark ? RGBA(0xA89060) : RGBA(0x483C10);
|
||||
case TokenKind::Type:
|
||||
return dark ? RGBA(0xC0B898) : RGBA(0x222238);
|
||||
case TokenKind::Error:
|
||||
return dark ? RGBA(0xD06060) : RGBA(0xA02020);
|
||||
default:
|
||||
return ink;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Everforest: warm forest palette on dark green-gray (bg 0x2B3339).
|
||||
// Default comment color (0x616E88) is too dim; boost it and tune others.
|
||||
static ImVec4
|
||||
SyntaxInkEverforest(const TokenKind k)
|
||||
{
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
return RGBA(0xE67E80); // everforest red
|
||||
case TokenKind::Type:
|
||||
return RGBA(0xD699B6); // everforest purple
|
||||
case TokenKind::String:
|
||||
case TokenKind::Char:
|
||||
return RGBA(0xA7C080); // everforest green
|
||||
case TokenKind::Comment:
|
||||
return RGBA(0x859289); // boosted from 0x616E88 for contrast
|
||||
case TokenKind::Number:
|
||||
case TokenKind::Constant:
|
||||
return RGBA(0xD8A657); // everforest yellow/orange
|
||||
case TokenKind::Preproc:
|
||||
return RGBA(0xE69875); // everforest orange
|
||||
case TokenKind::Function:
|
||||
return RGBA(0x83C092); // everforest aqua
|
||||
case TokenKind::Operator:
|
||||
case TokenKind::Punctuation:
|
||||
return RGBA(0xD3C6AA); // everforest fg
|
||||
case TokenKind::Error:
|
||||
return RGBA(0xE67E80);
|
||||
default:
|
||||
return RGBA(0xD3C6AA); // everforest fg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[[maybe_unused]] static ImVec4
|
||||
SyntaxInk(const TokenKind k)
|
||||
{
|
||||
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||
// Base text
|
||||
|
||||
// Per-theme syntax palettes
|
||||
if (gCurrentTheme == ThemeId::Tufte)
|
||||
return SyntaxInkTufte(k, dark);
|
||||
if (gCurrentTheme == ThemeId::Leuchtturm)
|
||||
return SyntaxInkLeuchtturm(k, dark);
|
||||
if (gCurrentTheme == ThemeId::Everforest)
|
||||
return SyntaxInkEverforest(k);
|
||||
|
||||
// Default palettes tuned for Nord-ish themes
|
||||
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
|
||||
25
HelpText.cc
25
HelpText.cc
@@ -41,6 +41,7 @@ HelpText::Text()
|
||||
" C-k j Jump to mark\n"
|
||||
" C-k k Center viewport on cursor\n"
|
||||
" C-k l Reload buffer from disk\n"
|
||||
" C-k m Toggle edit mode (code/writing)\n"
|
||||
" C-k n Previous buffer\n"
|
||||
" C-k o Change working directory (prompt)\n"
|
||||
" C-k p Next buffer\n"
|
||||
@@ -82,12 +83,24 @@ HelpText::Text()
|
||||
"\n"
|
||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||
"\n"
|
||||
"GUI appearance (command prompt):\n"
|
||||
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n"
|
||||
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n"
|
||||
"Edit modes:\n"
|
||||
" code Monospace font (default for source files)\n"
|
||||
" writing Proportional font (auto for .txt, .md, .rst, .org, .tex)\n"
|
||||
" C-k m or : mode [code|writing] to toggle\n"
|
||||
"\n"
|
||||
"GUI config file options:\n"
|
||||
" font_size=NUM Set font size in pixels (default: 16; e.g., font_size=18)\n"
|
||||
"GUI commands (command prompt):\n"
|
||||
" : theme NAME Set theme (amber, eink, everforest, gruvbox,\n"
|
||||
" kanagawa-paper, lcars, leuchtturm, nord, old-book,\n"
|
||||
" orbital, plan9, solarized, tufte, weyland-yutani,\n"
|
||||
" zenburn)\n"
|
||||
" : background MODE Background: light | dark\n"
|
||||
" : font NAME Set font (tab completes)\n"
|
||||
" : font-size NUM Set font size in pixels\n"
|
||||
" : mode [code|writing] Toggle or set edit mode\n"
|
||||
"\n"
|
||||
"Configuration:\n"
|
||||
" Config file: ~/.config/kte/kge.toml (see CONFIG.md)\n"
|
||||
" Legacy kge.ini is also supported.\n"
|
||||
"\n"
|
||||
"GUI window management:\n"
|
||||
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n"
|
||||
@@ -95,4 +108,4 @@ HelpText::Text()
|
||||
" Close window Secondary windows close independently; closing the\n"
|
||||
" primary window quits the editor\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
306
ImGuiFrontend.cc
306
ImGuiFrontend.cc
@@ -30,7 +30,7 @@
|
||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers shared between Init and OpenNewWindow_
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void
|
||||
@@ -38,6 +38,11 @@ apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
||||
{
|
||||
if (!b)
|
||||
return;
|
||||
|
||||
// Auto-detect edit mode from file extension
|
||||
if (!b->Filename().empty())
|
||||
b->SetEditMode(DetectEditMode(b->Filename()));
|
||||
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
b->EnsureHighlighter();
|
||||
@@ -96,6 +101,63 @@ update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetupImGuiStyle_ — apply theme, fonts, and flags to the current ImGui context
|
||||
// ---------------------------------------------------------------------------
|
||||
void
|
||||
GUIFrontend::SetupImGuiStyle_()
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
// Disable imgui.ini for secondary windows (primary sets its own path in Init)
|
||||
io.IniFilename = nullptr;
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
if (config_.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(config_.theme);
|
||||
|
||||
// Load fonts into this context's font atlas.
|
||||
// Font registry is global and already populated by Init; just load into this atlas.
|
||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Destroy a single window's ImGui context + SDL/GL resources
|
||||
// ---------------------------------------------------------------------------
|
||||
void
|
||||
GUIFrontend::DestroyWindowResources_(WindowState &ws)
|
||||
{
|
||||
if (ws.imgui_ctx) {
|
||||
// Must activate this window's GL context before shutting down the
|
||||
// OpenGL3 backend, otherwise it deletes another context's resources.
|
||||
if (ws.window && ws.gl_ctx)
|
||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext(ws.imgui_ctx);
|
||||
ws.imgui_ctx = nullptr;
|
||||
}
|
||||
if (ws.gl_ctx) {
|
||||
SDL_GL_DeleteContext(ws.gl_ctx);
|
||||
ws.gl_ctx = nullptr;
|
||||
}
|
||||
if (ws.window) {
|
||||
SDL_DestroyWindow(ws.window);
|
||||
ws.window = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
{
|
||||
@@ -172,9 +234,10 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||
SDL_GL_SetSwapInterval(1); // vsync
|
||||
|
||||
// Create primary ImGui context
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||
if (const char *home = std::getenv("HOME")) {
|
||||
@@ -236,11 +299,12 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
}
|
||||
|
||||
// Build primary WindowState
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->width = init_w;
|
||||
ws->height = init_h;
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->imgui_ctx = imgui_ctx;
|
||||
ws->width = init_w;
|
||||
ws->height = init_h;
|
||||
// The primary window's editor IS the editor passed in from main; we don't
|
||||
// use ws->editor for the primary — instead we keep a pointer to &ed.
|
||||
// We store a sentinel: window index 0 uses the external editor reference.
|
||||
@@ -255,8 +319,6 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||
bool
|
||||
GUIFrontend::OpenNewWindow_(Editor &primary)
|
||||
{
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
|
||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
int w = windows_[0]->width;
|
||||
int h = windows_[0]->height;
|
||||
@@ -277,25 +339,48 @@ GUIFrontend::OpenNewWindow_(Editor &primary)
|
||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
|
||||
// Secondary windows share the ImGui context already created in Init.
|
||||
// We need to init the SDL2/OpenGL backends for this new window.
|
||||
// ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks.
|
||||
ImGui_ImplOpenGL3_Init(kGlslVersion);
|
||||
// Each window gets its own ImGui context — ImGui requires exactly one
|
||||
// NewFrame/Render cycle per context per frame.
|
||||
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||
ImGui::SetCurrentContext(imgui_ctx);
|
||||
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->width = w;
|
||||
ws->height = h;
|
||||
SetupImGuiStyle_();
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) {
|
||||
ImGui::DestroyContext(imgui_ctx);
|
||||
SDL_GL_DeleteContext(gl_ctx);
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
}
|
||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) {
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext(imgui_ctx);
|
||||
SDL_GL_DeleteContext(gl_ctx);
|
||||
SDL_DestroyWindow(win);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ws = std::make_unique<WindowState>();
|
||||
ws->window = win;
|
||||
ws->gl_ctx = gl_ctx;
|
||||
ws->imgui_ctx = imgui_ctx;
|
||||
ws->width = w;
|
||||
ws->height = h;
|
||||
|
||||
// Secondary editor shares the primary's buffer list
|
||||
ws->editor.SetSharedBuffers(&primary.Buffers());
|
||||
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
|
||||
|
||||
// Open a new untitled buffer and switch to it in the new window.
|
||||
ws->editor.AddBuffer(Buffer());
|
||||
ws->editor.SwitchTo(ws->editor.BufferCount() - 1);
|
||||
|
||||
ws->input.Attach(&ws->editor);
|
||||
|
||||
windows_.push_back(std::move(ws));
|
||||
|
||||
// Restore primary GL context as current
|
||||
// Restore primary context
|
||||
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
return true;
|
||||
}
|
||||
@@ -305,10 +390,10 @@ void
|
||||
GUIFrontend::Step(Editor &ed, bool &running)
|
||||
{
|
||||
// --- Event processing ---
|
||||
// SDL events carry a window ID. Route each event to the correct window's
|
||||
// ImGui context (for ImGui_ImplSDL2_ProcessEvent) and input handler.
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||
|
||||
// Determine which window this event belongs to
|
||||
Uint32 event_win_id = 0;
|
||||
switch (e.type) {
|
||||
@@ -329,6 +414,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
case SDL_MOUSEWHEEL:
|
||||
event_win_id = e.wheel.windowID;
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
event_win_id = e.motion.windowID;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -338,59 +426,67 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the target window and route the event to its ImGui context
|
||||
WindowState *target = nullptr;
|
||||
std::size_t target_idx = 0;
|
||||
if (event_win_id != 0) {
|
||||
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
||||
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
||||
target = windows_[i].get();
|
||||
target_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target && target->imgui_ctx) {
|
||||
// Set this window's ImGui context so ImGui_ImplSDL2_ProcessEvent
|
||||
// updates the correct IO state.
|
||||
ImGui::SetCurrentContext(target->imgui_ctx);
|
||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||
}
|
||||
|
||||
if (e.type == SDL_WINDOWEVENT) {
|
||||
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||
// Mark the window as dead; primary window close = quit
|
||||
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
||||
if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) {
|
||||
if (i == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
windows_[i]->alive = false;
|
||||
}
|
||||
break;
|
||||
if (target) {
|
||||
if (target_idx == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
target->alive = false;
|
||||
}
|
||||
}
|
||||
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
for (auto &ws: windows_) {
|
||||
if (SDL_GetWindowID(ws->window) == e.window.windowID) {
|
||||
ws->width = e.window.data1;
|
||||
ws->height = e.window.data2;
|
||||
break;
|
||||
}
|
||||
if (target) {
|
||||
target->width = e.window.data1;
|
||||
target->height = e.window.data2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Route input events to the correct window's input handler
|
||||
if (event_win_id != 0) {
|
||||
// Primary window (index 0) uses the external editor &ed
|
||||
if (windows_.size() > 0 &&
|
||||
SDL_GetWindowID(windows_[0]->window) == event_win_id) {
|
||||
windows_[0]->input.ProcessSDLEvent(e);
|
||||
} else {
|
||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
||||
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
||||
windows_[i]->input.ProcessSDLEvent(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target) {
|
||||
target->input.ProcessSDLEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!running)
|
||||
return;
|
||||
|
||||
// --- Apply pending font change ---
|
||||
// --- Apply pending font change (to all contexts) ---
|
||||
{
|
||||
std::string fname;
|
||||
float fsize = 0.0f;
|
||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||
if (!fname.empty() && fsize > 0.0f) {
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
for (auto &ws : windows_) {
|
||||
if (!ws->alive || !ws->imgui_ctx)
|
||||
continue;
|
||||
ImGui::SetCurrentContext(ws->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(ws->window, ws->gl_ctx);
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +500,12 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
|
||||
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||
|
||||
// Shared buffer list may have been modified by another window.
|
||||
wed.ValidateBufferIndex();
|
||||
|
||||
// Activate this window's GL and ImGui contexts
|
||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
@@ -422,6 +523,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
// Allow deferred opens
|
||||
wed.ProcessPendingOpens();
|
||||
|
||||
// Ensure newly opened buffers get syntax + edit mode detection
|
||||
apply_syntax_to_buffer(wed.CurrentBuffer(), config_);
|
||||
|
||||
// Drain input queue
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
@@ -431,6 +535,21 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
if (mi.id == CommandId::NewWindow) {
|
||||
// Open a new window; handled after this loop
|
||||
wed.SetNewWindowRequested(true);
|
||||
} else if (mi.id == CommandId::FontZoomIn ||
|
||||
mi.id == CommandId::FontZoomOut ||
|
||||
mi.id == CommandId::FontZoomReset) {
|
||||
auto &fr = kte::Fonts::FontRegistry::Instance();
|
||||
float cur = fr.CurrentFontSize();
|
||||
if (cur <= 0.0f) cur = config_.font_size;
|
||||
float next = cur;
|
||||
if (mi.id == CommandId::FontZoomIn)
|
||||
next = std::min(cur + 2.0f, 72.0f);
|
||||
else if (mi.id == CommandId::FontZoomOut)
|
||||
next = std::max(cur - 2.0f, 8.0f);
|
||||
else
|
||||
next = config_.font_size; // reset to config default
|
||||
if (next != cur)
|
||||
fr.RequestLoadFont(fr.CurrentFontName(), next);
|
||||
} else {
|
||||
const std::string before = wed.KillRingHead();
|
||||
Execute(wed, mi.id, mi.arg, mi.count);
|
||||
@@ -442,16 +561,29 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new-window request
|
||||
if (wed.NewWindowRequested()) {
|
||||
wed.SetNewWindowRequested(false);
|
||||
OpenNewWindow_(ed); // always share primary editor's buffers
|
||||
}
|
||||
|
||||
if (wi == 0 && wed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Switch font based on current buffer's edit mode
|
||||
{
|
||||
Buffer *cur = wed.CurrentBuffer();
|
||||
if (cur) {
|
||||
auto &fr = kte::Fonts::FontRegistry::Instance();
|
||||
const std::string &expected =
|
||||
(cur->GetEditMode() == EditMode::Writing)
|
||||
? config_.writing_font
|
||||
: config_.code_font;
|
||||
if (fr.CurrentFontName() != expected && fr.HasFont(expected)) {
|
||||
float sz = fr.CurrentFontSize();
|
||||
if (sz <= 0.0f) sz = config_.font_size;
|
||||
fr.LoadFont(expected, sz);
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw
|
||||
ws.renderer.Draw(wed);
|
||||
|
||||
@@ -466,52 +598,40 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
SDL_GL_SwapWindow(ws.window);
|
||||
}
|
||||
|
||||
// Handle deferred new-window requests (must happen outside the render loop
|
||||
// to avoid corrupting an in-progress ImGui frame).
|
||||
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||
Editor &wed = (wi == 0) ? ed : windows_[wi]->editor;
|
||||
if (wed.NewWindowRequested()) {
|
||||
wed.SetNewWindowRequested(false);
|
||||
OpenNewWindow_(ed);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove dead secondary windows
|
||||
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
||||
if (!(*it)->alive) {
|
||||
SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx);
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
SDL_GL_DeleteContext((*it)->gl_ctx);
|
||||
SDL_DestroyWindow((*it)->window);
|
||||
DestroyWindowResources_(**it);
|
||||
it = windows_.erase(it);
|
||||
// Restore primary context
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore primary context
|
||||
if (!windows_.empty()) {
|
||||
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GUIFrontend::Shutdown()
|
||||
{
|
||||
// Destroy secondary windows first
|
||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
||||
SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx);
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
SDL_GL_DeleteContext(windows_[i]->gl_ctx);
|
||||
SDL_DestroyWindow(windows_[i]->window);
|
||||
}
|
||||
windows_.resize(std::min(windows_.size(), std::size_t(1)));
|
||||
|
||||
// Destroy primary window
|
||||
if (!windows_.empty()) {
|
||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||
}
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
if (!windows_.empty()) {
|
||||
if (windows_[0]->gl_ctx) {
|
||||
SDL_GL_DeleteContext(windows_[0]->gl_ctx);
|
||||
windows_[0]->gl_ctx = nullptr;
|
||||
}
|
||||
if (windows_[0]->window) {
|
||||
SDL_DestroyWindow(windows_[0]->window);
|
||||
windows_[0]->window = nullptr;
|
||||
}
|
||||
// Destroy all windows (secondary first, then primary)
|
||||
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
||||
DestroyWindowResources_(**it);
|
||||
}
|
||||
windows_.clear();
|
||||
SDL_Quit();
|
||||
@@ -549,4 +669,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
||||
|
||||
io.Fonts->Build();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
|
||||
struct SDL_Window;
|
||||
struct ImGuiContext;
|
||||
typedef void *SDL_GLContext;
|
||||
|
||||
class GUIFrontend final : public Frontend {
|
||||
@@ -28,10 +29,13 @@ public:
|
||||
void Shutdown() override;
|
||||
|
||||
private:
|
||||
// Per-window state
|
||||
// Per-window state — each window owns its own ImGui context so that
|
||||
// NewFrame/Render cycles are fully independent (ImGui requires exactly
|
||||
// one NewFrame per Render per context).
|
||||
struct WindowState {
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_GLContext gl_ctx = nullptr;
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_GLContext gl_ctx = nullptr;
|
||||
ImGuiContext *imgui_ctx = nullptr;
|
||||
ImGuiInputHandler input{};
|
||||
ImGuiRenderer renderer{};
|
||||
Editor editor{};
|
||||
@@ -44,6 +48,9 @@ private:
|
||||
// Returns false if window creation fails.
|
||||
bool OpenNewWindow_(Editor &primary);
|
||||
|
||||
// Initialize fonts and theme for a given ImGui context (must be current).
|
||||
void SetupImGuiStyle_();
|
||||
static void DestroyWindowResources_(WindowState &ws);
|
||||
static bool LoadGuiFont_(const char *path, float size_px);
|
||||
|
||||
GUIConfig config_{};
|
||||
|
||||
@@ -349,6 +349,26 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
}
|
||||
}
|
||||
|
||||
// Font zoom: Cmd+=/Cmd+-/Cmd+0 (macOS) or Ctrl+=/Ctrl+-/Ctrl+0
|
||||
if ((mods & (KMOD_CTRL | KMOD_GUI)) && !(mods & KMOD_SHIFT)) {
|
||||
bool is_zoom = true;
|
||||
CommandId zoom_cmd = CommandId::FontZoomIn;
|
||||
if (key == SDLK_EQUALS || key == SDLK_PLUS)
|
||||
zoom_cmd = CommandId::FontZoomIn;
|
||||
else if (key == SDLK_MINUS)
|
||||
zoom_cmd = CommandId::FontZoomOut;
|
||||
else if (key == SDLK_0)
|
||||
zoom_cmd = CommandId::FontZoomReset;
|
||||
else
|
||||
is_zoom = false;
|
||||
if (is_zoom) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, zoom_cmd, std::string(), 0});
|
||||
suppress_text_input_once_ = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
||||
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
|
||||
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||
|
||||
234
ImGuiRenderer.cc
234
ImGuiRenderer.cc
@@ -76,19 +76,16 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
// 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<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) {
|
||||
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));
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
if (prev_buf_coloffs_ >= 0 && buf_coloffs != prev_buf_coloffs_) {
|
||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
||||
@@ -116,25 +113,22 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
// 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<long>(scroll_y / row_h);
|
||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||
|
||||
// Check if rowoffs was programmatically changed this frame
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
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 (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());
|
||||
}
|
||||
}
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
||||
if (prev_scroll_x_ >= 0.0f && scroll_x != prev_scroll_x_ && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||
@@ -142,11 +136,11 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
// Update trackers for next frame
|
||||
prev_scroll_y = scroll_y;
|
||||
prev_scroll_x = scroll_x;
|
||||
prev_scroll_y_ = scroll_y;
|
||||
prev_scroll_x_ = scroll_x;
|
||||
}
|
||||
prev_buf_rowoffs = buf_rowoffs;
|
||||
prev_buf_coloffs = buf_coloffs;
|
||||
prev_buf_rowoffs_ = buf_rowoffs;
|
||||
prev_buf_coloffs_ = buf_coloffs;
|
||||
// Cache current horizontal offset in rendered columns for click handling
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
|
||||
@@ -169,7 +163,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||
|
||||
static bool mouse_selecting = false;
|
||||
// (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
|
||||
@@ -181,29 +175,54 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
if (by >= lines.size())
|
||||
by = lines.empty() ? 0 : (lines.size() - 1);
|
||||
|
||||
// Convert mouse pos to rendered x
|
||||
if (lines.empty())
|
||||
return {0, 0};
|
||||
|
||||
// Expand tabs for the clicked line
|
||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||
const std::size_t tabw = 8;
|
||||
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;
|
||||
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
||||
// Add scroll offset in pixels
|
||||
visual_x += scroll_x;
|
||||
|
||||
// Convert rendered column to source column
|
||||
if (lines.empty())
|
||||
return {0, 0};
|
||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||
const std::size_t tabw = 8;
|
||||
std::size_t rx = 0;
|
||||
std::size_t best_col = 0;
|
||||
float best_dist = std::numeric_limits<float>::infinity();
|
||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
||||
// 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 = i;
|
||||
}
|
||||
if (i < line_clicked.size()) {
|
||||
rx += (line_clicked[i] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
best_col = ci;
|
||||
}
|
||||
}
|
||||
return {by, best_col};
|
||||
@@ -211,7 +230,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
|
||||
// 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;
|
||||
mouse_selecting_ = true;
|
||||
auto [by, bx] = mouse_pos_to_buf();
|
||||
char tmp[64];
|
||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||
@@ -225,7 +244,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
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)) {
|
||||
@@ -242,19 +261,45 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
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;
|
||||
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
mouse_selecting_ = false;
|
||||
}
|
||||
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<std::string>(lines[i]);
|
||||
|
||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||
// Expand tabs to spaces with width=8
|
||||
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
|
||||
std::size_t rx_abs_draw = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: convert a rendered column position to pixel x offset
|
||||
// relative to the visible line start, using actual text measurement
|
||||
// so proportional fonts render correctly.
|
||||
auto rx_to_px = [&](std::size_t rx_col) -> float {
|
||||
if (rx_col <= coloffs_now)
|
||||
return 0.0f;
|
||||
std::size_t start = coloffs_now;
|
||||
std::size_t end = std::min(expanded.size(), rx_col);
|
||||
if (start >= expanded.size() || end <= start)
|
||||
return 0.0f;
|
||||
return ImGui::CalcTextSize(expanded.c_str() + start,
|
||||
expanded.c_str() + end).x;
|
||||
};
|
||||
|
||||
// Compute search highlight ranges for this line in source indices
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||
@@ -309,10 +354,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
// 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<float>(vx0) * space_w, line_pos.y);
|
||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
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;
|
||||
@@ -350,13 +393,9 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
std::size_t rx_start = src_to_rx(sx);
|
||||
std::size_t rx_end = src_to_rx(ex);
|
||||
if (rx_end > coloffs_now) {
|
||||
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<float>(vx0) * space_w,
|
||||
line_pos.y);
|
||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
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);
|
||||
@@ -375,31 +414,14 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
rx_end = rx_start + 1;
|
||||
}
|
||||
if (rx_end > coloffs_now) {
|
||||
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<float>(vx0) * space_w,
|
||||
line_pos.y);
|
||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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(
|
||||
@@ -451,10 +473,9 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
std::size_t draw_end = std::min<std::size_t>(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;
|
||||
// Screen position via actual text measurement
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(screen_x) * space_w,
|
||||
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);
|
||||
@@ -478,28 +499,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
|
||||
// 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;
|
||||
}
|
||||
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
|
||||
@@ -545,29 +546,40 @@ ImGuiRenderer::Draw(Editor &ed)
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
|
||||
// Horizontal scroll: ensure cursor column is visible
|
||||
long vis_cols = static_cast<long>(std::round(child_w_actual / space_w));
|
||||
if (vis_cols < 1)
|
||||
vis_cols = 1;
|
||||
long first_col = static_cast<long>(scroll_x_now / space_w);
|
||||
long last_col = first_col + vis_cols - 1;
|
||||
|
||||
std::size_t cursor_rx = 0;
|
||||
// Horizontal scroll: ensure cursor is visible (pixel-based for proportional fonts)
|
||||
float cursor_px_abs = 0.0f;
|
||||
if (cy < lines.size()) {
|
||||
std::string cur_line = static_cast<std::string>(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);
|
||||
// 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 {
|
||||
cursor_rx += 1;
|
||||
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;
|
||||
}
|
||||
long cxr = static_cast<long>(cursor_rx);
|
||||
if (cxr < first_col || cxr > last_col) {
|
||||
float target_x = static_cast<float>(cxr) * space_w;
|
||||
target_x -= (child_w_actual / 2.0f);
|
||||
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();
|
||||
|
||||
@@ -11,4 +11,13 @@ public:
|
||||
~ImGuiRenderer() override = default;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
|
||||
private:
|
||||
// Per-window scroll tracking for two-way sync between Buffer offsets and ImGui scroll.
|
||||
// These must be per-instance (not static) so each window maintains independent state.
|
||||
long prev_buf_rowoffs_ = -1;
|
||||
long prev_buf_coloffs_ = -1;
|
||||
float prev_scroll_y_ = -1.0f;
|
||||
float prev_scroll_x_ = -1.0f;
|
||||
bool mouse_selecting_ = false;
|
||||
};
|
||||
|
||||
@@ -84,6 +84,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case 'l':
|
||||
out = CommandId::ReloadBuffer;
|
||||
return true;
|
||||
case 'm':
|
||||
out = CommandId::ToggleEditMode;
|
||||
return true;
|
||||
case 'n':
|
||||
out = CommandId::BufferPrev;
|
||||
return true;
|
||||
|
||||
@@ -23,29 +23,34 @@ Current themes (alphabetically):
|
||||
- **gruvbox** — Retro groove color scheme (light/dark variants)
|
||||
- **kanagawa-paper** — Inspired by traditional Japanese art
|
||||
- **lcars** — Star Trek LCARS interface style
|
||||
- **leuchtturm** — Modern, clean theme (light/dark variants)
|
||||
- **nord** — Arctic, north-bluish color palette
|
||||
- **old-book** — Sepia-toned vintage book aesthetic (light/dark
|
||||
variants)
|
||||
- **orbital** — Space-themed dark palette
|
||||
- **plan9** — Minimalist Plan 9 from Bell Labs inspired
|
||||
- **solarized** — Ethan Schoonover's Solarized (light/dark variants)
|
||||
- **tufte** — Edward Tufte-inspired minimalist theme (light/dark variants)
|
||||
- **weyland-yutani** — Alien franchise corporate aesthetic
|
||||
- **zenburn** — Low-contrast, easy-on-the-eyes theme
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Themes are configured via `$HOME/.config/kte/kge.ini`:
|
||||
Themes are configured via `$HOME/.config/kte/kge.toml`:
|
||||
|
||||
```ini
|
||||
theme = nord
|
||||
background = dark
|
||||
```toml
|
||||
[appearance]
|
||||
theme = "nord"
|
||||
background = "dark"
|
||||
```
|
||||
|
||||
- `theme` — The theme name (e.g., "nord", "gruvbox", "solarized")
|
||||
- `background` — Either "dark" or "light" (for themes supporting both
|
||||
variants)
|
||||
|
||||
Legacy `kge.ini` format is also supported (see CONFIG.md).
|
||||
|
||||
Themes can also be switched at runtime using the `:theme <name>`
|
||||
command.
|
||||
|
||||
|
||||
17748
ext/tomlplusplus/toml.hpp
Normal file
17748
ext/tomlplusplus/toml.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1768
fonts/CrimsonPro.h
Normal file
1768
fonts/CrimsonPro.h
Normal file
File diff suppressed because it is too large
Load Diff
1203
fonts/ETBook.h
Normal file
1203
fonts/ETBook.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
||||
#include "BerkeleyMono.h"
|
||||
#include "BrassMono.h"
|
||||
#include "BrassMonoCode.h"
|
||||
#include "CrimsonPro.h"
|
||||
#include "ETBook.h"
|
||||
#include "FiraCode.h"
|
||||
#include "Go.h"
|
||||
#include "IBMPlexMono.h"
|
||||
@@ -13,6 +15,7 @@
|
||||
#include "IosevkaExtended.h"
|
||||
#include "ShareTech.h"
|
||||
#include "SpaceMono.h"
|
||||
#include "Spectral.h"
|
||||
#include "Syne.h"
|
||||
#include "Triplicate.h"
|
||||
#include "Unispace.h"
|
||||
|
||||
@@ -45,6 +45,16 @@ InstallDefaultFonts()
|
||||
BrassMonoCode::DefaultFontBoldCompressedData,
|
||||
BrassMonoCode::DefaultFontBoldCompressedSize
|
||||
));
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"crimsonpro",
|
||||
CrimsonPro::DefaultFontRegularCompressedData,
|
||||
CrimsonPro::DefaultFontRegularCompressedSize
|
||||
));
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"etbook",
|
||||
ETBook::DefaultFontRegularCompressedData,
|
||||
ETBook::DefaultFontRegularCompressedSize
|
||||
));
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"fira",
|
||||
FiraCode::DefaultFontRegularCompressedData,
|
||||
@@ -95,6 +105,11 @@ InstallDefaultFonts()
|
||||
SpaceMono::DefaultFontRegularCompressedData,
|
||||
SpaceMono::DefaultFontRegularCompressedSize
|
||||
));
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"spectral",
|
||||
Spectral::DefaultFontRegularCompressedData,
|
||||
Spectral::DefaultFontRegularCompressedSize
|
||||
));
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"syne",
|
||||
Syne::DefaultFontRegularCompressedData,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Font.h"
|
||||
|
||||
@@ -87,6 +89,19 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Return all registered font names (sorted)
|
||||
std::vector<std::string> FontNames() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<std::string> names;
|
||||
names.reserve(fonts_.size());
|
||||
for (const auto &[name, _] : fonts_)
|
||||
names.push_back(name);
|
||||
std::sort(names.begin(), names.end());
|
||||
return names;
|
||||
}
|
||||
|
||||
|
||||
// Current font name/size as last successfully loaded via LoadFont()
|
||||
std::string CurrentFontName() const
|
||||
{
|
||||
|
||||
3227
fonts/Spectral.h
Normal file
3227
fonts/Spectral.h
Normal file
File diff suppressed because it is too large
Load Diff
24
kge.toml.example
Normal file
24
kge.toml.example
Normal file
@@ -0,0 +1,24 @@
|
||||
# kge configuration
|
||||
# Place at ~/.config/kte/kge.toml
|
||||
|
||||
[window]
|
||||
fullscreen = false
|
||||
columns = 80
|
||||
rows = 42
|
||||
|
||||
[font]
|
||||
# Default font and size
|
||||
name = "default"
|
||||
size = 18.0
|
||||
# Font used in code mode (monospace)
|
||||
code = "default"
|
||||
# Font used in writing mode (proportional) — for .txt, .md, .rst, .org, .tex, etc.
|
||||
writing = "crimsonpro"
|
||||
|
||||
[appearance]
|
||||
theme = "nord"
|
||||
# "dark" or "light" for themes with variants
|
||||
background = "dark"
|
||||
|
||||
[editor]
|
||||
syntax = true
|
||||
@@ -15,20 +15,18 @@ sha256sum kge.app.zip
|
||||
open .
|
||||
cd ..
|
||||
|
||||
mkdir -p cmake-build-release-qt
|
||||
cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||
|
||||
cd cmake-build-release-qt
|
||||
make clean
|
||||
rm -fr kge.app* kge-qt.app*
|
||||
make
|
||||
mv -f kge.app kge-qt.app
|
||||
# Use the same Qt's macdeployqt as used for building; ensure it overwrites in-bundle paths
|
||||
macdeployqt kge-qt.app -always-overwrite -verbose=3
|
||||
|
||||
# Run CMake BundleUtilities fixup to internalize non-Qt dylibs and rewrite install names
|
||||
cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
||||
zip -r kge-qt.app.zip kge-qt.app
|
||||
sha256sum kge-qt.app.zip
|
||||
open .
|
||||
cd ..
|
||||
# Qt build disabled — ImGui frontend is the primary GUI.
|
||||
# mkdir -p cmake-build-release-qt
|
||||
# cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||
#
|
||||
# cd cmake-build-release-qt
|
||||
# make clean
|
||||
# rm -fr kge.app* kge-qt.app*
|
||||
# make
|
||||
# mv -f kge.app kge-qt.app
|
||||
# macdeployqt kge-qt.app -always-overwrite -verbose=3
|
||||
# cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
||||
# zip -r kge-qt.app.zip kge-qt.app
|
||||
# sha256sum kge-qt.app.zip
|
||||
# open .
|
||||
# cd ..
|
||||
|
||||
@@ -22,5 +22,6 @@ fi
|
||||
|
||||
git tag "${KTE_VERSION}"
|
||||
git push && git push --tags
|
||||
git push github && git push github --tags
|
||||
|
||||
( ./make-app-release )
|
||||
125
tests/test_swap_cleanup2.cc
Normal file
125
tests/test_swap_cleanup2.cc
Normal file
@@ -0,0 +1,125 @@
|
||||
#include "Test.h"
|
||||
|
||||
#include "Command.h"
|
||||
#include "Editor.h"
|
||||
|
||||
#include "tests/TestHarness.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
static void
|
||||
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out.write(bytes.data(), (std::streamsize) bytes.size());
|
||||
}
|
||||
|
||||
|
||||
// RAII helper to set XDG_STATE_HOME for the duration of a test and clean up.
|
||||
struct XdgStateGuard {
|
||||
fs::path root;
|
||||
std::string old_xdg;
|
||||
bool had_old;
|
||||
|
||||
explicit XdgStateGuard(const std::string &suffix)
|
||||
{
|
||||
root = fs::temp_directory_path() /
|
||||
(std::string("kte_ut_xdg_") + suffix + "_" + std::to_string((int) ::getpid()));
|
||||
fs::remove_all(root);
|
||||
fs::create_directories(root);
|
||||
|
||||
const char *p = std::getenv("XDG_STATE_HOME");
|
||||
had_old = (p != nullptr);
|
||||
if (p)
|
||||
old_xdg = p;
|
||||
setenv("XDG_STATE_HOME", root.string().c_str(), 1);
|
||||
}
|
||||
|
||||
~XdgStateGuard()
|
||||
{
|
||||
if (had_old)
|
||||
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
||||
else
|
||||
unsetenv("XDG_STATE_HOME");
|
||||
fs::remove_all(root);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TEST(SwapCleanup_SaveAndQuit)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgStateGuard xdg("save_quit");
|
||||
|
||||
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||
fs::create_directories(xdg.root / "work");
|
||||
write_file_bytes(path, "hello\n");
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
std::string err;
|
||||
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||
Buffer *b = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(b != nullptr);
|
||||
|
||||
// Edit to create swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Z"));
|
||||
ASSERT_TRUE(b->Dirty());
|
||||
|
||||
ed.Swap()->Flush(b);
|
||||
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||
ASSERT_TRUE(fs::exists(swp));
|
||||
|
||||
// Save-and-quit should clean up the swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::SaveAndQuit));
|
||||
ed.Swap()->Flush(b);
|
||||
ASSERT_TRUE(!fs::exists(swp));
|
||||
|
||||
// Cleanup
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
|
||||
|
||||
TEST(SwapCleanup_EditorReset)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgStateGuard xdg("editor_reset");
|
||||
|
||||
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||
fs::create_directories(xdg.root / "work");
|
||||
write_file_bytes(path, "hello\n");
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
std::string err;
|
||||
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||
Buffer *b = ed.CurrentBuffer();
|
||||
ASSERT_TRUE(b != nullptr);
|
||||
|
||||
// Edit to create swap file
|
||||
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "W"));
|
||||
ASSERT_TRUE(b->Dirty());
|
||||
|
||||
ed.Swap()->Flush(b);
|
||||
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||
ASSERT_TRUE(fs::exists(swp));
|
||||
|
||||
// Reset (simulates clean editor exit) should remove swap files
|
||||
ed.Reset();
|
||||
ASSERT_TRUE(!fs::exists(swp));
|
||||
|
||||
// Cleanup
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
204
themes/Leuchtturm.h
Normal file
204
themes/Leuchtturm.h
Normal file
@@ -0,0 +1,204 @@
|
||||
// themes/Leuchtturm.h — Fountain pen on cream paper, brass and leather (header-only)
|
||||
// Inspired by Kaweco Brass/Bronze Sport pens on Leuchtturm1917 notebook paper.
|
||||
// Light: warm cream paper with blue-black fountain pen ink.
|
||||
// Dark: leather case and patinated metal.
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
static inline void
|
||||
ApplyLeuchtturmLightTheme()
|
||||
{
|
||||
// Notebook paper and fountain pen ink
|
||||
const ImVec4 paper = RGBA(0xF2ECDF); // Leuchtturm cream paper
|
||||
const ImVec4 bg1 = RGBA(0xE8E2D5); // slightly darker cream
|
||||
const ImVec4 bg2 = RGBA(0xDDD7CA); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0xD1CBBD); // hover/active
|
||||
const ImVec4 ink = RGBA(0x040720); // blue-black fountain pen ink
|
||||
const ImVec4 dim = RGBA(0x7A756A); // faded text (like printed headers)
|
||||
const ImVec4 border = RGBA(0xCCC6B4); // faint ruled lines
|
||||
|
||||
// Metal accents from the pens
|
||||
const ImVec4 brass = RGBA(0x6B5E2A); // dark patinated brass
|
||||
const ImVec4 brown = RGBA(0x5C3D28); // leather/bronze
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = border;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = ink;
|
||||
colors[ImGuiCol_SliderGrab] = ink;
|
||||
colors[ImGuiCol_SliderGrabActive] = brass;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = brass;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.18f);
|
||||
colors[ImGuiCol_DragDropTarget] = brass;
|
||||
colors[ImGuiCol_NavHighlight] = brass;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_PlotLines] = brown;
|
||||
colors[ImGuiCol_PlotLinesHovered] = brass;
|
||||
colors[ImGuiCol_PlotHistogram] = brown;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = brass;
|
||||
}
|
||||
|
||||
|
||||
// Dark variant — leather pen case with warm metal and cream accents
|
||||
static inline void
|
||||
ApplyLeuchtturmDarkTheme()
|
||||
{
|
||||
const ImVec4 bg0 = RGBA(0x1C1610); // dark leather
|
||||
const ImVec4 bg1 = RGBA(0x251E16); // slightly lighter
|
||||
const ImVec4 bg2 = RGBA(0x30281E); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0x3E3428); // hover/active
|
||||
const ImVec4 ink = RGBA(0xE5DDD0); // warm cream text
|
||||
const ImVec4 dim = RGBA(0x978E7C); // secondary text
|
||||
const ImVec4 border = RGBA(0x4A3E30); // subtle borders
|
||||
|
||||
const ImVec4 brass = RGBA(0xB8A060); // polished brass
|
||||
const ImVec4 brown = RGBA(0x8B6848); // bronze pen
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = border;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = dim;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = brass;
|
||||
colors[ImGuiCol_SliderGrab] = brass;
|
||||
colors[ImGuiCol_SliderGrabActive] = brown;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = brass;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.22f);
|
||||
colors[ImGuiCol_DragDropTarget] = brass;
|
||||
colors[ImGuiCol_NavHighlight] = brass;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_PlotLines] = brass;
|
||||
colors[ImGuiCol_PlotLinesHovered] = brown;
|
||||
colors[ImGuiCol_PlotHistogram] = brass;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = brown;
|
||||
}
|
||||
203
themes/Tufte.h
Normal file
203
themes/Tufte.h
Normal file
@@ -0,0 +1,203 @@
|
||||
// themes/Tufte.h — Edward Tufte inspired ImGui theme (header-only)
|
||||
// Warm cream paper, dark ink, minimal chrome, restrained accent colors.
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Light variant (primary — Tufte's books are fundamentally light)
|
||||
static inline void
|
||||
ApplyTufteLightTheme()
|
||||
{
|
||||
// Tufte palette: warm cream paper with near-black ink
|
||||
const ImVec4 paper = RGBA(0xFFFFF8); // Tufte's signature warm white
|
||||
const ImVec4 bg1 = RGBA(0xF4F0E8); // slightly darker cream
|
||||
const ImVec4 bg2 = RGBA(0xEAE6DE); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0xDDD9D1); // hover/active
|
||||
const ImVec4 ink = RGBA(0x111111); // near-black text
|
||||
const ImVec4 dim = RGBA(0x6B6B6B); // disabled/secondary text
|
||||
const ImVec4 border = RGBA(0xD0CCC4); // subtle borders
|
||||
|
||||
// Tufte uses color sparingly: muted red for emphasis, navy for links
|
||||
const ImVec4 red = RGBA(0xA00000); // restrained dark red
|
||||
const ImVec4 blue = RGBA(0x1F3F6F); // dark navy
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f; // sharp edges — typographic, not app-like
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f; // minimal frame borders
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = border;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = ink;
|
||||
colors[ImGuiCol_SliderGrab] = ink;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = red;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = red;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.15f);
|
||||
colors[ImGuiCol_DragDropTarget] = red;
|
||||
colors[ImGuiCol_NavHighlight] = red;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||
colors[ImGuiCol_PlotLines] = blue;
|
||||
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||
colors[ImGuiCol_PlotHistogram] = blue;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||
}
|
||||
|
||||
|
||||
// Dark variant — warm charcoal with cream ink, same restrained accents
|
||||
static inline void
|
||||
ApplyTufteDarkTheme()
|
||||
{
|
||||
const ImVec4 bg0 = RGBA(0x1C1B19); // warm near-black
|
||||
const ImVec4 bg1 = RGBA(0x252420); // slightly lighter
|
||||
const ImVec4 bg2 = RGBA(0x302F2A); // UI elements
|
||||
const ImVec4 bg3 = RGBA(0x3D3C36); // hover/active
|
||||
const ImVec4 ink = RGBA(0xEAE6DE); // cream text (inverted paper)
|
||||
const ImVec4 dim = RGBA(0x9A9690); // disabled text
|
||||
const ImVec4 border = RGBA(0x4A4840); // subtle borders
|
||||
|
||||
const ImVec4 red = RGBA(0xD06060); // warmer red for dark bg
|
||||
const ImVec4 blue = RGBA(0x7098C0); // lighter navy for dark bg
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 12.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 0.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = dim;
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = border;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = dim;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = ink;
|
||||
colors[ImGuiCol_SliderGrab] = ink;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg2;
|
||||
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg2;
|
||||
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||
colors[ImGuiCol_HeaderActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||
colors[ImGuiCol_SeparatorActive] = red;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = red;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = border;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.20f);
|
||||
colors[ImGuiCol_DragDropTarget] = red;
|
||||
colors[ImGuiCol_NavHighlight] = red;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||
colors[ImGuiCol_PlotLines] = blue;
|
||||
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||
colors[ImGuiCol_PlotHistogram] = blue;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||
}
|
||||
Reference in New Issue
Block a user