Step 196: high contrast and colorblind modes
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HighlightSpan>& 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);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<float>();
|
||||
out.panelSpacing.y = layout["panelSpacing"][1].get<float>();
|
||||
}
|
||||
if (layout.contains("windowBorderSize")) {
|
||||
out.windowBorderSize = layout["windowBorderSize"].get<float>();
|
||||
}
|
||||
if (layout.contains("frameBorderSize")) {
|
||||
out.frameBorderSize = layout["frameBorderSize"].get<float>();
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
42
editor/tests/step196_test.cpp
Normal file
42
editor/tests/step196_test.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
// Step 196: High contrast + colorblind/shape markers.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
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 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;
|
||||
}
|
||||
75
editor/themes/high_contrast.json
Normal file
75
editor/themes/high_contrast.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user