fix(theme): use native GTK text scaling
Move UI text scaling from root CSS font-size into GtkSettings so scaled text renders more cleanly. Keep omnibox suggestion titles at a slightly heavier weight to avoid thin glyph artifacts at higher UI scales.
- commit
- c8834e506e447de888efa5f0a54ee43bac5c9606
- author
- brice · · 2026-04-08 05:27 UTC
- parent
- 3132aa3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +75 -25
M internal/ui/theme/css.go +6 -25
| @@ -12,7 +12,7 @@ func GenerateCSS(p Palette) string { | |||
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | // GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor. | |
| 15 | - | // Scale affects font sizes and widget padding/margins proportionally. | |
| 15 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. | |
| 16 | 16 | func GenerateCSSWithScale(p Palette, scale float64) string { | |
| 17 | 17 | return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig()) | |
| 18 | 18 | } | |
| @@ -49,14 +49,14 @@ window, tooltip, popover { | |||
| 49 | 49 | } | |
| 50 | 50 | ||
| 51 | 51 | // GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts. | |
| 52 | - | // Scale affects font sizes and widget padding/margins proportionally. | |
| 52 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. | |
| 53 | 53 | // Uses default mode colors. | |
| 54 | 54 | func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string { | |
| 55 | 55 | return GenerateCSSFull(p, scale, fonts, DefaultModeColors()) | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | 58 | // GenerateCSSFull creates GTK4 CSS using all provided configuration. | |
| 59 | - | // Scale affects font sizes and widget padding/margins proportionally. | |
| 59 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. | |
| 60 | 60 | func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors ModeColors) string { | |
| 61 | 61 | if scale <= 0 { | |
| 62 | 62 | scale = 1.0 | |
| @@ -82,12 +82,6 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||
| 82 | 82 | sb.WriteString(generateFontCSS()) | |
| 83 | 83 | sb.WriteString("\n") | |
| 84 | 84 | ||
| 85 | - | // Global UI scaling via font-size on all widgets | |
| 86 | - | if scale != 1.0 { | |
| 87 | - | sb.WriteString(generateScalingCSS(scale)) | |
| 88 | - | sb.WriteString("\n") | |
| 89 | - | } | |
| 90 | - | ||
| 91 | 85 | // Tab bar styling | |
| 92 | 86 | sb.WriteString(generateTabBarCSS(p)) | |
| 93 | 87 | sb.WriteString("\n") | |
| @@ -146,20 +140,6 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||
| 146 | 140 | return sb.String() | |
| 147 | 141 | } | |
| 148 | 142 | ||
| 149 | - | // generateScalingCSS creates CSS rules that scale the UI. | |
| 150 | - | // GTK4 CSS doesn't support transform:scale() well, so we scale font-size | |
| 151 | - | // and use relative units (em) for padding/margins in other rules. | |
| 152 | - | func generateScalingCSS(scale float64) string { | |
| 153 | - | // Use 16px base so em conversions are correct: | |
| 154 | - | // 1px = 0.0625em (1/16), 4px = 0.25em, etc. | |
| 155 | - | basePx := int(16 * scale) | |
| 156 | - | return fmt.Sprintf(`/* UI Scaling (%.0f%%) */ | |
| 157 | - | window { | |
| 158 | - | font-size: %dpx; | |
| 159 | - | } | |
| 160 | - | `, scale*100, basePx) | |
| 161 | - | } | |
| 162 | - | ||
| 163 | 143 | // generateTabBarCSS creates tab bar styles. | |
| 164 | 144 | // Uses em units for scalable UI. | |
| 165 | 145 | func generateTabBarCSS(p Palette) string { | |
| @@ -316,7 +296,8 @@ entry.omnibox-entry > text:focus-visible { | |||
| 316 | 296 | border-radius: 0; | |
| 317 | 297 | border-left: 0.1875em solid transparent; | |
| 318 | 298 | border-bottom: 0.0625em solid alpha(var(--border), 0.35); | |
| 319 | - | background-color: transparent; | |
| 299 | + | background-color: var(--surface); | |
| 300 | + | background-image: none; | |
| 320 | 301 | transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out; | |
| 321 | 302 | min-height: 2.75em; | |
| 322 | 303 | } | |
| @@ -362,7 +343,7 @@ entry.omnibox-entry > text:focus-visible { | |||
| 362 | 343 | .omnibox-suggestion-title { | |
| 363 | 344 | font-size: 0.875em; | |
| 364 | 345 | color: var(--text); | |
| 365 | - | font-weight: 400; | |
| 346 | + | font-weight: 500; | |
| 366 | 347 | } | |
| 367 | 348 | ||
| 368 | 349 | /* Also style labels inside omnibox rows directly */ | |
| @@ -12,7 +12,7 @@ func GenerateCSS(p Palette) string { | |||||
| 12 | } | 12 | } | ||
| 13 | 13 | ||||
| 14 | // GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor. | 14 | // GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor. | ||
| 15 | - | // Scale affects font sizes and widget padding/margins proportionally. | 15 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. |
| 16 | func GenerateCSSWithScale(p Palette, scale float64) string { | 16 | func GenerateCSSWithScale(p Palette, scale float64) string { | ||
| 17 | return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig()) | 17 | return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig()) | ||
| 18 | } | 18 | } | ||
| @@ -49,14 +49,14 @@ window, tooltip, popover { | |||||
| 49 | } | 49 | } | ||
| 50 | 50 | ||||
| 51 | // GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts. | 51 | // GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts. | ||
| 52 | - | // Scale affects font sizes and widget padding/margins proportionally. | 52 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. |
| 53 | // Uses default mode colors. | 53 | // Uses default mode colors. | ||
| 54 | func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string { | 54 | func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string { | ||
| 55 | return GenerateCSSFull(p, scale, fonts, DefaultModeColors()) | 55 | return GenerateCSSFull(p, scale, fonts, DefaultModeColors()) | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | // GenerateCSSFull creates GTK4 CSS using all provided configuration. | 58 | // GenerateCSSFull creates GTK4 CSS using all provided configuration. | ||
| 59 | - | // Scale affects font sizes and widget padding/margins proportionally. | 59 | + | // Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings. |
| 60 | func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors ModeColors) string { | 60 | func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors ModeColors) string { | ||
| 61 | if scale <= 0 { | 61 | if scale <= 0 { | ||
| 62 | scale = 1.0 | 62 | scale = 1.0 | ||
| @@ -82,12 +82,6 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||||
| 82 | sb.WriteString(generateFontCSS()) | 82 | sb.WriteString(generateFontCSS()) | ||
| 83 | sb.WriteString("\n") | 83 | sb.WriteString("\n") | ||
| 84 | 84 | ||||
| 85 | - | // Global UI scaling via font-size on all widgets | |||
| 86 | - | if scale != 1.0 { | |||
| 87 | - | sb.WriteString(generateScalingCSS(scale)) | |||
| 88 | - | sb.WriteString("\n") | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | // Tab bar styling | 85 | // Tab bar styling | ||
| 92 | sb.WriteString(generateTabBarCSS(p)) | 86 | sb.WriteString(generateTabBarCSS(p)) | ||
| 93 | sb.WriteString("\n") | 87 | sb.WriteString("\n") | ||
| @@ -146,20 +140,6 @@ func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors Mode | |||||
| 146 | return sb.String() | 140 | return sb.String() | ||
| 147 | } | 141 | } | ||
| 148 | 142 | ||||
| 149 | - | // generateScalingCSS creates CSS rules that scale the UI. | |||
| 150 | - | // GTK4 CSS doesn't support transform:scale() well, so we scale font-size | |||
| 151 | - | // and use relative units (em) for padding/margins in other rules. | |||
| 152 | - | func generateScalingCSS(scale float64) string { | |||
| 153 | - | // Use 16px base so em conversions are correct: | |||
| 154 | - | // 1px = 0.0625em (1/16), 4px = 0.25em, etc. | |||
| 155 | - | basePx := int(16 * scale) | |||
| 156 | - | return fmt.Sprintf(`/* UI Scaling (%.0f%%) */ | |||
| 157 | - | window { | |||
| 158 | - | font-size: %dpx; | |||
| 159 | - | } | |||
| 160 | - | `, scale*100, basePx) | |||
| 161 | - | } | |||
| 162 | - | ||||
| 163 | // generateTabBarCSS creates tab bar styles. | 143 | // generateTabBarCSS creates tab bar styles. | ||
| 164 | // Uses em units for scalable UI. | 144 | // Uses em units for scalable UI. | ||
| 165 | func generateTabBarCSS(p Palette) string { | 145 | func generateTabBarCSS(p Palette) string { | ||
| @@ -316,7 +296,8 @@ entry.omnibox-entry > text:focus-visible { | |||||
| 316 | border-radius: 0; | 296 | border-radius: 0; | ||
| 317 | border-left: 0.1875em solid transparent; | 297 | border-left: 0.1875em solid transparent; | ||
| 318 | border-bottom: 0.0625em solid alpha(var(--border), 0.35); | 298 | border-bottom: 0.0625em solid alpha(var(--border), 0.35); | ||
| 319 | - | background-color: transparent; | 299 | + | background-color: var(--surface); |
| 300 | + | background-image: none; | |||
| 320 | transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out; | 301 | transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out; | ||
| 321 | min-height: 2.75em; | 302 | min-height: 2.75em; | ||
| 322 | } | 303 | } | ||
| @@ -362,7 +343,7 @@ entry.omnibox-entry > text:focus-visible { | |||||
| 362 | .omnibox-suggestion-title { | 343 | .omnibox-suggestion-title { | ||
| 363 | font-size: 0.875em; | 344 | font-size: 0.875em; | ||
| 364 | color: var(--text); | 345 | color: var(--text); | ||
| 365 | - | font-weight: 400; | 346 | + | font-weight: 500; |
| 366 | } | 347 | } | ||
| 367 | 348 | ||||
| 368 | /* Also style labels inside omnibox rows directly */ | 349 | /* Also style labels inside omnibox rows directly */ | ||
M internal/ui/theme/css_test.go +32 -0
| @@ -4,6 +4,8 @@ import ( | |||
| 4 | 4 | "regexp" | |
| 5 | 5 | "strings" | |
| 6 | 6 | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/stretchr/testify/assert" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 9 | 11 | func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) { | |
| @@ -72,3 +74,33 @@ func TestGenerateCSS_OmniboxSearchAreaMatchesHeaderSurface(t *testing.T) { | |||
| 72 | 74 | t.Fatalf("expected omnibox results area to use the header surface color") | |
| 73 | 75 | } | |
| 74 | 76 | } | |
| 77 | + | ||
| 78 | + | func TestGenerateCSSWithScale_DoesNotEmitRootTextScaling(t *testing.T) { | |
| 79 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |
| 80 | + | ||
| 81 | + | assert.NotContains(t, css, "/* UI Scaling (130%) */") | |
| 82 | + | assert.NotContains(t, css, "window {\n\tfont-size:") | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func TestGenerateCSSWithScale_DoesNotEmitLineHeightOverrides(t *testing.T) { | |
| 86 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |
| 87 | + | ||
| 88 | + | assert.NotContains(t, css, "line-height:") | |
| 89 | + | assert.NotContains(t, css, "label,\nentry,\nentry > text {\n\tline-height:") | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func TestGenerateCSSWithScale_UsesOpaqueOmniboxRowBaseBackground(t *testing.T) { | |
| 93 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |
| 94 | + | ||
| 95 | + | assert.Contains(t, css, ".omnibox-row {") | |
| 96 | + | assert.Contains(t, css, "background-color: var(--surface);") | |
| 97 | + | assert.NotContains(t, css, ".omnibox-row {\n\tpadding: 0.5em 0.75em;\n\tmargin: 0;\n\tborder-radius: 0;\n\tborder-left: 0.1875em solid transparent;\n\tborder-bottom: 0.0625em solid alpha(var(--border), 0.35);\n\tbackground-color: transparent;") | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testing.T) { | |
| 101 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |
| 102 | + | ||
| 103 | + | assert.Contains(t, css, ".omnibox-suggestion-title {") | |
| 104 | + | assert.Contains(t, css, "font-weight: 500;") | |
| 105 | + | assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") | |
| 106 | + | } | |
| @@ -4,6 +4,8 @@ import ( | |||||
| 4 | "regexp" | 4 | "regexp" | ||
| 5 | "strings" | 5 | "strings" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | ||||
| 8 | + | "github.com/stretchr/testify/assert" | |||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| 9 | func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) { | 11 | func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) { | ||
| @@ -72,3 +74,33 @@ func TestGenerateCSS_OmniboxSearchAreaMatchesHeaderSurface(t *testing.T) { | |||||
| 72 | t.Fatalf("expected omnibox results area to use the header surface color") | 74 | t.Fatalf("expected omnibox results area to use the header surface color") | ||
| 73 | } | 75 | } | ||
| 74 | } | 76 | } | ||
| 77 | + | ||||
| 78 | + | func TestGenerateCSSWithScale_DoesNotEmitRootTextScaling(t *testing.T) { | |||
| 79 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |||
| 80 | + | ||||
| 81 | + | assert.NotContains(t, css, "/* UI Scaling (130%) */") | |||
| 82 | + | assert.NotContains(t, css, "window {\n\tfont-size:") | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func TestGenerateCSSWithScale_DoesNotEmitLineHeightOverrides(t *testing.T) { | |||
| 86 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |||
| 87 | + | ||||
| 88 | + | assert.NotContains(t, css, "line-height:") | |||
| 89 | + | assert.NotContains(t, css, "label,\nentry,\nentry > text {\n\tline-height:") | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func TestGenerateCSSWithScale_UsesOpaqueOmniboxRowBaseBackground(t *testing.T) { | |||
| 93 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |||
| 94 | + | ||||
| 95 | + | assert.Contains(t, css, ".omnibox-row {") | |||
| 96 | + | assert.Contains(t, css, "background-color: var(--surface);") | |||
| 97 | + | assert.NotContains(t, css, ".omnibox-row {\n\tpadding: 0.5em 0.75em;\n\tmargin: 0;\n\tborder-radius: 0;\n\tborder-left: 0.1875em solid transparent;\n\tborder-bottom: 0.0625em solid alpha(var(--border), 0.35);\n\tbackground-color: transparent;") | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testing.T) { | |||
| 101 | + | css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3) | |||
| 102 | + | ||||
| 103 | + | assert.Contains(t, css, ".omnibox-suggestion-title {") | |||
| 104 | + | assert.Contains(t, css, "font-weight: 500;") | |||
| 105 | + | assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;") | |||
| 106 | + | } | |||
M internal/ui/theme/manager.go +28 -0
| @@ -3,6 +3,8 @@ package theme | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "math" | |
| 7 | + | "strings" | |
| 6 | 8 | ||
| 7 | 9 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 10 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -150,6 +152,23 @@ func (m *Manager) GetWebUIThemeCSS() string { | |||
| 150 | 152 | return fmt.Sprintf(":root{\n%s}\n\n.dark{\n%s}\n", lightVars, darkVars) | |
| 151 | 153 | } | |
| 152 | 154 | ||
| 155 | + | const gtkDefaultFontPointSize = 11 | |
| 156 | + | ||
| 157 | + | func formatGTKFontName(family string, uiScale float64) string { | |
| 158 | + | family = strings.TrimSpace(family) | |
| 159 | + | if family == "" { | |
| 160 | + | family = DefaultFontConfig().SansFont | |
| 161 | + | } | |
| 162 | + | if uiScale <= 0 { | |
| 163 | + | uiScale = 1.0 | |
| 164 | + | } | |
| 165 | + | size := int(math.Round(gtkDefaultFontPointSize * uiScale)) | |
| 166 | + | if size < 1 { | |
| 167 | + | size = gtkDefaultFontPointSize | |
| 168 | + | } | |
| 169 | + | return fmt.Sprintf("%s %d", family, size) | |
| 170 | + | } | |
| 171 | + | ||
| 153 | 172 | // ApplyToDisplay loads the theme CSS into the display. | |
| 154 | 173 | func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) { | |
| 155 | 174 | log := logging.FromContext(ctx) | |
| @@ -162,6 +181,15 @@ func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) { | |||
| 162 | 181 | // Generate CSS with current palette, UI scale, fonts, and mode colors | |
| 163 | 182 | palette := m.GetCurrentPalette() | |
| 164 | 183 | css := GenerateCSSFull(palette, m.uiScale, m.fonts, m.modeColors) | |
| 184 | + | fontName := formatGTKFontName(m.fonts.SansFont, m.uiScale) | |
| 185 | + | ||
| 186 | + | settings := gtk.SettingsGetForDisplay(display) | |
| 187 | + | if settings == nil { | |
| 188 | + | log.Warn().Msg("cannot apply theme font scaling: settings unavailable") | |
| 189 | + | } else { | |
| 190 | + | settings.SetPropertyGtkFontName(fontName) | |
| 191 | + | settings.Unref() | |
| 192 | + | } | |
| 165 | 193 | ||
| 166 | 194 | // Create CSS provider if needed | |
| 167 | 195 | if m.cssProvider == nil { | |
| @@ -3,6 +3,8 @@ package theme | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "math" | |||
| 7 | + | "strings" | |||
| 6 | 8 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 9 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 10 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -150,6 +152,23 @@ func (m *Manager) GetWebUIThemeCSS() string { | |||||
| 150 | return fmt.Sprintf(":root{\n%s}\n\n.dark{\n%s}\n", lightVars, darkVars) | 152 | return fmt.Sprintf(":root{\n%s}\n\n.dark{\n%s}\n", lightVars, darkVars) | ||
| 151 | } | 153 | } | ||
| 152 | 154 | ||||
| 155 | + | const gtkDefaultFontPointSize = 11 | |||
| 156 | + | ||||
| 157 | + | func formatGTKFontName(family string, uiScale float64) string { | |||
| 158 | + | family = strings.TrimSpace(family) | |||
| 159 | + | if family == "" { | |||
| 160 | + | family = DefaultFontConfig().SansFont | |||
| 161 | + | } | |||
| 162 | + | if uiScale <= 0 { | |||
| 163 | + | uiScale = 1.0 | |||
| 164 | + | } | |||
| 165 | + | size := int(math.Round(gtkDefaultFontPointSize * uiScale)) | |||
| 166 | + | if size < 1 { | |||
| 167 | + | size = gtkDefaultFontPointSize | |||
| 168 | + | } | |||
| 169 | + | return fmt.Sprintf("%s %d", family, size) | |||
| 170 | + | } | |||
| 171 | + | ||||
| 153 | // ApplyToDisplay loads the theme CSS into the display. | 172 | // ApplyToDisplay loads the theme CSS into the display. | ||
| 154 | func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) { | 173 | func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) { | ||
| 155 | log := logging.FromContext(ctx) | 174 | log := logging.FromContext(ctx) | ||
| @@ -162,6 +181,15 @@ func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) { | |||||
| 162 | // Generate CSS with current palette, UI scale, fonts, and mode colors | 181 | // Generate CSS with current palette, UI scale, fonts, and mode colors | ||
| 163 | palette := m.GetCurrentPalette() | 182 | palette := m.GetCurrentPalette() | ||
| 164 | css := GenerateCSSFull(palette, m.uiScale, m.fonts, m.modeColors) | 183 | css := GenerateCSSFull(palette, m.uiScale, m.fonts, m.modeColors) | ||
| 184 | + | fontName := formatGTKFontName(m.fonts.SansFont, m.uiScale) | |||
| 185 | + | ||||
| 186 | + | settings := gtk.SettingsGetForDisplay(display) | |||
| 187 | + | if settings == nil { | |||
| 188 | + | log.Warn().Msg("cannot apply theme font scaling: settings unavailable") | |||
| 189 | + | } else { | |||
| 190 | + | settings.SetPropertyGtkFontName(fontName) | |||
| 191 | + | settings.Unref() | |||
| 192 | + | } | |||
| 165 | 193 | ||||
| 166 | // Create CSS provider if needed | 194 | // Create CSS provider if needed | ||
| 167 | if m.cssProvider == nil { | 195 | if m.cssProvider == nil { | ||
M internal/ui/theme/manager_test.go +9 -0
| @@ -313,3 +313,12 @@ func TestManager_ModeColors(t *testing.T) { | |||
| 313 | 313 | assert.Equal(t, "#0000ff", modeColors.SessionMode) | |
| 314 | 314 | assert.Equal(t, "#ffff00", modeColors.ResizeMode) | |
| 315 | 315 | } | |
| 316 | + | ||
| 317 | + | func TestFormatGTKFontName_UsesScaledConfiguredSansFont(t *testing.T) { | |
| 318 | + | assert.Equal(t, "Fira Sans 14", formatGTKFontName("Fira Sans", 1.3)) | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | func TestFormatGTKFontName_DefaultsScaleAndRoundsToNearestPoint(t *testing.T) { | |
| 322 | + | assert.Equal(t, "Inter 11", formatGTKFontName("Inter", 0)) | |
| 323 | + | assert.Equal(t, "Inter 12", formatGTKFontName("Inter", 1.05)) | |
| 324 | + | } | |
| @@ -313,3 +313,12 @@ func TestManager_ModeColors(t *testing.T) { | |||||
| 313 | assert.Equal(t, "#0000ff", modeColors.SessionMode) | 313 | assert.Equal(t, "#0000ff", modeColors.SessionMode) | ||
| 314 | assert.Equal(t, "#ffff00", modeColors.ResizeMode) | 314 | assert.Equal(t, "#ffff00", modeColors.ResizeMode) | ||
| 315 | } | 315 | } | ||
| 316 | + | ||||
| 317 | + | func TestFormatGTKFontName_UsesScaledConfiguredSansFont(t *testing.T) { | |||
| 318 | + | assert.Equal(t, "Fira Sans 14", formatGTKFontName("Fira Sans", 1.3)) | |||
| 319 | + | } | |||
| 320 | + | ||||
| 321 | + | func TestFormatGTKFontName_DefaultsScaleAndRoundsToNearestPoint(t *testing.T) { | |||
| 322 | + | assert.Equal(t, "Inter 11", formatGTKFontName("Inter", 0)) | |||
| 323 | + | assert.Equal(t, "Inter 12", formatGTKFontName("Inter", 1.05)) | |||
| 324 | + | } | |||