Step 175: typography and spacing

This commit is contained in:
Bill
2026-02-09 21:58:13 -07:00
parent 59b71aad64
commit f8ee687002
15 changed files with 294 additions and 3 deletions

View File

@@ -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. |

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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());
} }

View 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;
}

View 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;
}

View File

@@ -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