mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-15 07:44:01 -04:00
feat(theme): get expressive ~ new bold color palette options
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user