diff --git a/pkg/gui/style/color.go b/pkg/gui/style/color.go index b2df21bbf..2b13c9236 100644 --- a/pkg/gui/style/color.go +++ b/pkg/gui/style/color.go @@ -23,10 +23,17 @@ func (c Color) IsRGB() bool { return c.rgb != nil } -func (c Color) ToRGB() Color { +func (c Color) ToRGB(isBg bool) Color { if c.IsRGB() { return c } + if isBg { + // We need to convert bg color to fg color + // This is a gookit/color bug, + // https://github.com/gookit/color/issues/39 + return NewRGBColor((*c.basic - 10).RGB()) + } + return NewRGBColor(c.basic.RGB()) } diff --git a/pkg/gui/style/decoration.go b/pkg/gui/style/decoration.go index 51887fbdd..f6fedb879 100644 --- a/pkg/gui/style/decoration.go +++ b/pkg/gui/style/decoration.go @@ -8,15 +8,15 @@ type Decoration struct { reverse bool } -func (d Decoration) SetBold() { +func (d *Decoration) SetBold() { d.bold = true } -func (d Decoration) SetUnderline() { +func (d *Decoration) SetUnderline() { d.underline = true } -func (d Decoration) SetReverse() { +func (d *Decoration) SetReverse() { d.reverse = true } diff --git a/pkg/gui/style/style_test.go b/pkg/gui/style/style_test.go index 669bd4db5..3b301d160 100644 --- a/pkg/gui/style/style_test.go +++ b/pkg/gui/style/style_test.go @@ -7,308 +7,131 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewStyle(t *testing.T) { +func TestMerge(t *testing.T) { type scenario struct { name string - fg, bg color.Color - expectedStyle color.Style + toMerge []TextStyle + expectedStyle TextStyle } + fgRed := color.FgRed + bgRed := color.BgRed + fgBlue := color.FgBlue + + rgbPinkLib := color.Rgb(0xFF, 0x00, 0xFF) + rgbPink := NewRGBColor(rgbPinkLib) + + rgbYellowLib := color.Rgb(0xFF, 0xFF, 0x00) + rgbYellow := NewRGBColor(rgbYellowLib) + scenarios := []scenario{ { "no color", - 0, 0, - color.Style{}, + nil, + TextStyle{style: color.Style{}}, }, { "only fg color", - color.FgRed, 0, - color.Style{color.FgRed}, + []TextStyle{FgRed}, + TextStyle{fg: &Color{basic: &fgRed}, style: color.Style{fgRed}}, }, { "only bg color", - 0, color.BgRed, - color.Style{color.BgRed}, + []TextStyle{BgRed}, + TextStyle{bg: &Color{basic: &bgRed}, style: color.Style{bgRed}}, }, { "fg and bg color", - color.FgBlue, color.BgRed, - color.Style{color.FgBlue, color.BgRed}, + []TextStyle{FgBlue, BgRed}, + TextStyle{ + fg: &Color{basic: &fgBlue}, + bg: &Color{basic: &bgRed}, + style: color.Style{fgBlue, bgRed}, + }, + }, + { + "single attribute", + []TextStyle{AttrBold}, + TextStyle{ + decoration: Decoration{bold: true}, + style: color.Style{color.OpBold}, + }, + }, + { + "multiple attributes", + []TextStyle{AttrBold, AttrUnderline}, + TextStyle{ + decoration: Decoration{ + bold: true, + underline: true, + }, + style: color.Style{color.OpBold, color.OpUnderscore}, + }, + }, + { + "multiple attributes and colors", + []TextStyle{AttrBold, FgBlue, AttrUnderline, BgRed}, + TextStyle{ + fg: &Color{basic: &fgBlue}, + bg: &Color{basic: &bgRed}, + decoration: Decoration{ + bold: true, + underline: true, + }, + style: color.Style{fgBlue, bgRed, color.OpBold, color.OpUnderscore}, + }, + }, + { + "rgb fg color", + []TextStyle{New().SetFg(rgbPink)}, + TextStyle{ + fg: &rgbPink, + style: color.NewRGBStyle(rgbPinkLib).SetOpts(color.Opts{}), + }, + }, + { + "rgb fg and bg color", + []TextStyle{New().SetFg(rgbPink).SetBg(rgbYellow)}, + TextStyle{ + fg: &rgbPink, + bg: &rgbYellow, + style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{}), + }, + }, + { + "rgb fg and bg color with opts", + []TextStyle{AttrBold, New().SetFg(rgbPink).SetBg(rgbYellow), AttrUnderline}, + TextStyle{ + fg: &rgbPink, + bg: &rgbYellow, + decoration: Decoration{ + bold: true, + underline: true, + }, + style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{color.OpBold, color.OpUnderscore}), + }, + }, + { + "mix color-16 with rgb colors", + []TextStyle{New().SetFg(rgbYellow), BgRed}, + TextStyle{ + fg: &rgbYellow, + bg: &Color{basic: &bgRed}, + style: color.NewRGBStyle( + rgbYellowLib, + fgRed.RGB(), // We need to use FG here, https://github.com/gookit/color/issues/39 + ).SetOpts(color.Opts{}), + }, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - style := New(s.fg, s.bg) - basicStyle, ok := style.(BasicTextStyle) - assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") - assert.Equal(t, s.fg, basicStyle.fg) - assert.Equal(t, s.bg, basicStyle.bg) - assert.Equal(t, []color.Color(nil), basicStyle.opts) - assert.Equal(t, s.expectedStyle, basicStyle.style) - }) - } -} - -func TestBasicSetColor(t *testing.T) { - type scenario struct { - name string - colorToSet TextStyle - expect TextStyle - } - - scenarios := []scenario{ - { - "empty color", - TextStyle{}, - TextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}}, - { - "set new fg color", - TextStyle{fg: color.FgCyan}, - TextStyle{fg: color.FgCyan, bg: color.BgBlue, opts: []color.Color{color.OpBold}}, - }, - { - "set new bg color", - TextStyle{bg: color.BgGray}, - TextStyle{fg: color.FgRed, bg: color.BgGray, opts: []color.Color{color.OpBold}}, - }, - { - "set new fg and bg color", - TextStyle{fg: color.FgCyan, bg: color.BgGray}, - TextStyle{fg: color.FgCyan, bg: color.BgGray, opts: []color.Color{color.OpBold}}, - }, - { - "add options", - TextStyle{opts: []color.Color{color.OpUnderscore}}, - TextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold, color.OpUnderscore}}, - }, - { - "add options that already exists", - TextStyle{opts: []color.Color{color.OpBold}}, - TextStyle{fg: color.FgRed, bg: color.BgBlue, opts: []color.Color{color.OpBold}}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - style, ok := New(color.FgRed, color.BgBlue). - SetBold(true). - SetColor(s.colorToSet).(BasicTextStyle) - assert.True(t, ok, "SetColor should return a interface of type BasicTextStyle if the input was also BasicTextStyle") - - style.style = nil - assert.Equal(t, s.expect, style) - }) - } -} - -func TestRGBSetColor(t *testing.T) { - type scenario struct { - name string - colorToSet TextStyle - expect RGBTextStyle - } - - red := color.FgRed.RGB() - cyan := color.FgCyan.RGB() - blue := color.FgBlue.RGB() - gray := color.FgGray.RGB() - - toBg := func(c color.RGBColor) *color.RGBColor { - c[3] = 1 - return &c - } - - scenarios := []scenario{ - { - "empty RGBTextStyle input", - RGBTextStyle{}, - RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, - }, - { - "empty BasicTextStyle input", - TextStyle{}, - RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, - }, - { - "set fg and bg color using BasicTextStyle", - TextStyle{fg: color.FgCyan, bg: color.BgGray}, - RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray), opts: []color.Color{color.OpBold}}, - }, - { - "set fg and bg color using RGBTextStyle", - RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray)}, - RGBTextStyle{fgSet: true, fg: cyan, bg: toBg(gray), opts: []color.Color{color.OpBold}}, - }, - { - "add options", - RGBTextStyle{opts: []color.Color{color.OpUnderscore}}, - RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold, color.OpUnderscore}}, - }, - { - "add options using BasicTextStyle", - TextStyle{opts: []color.Color{color.OpUnderscore}}, - RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold, color.OpUnderscore}}, - }, - { - "add options that already exists", - RGBTextStyle{opts: []color.Color{color.OpBold}}, - RGBTextStyle{fgSet: true, fg: red, bg: toBg(blue), opts: []color.Color{color.OpBold}}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - style, ok := New(color.FgRed, color.BgBlue).SetBold().(BasicTextStyle) - assert.True(t, ok, "SetBold should return a interface of type BasicTextStyle") - - rgbStyle, ok := style.convertToRGB().MergeStyle(s.colorToSet).(RGBTextStyle) - assert.True(t, ok, "SetColor should return a interface of type RGBTextColor") - - rgbStyle.style = color.RGBStyle{} - assert.Equal(t, s.expect, rgbStyle) - }) - } -} - -func TestConvertBasicToRGB(t *testing.T) { - type scenario struct { - name string - test func(*testing.T) - } - - scenarios := []scenario{ - { - "convert to rgb with fg", - func(t *testing.T) { - basicStyle, ok := New(color.FgRed, 0).(BasicTextStyle) - assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") - - rgbStyle := basicStyle.convertToRGB() - assert.True(t, rgbStyle.fgSet) - assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) - assert.Nil(t, rgbStyle.bg) - }, - }, - { - "convert to rgb with fg and bg", - func(t *testing.T) { - basicStyle, ok := New(color.FgRed, color.BgRed).(BasicTextStyle) - assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") - - rgbStyle := basicStyle.convertToRGB() - assert.True(t, rgbStyle.fgSet) - assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) - assert.Equal(t, color.RGB(197, 30, 20, true), *rgbStyle.bg) - }, - }, - { - "convert to rgb using SetRGBColor", - func(t *testing.T) { - style := New(color.FgRed, 0) - rgbStyle, ok := style.SetRGBColor(255, 00, 255, true).(RGBTextStyle) - assert.True(t, ok, "SetRGBColor should return a interface of type RGBTextStyle") - - assert.True(t, rgbStyle.fgSet) - assert.Equal(t, color.RGB(197, 30, 20), rgbStyle.fg) - assert.Equal(t, color.RGB(255, 0, 255, true), *rgbStyle.bg) - }, - }, - { - "convert to rgb using SetRGBColor multiple times", - func(t *testing.T) { - style := New(color.FgRed, 0) - rgbStyle, ok := style.SetRGBColor(00, 255, 255, false).SetRGBColor(255, 00, 255, true).(RGBTextStyle) - assert.True(t, ok, "SetRGBColor should return a interface of type RGBTextStyle") - - assert.True(t, rgbStyle.fgSet) - assert.Equal(t, color.RGB(0, 255, 255), rgbStyle.fg) - assert.Equal(t, color.RGB(255, 0, 255, true), *rgbStyle.bg) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.name, s.test) - } -} - -func TestSettingAtributes(t *testing.T) { - type scenario struct { - name string - test func(s TextStyle) TextStyle - expectedOpts []color.Color - } - - scenarios := []scenario{ - { - "no attributes", - func(s TextStyle) TextStyle { - return s - }, - []color.Color{}, - }, - { - "set single attribute", - func(s TextStyle) TextStyle { - return s.SetBold(true) - }, - []color.Color{color.OpBold}, - }, - { - "set multiple attributes", - func(s TextStyle) TextStyle { - return s.SetBold(true).SetUnderline(true) - }, - []color.Color{color.OpBold, color.OpUnderscore}, - }, - { - "unset a attributes", - func(s TextStyle) TextStyle { - return s.SetBold(true).SetBold(false) - }, - []color.Color{}, - }, - { - "unset a attributes with multiple attributes", - func(s TextStyle) TextStyle { - return s.SetBold(true).SetUnderline(true).SetBold(false) - }, - []color.Color{color.OpUnderscore}, - }, - { - "unset all attributes with multiple attributes", - func(s TextStyle) TextStyle { - return s.SetBold(true).SetUnderline(true).SetBold(false).SetUnderline(false) - }, - []color.Color{}, - }, - } - - for _, s := range scenarios { - t.Run(s.name, func(t *testing.T) { - // Test basic style - style := New(color.FgRed, 0) - basicStyle, ok := style.(BasicTextStyle) - assert.True(t, ok, "New(..) should return a interface of type BasicTextStyle") - basicStyle, ok = s.test(basicStyle).(BasicTextStyle) - assert.True(t, ok, "underlaying type should not be changed after test") - assert.Len(t, basicStyle.opts, len(s.expectedOpts)) - for _, opt := range basicStyle.opts { - assert.Contains(t, s.expectedOpts, opt) - } - for _, opt := range s.expectedOpts { - assert.Contains(t, basicStyle.style, opt) - } - - // Test RGB style - rgbStyle := New(color.FgRed, 0).(BasicTextStyle).convertToRGB() - rgbStyle, ok = s.test(rgbStyle).(RGBTextStyle) - assert.True(t, ok, "underlaying type should not be changed after test") - assert.Len(t, rgbStyle.opts, len(s.expectedOpts)) - for _, opt := range rgbStyle.opts { - assert.Contains(t, s.expectedOpts, opt) + style := New() + for _, other := range s.toMerge { + style = style.MergeStyle(other) } + assert.Equal(t, s.expectedStyle, style) }) } } diff --git a/pkg/gui/style/text_style.go b/pkg/gui/style/text_style.go index d2bfccaeb..fd893a2be 100644 --- a/pkg/gui/style/text_style.go +++ b/pkg/gui/style/text_style.go @@ -134,11 +134,13 @@ func (b TextStyle) deriveRGBStyle() *color.RGBStyle { style := &color.RGBStyle{} if b.fg != nil { - style.SetFg(*b.fg.ToRGB().rgb) + style.SetFg(*b.fg.ToRGB(false).rgb) } if b.bg != nil { - style.SetBg(*b.bg.ToRGB().rgb) + // We need to convert the bg firstly to a foreground color, + // For more info see + style.SetBg(*b.bg.ToRGB(true).rgb) } style.SetOpts(b.decoration.ToOpts()) diff --git a/pkg/utils/color_test.go b/pkg/utils/color_test.go deleted file mode 100644 index 8d7f5c48e..000000000 --- a/pkg/utils/color_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetHexColorValues(t *testing.T) { - scenarios := []struct { - name string - hexColor string - rgb []int32 - valid bool - }{ - { - name: "valid uppercase hex color", - hexColor: "#00FF00", - rgb: []int32{0, 255, 0}, - valid: true, - }, - { - name: "valid lowercase hex color", - hexColor: "#00ff00", - rgb: []int32{0, 255, 0}, - valid: true, - }, - { - name: "valid short hex color", - hexColor: "#0bf", - rgb: []int32{0, 187, 255}, - valid: true, - }, - { - name: "invalid hex value", - hexColor: "#zz00ff", - valid: false, - }, - { - name: "invalid length hex color", - hexColor: "#", - valid: false, - }, - { - name: "invalid length hex color", - hexColor: "#aaaaaaaaaaa", - valid: false, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.name, func(t *testing.T) { - r, g, b, valid := GetHexColorValues(s.hexColor) - assert.EqualValues(t, s.valid, valid, s.hexColor) - if valid { - assert.EqualValues(t, s.rgb[0], r, s.hexColor) - assert.EqualValues(t, s.rgb[1], g, s.hexColor) - assert.EqualValues(t, s.rgb[2], b, s.hexColor) - } - }) - } -}