diff --git a/ThemeProvider.ImGui/ImGuiPaletteMapper.cs b/ThemeProvider.ImGui/ImGuiPaletteMapper.cs index 6ebf714..1720d42 100644 --- a/ThemeProvider.ImGui/ImGuiPaletteMapper.cs +++ b/ThemeProvider.ImGui/ImGuiPaletteMapper.cs @@ -35,12 +35,13 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) Dictionary colorMapping = new() { { ImGuiCol.WindowBg, new(SemanticMeaning.Neutral, Priority.VeryLow)}, - { ImGuiCol.ChildBg, new(SemanticMeaning.Neutral, Priority.Low)}, - { ImGuiCol.PopupBg, new(SemanticMeaning.Neutral, Priority.Low)}, - { ImGuiCol.MenuBarBg, new(SemanticMeaning.Neutral, Priority.Low)}, - { ImGuiCol.FrameBg, new(SemanticMeaning.Neutral, Priority.MediumLow)}, - { ImGuiCol.FrameBgHovered, new(SemanticMeaning.Neutral, Priority.Medium)}, - { ImGuiCol.FrameBgActive, new(SemanticMeaning.Neutral, Priority.MediumHigh)}, + // Base background surfaces share the window's lightness so body text keeps the same contrast on them. + { ImGuiCol.ChildBg, new(SemanticMeaning.Neutral, Priority.VeryLow)}, + { ImGuiCol.PopupBg, new(SemanticMeaning.Neutral, Priority.VeryLow)}, + { ImGuiCol.MenuBarBg, new(SemanticMeaning.Neutral, Priority.VeryLow)}, + { ImGuiCol.FrameBg, new(SemanticMeaning.Neutral, Priority.Low)}, + { ImGuiCol.FrameBgHovered, new(SemanticMeaning.Neutral, Priority.MediumLow)}, + { ImGuiCol.FrameBgActive, new(SemanticMeaning.Neutral, Priority.Medium)}, { ImGuiCol.TextDisabled, new(SemanticMeaning.Neutral, Priority.High)}, { ImGuiCol.Text, new(SemanticMeaning.Neutral, Priority.VeryHigh)}, @@ -49,16 +50,18 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) { ImGuiCol.ScrollbarGrabHovered, new(SemanticMeaning.Neutral, Priority.MediumHigh)}, { ImGuiCol.ScrollbarGrabActive, new(SemanticMeaning.Neutral, Priority.High)}, - // Primary elements now use more spread out priorities to maximize contrast within the 50-90% range - { ImGuiCol.Button, new(SemanticMeaning.Primary, Priority.Medium)}, - { ImGuiCol.ButtonHovered, new(SemanticMeaning.Primary, Priority.High)}, - { ImGuiCol.ButtonActive, new(SemanticMeaning.Primary, Priority.VeryHigh)}, + // Accent fills sit low (near the background end) so the brightest-neutral body text drawn on + // them keeps contrast; hover/active step up in small increments while staying off the text end. + { ImGuiCol.Button, new(SemanticMeaning.Primary, Priority.VeryLow)}, + { ImGuiCol.ButtonHovered, new(SemanticMeaning.Primary, Priority.Low)}, + { ImGuiCol.ButtonActive, new(SemanticMeaning.Primary, Priority.MediumLow)}, - { ImGuiCol.Header, new(SemanticMeaning.Primary, Priority.Medium)}, - { ImGuiCol.HeaderHovered, new(SemanticMeaning.Primary, Priority.High)}, - { ImGuiCol.HeaderActive, new(SemanticMeaning.Primary, Priority.VeryHigh)}, + { ImGuiCol.Header, new(SemanticMeaning.Primary, Priority.VeryLow)}, + { ImGuiCol.HeaderHovered, new(SemanticMeaning.Primary, Priority.Low)}, + { ImGuiCol.HeaderActive, new(SemanticMeaning.Primary, Priority.MediumLow)}, - { ImGuiCol.CheckMark, new(SemanticMeaning.Primary, Priority.High)}, + // The checkmark is a glyph, not a text surface: it takes the most visible accent so it contrasts the frame. + { ImGuiCol.CheckMark, new(SemanticMeaning.Primary, Priority.VeryHigh)}, { ImGuiCol.ResizeGrip, new(SemanticMeaning.Neutral, Priority.MediumHigh)}, { ImGuiCol.ResizeGripHovered, new(SemanticMeaning.Neutral, Priority.High)}, @@ -73,9 +76,9 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) { ImGuiCol.SeparatorHovered, new(SemanticMeaning.Neutral, Priority.High)}, { ImGuiCol.SeparatorActive, new(SemanticMeaning.Neutral, Priority.VeryHigh)}, - { ImGuiCol.Tab, new(SemanticMeaning.Neutral, Priority.Medium)}, - { ImGuiCol.TabSelected, new(SemanticMeaning.Primary, Priority.Medium)}, - { ImGuiCol.TabHovered, new(SemanticMeaning.Primary, Priority.High)}, + { ImGuiCol.Tab, new(SemanticMeaning.Neutral, Priority.Low)}, + { ImGuiCol.TabSelected, new(SemanticMeaning.Primary, Priority.VeryLow)}, + { ImGuiCol.TabHovered, new(SemanticMeaning.Primary, Priority.Low)}, // Alternate elements spread across the full 50-90% range for better contrast { ImGuiCol.PlotLines, new(SemanticMeaning.Alternate, Priority.Medium)}, @@ -84,7 +87,7 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) { ImGuiCol.PlotHistogram, new(SemanticMeaning.Alternate, Priority.Medium)}, { ImGuiCol.PlotHistogramHovered, new(SemanticMeaning.Alternate, Priority.High)}, - { ImGuiCol.TableHeaderBg, new(SemanticMeaning.Neutral, Priority.Medium)}, + { ImGuiCol.TableHeaderBg, new(SemanticMeaning.Neutral, Priority.Low)}, { ImGuiCol.TableBorderStrong, new(SemanticMeaning.Neutral, Priority.Medium)}, { ImGuiCol.TableBorderLight, new(SemanticMeaning.Neutral, Priority.Medium)}, { ImGuiCol.TableRowBg, new(SemanticMeaning.Neutral, Priority.VeryLow) }, @@ -92,9 +95,9 @@ public IReadOnlyDictionary MapTheme(ISemanticTheme theme) { ImGuiCol.TextSelectedBg, new(SemanticMeaning.Alternate, Priority.High) }, - { ImGuiCol.TitleBg, new(SemanticMeaning.Neutral, Priority.Medium)}, - { ImGuiCol.TitleBgActive, new(SemanticMeaning.Primary, Priority.Medium)}, - { ImGuiCol.TitleBgCollapsed, new(SemanticMeaning.Neutral, Priority.MediumLow)}, + { ImGuiCol.TitleBg, new(SemanticMeaning.Neutral, Priority.Low)}, + { ImGuiCol.TitleBgActive, new(SemanticMeaning.Primary, Priority.VeryLow)}, + { ImGuiCol.TitleBgCollapsed, new(SemanticMeaning.Neutral, Priority.Low)}, { ImGuiCol.Border, new(SemanticMeaning.Neutral, Priority.Medium) }, { ImGuiCol.BorderShadow, new(SemanticMeaning.Neutral, Priority.Low) }, diff --git a/ThemeProvider/SemanticColorMapper.cs b/ThemeProvider/SemanticColorMapper.cs index 02311a9..59c3c88 100644 --- a/ThemeProvider/SemanticColorMapper.cs +++ b/ThemeProvider/SemanticColorMapper.cs @@ -193,10 +193,15 @@ private static double CalculateTargetLightnessForSemantic( } else { - // Non-neutral uses 50-90% of the global range + // Non-neutral uses 20-82% of the global range. The low floor lets accent surfaces sit far + // enough from the brightest neutral (which body text uses) to keep text readable, and lets the + // most-visible accent (glyphs) reach a dark enough value to contrast a light frame. Capping the + // top below the global maximum keeps saturated source hues in gamut instead of forcing them to a + // near-white lightness where their chroma collapses; the floor is held off pure black for the + // same reason at the dark end. double globalRange = globalMaxLightness - globalMinLightness; - double rangeStart = globalMinLightness + (globalRange * 0.5); // 50% - double rangeEnd = globalMinLightness + (globalRange * 0.9); // 90% + double rangeStart = globalMinLightness + (globalRange * 0.20); // 20% + double rangeEnd = globalMinLightness + (globalRange * 0.82); // 82% minLightness = rangeStart; maxLightness = rangeEnd;