feat(theme): get expressive ~ new bold color palette options

This commit is contained in:
purian23
2026-09-19 00:54:53 -04:00
parent 2447ebec4f
commit 79abcdfd7b
12 changed files with 597 additions and 42 deletions
+11 -1
View File
@@ -73,6 +73,8 @@ func init() {
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
cmd.Flags().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value")
cmd.Flags().String("seed-color", "", "Hex color to build the palette from instead of extracting one from the wallpaper")
cmd.Flags().String("spec", "", "Material color spec: 2021 (matugen native, default) or 2025 (Material 3 Expressive)")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
@@ -80,6 +82,7 @@ func init() {
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
matugenPreviewCmd.Flags().String("image", "", "Wallpaper image used to resolve the scheme-smart preview")
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
matugenPreviewCmd.Flags().String("spec", "", "Material color spec: 2021 (matugen native, default) or 2025 (Material 3 Expressive)")
matugenQtengineCmd.Flags().String("config-dir", "", "User config directory")
matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name")
}
@@ -100,6 +103,8 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
contrast, _ := cmd.Flags().GetFloat64("contrast")
sourceMode, _ := cmd.Flags().GetString("source-mode")
seedColor, _ := cmd.Flags().GetString("seed-color")
spec, _ := cmd.Flags().GetString("spec")
return matugen.Options{
StateDir: stateDir,
@@ -117,6 +122,8 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
TerminalsAlwaysDark: terminalsAlwaysDark,
SkipTemplates: skipTemplates,
SourceMode: sourceMode,
SeedColor: seedColor,
Spec: spec,
}
}
@@ -155,6 +162,8 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
"skipTemplates": opts.SkipTemplates,
"contrast": opts.Contrast,
"sourceMode": opts.SourceMode,
"seedColor": opts.SeedColor,
"spec": opts.Spec,
"wait": wait,
},
}
@@ -228,7 +237,8 @@ func runMatugenPreview(cmd *cobra.Command, args []string) {
sourceColor, _ := cmd.Flags().GetString("source-color")
imagePath, _ := cmd.Flags().GetString("image")
contrast, _ := cmd.Flags().GetFloat64("contrast")
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath)
spec, _ := cmd.Flags().GetString("spec")
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath, spec)
if err != nil {
log.Fatalf("Failed to generate Matugen previews: %v", err)
}
+85 -22
View File
@@ -110,6 +110,8 @@ type Options struct {
MatugenType string
Contrast float64
SourceMode string
SeedColor string
Spec string
RunUserTemplates bool
ColorsOnly bool
StockColors string
@@ -145,6 +147,14 @@ func schemeColors(output, mode string) SchemeColors {
}
}
func nestedSchemeColors(colors, mode string) SchemeColors {
return SchemeColors{
Primary: extractNestedColor(colors, "primary", mode),
Secondary: extractNestedColor(colors, "secondary", mode),
Tertiary: extractNestedColor(colors, "tertiary", mode),
}
}
var previewSchemeTypes = []string{
"scheme-tonal-spot",
"scheme-vibrant",
@@ -157,13 +167,21 @@ var previewSchemeTypes = []string{
"scheme-rainbow",
}
func PreviewSchemes(sourceColor string, contrast float64, imagePath string) (map[string]SchemePreview, error) {
func PreviewSchemes(sourceColor string, contrast float64, imagePath, spec string) (map[string]SchemePreview, error) {
if sourceColor == "" {
return nil, fmt.Errorf("source color is required")
}
previews := make(map[string]SchemePreview, len(previewSchemeTypes)+1)
for _, schemeType := range previewSchemeTypes {
if spec == Spec2025 {
colors, err := GenerateSpecColors(sourceColor, schemeType, contrast, ColorModeDark, Spec2025)
if err != nil {
return nil, fmt.Errorf("preview %s: %w", schemeType, err)
}
previews[schemeType] = SchemePreview{Dark: nestedSchemeColors(colors, "dark"), Light: nestedSchemeColors(colors, "light")}
continue
}
output, err := runMatugenDryRun(&Options{
Kind: "hex",
Value: sourceColor,
@@ -183,25 +201,27 @@ func PreviewSchemes(sourceColor string, contrast float64, imagePath string) (map
previews[schemeType] = SchemePreview{Dark: dark, Light: light}
}
previews["scheme-smart"] = smartSchemePreview(previews["scheme-tonal-spot"], contrast, imagePath)
previews["scheme-smart"] = smartSchemePreview(previews["scheme-tonal-spot"], sourceColor, contrast, imagePath)
return previews, nil
}
func smartSchemePreview(fallback SchemePreview, contrast float64, imagePath string) SchemePreview {
if imagePath == "" {
return fallback
}
func smartSchemePreview(fallback SchemePreview, sourceColor string, contrast float64, imagePath string) SchemePreview {
flags, err := detectMatugenVersion()
if err != nil || !flags.isV42 {
return fallback
}
output, err := runMatugenDryRun(&Options{
Kind: "image",
Value: imagePath,
opts := &Options{
Kind: "hex",
Value: sourceColor,
Mode: ColorModeDark,
MatugenType: "scheme-smart",
Contrast: contrast,
})
}
if imagePath != "" {
opts.Kind = "image"
opts.Value = imagePath
}
output, err := runMatugenDryRun(opts)
if err != nil {
log.Warnf("Smart scheme preview failed falling back to tonal-spot: %v", err)
return fallback
@@ -368,6 +388,23 @@ func buildOnce(opts *Options) (bool, error) {
// they cannot disagree about the seed. Extraction failure (a format
// image.Decode cannot read, an unreadable file) falls through to matugen's
// own extraction: this must never fail a theme build.
if opts.StockColors == "" && opts.SeedColor != "" {
seed, err := NormalizeHexColor(opts.SeedColor)
if err != nil {
return false, err
}
log.Infof("Seed color override: %s -> %s", opts.Value, seed)
if opts.Kind == "image" {
sourceImage = opts.Value
if abs, err := filepath.Abs(sourceImage); err == nil {
sourceImage = abs
}
}
opts.Kind = "hex"
opts.Value = seed
opts.SourceMode = SourceModeDominant
}
if opts.StockColors == "" && opts.Kind == "image" && opts.SourceMode == SourceModeColorful {
if seed, err := ExtractSourceColor(opts.Value); err != nil {
log.Warnf("Colorful source extraction failed for %s, using matugen's own: %v", opts.Value, err)
@@ -418,12 +455,33 @@ func buildOnce(opts *Options) (bool, error) {
return false, fmt.Errorf("matugen dry-run failed: %w", err)
}
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
primaryLight = extractMatugenColor(matJSON, "primary", "light")
surfaceDark = extractMatugenColor(matJSON, "surface", "dark")
surfaceLight = extractMatugenColor(matJSON, "surface", "light")
containerDark = extractMatugenColor(matJSON, "primary_container", "dark")
containerLight = extractMatugenColor(matJSON, "primary_container", "light")
// The 2025 spec is generated here from the seed matugen settled on and
// overrides matugen's own roles through the import, so templates,
// {{image}} and the user's own config keep working unchanged.
var specColors string
if opts.Spec == Spec2025 && SpecSupportsScheme(opts.MatugenType) {
seed := opts.Value
if opts.Kind != "hex" {
seed = extractMatugenColor(matJSON, "source_color", "dark")
}
specColors, err = GenerateSpecColors(seed, opts.MatugenType, opts.Contrast, opts.Mode, Spec2025)
if err != nil {
return false, fmt.Errorf("spec 2025 palette failed: %w", err)
}
}
extract := func(role, mode string) string {
if specColors != "" {
return extractNestedColor(specColors, role, mode)
}
return extractMatugenColor(matJSON, role, mode)
}
primaryDark = extract("primary", "dark")
primaryLight = extract("primary", "light")
surfaceDark = extract("surface", "dark")
surfaceLight = extract("surface", "light")
containerDark = extract("primary_container", "dark")
containerLight = extract("primary_container", "light")
if primaryDark == "" {
return false, fmt.Errorf("failed to extract primary color")
@@ -433,7 +491,7 @@ func buildOnce(opts *Options) (bool, error) {
}
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight, opts.Mode)
importArgs = []string{"--import-json-string", buildImportData(dank16JSON, sourceImage)}
importArgs = []string{"--import-json-string", buildImportData(dank16JSON, sourceImage, specColors)}
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
var args []string
@@ -512,12 +570,17 @@ func appendContrastArg(args []string, contrast float64) []string {
// buildImportData is the JSON passed to matugen's --import-json-string. image is
// set only when the source was rewritten from a wallpaper to a hex color, where
// matugen leaves {{image}} unset and templates using it would render "Null".
func buildImportData(dank16JSON, image string) string {
if image == "" {
return fmt.Sprintf(`{"dank16": %s}`, dank16JSON)
// colors, when set, is a full role map that replaces matugen's own palette.
func buildImportData(dank16JSON, image, colors string) string {
fields := []string{fmt.Sprintf(`"dank16": %s`, dank16JSON)}
if image != "" {
path, _ := json.Marshal(image)
fields = append(fields, fmt.Sprintf(`"image": %s`, path))
}
path, _ := json.Marshal(image)
return fmt.Sprintf(`{"dank16": %s, "image": %s}`, dank16JSON, path)
if colors != "" {
fields = append(fields, fmt.Sprintf(`"colors": %s`, colors))
}
return "{" + strings.Join(fields, ", ") + "}"
}
func userConfigSection(opts *Options) string {
+3 -3
View File
@@ -416,12 +416,12 @@ func TestBuildMatugenArgsDefaultPreservesExistingBehavior(t *testing.T) {
func TestBuildImportData(t *testing.T) {
const dank16 = `{"color0":"#000000"}`
assert.Equal(t, `{"dank16": {"color0":"#000000"}}`, buildImportData(dank16, ""),
assert.Equal(t, `{"dank16": {"color0":"#000000"}}`, buildImportData(dank16, "", ""),
"no image must produce byte-identical import data to pre-feature behavior")
assert.Equal(t, `{"dank16": {"color0":"#000000"}, "image": "/home/u/My Wallpaper.png"}`,
buildImportData(dank16, "/home/u/My Wallpaper.png"))
buildImportData(dank16, "/home/u/My Wallpaper.png", ""))
assert.Equal(t, `{"dank16": {"color0":"#000000"}, "image": "/home/u/a\"b\\c.png"}`,
buildImportData(dank16, `/home/u/a"b\c.png`), "paths must be escaped, not interpolated raw")
buildImportData(dank16, `/home/u/a"b\c.png`, ""), "paths must be escaped, not interpolated raw")
}
// writeTestPNG encodes img as a PNG at path, failing the test on any error.
+122
View File
@@ -0,0 +1,122 @@
package matugen
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/Nadim147c/material/v3/color"
"github.com/Nadim147c/material/v3/dynamic"
)
// Palette spec versions. Spec2021 is matugen's own output and the historical
// default. Spec2025 is the Material 3 Expressive color spec, which matugen 4.x
// cannot produce: the palette is generated in-process and handed to matugen
// through --import-json-string, the same override path stock themes use.
const (
Spec2021 = "2021"
Spec2025 = "2025"
)
var hexColorPattern = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`)
// NormalizeHexColor returns "#rrggbb" or an error. matugen and the shell both
// compare generated colors as strings, so casing must not vary between runs.
func NormalizeHexColor(value string) (string, error) {
value = strings.TrimSpace(value)
if !hexColorPattern.MatchString(value) {
return "", fmt.Errorf("invalid hex color %q", value)
}
return "#" + strings.ToLower(strings.TrimPrefix(value, "#")), nil
}
var schemeVariants = map[string]dynamic.Variant{
"scheme-tonal-spot": dynamic.VariantTonalSpot,
"scheme-vibrant": dynamic.VariantVibrant,
"scheme-content": dynamic.VariantContent,
"scheme-expressive": dynamic.VariantExpressive,
"scheme-fidelity": dynamic.VariantFidelity,
"scheme-fruit-salad": dynamic.VariantFruitSalad,
"scheme-monochrome": dynamic.VariantMonochrome,
"scheme-neutral": dynamic.VariantNeutral,
"scheme-rainbow": dynamic.VariantRainbow,
}
// SpecSupportsScheme reports whether the in-process generator can build the
// scheme. scheme-smart is chosen by matugen at run time and is not exposed in
// its output, so it stays on matugen's native palette.
func SpecSupportsScheme(schemeType string) bool {
_, ok := schemeVariants[schemeType]
return ok
}
// GenerateSpecColors builds the full Material color map for a seed under the
// given spec version and returns it in matugen's {"role": {"dark","light",
// "default": {"color"}}} shape. defaultMode picks which side "default" mirrors.
func GenerateSpecColors(seedHex, schemeType string, contrast float64, defaultMode ColorMode, version string) (string, error) {
variant, ok := schemeVariants[schemeType]
if !ok {
return "", fmt.Errorf("scheme %q is not supported by the in-process generator", schemeType)
}
seed, err := NormalizeHexColor(seedHex)
if err != nil {
return "", err
}
specVersion := dynamic.Version2021
if version == Spec2025 {
specVersion = dynamic.Version2025
// The 2025 spec has no reduced-contrast schemes.
contrast = max(contrast, 0)
}
seedArgb, err := color.ARGBFromHex(seed)
if err != nil {
return "", err
}
// Colors.Map() in material/v3 v3.1.1 is backed by a field nothing fills,
// so the roles are read off the scheme directly.
generate := func(dark bool) map[string]string {
scheme := dynamic.NewDynamicScheme(seedArgb.ToHct(), variant, contrast, dark, dynamic.PlatformPhone, specVersion)
roles := scheme.ToColorMap()
out := make(map[string]string, len(roles)+1)
for name, role := range roles {
if role == nil {
continue
}
out[name] = strings.ToLower(role.GetArgb(scheme).HexRGB())
}
out["source_color"] = seed
return out
}
dark := generate(true)
light := generate(false)
type shade struct {
Color string `json:"color"`
}
type role struct {
Dark shade `json:"dark"`
Light shade `json:"light"`
Default shade `json:"default"`
}
roles := make(map[string]role, len(dark))
for name, darkHex := range dark {
lightHex, ok := light[name]
if !ok {
continue
}
def := darkHex
if defaultMode == ColorModeLight {
def = lightHex
}
roles[name] = role{Dark: shade{darkHex}, Light: shade{lightHex}, Default: shade{def}}
}
data, err := json.Marshal(roles)
if err != nil {
return "", err
}
return string(data), nil
}
+190
View File
@@ -0,0 +1,190 @@
package matugen
import (
"encoding/json"
"image"
"image/png"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/stretchr/testify/require"
)
func TestNormalizeHexColor(t *testing.T) {
for _, in := range []string{"#FF3D00", "ff3d00", " #ff3d00 "} {
got, err := NormalizeHexColor(in)
require.NoError(t, err, in)
require.Equal(t, "#ff3d00", got, in)
}
for _, in := range []string{"", "#fff", "#ff3d00aa", "red", "#gg3d00", "#ff3d00; rm -rf"} {
_, err := NormalizeHexColor(in)
require.Error(t, err, in)
}
}
func TestGenerateSpecColorsRejectsSmart(t *testing.T) {
_, err := GenerateSpecColors("#1e88e5", "scheme-smart", 0, ColorModeDark, Spec2025)
require.Error(t, err)
require.False(t, SpecSupportsScheme("scheme-smart"))
require.True(t, SpecSupportsScheme("scheme-vibrant"))
}
func TestGenerateSpecColors2025IsBolderThan2021(t *testing.T) {
colors2021 := decodeSpecColors(t, "#ff3d00", "scheme-vibrant", Spec2021)
colors2025 := decodeSpecColors(t, "#ff3d00", "scheme-vibrant", Spec2025)
require.Equal(t, "#ffb4a2", colors2021["primary"].Dark.Color)
require.Equal(t, "#ff7c5b", colors2025["primary"].Dark.Color)
require.Equal(t, colors2025["primary"].Dark.Color, colors2025["primary"].Default.Color)
require.Equal(t, "#ff3d00", colors2025["source_color"].Dark.Color)
}
func TestGenerateSpecColorsDefaultFollowsMode(t *testing.T) {
colors := decodeSpecColorsMode(t, "#1e88e5", "scheme-tonal-spot", Spec2021, ColorModeLight)
require.Equal(t, colors["primary"].Light.Color, colors["primary"].Default.Color)
require.NotEqual(t, colors["primary"].Dark.Color, colors["primary"].Default.Color)
}
func TestPreviewSchemesSmartMatchesNativeHex(t *testing.T) {
if !SupportsSmart() {
t.Skip("matugen 4.2+ required")
}
for _, seed := range []string{"#ff3d00", "#1e88e5"} {
out, err := runMatugenDryRun(&Options{Kind: "hex", Value: seed, Mode: ColorModeDark, MatugenType: "scheme-smart"})
require.NoError(t, err)
expected := SchemePreview{Dark: schemeColors(out, "dark"), Light: schemeColors(out, "light")}
require.NotEmpty(t, expected.Dark.Primary)
for _, spec := range []string{Spec2021, Spec2025} {
t.Run(seed+"/"+spec, func(t *testing.T) {
previews, err := PreviewSchemes(seed, 0, "", spec)
require.NoError(t, err)
require.Equal(t, expected, previews["scheme-smart"])
})
}
}
}
// The Go port and matugen's Rust port round differently in the last bit, and
// matugen still uses the pre-2023 on_*_container tones, so exact parity is not
// expected. The accent and surface roles the shell builds everything from must
// agree to within two channel steps or the in-process path is not the same
// palette users see today.
func TestGenerateSpecColors2021MatchesMatugen(t *testing.T) {
if _, err := exec.LookPath("matugen"); err != nil {
t.Skip("matugen not installed")
}
for _, seed := range []string{"#1e88e5", "#ff3d00", "#8bc34a"} {
out, err := exec.Command("matugen", "color", "hex", seed, "-t", "scheme-vibrant", "-m", "dark", "--json", "hex", "--dry-run", "-q").Output()
require.NoError(t, err)
var matugenOut struct {
Colors map[string]specRole `json:"colors"`
}
require.NoError(t, json.Unmarshal(out, &matugenOut))
ours := decodeSpecColors(t, seed, "scheme-vibrant", Spec2021)
for name := range matugenOut.Colors {
require.Contains(t, ours, name, "role %s missing from generator", name)
}
for _, name := range []string{"primary", "primary_container", "secondary", "tertiary", "surface", "surface_container", "on_surface"} {
theirs, mine := matugenOut.Colors[name], ours[name]
require.LessOrEqual(t, maxChannelDelta(t, theirs.Dark.Color, mine.Dark.Color), 2, "%s %s dark %s vs %s", seed, name, theirs.Dark.Color, mine.Dark.Color)
require.LessOrEqual(t, maxChannelDelta(t, theirs.Light.Color, mine.Light.Color), 2, "%s %s light %s vs %s", seed, name, theirs.Light.Color, mine.Light.Color)
}
}
}
func maxChannelDelta(t *testing.T, a, b string) int {
t.Helper()
require.Len(t, a, 7)
require.Len(t, b, 7)
delta := 0
for i := 1; i < 7; i += 2 {
x, err := strconv.ParseInt(a[i:i+2], 16, 0)
require.NoError(t, err)
y, err := strconv.ParseInt(b[i:i+2], 16, 0)
require.NoError(t, err)
if d := int(x - y); d > delta {
delta = d
} else if -d > delta {
delta = -d
}
}
return delta
}
type specShade struct {
Color string `json:"color"`
}
type specRole struct {
Dark specShade `json:"dark"`
Light specShade `json:"light"`
Default specShade `json:"default"`
}
func decodeSpecColors(t *testing.T, seed, scheme, version string) map[string]specRole {
return decodeSpecColorsMode(t, seed, scheme, version, ColorModeDark)
}
func decodeSpecColorsMode(t *testing.T, seed, scheme, version string, mode ColorMode) map[string]specRole {
t.Helper()
raw, err := GenerateSpecColors(seed, scheme, 0, mode, version)
require.NoError(t, err)
var roles map[string]specRole
require.NoError(t, json.Unmarshal([]byte(raw), &roles))
return roles
}
// Runs the real matugen against the repo's dank.json template, colors only,
// into a temp state dir: the shell must read back the seed and the 2025 primary
// from dms-colors.json.
func TestBuildOnceSeedColorAndSpec2025(t *testing.T) {
if _, err := exec.LookPath("matugen"); err != nil {
t.Skip("matugen not installed")
}
shellDir, err := filepath.Abs(filepath.Join("..", "..", "..", "quickshell"))
require.NoError(t, err)
if _, err := os.Stat(filepath.Join(shellDir, "matugen", "templates", "dank.json")); err != nil {
t.Skip("quickshell dir not available")
}
wallpaper := filepath.Join(t.TempDir(), "wall.png")
img := image.NewRGBA(image.Rect(0, 0, 8, 8))
for i := range img.Pix {
img.Pix[i] = 0x40
}
f, err := os.Create(wallpaper)
require.NoError(t, err)
require.NoError(t, png.Encode(f, img))
require.NoError(t, f.Close())
opts := &Options{
StateDir: t.TempDir(),
ShellDir: shellDir,
ConfigDir: t.TempDir(),
Kind: "image",
Value: wallpaper,
Mode: ColorModeDark,
MatugenType: "scheme-vibrant",
SeedColor: "#FF3D00",
Spec: Spec2025,
ColorsOnly: true,
AppChecker: utils.DefaultAppChecker{},
}
changed, err := buildOnce(opts)
require.NoError(t, err)
require.True(t, changed)
raw, err := os.ReadFile(opts.ColorsOutput())
require.NoError(t, err)
var out struct {
Colors struct {
Dark map[string]string `json:"dark"`
} `json:"colors"`
}
require.NoError(t, json.Unmarshal(raw, &out))
require.Equal(t, "#ff3d00", out.Colors.Dark["source_color"])
require.Equal(t, "#ff7c5b", out.Colors.Dark["primary"])
}
+2
View File
@@ -31,6 +31,8 @@ func handleMatugenQueue(conn *ipc.ConnWriter, req ipc.Request) {
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
Contrast: models.GetOr(req, "contrast", 0.0),
SourceMode: models.GetOr(req, "sourceMode", ""),
SeedColor: models.GetOr(req, "seedColor", ""),
Spec: models.GetOr(req, "spec", ""),
}
wait := models.GetOr(req, "wait", true)
+18
View File
@@ -168,6 +168,8 @@ Singleton {
property bool matugenSmartMode: Spec.SPEC.matugenSmartMode.def
property string matugenSourceMode: Spec.SPEC.matugenSourceMode.def
property real matugenContrast: Spec.SPEC.matugenContrast.def
property string matugenSeedColor: Spec.SPEC.matugenSeedColor.def
property string matugenSpec: Spec.SPEC.matugenSpec.def
property bool runUserMatugenTemplates: Spec.SPEC.runUserMatugenTemplates.def
property string matugenTargetMonitor: Spec.SPEC.matugenTargetMonitor.def
property real popupTransparency: Spec.SPEC.popupTransparency.def
@@ -2506,6 +2508,22 @@ Singleton {
set("matugenContrast", value);
}
function setMatugenSeedColor(hex) {
var normalized = /^#[0-9a-f]{6}$/i.test(hex || "") ? hex.toLowerCase() : "";
if (matugenSeedColor === normalized)
return;
set("matugenSeedColor", normalized);
}
function setMatugenSpec(spec) {
var normalized = spec === "2025" ? "2025" : "2021";
if (matugenSpec === normalized)
return;
if (normalized === "2025" && matugenContrast < 0)
set("matugenContrast", 0);
set("matugenSpec", normalized);
}
function setMatugenTargetMonitor(monitorName) {
if (matugenTargetMonitor === monitorName)
return;
+7 -1
View File
@@ -1777,9 +1777,15 @@ Singleton {
// binary is a supported setup (DMS_SHELL_DIR / -c), and an older binary
// exits with "unknown flag: --source-mode" rather than ignoring it, so
// the default must not put the flag on the command line at all.
if (typeof SettingsData !== "undefined" && SettingsData.matugenSourceMode && SettingsData.matugenSourceMode !== "dominant") {
const seedColor = (typeof SettingsData !== "undefined" && !stockColors) ? SettingsData.matugenSeedColor : "";
if (seedColor) {
args.push("--seed-color", seedColor);
} else if (typeof SettingsData !== "undefined" && SettingsData.matugenSourceMode && SettingsData.matugenSourceMode !== "dominant") {
args.push("--source-mode", SettingsData.matugenSourceMode);
}
if (typeof SettingsData !== "undefined" && !stockColors && SettingsData.matugenSpec === "2025") {
args.push("--spec", "2025");
}
if (typeof SettingsData !== "undefined") {
const skipTemplates = [];
@@ -25,6 +25,14 @@ var LOCAL_SPEC = {
def: 0,
onChange: "regenSystemThemes"
},
matugenSeedColor: {
def: "",
onChange: "regenSystemThemes"
},
matugenSpec: {
def: "2021",
onChange: "regenSystemThemes"
},
runUserMatugenTemplates: {
def: true,
onChange: "regenSystemThemes"
+79 -14
View File
@@ -18,9 +18,9 @@ Item {
property var matugenSchemePreviews: ({})
property string matugenPreviewRequestKey: ""
property string matugenPreviewLoadedKey: ""
readonly property string matugenPreviewSource: Theme.getMatugenColor("source_color", Theme.primary).toString()
readonly property string matugenPreviewImage: (Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : ""
readonly property string matugenPreviewKey: matugenPreviewSource + "|" + (SettingsData.matugenContrast ?? 0) + "|" + matugenPreviewImage
readonly property string matugenPreviewSource: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary).toString()
readonly property string matugenPreviewImage: (!SettingsData.matugenSeedColor && Theme.rawWallpaperPath && !Theme.rawWallpaperPath.startsWith("#")) ? Theme.rawWallpaperPath : ""
readonly property string matugenPreviewKey: matugenPreviewSource + "|" + (SettingsData.matugenContrast ?? 0) + "|" + matugenPreviewImage + "|" + SettingsData.matugenSpec
property bool matugenGenerating: false
readonly property var currentPalette: ThemePalette.pick({
"primary": Theme.primary,
@@ -67,6 +67,8 @@ Item {
const args = [Proc.dmsBin, "matugen", "preview", "--source-color", matugenPreviewSource, "--contrast", String(SettingsData.matugenContrast ?? 0)];
if (matugenPreviewImage)
args.push("--image", matugenPreviewImage);
if (SettingsData.matugenSpec === "2025")
args.push("--spec", "2025");
Proc.runCommand("", args, (output, exitCode) => {
if (requestKey !== themeColorsTab.matugenPreviewRequestKey)
return;
@@ -431,21 +433,29 @@ Item {
enabled: Theme.matugenAvailable
body: Item {
width: parent.width
height: themeColorsTab.matugenPreviewsReady ? schemeGrid.implicitHeight : Theme.minimumTouchTargetSize
// the grid keeps its height while previews regenerate so the page does not jump
height: schemeGrid.implicitHeight
SettingsSwatchGrid {
id: schemeGrid
options: themeColorsTab.matugenSchemeOptions
currentValue: SettingsData.matugenScheme
enabled: themeColorsTab.matugenPreviewsReady
opacity: themeColorsTab.matugenPreviewsReady ? 1 : Theme.pendingOpacity
onSelected: value => SettingsData.setMatugenScheme(value)
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
DankSpinner {
anchors.centerIn: parent
running: !themeColorsTab.matugenPreviewsReady
visible: running
}
SettingsSwatchGrid {
id: schemeGrid
visible: themeColorsTab.matugenPreviewsReady
options: themeColorsTab.matugenSchemeOptions
currentValue: SettingsData.matugenScheme
onSelected: value => SettingsData.setMatugenScheme(value)
}
}
}
@@ -470,7 +480,7 @@ Item {
text: I18n.tr("Source color")
options: cachedSourceModes
currentValue: Theme.getSourceMode(SettingsData.matugenSourceMode).label
enabled: Theme.matugenAvailable
enabled: Theme.matugenAvailable && !SettingsData.matugenSeedColor
onValueChanged: value => {
for (var i = 0; i < Theme.availableSourceModes.length; i++) {
var option = Theme.availableSourceModes[i];
@@ -482,7 +492,57 @@ Item {
}
}
ColorDropdownRow {
visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
tab: "theme"
tags: ["matugen", "seed", "pick", "eyedropper", "dynamic"]
settingKey: "matugenSeedColor"
text: I18n.tr("Derived color")
enabled: Theme.matugenAvailable
options: [
{
"value": "default",
"previewColor": Theme.getMatugenColor("source_color", Theme.primary),
"label": I18n.tr("From wallpaper", "matugen seed color option")
},
{
"value": "custom",
"label": I18n.tr("Custom")
}
]
currentMode: SettingsData.matugenSeedColor ? "custom" : "default"
customColor: SettingsData.matugenSeedColor || Theme.getMatugenColor("source_color", Theme.primary)
pickerTitle: I18n.tr("Seed color")
onModeSelected: mode => {
if (mode !== "custom") {
SettingsData.setMatugenSeedColor("");
return;
}
if (SettingsData.matugenSeedColor)
return;
SettingsData.setMatugenSeedColor(Theme.getMatugenColor("source_color", Theme.primary).toString());
}
onCustomColorSelected: selectedColor => SettingsData.setMatugenSeedColor(Theme.withAlpha(selectedColor, 1).toString())
}
SettingsButtonGroupRow {
visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
tab: "theme"
tags: ["matugen", "spec", "expressive", "vivid", "saturated", "bold", "dynamic"]
settingKey: "matugenSpec"
text: I18n.tr("Material palette")
enabled: Theme.matugenAvailable
model: [I18n.tr("Standard", "adjective, panel motion option and bar layout mode option"), I18n.tr("Expressive", "matugen color scheme option")]
currentIndex: SettingsData.matugenSpec === "2025" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.setMatugenSpec(index === 1 ? "2025" : "2021");
}
}
SettingsSliderRow {
id: contrastRow
visible: Theme.currentTheme === Theme.dynamic && Theme.currentThemeCategory !== "registry"
tab: "theme"
tags: ["matugen", "contrast", "dynamic"]
@@ -492,7 +552,12 @@ Item {
minimum: -100
maximum: 100
enabled: Theme.matugenAvailable
onSliderDragFinished: finalValue => SettingsData.setMatugenContrast(finalValue / 100)
onSliderDragFinished: finalValue => {
const clamped = SettingsData.matugenSpec === "2025" ? Math.max(0, finalValue) : finalValue;
SettingsData.setMatugenContrast(clamped / 100);
if (clamped !== finalValue)
contrastRow.resync();
}
}
Column {
@@ -33,9 +33,13 @@ SettingsRow {
sliderDragFinished(next);
}
function resync() {
slider.value = Math.round(value);
}
title: text
subtitle: description
onValueChanged: slider.value = Math.round(value)
onValueChanged: resync()
StyledText {
text: root.minimumLabel
@@ -5051,6 +5051,73 @@
],
"parentLabel": "Wallpaper & colors"
},
{
"section": "matugenSeedColor",
"label": "Derived color",
"tabIndex": 10,
"category": "Theme & colors",
"keywords": [
"appearance",
"background",
"bg",
"color",
"colors",
"colour",
"colours",
"derived",
"desktop",
"dynamic",
"eyedropper",
"hue",
"image",
"look",
"matugen",
"palette",
"pick",
"picture",
"scheme",
"seed",
"style",
"theme",
"tint",
"wallpaper"
],
"parentLabel": "Wallpaper & colors"
},
{
"section": "matugenSpec",
"label": "Material palette",
"tabIndex": 10,
"category": "Theme & colors",
"keywords": [
"appearance",
"background",
"bg",
"bold",
"colors",
"colour",
"colours",
"desktop",
"dynamic",
"expressive",
"hue",
"image",
"look",
"material",
"matugen",
"palette",
"picture",
"saturated",
"scheme",
"spec",
"style",
"theme",
"tint",
"vivid",
"wallpaper"
],
"parentLabel": "Wallpaper & colors"
},
{
"section": "matugenScheme",
"label": "Matugen palette",