Step 175: typography and spacing
This commit is contained in:
@@ -522,3 +522,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
|||||||
| 2026-02-10 | Codex | Step 172: Bundled theme pack renamed to Whetstone Dark/Light and Monokai Pro with tests. 2/2 tests pass (step172_test, step172_integration_test). file_limits_test 4/4 passes. |
|
| 2026-02-10 | Codex | Step 172: Bundled theme pack renamed to Whetstone Dark/Light and Monokai Pro with tests. 2/2 tests pass (step172_test, step172_integration_test). file_limits_test 4/4 passes. |
|
||||||
| 2026-02-10 | Codex | Step 173: Theme gallery UI with hover preview, apply/reset, and swatches. 2/2 tests pass (step173_test, step173_integration_test). file_limits_test 4/4 passes. |
|
| 2026-02-10 | Codex | Step 173: Theme gallery UI with hover preview, apply/reset, and swatches. 2/2 tests pass (step173_test, step173_integration_test). file_limits_test 4/4 passes. |
|
||||||
| 2026-02-10 | Codex | Step 174: Added IconSet system with theme-aware, zoom-scaled icons + tests. 2/2 tests pass (step174_test, step174_integration_test). file_limits_test 4/4 passes. |
|
| 2026-02-10 | Codex | Step 174: Added IconSet system with theme-aware, zoom-scaled icons + tests. 2/2 tests pass (step174_test, step174_integration_test). file_limits_test 4/4 passes. |
|
||||||
|
| 2026-02-10 | Codex | Step 175: Typography controls (fonts, line height, letter spacing) + theme layout spacing. 2/2 tests pass (step175_test, step175_integration_test). file_limits_test 4/4 passes. |
|
||||||
|
|||||||
@@ -1009,6 +1009,13 @@ add_executable(step174_integration_test tests/step174_integration_test.cpp)
|
|||||||
target_include_directories(step174_integration_test PRIVATE src)
|
target_include_directories(step174_integration_test PRIVATE src)
|
||||||
target_link_libraries(step174_integration_test PRIVATE imgui::imgui)
|
target_link_libraries(step174_integration_test PRIVATE imgui::imgui)
|
||||||
|
|
||||||
|
add_executable(step175_test tests/step175_test.cpp)
|
||||||
|
target_include_directories(step175_test PRIVATE src)
|
||||||
|
|
||||||
|
add_executable(step175_integration_test tests/step175_integration_test.cpp)
|
||||||
|
target_include_directories(step175_integration_test PRIVATE src)
|
||||||
|
target_link_libraries(step175_integration_test PRIVATE nlohmann_json::nlohmann_json)
|
||||||
|
|
||||||
find_package(SDL2 CONFIG REQUIRED)
|
find_package(SDL2 CONFIG REQUIRED)
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
find_package(glad CONFIG REQUIRED)
|
find_package(glad CONFIG REQUIRED)
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Measure
|
// Measure
|
||||||
const float lineHeight = ImGui::GetTextLineHeightWithSpacing();
|
const float lineHeight =
|
||||||
const float charAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x;
|
ImGui::GetTextLineHeightWithSpacing() * std::max(0.5f, options.lineHeightScale);
|
||||||
|
const float baseAdvance = font->CalcTextSizeA(font->FontSize, FLT_MAX, -1.0f, "M").x;
|
||||||
|
const float charAdvance = baseAdvance + std::max(0.0f, options.letterSpacing);
|
||||||
const float gutterWidth = options.showLineNumbers ?
|
const float gutterWidth = options.showLineNumbers ?
|
||||||
calcGutterWidth(lineCount, font, charAdvance) : 12.0f;
|
calcGutterWidth(lineCount, font, charAdvance) : 12.0f;
|
||||||
const float minimapWidth = options.showMinimap ? 80.0f : 0.0f;
|
const float minimapWidth = options.showMinimap ? 80.0f : 0.0f;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ struct CodeEditorOptions {
|
|||||||
bool showAnnotations = false;
|
bool showAnnotations = false;
|
||||||
bool showLineNumbers = true;
|
bool showLineNumbers = true;
|
||||||
bool showCurrentLine = true;
|
bool showCurrentLine = true;
|
||||||
|
float lineHeightScale = 1.0f;
|
||||||
|
float letterSpacing = 0.0f;
|
||||||
int annotationLayout = 0; // 0=above, 1=margin, 2=beside
|
int annotationLayout = 0; // 0=above, 1=margin, 2=beside
|
||||||
const std::vector<int>* errorLines = nullptr;
|
const std::vector<int>* errorLines = nullptr;
|
||||||
const std::vector<int>* warningLines = nullptr;
|
const std::vector<int>* warningLines = nullptr;
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ struct EditorState {
|
|||||||
ImFont* monoFont = nullptr;
|
ImFont* monoFont = nullptr;
|
||||||
ImFont* uiFont = nullptr;
|
ImFont* uiFont = nullptr;
|
||||||
float baseFontSize = 15.0f;
|
float baseFontSize = 15.0f;
|
||||||
|
bool fontsDirty = false;
|
||||||
std::string lastDialogPath;
|
std::string lastDialogPath;
|
||||||
bool exitRequested = false;
|
bool exitRequested = false;
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ static bool renderOrgDocument(OrgDocumentState& state,
|
|||||||
bool showWhitespace,
|
bool showWhitespace,
|
||||||
ImFont* monoFont,
|
ImFont* monoFont,
|
||||||
ImFont* uiFont,
|
ImFont* uiFont,
|
||||||
|
float lineHeightScale,
|
||||||
|
float letterSpacing,
|
||||||
const std::function<void(const std::string&)>& onOrgTextChanged,
|
const std::function<void(const std::string&)>& onOrgTextChanged,
|
||||||
const std::function<void(int, const std::string&, const std::string&)>& onBlockChanged,
|
const std::function<void(int, const std::string&, const std::string&)>& onBlockChanged,
|
||||||
const std::function<std::string(const std::string&, const std::string&)>& onRunBlock) {
|
const std::function<std::string(const std::string&, const std::string&)>& onRunBlock) {
|
||||||
@@ -200,6 +202,8 @@ static bool renderOrgDocument(OrgDocumentState& state,
|
|||||||
opts.readOnly = false;
|
opts.readOnly = false;
|
||||||
opts.showWhitespace = showWhitespace;
|
opts.showWhitespace = showWhitespace;
|
||||||
opts.showLineNumbers = true;
|
opts.showLineNumbers = true;
|
||||||
|
opts.lineHeightScale = lineHeightScale;
|
||||||
|
opts.letterSpacing = letterSpacing;
|
||||||
EditorMode& mode = state.modes[srcIndex];
|
EditorMode& mode = state.modes[srcIndex];
|
||||||
mode.setLanguage(block.language);
|
mode.setLanguage(block.language);
|
||||||
opts.mode = &mode;
|
opts.mode = &mode;
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ public:
|
|||||||
std::string& getEmacsConfigPathMutable() { return emacsConfigPath_; }
|
std::string& getEmacsConfigPathMutable() { return emacsConfigPath_; }
|
||||||
int getFontSize() const { return fontSize_; }
|
int getFontSize() const { return fontSize_; }
|
||||||
void setFontSize(int size) { fontSize_ = size; }
|
void setFontSize(int size) { fontSize_ = size; }
|
||||||
|
const std::string& getCodeFontPath() const { return codeFontPath_; }
|
||||||
|
void setCodeFontPath(const std::string& path) { codeFontPath_ = path; }
|
||||||
|
const std::string& getUiFontPath() const { return uiFontPath_; }
|
||||||
|
void setUiFontPath(const std::string& path) { uiFontPath_ = path; }
|
||||||
|
float getLineHeightScale() const { return lineHeightScale_; }
|
||||||
|
void setLineHeightScale(float value) { lineHeightScale_ = value; }
|
||||||
|
float getLetterSpacing() const { return letterSpacing_; }
|
||||||
|
void setLetterSpacing(float value) { letterSpacing_ = value; }
|
||||||
int getTabSize() const { return tabSize_; }
|
int getTabSize() const { return tabSize_; }
|
||||||
void setTabSize(int size) { tabSize_ = size; }
|
void setTabSize(int size) { tabSize_ = size; }
|
||||||
const std::string& getTheme() const { return theme_; }
|
const std::string& getTheme() const { return theme_; }
|
||||||
@@ -58,6 +66,10 @@ public:
|
|||||||
nlohmann::json j;
|
nlohmann::json j;
|
||||||
in >> j;
|
in >> j;
|
||||||
fontSize_ = j.value("fontSize", fontSize_);
|
fontSize_ = j.value("fontSize", fontSize_);
|
||||||
|
codeFontPath_ = j.value("codeFont", codeFontPath_);
|
||||||
|
uiFontPath_ = j.value("uiFont", uiFontPath_);
|
||||||
|
lineHeightScale_ = j.value("lineHeight", lineHeightScale_);
|
||||||
|
letterSpacing_ = j.value("letterSpacing", letterSpacing_);
|
||||||
tabSize_ = j.value("tabSize", tabSize_);
|
tabSize_ = j.value("tabSize", tabSize_);
|
||||||
theme_ = j.value("theme", theme_);
|
theme_ = j.value("theme", theme_);
|
||||||
telemetryOptIn_ = j.value("telemetryOptIn", telemetryOptIn_);
|
telemetryOptIn_ = j.value("telemetryOptIn", telemetryOptIn_);
|
||||||
@@ -90,6 +102,10 @@ public:
|
|||||||
try {
|
try {
|
||||||
nlohmann::json j;
|
nlohmann::json j;
|
||||||
j["fontSize"] = fontSize_;
|
j["fontSize"] = fontSize_;
|
||||||
|
j["codeFont"] = codeFontPath_;
|
||||||
|
j["uiFont"] = uiFontPath_;
|
||||||
|
j["lineHeight"] = lineHeightScale_;
|
||||||
|
j["letterSpacing"] = letterSpacing_;
|
||||||
j["tabSize"] = tabSize_;
|
j["tabSize"] = tabSize_;
|
||||||
j["theme"] = theme_;
|
j["theme"] = theme_;
|
||||||
j["telemetryOptIn"] = telemetryOptIn_;
|
j["telemetryOptIn"] = telemetryOptIn_;
|
||||||
@@ -159,6 +175,10 @@ private:
|
|||||||
std::vector<LSPServerConfig> lspServers_;
|
std::vector<LSPServerConfig> lspServers_;
|
||||||
std::string emacsConfigPath_;
|
std::string emacsConfigPath_;
|
||||||
int fontSize_ = 15;
|
int fontSize_ = 15;
|
||||||
|
std::string codeFontPath_;
|
||||||
|
std::string uiFontPath_;
|
||||||
|
float lineHeightScale_ = 1.0f;
|
||||||
|
float letterSpacing_ = 0.0f;
|
||||||
int tabSize_ = 4;
|
int tabSize_ = 4;
|
||||||
std::string theme_ = "Whetstone Dark";
|
std::string theme_ = "Whetstone Dark";
|
||||||
bool telemetryOptIn_ = false;
|
bool telemetryOptIn_ = false;
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ struct ThemeDefinition {
|
|||||||
std::unordered_map<std::string, ImU32> editorColors;
|
std::unordered_map<std::string, ImU32> editorColors;
|
||||||
std::unordered_map<int, ImVec4> imguiColors;
|
std::unordered_map<int, ImVec4> imguiColors;
|
||||||
std::string sourcePath;
|
std::string sourcePath;
|
||||||
|
ImVec2 panelPadding = ImVec2(8.0f, 6.0f);
|
||||||
|
ImVec2 panelSpacing = ImVec2(8.0f, 6.0f);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ThemeEngine {
|
class ThemeEngine {
|
||||||
@@ -312,6 +314,19 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (j.contains("layout") && j["layout"].is_object()) {
|
||||||
|
const auto& layout = j["layout"];
|
||||||
|
if (layout.contains("panelPadding") && layout["panelPadding"].is_array() &&
|
||||||
|
layout["panelPadding"].size() >= 2) {
|
||||||
|
out.panelPadding.x = layout["panelPadding"][0].get<float>();
|
||||||
|
out.panelPadding.y = layout["panelPadding"][1].get<float>();
|
||||||
|
}
|
||||||
|
if (layout.contains("panelSpacing") && layout["panelSpacing"].is_array() &&
|
||||||
|
layout["panelSpacing"].size() >= 2) {
|
||||||
|
out.panelSpacing.x = layout["panelSpacing"][0].get<float>();
|
||||||
|
out.panelSpacing.y = layout["panelSpacing"][1].get<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +337,8 @@ private:
|
|||||||
style.Colors[colId] = color;
|
style.Colors[colId] = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
style.WindowPadding = theme.panelPadding;
|
||||||
|
style.ItemSpacing = theme.panelSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isSyntaxColor(ThemeColor color) {
|
static bool isSyntaxColor(ThemeColor color) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <SDL2/SDL_opengl.h>
|
#include <SDL2/SDL_opengl.h>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "EditorState.h"
|
#include "EditorState.h"
|
||||||
#include "EditorUtils.h"
|
#include "EditorUtils.h"
|
||||||
@@ -120,6 +122,60 @@ int main(int, char**) {
|
|||||||
state.uiFont = uiFont;
|
state.uiFont = uiFont;
|
||||||
state.baseFontSize = baseFontSize;
|
state.baseFontSize = baseFontSize;
|
||||||
state.init();
|
state.init();
|
||||||
|
|
||||||
|
auto loadFontFromPath = [&](const std::string& preferred,
|
||||||
|
const std::vector<std::string>& fallbacks,
|
||||||
|
float size) -> ImFont* {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
if (!preferred.empty() && fs::exists(preferred)) {
|
||||||
|
if (auto* f = io.Fonts->AddFontFromFileTTF(preferred.c_str(), size)) return f;
|
||||||
|
}
|
||||||
|
for (const auto& path : fallbacks) {
|
||||||
|
if (fs::exists(path)) {
|
||||||
|
if (auto* f = io.Fonts->AddFontFromFileTTF(path.c_str(), size)) return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return io.Fonts->AddFontDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto reloadFonts = [&]() {
|
||||||
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
|
io.Fonts->Clear();
|
||||||
|
std::vector<std::string> codeFallbacks;
|
||||||
|
std::vector<std::string> uiFallbacks;
|
||||||
|
#ifdef _WIN32
|
||||||
|
codeFallbacks = {
|
||||||
|
"C:\\Windows\\Fonts\\consola.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\CascadiaCode.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\CascadiaMono.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\cour.ttf"
|
||||||
|
};
|
||||||
|
uiFallbacks = {
|
||||||
|
"C:\\Windows\\Fonts\\segoeui.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\calibri.ttf",
|
||||||
|
"C:\\Windows\\Fonts\\arial.ttf"
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
codeFallbacks = {
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf"
|
||||||
|
};
|
||||||
|
uiFallbacks = {
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
state.monoFont = loadFontFromPath(state.settings.getCodeFontPath(),
|
||||||
|
codeFallbacks,
|
||||||
|
state.baseFontSize);
|
||||||
|
state.uiFont = loadFontFromPath(state.settings.getUiFontPath(),
|
||||||
|
uiFallbacks,
|
||||||
|
state.baseFontSize);
|
||||||
|
io.FontGlobalScale = state.settings.getFontSize() / state.baseFontSize;
|
||||||
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadFonts();
|
||||||
ThemeEngine& themes = ThemeEngine::instance();
|
ThemeEngine& themes = ThemeEngine::instance();
|
||||||
themes.loadThemesFromDirectory("themes");
|
themes.loadThemesFromDirectory("themes");
|
||||||
themes.loadThemesFromDirectory("editor/themes");
|
themes.loadThemesFromDirectory("editor/themes");
|
||||||
@@ -256,6 +312,10 @@ int main(int, char**) {
|
|||||||
state.refreshEmacsModeLine(ImGui::GetTime());
|
state.refreshEmacsModeLine(ImGui::GetTime());
|
||||||
state.events.tick(ImGui::GetTime());
|
state.events.tick(ImGui::GetTime());
|
||||||
themes.refreshWatchedThemes();
|
themes.refreshWatchedThemes();
|
||||||
|
if (state.fontsDirty) {
|
||||||
|
reloadFonts();
|
||||||
|
state.fontsDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Start frame ---
|
// --- Start frame ---
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
|
|||||||
@@ -538,6 +538,8 @@ static void renderBottomPanel(EditorState& state) {
|
|||||||
leftOpts.showWhitespace = state.ui.showWhitespace;
|
leftOpts.showWhitespace = state.ui.showWhitespace;
|
||||||
leftOpts.showLineNumbers = state.ui.showLineNumbers;
|
leftOpts.showLineNumbers = state.ui.showLineNumbers;
|
||||||
leftOpts.showCurrentLine = false;
|
leftOpts.showCurrentLine = false;
|
||||||
|
leftOpts.lineHeightScale = state.settings.getLineHeightScale();
|
||||||
|
leftOpts.letterSpacing = state.settings.getLetterSpacing();
|
||||||
leftOpts.highlightLines = &state.diff.beforeLines;
|
leftOpts.highlightLines = &state.diff.beforeLines;
|
||||||
leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120);
|
leftOpts.highlightLineColor = IM_COL32(160, 80, 80, 120);
|
||||||
leftOpts.syncScrollX = &state.diffScrollX;
|
leftOpts.syncScrollX = &state.diffScrollX;
|
||||||
@@ -553,6 +555,8 @@ static void renderBottomPanel(EditorState& state) {
|
|||||||
rightOpts.showWhitespace = state.ui.showWhitespace;
|
rightOpts.showWhitespace = state.ui.showWhitespace;
|
||||||
rightOpts.showLineNumbers = state.ui.showLineNumbers;
|
rightOpts.showLineNumbers = state.ui.showLineNumbers;
|
||||||
rightOpts.showCurrentLine = false;
|
rightOpts.showCurrentLine = false;
|
||||||
|
rightOpts.lineHeightScale = state.settings.getLineHeightScale();
|
||||||
|
rightOpts.letterSpacing = state.settings.getLetterSpacing();
|
||||||
rightOpts.highlightLines = &state.diff.afterLines;
|
rightOpts.highlightLines = &state.diff.afterLines;
|
||||||
rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120);
|
rightOpts.highlightLineColor = IM_COL32(80, 160, 80, 120);
|
||||||
rightOpts.syncScrollX = &state.diffScrollX;
|
rightOpts.syncScrollX = &state.diffScrollX;
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ static void renderEditorPanel(EditorState& state) {
|
|||||||
state.ui.showWhitespace,
|
state.ui.showWhitespace,
|
||||||
state.monoFont,
|
state.monoFont,
|
||||||
state.uiFont,
|
state.uiFont,
|
||||||
|
state.settings.getLineHeightScale(),
|
||||||
|
state.settings.getLetterSpacing(),
|
||||||
[&](const std::string& newText) {
|
[&](const std::string& newText) {
|
||||||
buf->editBuf = newText;
|
buf->editBuf = newText;
|
||||||
state.onTextChanged();
|
state.onTextChanged();
|
||||||
@@ -173,6 +175,8 @@ static void renderEditorPanel(EditorState& state) {
|
|||||||
opts.showMinimap = state.ui.showMinimap;
|
opts.showMinimap = state.ui.showMinimap;
|
||||||
opts.showAnnotations = state.ui.showAnnotations;
|
opts.showAnnotations = state.ui.showAnnotations;
|
||||||
opts.showLineNumbers = state.ui.showLineNumbers;
|
opts.showLineNumbers = state.ui.showLineNumbers;
|
||||||
|
opts.lineHeightScale = state.settings.getLineHeightScale();
|
||||||
|
opts.letterSpacing = state.settings.getLetterSpacing();
|
||||||
if (state.ui.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1;
|
if (state.ui.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1;
|
||||||
else if (state.ui.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2;
|
else if (state.ui.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2;
|
||||||
else opts.annotationLayout = 0;
|
else opts.annotationLayout = 0;
|
||||||
@@ -226,6 +230,8 @@ static void renderEditorPanel(EditorState& state) {
|
|||||||
genOpts.mode = &buf->generatedMode;
|
genOpts.mode = &buf->generatedMode;
|
||||||
genOpts.showLineNumbers = state.ui.showLineNumbers;
|
genOpts.showLineNumbers = state.ui.showLineNumbers;
|
||||||
genOpts.showCurrentLine = false;
|
genOpts.showCurrentLine = false;
|
||||||
|
genOpts.lineHeightScale = state.settings.getLineHeightScale();
|
||||||
|
genOpts.letterSpacing = state.settings.getLetterSpacing();
|
||||||
genOpts.highlightLine = buf->generatedHighlightLine;
|
genOpts.highlightLine = buf->generatedHighlightLine;
|
||||||
genOpts.syncScrollX = &buf->splitScrollX;
|
genOpts.syncScrollX = &buf->splitScrollX;
|
||||||
genOpts.syncScrollY = &buf->splitScrollY;
|
genOpts.syncScrollY = &buf->splitScrollY;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "../EditorUtils.h"
|
#include "../EditorUtils.h"
|
||||||
#include "../ThemeEngine.h"
|
#include "../ThemeEngine.h"
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
static void openThemesFolder(const std::string& path) {
|
static void openThemesFolder(const std::string& path) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -17,6 +19,53 @@ static void openThemesFolder(const std::string& path) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FontOption {
|
||||||
|
std::string label;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void pushFontOption(std::vector<FontOption>& out,
|
||||||
|
const std::string& label,
|
||||||
|
const std::string& path) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (path.empty() || std::filesystem::exists(path, ec)) {
|
||||||
|
out.push_back({label, path});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<FontOption> collectFontOptions(bool monospace) {
|
||||||
|
std::vector<FontOption> out;
|
||||||
|
pushFontOption(out, "Default", "");
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (monospace) {
|
||||||
|
pushFontOption(out, "Consolas", "C:\\Windows\\Fonts\\consola.ttf");
|
||||||
|
pushFontOption(out, "Cascadia Code", "C:\\Windows\\Fonts\\CascadiaCode.ttf");
|
||||||
|
pushFontOption(out, "Cascadia Mono", "C:\\Windows\\Fonts\\CascadiaMono.ttf");
|
||||||
|
pushFontOption(out, "Courier New", "C:\\Windows\\Fonts\\cour.ttf");
|
||||||
|
} else {
|
||||||
|
pushFontOption(out, "Segoe UI", "C:\\Windows\\Fonts\\segoeui.ttf");
|
||||||
|
pushFontOption(out, "Calibri", "C:\\Windows\\Fonts\\calibri.ttf");
|
||||||
|
pushFontOption(out, "Arial", "C:\\Windows\\Fonts\\arial.ttf");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (monospace) {
|
||||||
|
pushFontOption(out, "DejaVu Sans Mono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf");
|
||||||
|
pushFontOption(out, "Liberation Mono", "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf");
|
||||||
|
} else {
|
||||||
|
pushFontOption(out, "DejaVu Sans", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf");
|
||||||
|
pushFontOption(out, "Liberation Sans", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int findFontIndex(const std::vector<FontOption>& options, const std::string& path) {
|
||||||
|
for (size_t i = 0; i < options.size(); ++i) {
|
||||||
|
if (options[i].path == path) return (int)i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void renderSettingsPanel(EditorState& state) {
|
static void renderSettingsPanel(EditorState& state) {
|
||||||
if (!state.ui.showSettingsPanel) return;
|
if (!state.ui.showSettingsPanel) return;
|
||||||
ImGui::Begin("Settings", &state.ui.showSettingsPanel);
|
ImGui::Begin("Settings", &state.ui.showSettingsPanel);
|
||||||
@@ -24,6 +73,7 @@ static void renderSettingsPanel(EditorState& state) {
|
|||||||
bool settingsChanged = false;
|
bool settingsChanged = false;
|
||||||
bool emacsConfigChanged = false;
|
bool emacsConfigChanged = false;
|
||||||
bool themeChanged = false;
|
bool themeChanged = false;
|
||||||
|
bool fontsChanged = false;
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
int fontSize = state.settings.getFontSize();
|
int fontSize = state.settings.getFontSize();
|
||||||
@@ -33,6 +83,56 @@ static void renderSettingsPanel(EditorState& state) {
|
|||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Typography");
|
||||||
|
|
||||||
|
auto codeFonts = collectFontOptions(true);
|
||||||
|
int codeFontIndex = findFontIndex(codeFonts, state.settings.getCodeFontPath());
|
||||||
|
std::vector<const char*> codeFontLabels;
|
||||||
|
codeFontLabels.reserve(codeFonts.size());
|
||||||
|
for (const auto& opt : codeFonts) codeFontLabels.push_back(opt.label.c_str());
|
||||||
|
if (ImGui::Combo("Code Font", &codeFontIndex, codeFontLabels.data(),
|
||||||
|
(int)codeFontLabels.size())) {
|
||||||
|
state.settings.setCodeFontPath(codeFonts[codeFontIndex].path);
|
||||||
|
settingsChanged = true;
|
||||||
|
fontsChanged = true;
|
||||||
|
}
|
||||||
|
std::string codeFontPath = state.settings.getCodeFontPath();
|
||||||
|
if (InputTextStr("Code Font Path", &codeFontPath)) {
|
||||||
|
state.settings.setCodeFontPath(codeFontPath);
|
||||||
|
settingsChanged = true;
|
||||||
|
fontsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto uiFonts = collectFontOptions(false);
|
||||||
|
int uiFontIndex = findFontIndex(uiFonts, state.settings.getUiFontPath());
|
||||||
|
std::vector<const char*> uiFontLabels;
|
||||||
|
uiFontLabels.reserve(uiFonts.size());
|
||||||
|
for (const auto& opt : uiFonts) uiFontLabels.push_back(opt.label.c_str());
|
||||||
|
if (ImGui::Combo("UI Font", &uiFontIndex, uiFontLabels.data(),
|
||||||
|
(int)uiFontLabels.size())) {
|
||||||
|
state.settings.setUiFontPath(uiFonts[uiFontIndex].path);
|
||||||
|
settingsChanged = true;
|
||||||
|
fontsChanged = true;
|
||||||
|
}
|
||||||
|
std::string uiFontPath = state.settings.getUiFontPath();
|
||||||
|
if (InputTextStr("UI Font Path", &uiFontPath)) {
|
||||||
|
state.settings.setUiFontPath(uiFontPath);
|
||||||
|
settingsChanged = true;
|
||||||
|
fontsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float lineHeight = state.settings.getLineHeightScale();
|
||||||
|
if (ImGui::SliderFloat("Line Height", &lineHeight, 1.0f, 2.0f, "%.2f")) {
|
||||||
|
state.settings.setLineHeightScale(lineHeight);
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
float letterSpacing = state.settings.getLetterSpacing();
|
||||||
|
if (ImGui::SliderFloat("Letter Spacing", &letterSpacing, 0.0f, 3.0f, "%.1f")) {
|
||||||
|
state.settings.setLetterSpacing(letterSpacing);
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
int tabSize = state.settings.getTabSize();
|
int tabSize = state.settings.getTabSize();
|
||||||
const int tabSizes[] = {2, 4, 8};
|
const int tabSizes[] = {2, 4, 8};
|
||||||
int tabIndex = 1;
|
int tabIndex = 1;
|
||||||
@@ -273,6 +373,9 @@ static void renderSettingsPanel(EditorState& state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (settingsChanged) {
|
if (settingsChanged) {
|
||||||
|
if (fontsChanged) {
|
||||||
|
state.fontsDirty = true;
|
||||||
|
}
|
||||||
state.saveSettingsToDisk();
|
state.saveSettingsToDisk();
|
||||||
state.events.publish(UIEventType::SettingsChanged, {}, {}, ImGui::GetTime());
|
state.events.publish(UIEventType::SettingsChanged, {}, {}, ImGui::GetTime());
|
||||||
}
|
}
|
||||||
|
|||||||
20
editor/tests/step175_integration_test.cpp
Normal file
20
editor/tests/step175_integration_test.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Step 175: Typography settings integration checks.
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "SettingsManager.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
SettingsManager settings;
|
||||||
|
settings.setCodeFontPath("code.ttf");
|
||||||
|
settings.setUiFontPath("ui.ttf");
|
||||||
|
settings.setLineHeightScale(1.5f);
|
||||||
|
settings.setLetterSpacing(1.0f);
|
||||||
|
assert(settings.getCodeFontPath() == "code.ttf");
|
||||||
|
assert(settings.getUiFontPath() == "ui.ttf");
|
||||||
|
assert(settings.getLineHeightScale() == 1.5f);
|
||||||
|
assert(settings.getLetterSpacing() == 1.0f);
|
||||||
|
|
||||||
|
printf("step175_integration_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
44
editor/tests/step175_test.cpp
Normal file
44
editor/tests/step175_test.cpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Step 175: Typography and spacing checks.
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static std::string readFile(const std::string& path) {
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f.is_open()) return {};
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertContains(const std::string& text, const std::string& needle) {
|
||||||
|
assert(text.find(needle) != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const std::string settings = readFile("src/SettingsManager.h");
|
||||||
|
assertContains(settings, "codeFont");
|
||||||
|
assertContains(settings, "uiFont");
|
||||||
|
assertContains(settings, "lineHeight");
|
||||||
|
assertContains(settings, "letterSpacing");
|
||||||
|
|
||||||
|
const std::string editorOpts = readFile("src/CodeEditorWidget.h");
|
||||||
|
assertContains(editorOpts, "lineHeightScale");
|
||||||
|
assertContains(editorOpts, "letterSpacing");
|
||||||
|
|
||||||
|
const std::string theme = readFile("src/ThemeEngine.h");
|
||||||
|
assertContains(theme, "panelPadding");
|
||||||
|
assertContains(theme, "panelSpacing");
|
||||||
|
|
||||||
|
const std::string settingsPanel = readFile("src/panels/SettingsPanel.h");
|
||||||
|
assertContains(settingsPanel, "Code Font");
|
||||||
|
assertContains(settingsPanel, "UI Font");
|
||||||
|
assertContains(settingsPanel, "Line Height");
|
||||||
|
assertContains(settingsPanel, "Letter Spacing");
|
||||||
|
|
||||||
|
printf("step175_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -132,7 +132,7 @@ look professional and feel personal.
|
|||||||
- Theme-aware: icon colors adapt to current theme
|
- Theme-aware: icon colors adapt to current theme
|
||||||
*New:* `IconSet.h`
|
*New:* `IconSet.h`
|
||||||
|
|
||||||
- [ ] **Step 175: Typography and spacing**
|
- [x] **Step 175: Typography and spacing**
|
||||||
Configurable typography beyond just font size:
|
Configurable typography beyond just font size:
|
||||||
- Code font family selection (from system monospace fonts or bundled)
|
- Code font family selection (from system monospace fonts or bundled)
|
||||||
- UI font separate from code font
|
- UI font separate from code font
|
||||||
|
|||||||
Reference in New Issue
Block a user