Step 196: high contrast and colorblind modes

This commit is contained in:
Bill
2026-02-10 04:55:49 -07:00
parent 93f2d7d1e9
commit 09fd7b011e
13 changed files with 219 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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