Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion ThemeProvider.ImGui/ImGuiPaletteMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public IReadOnlyDictionary<ImGuiCol, Vector4> MapTheme(ISemanticTheme theme)
{ ImGuiCol.FrameBgActive, new(SemanticMeaning.Neutral, Priority.Medium)},
{ ImGuiCol.TextDisabled, new(SemanticMeaning.Neutral, Priority.High)},
{ ImGuiCol.Text, new(SemanticMeaning.Neutral, Priority.VeryHigh)},
// The caret is drawn over input frames like text is, so it takes the text color.
{ ImGuiCol.InputTextCursor, new(SemanticMeaning.Neutral, Priority.VeryHigh)},
{ ImGuiCol.TextLink, new(SemanticMeaning.Primary, Priority.High)},

{ ImGuiCol.ScrollbarBg, new(SemanticMeaning.Neutral, Priority.Low)},
{ ImGuiCol.ScrollbarGrab, new(SemanticMeaning.Neutral, Priority.Medium)},
Expand All @@ -66,17 +69,31 @@ public IReadOnlyDictionary<ImGuiCol, Vector4> MapTheme(ISemanticTheme theme)
{ ImGuiCol.ResizeGripActive, new(SemanticMeaning.Neutral, Priority.VeryHigh)},

{ ImGuiCol.NavWindowingHighlight, new(SemanticMeaning.Primary, Priority.VeryHigh)},
{ ImGuiCol.NavCursor, new(SemanticMeaning.Primary, Priority.High)},
{ ImGuiCol.DragDropTarget, new(SemanticMeaning.Primary, Priority.High)},
{ ImGuiCol.DockingPreview, new(SemanticMeaning.Primary, Priority.High)},
{ ImGuiCol.DockingEmptyBg, new(SemanticMeaning.Neutral, Priority.VeryLow)},

// The dimming overlays cover the whole viewport; their alpha is set below.
{ ImGuiCol.NavWindowingDimBg, new(SemanticMeaning.Neutral, Priority.Medium)},
{ ImGuiCol.ModalWindowDimBg, new(SemanticMeaning.Neutral, Priority.Medium)},

{ ImGuiCol.SliderGrab, new(SemanticMeaning.Primary, Priority.MediumLow)},
{ ImGuiCol.SliderGrabActive, new(SemanticMeaning.Primary, Priority.High)},

{ ImGuiCol.Separator, new(SemanticMeaning.Neutral, Priority.MediumHigh)},
{ ImGuiCol.SeparatorHovered, new(SemanticMeaning.Neutral, Priority.High)},
{ ImGuiCol.SeparatorActive, new(SemanticMeaning.Neutral, Priority.VeryHigh)},
{ ImGuiCol.TreeLines, new(SemanticMeaning.Neutral, Priority.MediumHigh)},

{ ImGuiCol.Tab, new(SemanticMeaning.Neutral, Priority.Low)},
{ ImGuiCol.TabSelected, new(SemanticMeaning.Primary, Priority.VeryLow)},
{ ImGuiCol.TabHovered, new(SemanticMeaning.Primary, Priority.Low)},
{ ImGuiCol.TabSelectedOverline, new(SemanticMeaning.Primary, Priority.High)},
// Tabs in an unfocused window drop to neutral so the focused window's tabs stand out.
{ ImGuiCol.TabDimmed, new(SemanticMeaning.Neutral, Priority.VeryLow)},
{ ImGuiCol.TabDimmedSelected, new(SemanticMeaning.Neutral, Priority.Low)},
{ ImGuiCol.TabDimmedSelectedOverline, new(SemanticMeaning.Primary, Priority.High)},

// Alternate elements spread across the full 50-90% range for better contrast
{ ImGuiCol.PlotLines, new(SemanticMeaning.Alternate, Priority.Medium)},
Expand All @@ -101,6 +118,13 @@ public IReadOnlyDictionary<ImGuiCol, Vector4> MapTheme(ISemanticTheme theme)
{ ImGuiCol.BorderShadow, new(SemanticMeaning.Neutral, Priority.Low) },
};

