diff --git a/ThemeProvider.ImGui/ImGuiPaletteMapper.cs b/ThemeProvider.ImGui/ImGuiPaletteMapper.cs index 96b0e76..6d6159b 100644 --- a/ThemeProvider.ImGui/ImGuiPaletteMapper.cs +++ b/ThemeProvider.ImGui/ImGuiPaletteMapper.cs @@ -42,6 +42,9 @@ public IReadOnlyDictionary 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)}, @@ -66,6 +69,14 @@ public IReadOnlyDictionary 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)}, @@ -73,10 +84,16 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) { 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)}, @@ -101,6 +118,13 @@ public IReadOnlyDictionary 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 alphaOverrides = new() + { + { ImGuiCol.NavWindowingDimBg, 0.20f }, + { ImGuiCol.ModalWindowDimBg, 0.35f }, + }; + // Convert the semantic colors to ImGui Vector4 format Dictionary result = []; foreach (KeyValuePair kv in colorMapping) @@ -109,7 +133,13 @@ public IReadOnlyDictionary 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; } } diff --git a/ThemeProvider.Test/ImGuiPaletteMapperTests.cs b/ThemeProvider.Test/ImGuiPaletteMapperTests.cs index f543a9d..99e32cf 100644 --- a/ThemeProvider.Test/ImGuiPaletteMapperTests.cs +++ b/ThemeProvider.Test/ImGuiPaletteMapperTests.cs @@ -75,6 +75,66 @@ public void MapTheme_ProducesSamePaletteSizeForEveryTheme() } } + /// + /// Every slot must be assigned. An unassigned slot keeps whatever style was + /// active before the theme was applied, usually StyleColorsDark(), 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. + /// + [TestMethod] + public void MapTheme_AssignsEveryImGuiColor() + { + ImGuiPaletteMapper mapper = new(); + + foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes) + { + IReadOnlyDictionary 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}"); + } + } + } + + /// + /// The text cursor is drawn over input frames, so it takes the text color rather than a fixed one. + /// + [TestMethod] + public void MapTheme_InputTextCursorMatchesText() + { + ImGuiPaletteMapper mapper = new(); + + foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes) + { + IReadOnlyDictionary 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); + } + } + + /// + /// The dimming overlays cover the whole viewport, so they must be translucent or they would hide + /// everything behind a modal or the window switcher. + /// + [TestMethod] + public void MapTheme_DimBackgroundsAreTranslucent() + { + ImGuiPaletteMapper mapper = new(); + + foreach (ThemeRegistry.ThemeInfo info in ThemeRegistry.AllThemes) + { + IReadOnlyDictionary 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}"); + } + } + } + /// /// ImGui expects color components in [0, 1]; anything outside that renders as a clipped or /// wrapped color.