diff --git a/PROGRESS.md b/PROGRESS.md index 1e62ceb..3325daf 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -543,4 +543,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 193: Semantic library tags (semantic_tags.json storage, auto-tagging heuristics, tags attached to ExternalModule/TypeSignature). 1/1 tests pass (step193_test). | | 2026-02-10 | Codex | Step 194: Semantic-filtered library browser (tag filter bar, active tag chips, tag badges, context-aware completion boosting, agent tag context). 1/1 tests pass (step194_test). | | 2026-02-10 | Codex | Step 195: Security & semantic UX tests (vulnerability badges/advisories, security diagnostics severity mapping, semantic tag auto-assign, library tag filtering, vulnerable import deprioritization, safe upgrade writeback). 1/1 tests pass (step195_test). | +| 2026-02-10 | Codex | Step 196: High contrast + colorblind modes (high contrast theme, annotation shapes toggle, diagnostic pattern underlines). 1/1 tests pass (step196_test). | | 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4e28fb6..3476da8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1125,6 +1125,9 @@ target_include_directories(step194_test PRIVATE src) add_executable(step195_test tests/step195_test.cpp) target_include_directories(step195_test PRIVATE src) +add_executable(step196_test tests/step196_test.cpp) +target_include_directories(step196_test PRIVATE src) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/CodeEditorRenderHelpers.h b/editor/src/CodeEditorRenderHelpers.h index 30602ca..de87a00 100644 --- a/editor/src/CodeEditorRenderHelpers.h +++ b/editor/src/CodeEditorRenderHelpers.h @@ -174,13 +174,27 @@ float x = xStart; float amp = 2.0f; bool up = true; + int segment = 0; + bool dotted = (d.severity != 1 && d.severity != 2); while (x < xEnd) { + if (dotted) { + drawList->AddCircleFilled(ImVec2(x, baseY), 1.3f, color); + x += 4.0f; + continue; + } float x2 = std::min(x + 4.0f, xEnd); float y1 = baseY + (up ? -amp : amp); float y2 = baseY + (up ? amp : -amp); - drawList->AddLine(ImVec2(x, y1), ImVec2(x2, y2), color, 1.0f); + bool drawSegment = true; + if (d.severity == 2) { + drawSegment = (segment % 2 == 0); + } + if (drawSegment) { + drawList->AddLine(ImVec2(x, y1), ImVec2(x2, y2), color, 1.0f); + } up = !up; x = x2; + segment++; } } } diff --git a/editor/src/CodeEditorRendering.h b/editor/src/CodeEditorRendering.h index 08fb34d..62267df 100644 --- a/editor/src/CodeEditorRendering.h +++ b/editor/src/CodeEditorRendering.h @@ -1,6 +1,57 @@ #pragma once // Included inside CodeEditorWidget (rendering + folds). public: + static int annotationShapeIndex(const std::string& label) { + size_t h = 0; + for (unsigned char c : label) h = h * 131 + c; + return (int)(h % 5); + } + + static void drawAnnotationShape(ImDrawList* drawList, + const ImVec2& center, + float size, + ImU32 color, + int shape) { + switch (shape) { + case 0: { // circle + drawList->AddCircleFilled(center, size, color); + break; + } + case 1: { // triangle + ImVec2 top(center.x, center.y - size); + ImVec2 left(center.x - size, center.y + size); + ImVec2 right(center.x + size, center.y + size); + drawList->AddTriangleFilled(top, left, right, color); + break; + } + case 2: { // square + ImVec2 a(center.x - size, center.y - size); + ImVec2 b(center.x + size, center.y + size); + drawList->AddRectFilled(a, b, color, 0.0f); + break; + } + case 3: { // diamond + ImVec2 top(center.x, center.y - size); + ImVec2 right(center.x + size, center.y); + ImVec2 bottom(center.x, center.y + size); + ImVec2 left(center.x - size, center.y); + drawList->AddQuadFilled(top, right, bottom, left, color); + break; + } + default: { // star (outline) + ImVec2 pts[10]; + for (int i = 0; i < 10; ++i) { + float angle = (float)(IM_PI * 0.5 + i * (IM_PI / 5.0)); + float radius = (i % 2 == 0) ? size : size * 0.5f; + pts[i] = ImVec2(center.x + std::cos(angle) * radius, + center.y - std::sin(angle) * radius); + } + drawList->AddPolyline(pts, 10, color, true, 1.5f); + break; + } + } + } + CodeEditorResult render(const char* id, std::string& text, const std::vector& spans, @@ -370,7 +421,12 @@ public: AnnotationMarker marker; if (annotationAtLine(*options.annotations, ln, marker)) { ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); - drawList->AddCircleFilled(center, 3.0f, marker.color); + if (options.useAnnotationShapes) { + int shape = annotationShapeIndex(marker.message); + drawAnnotationShape(drawList, center, 3.5f, marker.color, shape); + } else { + drawList->AddCircleFilled(center, 3.0f, marker.color); + } ImVec2 a(center.x - 4.0f, center.y - 4.0f); ImVec2 b(center.x + 4.0f, center.y + 4.0f); bool hoverAnno = ImGui::IsMouseHoveringRect(a, b); diff --git a/editor/src/CodeEditorWidget.h b/editor/src/CodeEditorWidget.h index 5fe129b..0247eae 100644 --- a/editor/src/CodeEditorWidget.h +++ b/editor/src/CodeEditorWidget.h @@ -17,6 +17,7 @@ #include #include #include +#include #include struct MultiCursor { @@ -34,6 +35,7 @@ struct CodeEditorOptions { bool showAnnotations = false; bool showLineNumbers = true; bool showCurrentLine = true; + bool useAnnotationShapes = true; float lineHeightScale = 1.0f; float letterSpacing = 0.0f; float cursorBlinkRate = 2.0f; diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h index 870600d..2eae898 100644 --- a/editor/src/SettingsManager.h +++ b/editor/src/SettingsManager.h @@ -58,6 +58,8 @@ public: void setShowLineNumbers(bool value) { showLineNumbers_ = value; } bool getReduceMotion() const { return reduceMotion_; } void setReduceMotion(bool value) { reduceMotion_ = value; } + bool getUseAnnotationShapes() const { return useAnnotationShapes_; } + void setUseAnnotationShapes(bool value) { useAnnotationShapes_ = value; } bool getBlockVulnerableImports() const { return blockVulnerableImports_; } void setBlockVulnerableImports(bool value) { blockVulnerableImports_ = value; } const std::string& getLayoutPreset() const { return layoutPreset_; } @@ -85,6 +87,7 @@ public: showMinimap_ = j.value("showMinimap", showMinimap_); showLineNumbers_ = j.value("showLineNumbers", showLineNumbers_); reduceMotion_ = j.value("reduceMotion", reduceMotion_); + useAnnotationShapes_ = j.value("useAnnotationShapes", useAnnotationShapes_); blockVulnerableImports_ = j.value("blockVulnerableImports", blockVulnerableImports_); layoutPreset_ = j.value("layoutPreset", layoutPreset_); keybindingProfile_ = j.value("keybindingProfile", keybindingProfile_); @@ -124,6 +127,7 @@ public: j["showMinimap"] = showMinimap_; j["showLineNumbers"] = showLineNumbers_; j["reduceMotion"] = reduceMotion_; + j["useAnnotationShapes"] = useAnnotationShapes_; j["blockVulnerableImports"] = blockVulnerableImports_; j["layoutPreset"] = layoutPreset_; j["keybindingProfile"] = keybindingProfile_; @@ -200,6 +204,7 @@ private: bool showMinimap_ = false; bool showLineNumbers_ = true; bool reduceMotion_ = false; + bool useAnnotationShapes_ = true; bool blockVulnerableImports_ = false; std::string layoutPreset_ = "VSCode"; std::string keybindingProfile_ = "VSCode"; diff --git a/editor/src/ThemeEngine.h b/editor/src/ThemeEngine.h index 7a46c61..1bd77d1 100644 --- a/editor/src/ThemeEngine.h +++ b/editor/src/ThemeEngine.h @@ -47,6 +47,8 @@ struct ThemeDefinition { std::string sourcePath; ImVec2 panelPadding = ImVec2(8.0f, 6.0f); ImVec2 panelSpacing = ImVec2(8.0f, 6.0f); + float windowBorderSize = 1.0f; + float frameBorderSize = 0.0f; }; class ThemeEngine { @@ -326,6 +328,12 @@ private: out.panelSpacing.x = layout["panelSpacing"][0].get(); out.panelSpacing.y = layout["panelSpacing"][1].get(); } + if (layout.contains("windowBorderSize")) { + out.windowBorderSize = layout["windowBorderSize"].get(); + } + if (layout.contains("frameBorderSize")) { + out.frameBorderSize = layout["frameBorderSize"].get(); + } } return true; } @@ -339,6 +347,8 @@ private: } style.WindowPadding = theme.panelPadding; style.ItemSpacing = theme.panelSpacing; + style.WindowBorderSize = theme.windowBorderSize; + style.FrameBorderSize = theme.frameBorderSize; } static bool isSyntaxColor(ThemeColor color) { diff --git a/editor/src/panels/BottomPanel.h b/editor/src/panels/BottomPanel.h index 7841cf9..6f4ecdf 100644 --- a/editor/src/panels/BottomPanel.h +++ b/editor/src/panels/BottomPanel.h @@ -624,6 +624,7 @@ static void renderBottomPanel(EditorState& state) { leftOpts.showWhitespace = state.ui.showWhitespace; leftOpts.showLineNumbers = state.ui.showLineNumbers; leftOpts.showCurrentLine = false; + leftOpts.useAnnotationShapes = state.settings.getUseAnnotationShapes(); leftOpts.lineHeightScale = state.settings.getLineHeightScale(); leftOpts.letterSpacing = state.settings.getLetterSpacing(); leftOpts.highlightLines = &state.diff.beforeLines; @@ -641,6 +642,7 @@ static void renderBottomPanel(EditorState& state) { rightOpts.showWhitespace = state.ui.showWhitespace; rightOpts.showLineNumbers = state.ui.showLineNumbers; rightOpts.showCurrentLine = false; + rightOpts.useAnnotationShapes = state.settings.getUseAnnotationShapes(); rightOpts.lineHeightScale = state.settings.getLineHeightScale(); rightOpts.letterSpacing = state.settings.getLetterSpacing(); rightOpts.highlightLines = &state.diff.afterLines; diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h index 0b6b976..f821135 100644 --- a/editor/src/panels/EditorPanel.h +++ b/editor/src/panels/EditorPanel.h @@ -216,6 +216,7 @@ static void renderEditorPanel(EditorState& state) { opts.showMinimap = state.ui.showMinimap; opts.showAnnotations = state.ui.showAnnotations; opts.showLineNumbers = state.ui.showLineNumbers; + opts.useAnnotationShapes = state.settings.getUseAnnotationShapes(); opts.lineHeightScale = state.settings.getLineHeightScale(); opts.letterSpacing = state.settings.getLetterSpacing(); opts.cursorBlinkRate = state.settings.getCursorBlinkRate(); diff --git a/editor/src/panels/SettingsPanel.h b/editor/src/panels/SettingsPanel.h index 36aab37..2a5b99c 100644 --- a/editor/src/panels/SettingsPanel.h +++ b/editor/src/panels/SettingsPanel.h @@ -349,6 +349,11 @@ static void renderSettingsPanel(EditorState& state) { state.settings.setReduceMotion(reduceMotionSetting); settingsChanged = true; } + bool useAnnotationShapes = state.settings.getUseAnnotationShapes(); + if (ImGui::Checkbox("Use Shapes for Annotations", &useAnnotationShapes)) { + state.settings.setUseAnnotationShapes(useAnnotationShapes); + settingsChanged = true; + } bool blockVulnImports = state.settings.getBlockVulnerableImports(); if (ImGui::Checkbox("Block Vulnerable Imports", &blockVulnImports)) { state.settings.setBlockVulnerableImports(blockVulnImports); diff --git a/editor/tests/step196_test.cpp b/editor/tests/step196_test.cpp new file mode 100644 index 0000000..197dcb9 --- /dev/null +++ b/editor/tests/step196_test.cpp @@ -0,0 +1,42 @@ +// Step 196: High contrast + colorblind/shape markers. + +#include +#include +#include + +static std::string readFile(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) return {}; + return std::string((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); +} + +static void assertContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) != std::string::npos); +} + +int main() { + const std::string theme = readFile("themes/high_contrast.json"); + const std::string settings = readFile("src/SettingsManager.h"); + const std::string settingsPanel = readFile("src/panels/SettingsPanel.h"); + const std::string widget = readFile("src/CodeEditorWidget.h"); + const std::string rendering = readFile("src/CodeEditorRendering.h"); + const std::string helpers = readFile("src/CodeEditorRenderHelpers.h"); + + assertContains(theme, "\"High Contrast\""); + assertContains(theme, "windowBorderSize"); + assertContains(theme, "frameBorderSize"); + + assertContains(settings, "useAnnotationShapes"); + assertContains(settingsPanel, "Use Shapes for Annotations"); + assertContains(widget, "useAnnotationShapes"); + assertContains(rendering, "drawAnnotationShape"); + assertContains(rendering, "annotationShapeIndex"); + + assertContains(helpers, "dotted"); + assertContains(helpers, "segment % 2"); + assertContains(helpers, "AddCircleFilled"); + + printf("step196_test: all assertions passed\n"); + return 0; +} diff --git a/editor/themes/high_contrast.json b/editor/themes/high_contrast.json new file mode 100644 index 0000000..35cb0e7 --- /dev/null +++ b/editor/themes/high_contrast.json @@ -0,0 +1,75 @@ +{ + "name": "High Contrast", + "syntax": { + "keyword": "#FFFF00", + "string": "#00FF00", + "comment": "#00FFFF", + "number": "#FFA500", + "function": "#FF66FF", + "parameter": "#FFFFFF", + "type": "#00BFFF", + "operator": "#FFFFFF", + "punctuation": "#FFFFFF", + "builtin": "#FFD700", + "identifier": "#FFFFFF", + "plain": "#FFFFFF" + }, + "editor": { + "gutter_bg": "#000000", + "gutter_text": "#FFFFFF", + "diag_error": "#FF0000", + "diag_warning": "#FFA500", + "diag_info": "#00BFFF", + "suggestion": "#FFFF00", + "conflict": "#FF0000", + "conflict_line": "#FF0000B4", + "fold_indicator": "#FFFFFF", + "line_highlight": "#1A1A1A", + "selection": "#003366", + "annotation_tag_bg": "#1A1A1A", + "fold_placeholder": "#FFFFFF", + "caret": "#FFFFFF", + "minimap_bg": "#000000", + "minimap_line": "#333333", + "minimap_line_active": "#666666", + "minimap_view": "#FFFFFF" + }, + "imgui": { + "WindowBg": "#000000", + "ChildBg": "#000000", + "PopupBg": "#000000", + "Border": "#FFFFFF", + "Text": "#FFFFFF", + "TextDisabled": "#AAAAAA", + "FrameBg": "#111111", + "FrameBgHovered": "#222222", + "FrameBgActive": "#333333", + "TitleBg": "#000000", + "TitleBgActive": "#000000", + "Header": "#111111", + "HeaderHovered": "#222222", + "HeaderActive": "#333333", + "Tab": "#111111", + "TabHovered": "#222222", + "TabActive": "#000000", + "Button": "#111111", + "ButtonHovered": "#222222", + "ButtonActive": "#333333", + "ScrollbarBg": "#000000", + "ScrollbarGrab": "#444444", + "ScrollbarGrabHovered": "#666666", + "ScrollbarGrabActive": "#888888", + "CheckMark": "#FFFFFF", + "SliderGrab": "#FFFFFF", + "SliderGrabActive": "#FFFFFF", + "Separator": "#FFFFFF", + "SeparatorHovered": "#FFFFFF", + "SeparatorActive": "#FFFFFF" + }, + "layout": { + "panelPadding": [8.0, 6.0], + "panelSpacing": [8.0, 6.0], + "windowBorderSize": 2.0, + "frameBorderSize": 2.0 + } +} diff --git a/sprint6_plan.md b/sprint6_plan.md index ea37b36..de3e006 100644 --- a/sprint6_plan.md +++ b/sprint6_plan.md @@ -383,7 +383,7 @@ feature requests with polished UX. Make Whetstone usable by everyone and fast with large files. -- [ ] **Step 196: High contrast and colorblind modes** +- [x] **Step 196: High contrast and colorblind modes** Accessibility-first visual options: - "High Contrast" theme (WCAG AAA contrast ratios, bold borders) - Colorblind-safe annotation markers: use **shapes** (circle, triangle,