// Overlays drawn over other content, with the opacity ImGui's stock styles give them.
Dictionary<ImGuiCol, float> alphaOverrides = new()
{
{ ImGuiCol.NavWindowingDimBg, 0.20f },
{ ImGuiCol.ModalWindowDimBg, 0.35f },
};

// Convert the semantic colors to ImGui Vector4 format
Dictionary<ImGuiCol, Vector4> result = [];
foreach (KeyValuePair<ImGuiCol, SemanticColorRequest> kv in colorMapping)
Expand All @@ -109,7 +133,13 @@ public IReadOnlyDictionary<ImGuiCol, Vector4> MapTheme(ISemanticTheme theme)
SemanticColorRequest request = kv.Value;
if (completePalette.TryGetValue(request, out Color color))
{
result[imguiCol] = color.ToSrgbVector4();
Vector4 value = color.ToSrgbVector4();
if (alphaOverrides.TryGetValue(imguiCol, out float alpha))
{
value.W = alpha;
}

result[imguiCol] = value;
}
}

Expand Down
60 changes: 60 additions & 0 deletions ThemeProvider.Test/ImGuiPaletteMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,66 @@ public void MapTheme_ProducesSamePaletteSizeForEveryTheme()
}
}

/// <summary>
/// Every <see cref="ImGuiCol"/> slot must be assigned. An unassigned slot keeps whatever style was
/// active before the theme was applied, usually <c>StyleColorsDark()</c>, so a light theme would be
/// left with a white text cursor and navy dimmed tabs. Comparing themes against each other cannot
/// catch a slot that every theme is missing.
/// </summary>
[TestMethod]
public void MapTheme_AssignsEveryImGuiColor()
{
ImGuiPaletteMapper mapper = new();

foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes)
{
IReadOnlyDictionary<ImGuiCol, Vector4> palette = mapper.MapTheme(info.CreateInstance());

for (int i = 0; i < (int)ImGuiCol.Count; i++)
{
ImGuiCol key = (ImGuiCol)i;
Assert.IsTrue(palette.ContainsKey(key), $"{info.Name}: missing ImGui color {key}");
}
}
}

/// <summary>
/// The text cursor is drawn over input frames, so it takes the text color rather than a fixed one.
/// </summary>
[TestMethod]
public void MapTheme_InputTextCursorMatchesText()
{
ImGuiPaletteMapper mapper = new();

foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes)
{
IReadOnlyDictionary<ImGuiCol, Vector4> palette = mapper.MapTheme(info.CreateInstance());
Assert.IsTrue(palette.TryGetValue(ImGuiCol.InputTextCursor, out Vector4 cursor), $"{info.Name}: InputTextCursor unmapped");
Assert.AreEqual(palette[ImGuiCol.Text], cursor, info.Name);
}
}

/// <summary>
/// The dimming overlays cover the whole viewport, so they must be translucent or they would hide
/// everything behind a modal or the window switcher.
/// </summary>
[TestMethod]
public void MapTheme_DimBackgroundsAreTranslucent()
{
ImGuiPaletteMapper mapper = new();

foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes)
{
IReadOnlyDictionary<ImGuiCol, Vector4> palette = mapper.MapTheme(info.CreateInstance());

foreach (ImGuiCol key in new[] { ImGuiCol.ModalWindowDimBg, ImGuiCol.NavWindowingDimBg })
{
Assert.IsTrue(palette.TryGetValue(key, out Vector4 dim), $"{info.Name}: {key} unmapped");
Assert.IsTrue(dim.W is > 0f and < 1f, $"{info.Name}: {key} alpha is {dim.W}");
}
}
}

/// <summary>
/// ImGui expects color components in [0, 1]; anything outside that renders as a clipped or
/// wrapped color.
Expand Down
Loading