diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java index 59576d14..367858c9 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java @@ -1109,13 +1109,14 @@ class UIDefaultsLoader } /** - * Syntax: mix(color1,color2[,weight]) or - * tint(color[,weight]) or - * shade(color[,weight]) + * Syntax: mix(color1,color2[,weight][,options]) or + * tint(color[,weight][,options]) or + * shade(color[,weight][,options]) * - color1: a color (e.g. #f00) or a color function * - color2: a color (e.g. #f00) or a color function * - weight: the weight (in range 0-100%) to mix the two colors * larger weight uses more of first color, smaller weight more of second color + * - options: [derived] */ private static Object parseColorMix( String color1Str, List params, Function resolver ) throws IllegalArgumentException @@ -1124,18 +1125,31 @@ class UIDefaultsLoader if( color1Str == null ) color1Str = params.get( i++ ); String color2Str = params.get( i++ ); - int weight = (params.size() > i) ? parsePercentage( params.get( i ) ) : 50; + int weight = 50; + boolean derived = false; + + if( params.size() > i ) { + String weightStr = params.get( i ); + if( !weightStr.isEmpty() && Character.isDigit( weightStr.charAt( 0 ) ) ) { + weight = parsePercentage( weightStr ); + i++; + } + } + if( params.size() > i ) { + String options = params.get( i ); + derived = options.contains( "derived" ); + } // parse second color - ColorUIResource color2 = (ColorUIResource) parseColorOrFunction( resolver.apply( color2Str ), resolver ); - if( color2 == null ) + ColorUIResource color1 = (ColorUIResource) parseColorOrFunction( resolver.apply( color1Str ), resolver ); + if( color1 == null ) return null; // create function - ColorFunction function = new ColorFunctions.Mix( color2, weight ); + ColorFunction function = new ColorFunctions.Mix2( color1, weight ); // parse first color, apply function and create mixed color - return parseFunctionBaseColor( color1Str, function, false, resolver ); + return parseFunctionBaseColor( color2Str, function, derived, resolver ); } /** diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java index fc5f58d2..c480cce6 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java @@ -224,6 +224,9 @@ public class ColorFunctions if( functions.length == 1 && functions[0] instanceof Mix ) { Mix mixFunction = (Mix) functions[0]; return mix( color, mixFunction.color2, mixFunction.weight / 100 ); + } else if( functions.length == 1 && functions[0] instanceof Mix2 ) { + Mix2 mixFunction = (Mix2) functions[0]; + return mix( mixFunction.color1, color, mixFunction.weight / 100 ); } // convert RGB to HSL @@ -386,7 +389,11 @@ public class ColorFunctions //---- class Mix ---------------------------------------------------------- /** - * Mix two colors. + * Mix two colors using {@link ColorFunctions#mix(Color, Color, float)}. + * First color is passed to {@link #apply(float[])}. + * Second color is {@link #color2}. + *

