Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: RGBA implementations for non-hex color values #126

Merged
merged 2 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 4 additions & 51 deletions color.go
@@ -1,7 +1,6 @@
package lipgloss

import (
"image/color"
"sync"

"github.com/muesli/termenv"
Expand Down Expand Up @@ -144,7 +143,7 @@ func (c Color) color() termenv.Color {
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFF.
func (c Color) RGBA() (r, g, b, a uint32) {
return hexToColor(c.value()).RGBA()
return termenv.ConvertToRGB(c.color()).RGBA()
}

// AdaptiveColor provides color options for light and dark backgrounds. The
Expand Down Expand Up @@ -175,8 +174,7 @@ func (ac AdaptiveColor) color() termenv.Color {
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFF.
func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) {
cf := hexToColor(ac.value())
return cf.RGBA()
return termenv.ConvertToRGB(ac.color()).RGBA()
}

// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
Expand Down Expand Up @@ -209,7 +207,7 @@ func (c CompleteColor) color() termenv.Color {
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF
func (c CompleteColor) RGBA() (r, g, b, a uint32) {
return hexToColor(c.value()).RGBA()
return termenv.ConvertToRGB(c.color()).RGBA()
}

// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
Expand All @@ -236,50 +234,5 @@ func (cac CompleteAdaptiveColor) color() termenv.Color {
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF
func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) {
return hexToColor(cac.value()).RGBA()
}

// hexToColor translates a hex color string (#RRGGBB or #RGB) into a color.RGB,
// which satisfies the color.Color interface. If an invalid string is passed
// black with 100% opacity will be returned: or, in hex format, 0x000000FF.
func hexToColor(hex string) (c color.RGBA) {
c.A = 0xFF

if hex == "" || hex[0] != '#' {
return c
}

const (
fullFormat = 7 // #RRGGBB
shortFormat = 4 // #RGB
)

switch len(hex) {
case fullFormat:
const offset = 4
c.R = hexToByte(hex[1])<<offset + hexToByte(hex[2])
c.G = hexToByte(hex[3])<<offset + hexToByte(hex[4])
c.B = hexToByte(hex[5])<<offset + hexToByte(hex[6])
case shortFormat:
const offset = 0x11
c.R = hexToByte(hex[1]) * offset
c.G = hexToByte(hex[2]) * offset
c.B = hexToByte(hex[3]) * offset
}

return c
}

func hexToByte(b byte) byte {
const offset = 10
switch {
case b >= '0' && b <= '9':
return b - '0'
case b >= 'a' && b <= 'f':
return b - 'a' + offset
case b >= 'A' && b <= 'F':
return b - 'A' + offset
}
// Invalid, but just return 0.
return 0
return termenv.ConvertToRGB(cac.color()).RGBA()
}
190 changes: 188 additions & 2 deletions color_test.go
@@ -1,14 +1,13 @@
package lipgloss

import (
"image/color"
"testing"

"github.com/muesli/termenv"
)

func TestSetColorProfile(t *testing.T) {
t.Parallel()

tt := []struct {
profile termenv.Profile
input string
Expand Down Expand Up @@ -85,3 +84,190 @@ func TestHexToColor(t *testing.T) {
}
}
}

func TestRGBA(t *testing.T) {
tt := []struct {
profile termenv.Profile
darkBg bool
input TerminalColor
expected uint
}{
// lipgloss.Color
{
termenv.TrueColor,
true,
Color("#FF0000"),
0xFF0000,
},
{
termenv.TrueColor,
true,
Color("9"),
0xFF0000,
},
{
termenv.TrueColor,
true,
Color("21"),
0x0000FF,
},
// lipgloss.AdaptiveColor
{
termenv.TrueColor,
true,
AdaptiveColor{Dark: "#FF0000", Light: "#0000FF"},
0xFF0000,
},
{
termenv.TrueColor,
false,
AdaptiveColor{Dark: "#FF0000", Light: "#0000FF"},
0x0000FF,
},
{
termenv.TrueColor,
true,
AdaptiveColor{Dark: "9", Light: "21"},
0xFF0000,
},
{
termenv.TrueColor,
false,
AdaptiveColor{Dark: "9", Light: "21"},
0x0000FF,
},
// lipgloss.CompleteColor
{
termenv.TrueColor,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0xFF0000,
},
{
termenv.ANSI256,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0xFFFFFF,
},
{
termenv.ANSI,
true,
CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
0x0000FF,
},
// lipgloss.CompleteAdaptiveColor
// dark
{
termenv.TrueColor,
true,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
},
0xFF0000,
},
{
termenv.ANSI256,
true,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
},
0xFFFFFF,
},
{
termenv.ANSI,
true,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
},
0x0000FF,
},
// light
{
termenv.TrueColor,
false,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"},
},
0x0000FF,
},
{
termenv.ANSI256,
false,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"},
},
0x0000FF,
},
{
termenv.ANSI,
false,
CompleteAdaptiveColor{
Dark: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"},
Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"},
},
0xFF0000,
},
}

for i, tc := range tt {
SetColorProfile(tc.profile)
SetHasDarkBackground(tc.darkBg)

r, g, b, _ := tc.input.RGBA()
o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256)

if o != tc.expected {
t.Errorf("expected %X, got %X (test #%d)", tc.expected, o, i+1)
}
}
}

// hexToColor translates a hex color string (#RRGGBB or #RGB) into a color.RGB,
// which satisfies the color.Color interface. If an invalid string is passed
// black with 100% opacity will be returned: or, in hex format, 0x000000FF.
func hexToColor(hex string) (c color.RGBA) {
c.A = 0xFF

if hex == "" || hex[0] != '#' {
return c
}

const (
fullFormat = 7 // #RRGGBB
shortFormat = 4 // #RGB
)

switch len(hex) {
case fullFormat:
const offset = 4
c.R = hexToByte(hex[1])<<offset + hexToByte(hex[2])
c.G = hexToByte(hex[3])<<offset + hexToByte(hex[4])
c.B = hexToByte(hex[5])<<offset + hexToByte(hex[6])
case shortFormat:
const offset = 0x11
c.R = hexToByte(hex[1]) * offset
c.G = hexToByte(hex[2]) * offset
c.B = hexToByte(hex[3]) * offset
}

return c
}

func hexToByte(b byte) byte {
const offset = 10
switch {
case b >= '0' && b <= '9':
return b - '0'
case b >= 'a' && b <= 'f':
return b - 'a' + offset
case b >= 'A' && b <= 'F':
return b - 'A' + offset
}
// Invalid, but just return 0.
return 0
}