+ * Use {@link Mix2} to tint or shade color. * * @since 1.6 */ @@ -420,4 +427,44 @@ public class ColorFunctions return String.format( "mix(#%08x,%.0f%%)", color2.getRGB(), weight ); } } + + //---- class Mix2 --------------------------------------------------------- + + /** + * Mix two colors using {@link ColorFunctions#mix(Color, Color, float)}. + * First color is {@link #color1}. + * Second color is passed to {@link #apply(float[])}. + * + * @since 3.6 + */ + public static class Mix2 + implements ColorFunction + { + public final Color color1; + public final float weight; + + public Mix2( Color color1, float weight ) { + this.color1 = color1; + this.weight = weight; + } + + @Override + public void apply( float[] hsla ) { + // convert from HSL to RGB because color mixing is done on RGB values + Color color2 = HSLColor.toRGB( hsla[0], hsla[1], hsla[2], hsla[3] / 100 ); + + // mix + Color color = mix( color1, color2, weight / 100 ); + + // convert RGB to HSL + float[] hsl = HSLColor.fromRGB( color ); + System.arraycopy( hsl, 0, hsla, 0, hsl.length ); + hsla[3] = (color.getAlpha() / 255f) * 100; + } + + @Override + public String toString() { + return String.format( "mix2(#%08x,%.0f%%)", color1.getRGB(), weight ); + } + } } diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java index 798daa9a..73f75419 100644 --- a/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/TestUIDefaultsLoader.java @@ -17,6 +17,8 @@ package com.formdev.flatlaf; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; @@ -29,6 +31,13 @@ import javax.swing.UIDefaults.LazyValue; import org.junit.jupiter.api.Test; import com.formdev.flatlaf.ui.FlatEmptyBorder; import com.formdev.flatlaf.ui.FlatLineBorder; +import com.formdev.flatlaf.util.DerivedColor; +import com.formdev.flatlaf.util.ColorFunctions.ColorFunction; +import com.formdev.flatlaf.util.ColorFunctions.Fade; +import com.formdev.flatlaf.util.ColorFunctions.HSLChange; +import com.formdev.flatlaf.util.ColorFunctions.HSLIncreaseDecrease; +import com.formdev.flatlaf.util.ColorFunctions.Mix; +import com.formdev.flatlaf.util.ColorFunctions.Mix2; /** * @author Karl Tauber @@ -180,6 +189,217 @@ public class TestUIDefaultsLoader assertEquals( expected, ((LazyValue)UIDefaultsLoader.parseValue( "dummyIcon", value, null )).createValue( null ) ); } + @Test + void parseColorFunctions() { + // lighten + assertEquals( new Color( 0xff6666 ), parseColor( "lighten(#f00, 20%)" ) ); + assertEquals( new Color( 0xff3333 ), parseColor( "lighten(#f00, 20%, relative)" ) ); + assertEquals( new Color( 0xaaaaaa ), parseColor( "lighten(#ddd, 20%, autoInverse)" ) ); + assertEquals( new Color( 0xb1b1b1 ), parseColor( "lighten(#ddd, 20%, relative autoInverse)" ) ); + + // darken + assertEquals( new Color( 0x990000 ), parseColor( "darken(#f00, 20%)" ) ); + assertEquals( new Color( 0xcc0000 ), parseColor( "darken(#f00, 20%, relative)" ) ); + assertEquals( new Color( 0x555555 ), parseColor( "darken(#222, 20%, autoInverse)" ) ); + assertEquals( new Color( 0x292929 ), parseColor( "darken(#222, 20%, relative autoInverse)" ) ); + + // saturate + assertEquals( new Color( 0xf32e2e ), parseColor( "saturate(#d44, 20%)" ) ); + assertEquals( new Color( 0xec3535 ), parseColor( "saturate(#d44, 20%, relative)" ) ); + assertEquals( new Color( 0xc75a5a ), parseColor( "saturate(#d44, 20%, autoInverse)" ) ); + assertEquals( new Color( 0xce5353 ), parseColor( "saturate(#d44, 20%, relative autoInverse)" ) ); + + // desaturate + assertEquals( new Color( 0x745858 ), parseColor( "desaturate(#844, 20%)" ) ); + assertEquals( new Color( 0x814b4b ), parseColor( "desaturate(#844, 20%, relative)" ) ); + assertEquals( new Color( 0x9c3030 ), parseColor( "desaturate(#844, 20%, autoInverse)" ) ); + assertEquals( new Color( 0x8f3d3d ), parseColor( "desaturate(#844, 20%, relative autoInverse)" ) ); + + // fadein + assertEquals( new Color( 0xddff0000, true ), parseColor( "fadein(#f00a, 20%)" ) ); + assertEquals( new Color( 0xccff0000, true ), parseColor( "fadein(#f00a, 20%, relative)" ) ); + assertEquals( new Color( 0x77ff0000, true ), parseColor( "fadein(#f00a, 20%, autoInverse)" ) ); + assertEquals( new Color( 0x88ff0000, true ), parseColor( "fadein(#f00a, 20%, relative autoInverse)" ) ); + + // fadeout + assertEquals( new Color( 0x11ff0000, true ), parseColor( "fadeout(#f004, 20%)" ) ); + assertEquals( new Color( 0x36ff0000, true ), parseColor( "fadeout(#f004, 20%, relative)" ) ); + assertEquals( new Color( 0x77ff0000, true ), parseColor( "fadeout(#f004, 20%, autoInverse)" ) ); + assertEquals( new Color( 0x52ff0000, true ), parseColor( "fadeout(#f004, 20%, relative autoInverse)" ) ); + + // fade + assertEquals( new Color( 0x33ff0000, true ), parseColor( "fade(#f00, 20%)" ) ); + assertEquals( new Color( 0xccff0000, true ), parseColor( "fade(#ff000010, 80%)" ) ); + + // spin + assertEquals( new Color( 0xffaa00 ), parseColor( "spin(#f00, 40)" ) ); + assertEquals( new Color( 0xff00aa ), parseColor( "spin(#f00, -40)" ) ); + + // changeHue / changeSaturation / changeLightness / changeAlpha + assertEquals( new Color( 0xffaa00 ), parseColor( "changeHue(#f00, 40)" ) ); + assertEquals( new Color( 0xb34d4d ), parseColor( "changeSaturation(#f00, 40%)" ) ); + assertEquals( new Color( 0xcc0000 ), parseColor( "changeLightness(#f00, 40%)" ) ); + assertEquals( new Color( 0x66ff0000, true ), parseColor( "changeAlpha(#f00, 40%)" ) ); + + // mix + assertEquals( new Color( 0x808000 ), parseColor( "mix(#f00, #0f0)" ) ); + assertEquals( new Color( 0xbf4000 ), parseColor( "mix(#f00, #0f0, 75%)" ) ); + + // tint + assertEquals( new Color( 0xff80ff ), parseColor( "tint(#f0f)" ) ); + assertEquals( new Color( 0xffbfff ), parseColor( "tint(#f0f, 75%)" ) ); + + // shade + assertEquals( new Color( 0x800080 ), parseColor( "shade(#f0f)" ) ); + assertEquals( new Color( 0x400040 ), parseColor( "shade(#f0f, 75%)" ) ); + + // contrast + assertEquals( new Color( 0x0000ff ), parseColor( "contrast(#bbb, #00f, #0f0)" ) ); + assertEquals( new Color( 0x00ff00 ), parseColor( "contrast(#444, #00f, #0f0)" ) ); + assertEquals( new Color( 0x00ff00 ), parseColor( "contrast(#bbb, #00f, #0f0, 60%)" ) ); + + // rgb / rgba + assertEquals( new Color( 0x5a8120 ), parseColor( "rgb(90, 129, 32)" ) ); + assertEquals( new Color( 0x5a8120 ), parseColor( "rgb(90, 129, 32)" ) ); + assertEquals( new Color( 0x197fb2 ), parseColor( "rgb(10%,50%,70%)" ) ); + assertEquals( new Color( 0x197f46 ), parseColor( "rgb(10%,50%,70)" ) ); + assertEquals( new Color( 0x405a8120, true ), parseColor( "rgba(90, 129, 32, 64)" ) ); + assertEquals( new Color( 0x335a8120, true ), parseColor( "rgba(90, 129, 32, 20%)" ) ); + + // hsl / hsla + assertEquals( new Color( 0x7fff00 ), parseColor( "hsl(90, 100%, 50%)" ) ); + assertEquals( new Color( 0x337fff00, true ), parseColor( "hsla(90, 100%, 50%, 20%)" ) ); + } + + @Test + void parseDerivedColorFunctions() { + // lighten, darken + + // mix + assertDerivedColorEquals( new Color( 0x808000 ), "mix(#f00, #0f0, derived)", new Mix2( Color.red, 50 ) ); + assertDerivedColorEquals( new Color( 0xbf4000 ), "mix(#f00, #0f0, 75%, derived)", new Mix2( Color.red, 75 ) ); + + // tint + assertDerivedColorEquals( new Color( 0xff80ff ), "tint(#f0f, derived)", new Mix2( Color.white, 50 ) ); + assertDerivedColorEquals( new Color( 0xffbfff ), "tint(#f0f, 75%, derived)", new Mix2( Color.white, 75 ) ); + + // shade + assertDerivedColorEquals( new Color( 0x800080 ), "shade(#f0f, derived)", new Mix2( Color.black, 50 ) ); + assertDerivedColorEquals( new Color( 0x400040 ), "shade(#f0f, 75%, derived)", new Mix2( Color.black, 75 ) ); + + + // lighten + assertDerivedColorEquals( new Color( 0xff6666 ), "lighten(#f00, 20%, derived)", new HSLIncreaseDecrease( 2, true, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0xff3333 ), "lighten(#f00, 20%, derived relative)", new HSLIncreaseDecrease( 2, true, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0xffffff ), "lighten(#ddd, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 2, true, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0xffffff ), "lighten(#ddd, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 2, true, 20, true, false ) ); + + // darken + assertDerivedColorEquals( new Color( 0x990000 ), "darken(#f00, 20%, derived)", new HSLIncreaseDecrease( 2, false, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0xcc0000 ), "darken(#f00, 20%, derived relative)", new HSLIncreaseDecrease( 2, false, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0x000000 ), "darken(#222, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 2, false, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0x1b1b1b ), "darken(#222, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 2, false, 20, true, false ) ); + + // saturate + assertDerivedColorEquals( new Color( 0xc75a5a ), "saturate(#d44, 20%, derived)", new HSLIncreaseDecrease( 1, true, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0xce5353 ), "saturate(#d44, 20%, derived relative)", new HSLIncreaseDecrease( 1, true, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0xf32e2e ), "saturate(#d44, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 1, true, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0xec3535 ), "saturate(#d44, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 1, true, 20, true, false ) ); + + // desaturate + assertDerivedColorEquals( new Color( 0x9c3030 ), "desaturate(#844, 20%, derived)", new HSLIncreaseDecrease( 1, false, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0x8f3d3d ), "desaturate(#844, 20%, derived relative)", new HSLIncreaseDecrease( 1, false, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0x745858 ), "desaturate(#844, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 1, false, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0x814b4b ), "desaturate(#844, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 1, false, 20, true, false ) ); + + // fadein + assertDerivedColorEquals( new Color( 0x77ff0000, true ), "fadein(#f00a, 20%, derived)", new HSLIncreaseDecrease( 3, true, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0x88ff0000, true ), "fadein(#f00a, 20%, derived relative)", new HSLIncreaseDecrease( 3, true, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0xddff0000, true ), "fadein(#f00a, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 3, true, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0xccff0000, true ), "fadein(#f00a, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 3, true, 20, true, false ) ); + + // fadeout + assertDerivedColorEquals( new Color( 0x77ff0000, true ), "fadeout(#f004, 20%, derived)", new HSLIncreaseDecrease( 3, false, 20, false, true ) ); + assertDerivedColorEquals( new Color( 0x52ff0000, true ), "fadeout(#f004, 20%, derived relative)", new HSLIncreaseDecrease( 3, false, 20, true, true ) ); + assertDerivedColorEquals( new Color( 0x11ff0000, true ), "fadeout(#f004, 20%, derived noAutoInverse)", new HSLIncreaseDecrease( 3, false, 20, false, false ) ); + assertDerivedColorEquals( new Color( 0x36ff0000, true ), "fadeout(#f004, 20%, derived relative noAutoInverse)", new HSLIncreaseDecrease( 3, false, 20, true, false ) ); + + // fade + assertDerivedColorEquals( new Color( 0x33ff0000, true ), "fade(#f00, 20%, derived)", new Fade( 20 ) ); + assertDerivedColorEquals( new Color( 0xccff0000, true ), "fade(#ff000010, 80%, derived)", new Fade( 80 ) ); + + // spin + assertDerivedColorEquals( new Color( 0xffaa00 ), "spin(#f00, 40, derived)", new HSLIncreaseDecrease( 0, true, 40, false, false ) ); + assertDerivedColorEquals( new Color( 0xff00aa ), "spin(#f00, -40, derived)", new HSLIncreaseDecrease( 0, true, -40, false, false ) ); + + // changeHue / changeSaturation / changeLightness / changeAlpha + assertDerivedColorEquals( new Color( 0xffaa00 ), "changeHue(#f00, 40, derived)", new HSLChange( 0, 40 ) ); + assertDerivedColorEquals( new Color( 0xb34d4d ), "changeSaturation(#f00, 40%, derived)", new HSLChange( 1, 40 ) ); + assertDerivedColorEquals( new Color( 0xcc0000 ), "changeLightness(#f00, 40%, derived)", new HSLChange( 2, 40 ) ); + assertDerivedColorEquals( new Color( 0x66ff0000, true ), "changeAlpha(#f00, 40%, derived)", new HSLChange( 3, 40 ) ); + + // mix + assertDerivedColorEquals( new Color( 0x808000 ), "mix(#f00, #0f0, derived)", new Mix2( new Color( 0xff0000 ), 50 ) ); + assertDerivedColorEquals( new Color( 0xbf4000 ), "mix(#f00, #0f0, 75%, derived)", new Mix2( new Color( 0xff0000 ), 75 ) ); + + // tint + assertDerivedColorEquals( new Color( 0xff80ff ), "tint(#f0f, derived)", new Mix2( new Color( 0xffffff ), 50 ) ); + assertDerivedColorEquals( new Color( 0xffbfff ), "tint(#f0f, 75%, derived)", new Mix2( new Color( 0xffffff ), 75 ) ); + + // shade + assertDerivedColorEquals( new Color( 0x800080 ), "shade(#f0f, derived)", new Mix2( new Color( 0x000000 ), 50 ) ); + assertDerivedColorEquals( new Color( 0x400040 ), "shade(#f0f, 75%, derived)", new Mix2( new Color( 0x000000 ), 75 ) ); + } + + private void assertDerivedColorEquals( Color expectedColor, String actualStyle, ColorFunction... expectedFunctions ) { + Object actual = parseColor( actualStyle ); + assertInstanceOf( DerivedColor.class, actual ); + assertEquals( expectedColor, actual ); + + ColorFunction[] actualFunctions = ((DerivedColor)actual).getFunctions(); + assertEquals( expectedFunctions.length, actualFunctions.length ); + for( int i = 0; i < expectedFunctions.length; i++ ) + assertColorFunctionEquals( expectedFunctions[i], actualFunctions[i] ); + } + + private void assertColorFunctionEquals( ColorFunction expected, ColorFunction actual ) { + assertEquals( expected.getClass(), actual.getClass() ); + + if( expected instanceof HSLIncreaseDecrease ) { + HSLIncreaseDecrease e = (HSLIncreaseDecrease) expected; + HSLIncreaseDecrease a = (HSLIncreaseDecrease) actual; + assertEquals( e.hslIndex, a.hslIndex ); + assertEquals( e.increase, a.increase ); + assertEquals( e.amount, a.amount ); + assertEquals( e.relative, a.relative ); + assertEquals( e.autoInverse, a.autoInverse ); + } else if( expected instanceof HSLChange ) { + HSLChange e = (HSLChange) expected; + HSLChange a = (HSLChange) actual; + assertEquals( e.hslIndex, a.hslIndex ); + assertEquals( e.value, a.value ); + } else if( expected instanceof Fade ) { + Fade e = (Fade) expected; + Fade a = (Fade) actual; + assertEquals( e.amount, a.amount ); + } else if( expected instanceof Mix ) { + Mix e = (Mix) expected; + Mix a = (Mix) actual; + assertEquals( e.color2, a.color2 ); + assertEquals( e.weight, a.weight ); + } else if( expected instanceof Mix2 ) { + Mix2 e = (Mix2) expected; + Mix2 a = (Mix2) actual; + assertEquals( e.color1, a.color1 ); + assertEquals( e.weight, a.weight ); + } else + assertTrue( false ); + } + + private Object parseColor( String value ) { + return UIDefaultsLoader.parseValue( "dummyColor", value, null ); + } + //---- class TestInstance ------------------------------------------------- @SuppressWarnings( "EqualsHashCode" ) // Error Prone