diff --git a/res/bundled.go b/res/bundled.go index 7b4a86b..d284ce7 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -118,7 +118,7 @@ var ResUpdownarrowSvg = &fyne.StaticResource{ var ResDefaultToml = &fyne.StaticResource{ StaticName: "default.toml", StaticContent: []byte( - "[SupersonicTheme]\nName = \"Default\"\nVersion = \"0.2\"\nSupportsDark = true\nSupportsLight = true\n\n[DarkColors]\nPageBackground = \"#0F0F0F\"\nListHeader = \"#232323\"\nPageHeader = \"#181d25\"\nBackground = \"#232323\"\nScrollBar = \"#F3F3F3\"\nButton = \"#14141432\"\nInputBackground = \"#14141432\"\n\n[LightColors]\nPageBackground = \"#FAFAFA\"\nListHeader = \"#E1DFE1\"\nPageHeader = \"#e1dfe1\"\nBackground = \"#E1DFE1\"\nScrollBar = \"#565656\"\nButton = \"#C8C8C8F0\"\nDisabledButton = \"#CDCDCDF0\"\nForeground = \"#0A0A0A\"\nHyperlink = \"#3737FB\""), + "[SupersonicTheme]\nName = \"Default\"\nVersion = \"0.2\"\nSupportsDark = true\nSupportsLight = true\n\n[DarkColors]\nPageBackground = \"#0F0F0F\"\nListHeader = \"#232323\"\nPageHeader = \"#181d25\"\nBackground = \"#232323\"\nScrollBar = \"#F3F3F3\"\nButton = \"#14141432\"\nForeground = \"#e6e6e6\"\nInputBackground = \"#14141432\"\n\n[LightColors]\nPageBackground = \"#FAFAFA\"\nListHeader = \"#E1DFE1\"\nPageHeader = \"#e1dfe1\"\nBackground = \"#E1DFE1\"\nScrollBar = \"#565656\"\nButton = \"#C8C8C8F0\"\nDisabledButton = \"#CDCDCDF0\"\nForeground = \"#262626\"\nHyperlink = \"#3737FB\""), } var ResLICENSE = &fyne.StaticResource{ StaticName: "LICENSE", diff --git a/res/themes/default.toml b/res/themes/default.toml index e105a42..b823b6c 100644 --- a/res/themes/default.toml +++ b/res/themes/default.toml @@ -11,6 +11,7 @@ PageHeader = "#181d25" Background = "#232323" ScrollBar = "#F3F3F3" Button = "#14141432" +Foreground = "#e6e6e6" InputBackground = "#14141432" [LightColors] @@ -21,5 +22,5 @@ Background = "#E1DFE1" ScrollBar = "#565656" Button = "#C8C8C8F0" DisabledButton = "#CDCDCDF0" -Foreground = "#0A0A0A" +Foreground = "#262626" Hyperlink = "#3737FB" \ No newline at end of file diff --git a/ui/theme/theme.go b/ui/theme/theme.go index 117f3da..436c56c 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -19,10 +19,12 @@ import ( ) const ( - ColorNameListHeader fyne.ThemeColorName = "ListHeader" - ColorNamePageBackground fyne.ThemeColorName = "PageBackground" - ColorNamePageHeader fyne.ThemeColorName = "PageHeader" - ColorNameInactiveLyric fyne.ThemeColorName = "InactiveLyric" + ColorNameListHeader fyne.ThemeColorName = "ListHeader" + ColorNamePageBackground fyne.ThemeColorName = "PageBackground" + ColorNamePageHeader fyne.ThemeColorName = "PageHeader" + ColorNameInactiveLyric fyne.ThemeColorName = "InactiveLyric" + ColorNameIconButton fyne.ThemeColorName = "IconButton" + ColorNameHoveredIconButton fyne.ThemeColorName = "HoveredIconButton" ) var ( @@ -111,6 +113,17 @@ func (m *MyTheme) Color(name fyne.ThemeColorName, _ fyne.ThemeVariant) color.Col foreground := colorOrDefault(colors.Foreground, defColors.Foreground, theme.ColorNameForeground, variant) disabled := colorOrDefault(colors.Disabled, defColors.Disabled, theme.ColorNameDisabled, variant) return blendColors(foreground, disabled, 0.33) + case ColorNameHoveredIconButton: + if variant == theme.VariantDark { + return color.White + } + return color.Black + case ColorNameIconButton: + foreground := colorOrDefault(colors.Foreground, defColors.Foreground, theme.ColorNameForeground, variant) + if variant == theme.VariantDark { + return darkenColor(foreground, 0.05) + } + return brightenColor(foreground, 0.2) case ColorNameListHeader: return colorOrDefault(colors.ListHeader, defColors.ListHeader, name, variant) case ColorNamePageBackground: @@ -256,6 +269,44 @@ func blendColors(a, b color.Color, fractionA float64) color.Color { return color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: aAvg} } +func brightenColor(c color.Color, fraction float64) color.Color { + r, g, b, a := c.RGBA() + r, g, b = brightenComponent(r, fraction), brightenComponent(g, fraction), brightenComponent(b, fraction) + return color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + } +} + +func darkenColor(c color.Color, fraction float64) color.Color { + r, g, b, a := c.RGBA() + r, g, b = darkenComponent(r, fraction), darkenComponent(g, fraction), darkenComponent(b, fraction) + return color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + } +} + +func brightenComponent(component uint32, fraction float64) uint32 { + brightened := component + uint32(float64(component)*fraction) + if brightened > 0xffff { + brightened = 0xffff + } + return brightened +} + +func darkenComponent(component uint32, fraction float64) uint32 { + i := uint32(float64(component) * fraction) + if i > component { + return 0 + } + return component - i +} + func readTTFFile(filepath string) ([]byte, error) { if !strings.HasSuffix(filepath, ".ttf") { err := errors.New("only .ttf fonts are supported") diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index e9b11c7..c23f7bc 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -12,45 +12,23 @@ import ( "github.com/dweymouth/supersonic/ui/util" ) -var ( - repeatThemedResource = theme.NewThemedResource(myTheme.RepeatIcon) - repeatOneThemedResource = theme.NewThemedResource(myTheme.RepeatOneIcon) -) - // The "aux" controls for playback, positioned to the right // of the BottomPanel. Currently only volume control. type AuxControls struct { widget.BaseWidget VolumeControl *VolumeControl - loop *miniButton + loop *IconButton container *fyne.Container } -type miniButton struct { - widget.Button -} - -func newMiniButton(icon fyne.Resource) *miniButton { - b := &miniButton{ - Button: widget.Button{ - Icon: icon, - }, - } - b.ExtendBaseWidget(b) - return b -} - -func (b *miniButton) MinSize() fyne.Size { - return fyne.NewSize(24, 24) -} - func NewAuxControls(initialVolume int) *AuxControls { a := &AuxControls{ VolumeControl: NewVolumeControl(initialVolume), - loop: newMiniButton(myTheme.RepeatIcon), + loop: NewIconButton(myTheme.RepeatIcon, nil), } + a.loop.IconSize = IconButtonSizeSmaller a.container = container.NewHBox( layout.NewSpacer(), container.NewVBox( @@ -75,16 +53,15 @@ func (a *AuxControls) OnChangeLoopMode(f func()) { func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { switch mode { case backend.LoopAll: - a.loop.Icon = repeatThemedResource - repeatThemedResource.ColorName = theme.ColorNamePrimary + a.loop.Highlighted = true + a.loop.SetIcon(myTheme.RepeatIcon) case backend.LoopOne: - a.loop.Icon = repeatOneThemedResource - repeatOneThemedResource.ColorName = theme.ColorNamePrimary + a.loop.Highlighted = true + a.loop.SetIcon(myTheme.RepeatOneIcon) case backend.LoopNone: - a.loop.Importance = widget.MediumImportance - a.loop.Icon = myTheme.RepeatIcon + a.loop.Highlighted = false + a.loop.SetIcon(myTheme.RepeatIcon) } - a.loop.Refresh() } type volumeSlider struct { @@ -114,9 +91,7 @@ func (v *volumeSlider) Tapped(e *fyne.PointEvent) { } func (v *volumeSlider) MinSize() fyne.Size { - h := v.Slider.MinSize().Height - // give extra space for height to hack around mini button covering part of focus circle - return fyne.NewSize(v.Width, h+theme.Padding()*3) + return fyne.NewSize(v.Width, v.Slider.MinSize().Height) } func (v *volumeSlider) Scrolled(e *fyne.ScrollEvent) { @@ -126,7 +101,7 @@ func (v *volumeSlider) Scrolled(e *fyne.ScrollEvent) { type VolumeControl struct { widget.BaseWidget - icon *TappableIcon + icon *IconButton slider *volumeSlider OnSetVolume func(int) @@ -140,8 +115,8 @@ type VolumeControl struct { func NewVolumeControl(initialVol int) *VolumeControl { v := &VolumeControl{} v.ExtendBaseWidget(v) - v.icon = NewTappableIcon(theme.VolumeUpIcon()) - v.icon.OnTapped = v.toggleMute + v.icon = NewIconButton(theme.VolumeUpIcon(), v.toggleMute) + v.icon.IconSize = IconButtonSizeSmaller v.slider = NewVolumeSlider(100) v.lastVol = initialVol v.slider.Step = 1 @@ -203,11 +178,10 @@ func (v *VolumeControl) invokeOnVolumeChange(vol int) { func (v *VolumeControl) updateIconForVolume(vol int) { if vol <= 0 { - v.icon.Resource = theme.VolumeMuteIcon() + v.icon.SetIcon(theme.VolumeMuteIcon()) } else if vol < 50 { - v.icon.Resource = theme.VolumeDownIcon() + v.icon.SetIcon(theme.VolumeDownIcon()) } else { - v.icon.Resource = theme.VolumeUpIcon() + v.icon.SetIcon(theme.VolumeUpIcon()) } - v.icon.Refresh() } diff --git a/ui/widgets/iconbutton.go b/ui/widgets/iconbutton.go new file mode 100644 index 0000000..075aea9 --- /dev/null +++ b/ui/widgets/iconbutton.go @@ -0,0 +1,142 @@ +package widgets + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + + myTheme "github.com/dweymouth/supersonic/ui/theme" +) + +type IconButtonSize int + +const ( + IconButtonSizeNormal IconButtonSize = iota + IconButtonSizeBigger + IconButtonSizeSmaller +) + +type IconButton struct { + widget.BaseWidget + + Highlighted bool + IconSize IconButtonSize + OnTapped func() + + icon fyne.Resource + focused bool + hovered bool + + themed *theme.ThemedResource + img *canvas.Image +} + +var ( + _ fyne.Tappable = (*IconButton)(nil) + _ fyne.Focusable = (*IconButton)(nil) + _ desktop.Hoverable = (*IconButton)(nil) +) + +func NewIconButton(icon fyne.Resource, onTapped func()) *IconButton { + i := &IconButton{icon: icon, OnTapped: onTapped} + i.ExtendBaseWidget(i) + return i +} + +func (i *IconButton) SetIcon(icon fyne.Resource) { + i.icon = icon + if i.img != nil { + i.themed = theme.NewThemedResource(icon) + i.img.Resource = i.themed + i.Refresh() + } +} + +func (i *IconButton) Tapped(*fyne.PointEvent) { + if i.OnTapped != nil { + i.OnTapped() + } +} + +func (i *IconButton) FocusGained() { + if !i.focused { + defer i.Refresh() + } + i.focused = true +} + +func (i *IconButton) FocusLost() { + if i.focused { + defer i.Refresh() + } + i.focused = false +} + +func (i *IconButton) TypedKey(e *fyne.KeyEvent) { + if e.Name == fyne.KeySpace { + i.Tapped(nil) + } +} + +func (i *IconButton) TypedRune(r rune) { +} + +func (i *IconButton) MouseIn(*desktop.MouseEvent) { + if !i.hovered { + defer i.Refresh() + } + i.hovered = true +} + +func (i *IconButton) MouseOut() { + if i.hovered { + defer i.Refresh() + } + i.hovered = false +} + +func (i *IconButton) MouseMoved(*desktop.MouseEvent) { +} + +func (i *IconButton) MinSize() fyne.Size { + return i.iconSize() +} + +func (i *IconButton) iconSize() fyne.Size { + switch i.IconSize { + case IconButtonSizeBigger: + return fyne.NewSquareSize(theme.IconInlineSize() * 2) + case IconButtonSizeSmaller: + return fyne.NewSquareSize(theme.IconInlineSize()) + default: + return fyne.NewSquareSize(theme.IconInlineSize() * 1.3333) + } +} + +func (i *IconButton) Refresh() { + if i.img == nil { + return + } + if i.Highlighted || i.focused { + i.themed.ColorName = theme.ColorNamePrimary + } else if i.hovered { + i.themed.ColorName = myTheme.ColorNameHoveredIconButton + } else { + i.themed.ColorName = myTheme.ColorNameIconButton + } + i.img.SetMinSize(i.iconSize()) + i.img.Refresh() +} + +func (i *IconButton) CreateRenderer() fyne.WidgetRenderer { + if i.img == nil { + i.themed = theme.NewThemedResource(i.icon) + i.img = canvas.NewImageFromResource(i.themed) + i.img.FillMode = canvas.ImageFillContain + i.img.SetMinSize(i.iconSize()) + } + return widget.NewSimpleRenderer(container.NewCenter(i.img)) +} diff --git a/ui/widgets/playercontrols.go b/ui/widgets/playercontrols.go index d71237d..b3dd72e 100644 --- a/ui/widgets/playercontrols.go +++ b/ui/widgets/playercontrols.go @@ -83,9 +83,9 @@ type PlayerControls struct { slider *TrackPosSlider curTimeLabel *labelMinSize totalTimeLabel *labelMinSize - prev *widget.Button - playpause *widget.Button - next *widget.Button + prev *IconButton + playpause *IconButton + next *IconButton container *fyne.Container totalTime float64 @@ -127,15 +127,15 @@ func NewPlayerControls() *PlayerControls { } } - pc.prev = widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func() {}) - pc.next = widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func() {}) - pc.playpause = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {}) + pc.prev = NewIconButton(theme.MediaSkipPreviousIcon(), func() {}) + pc.next = NewIconButton(theme.MediaSkipNextIcon(), func() {}) + pc.playpause = NewIconButton(theme.MediaPlayIcon(), func() {}) + pc.playpause.IconSize = IconButtonSizeBigger - buttons := container.NewHBox(pc.prev, pc.playpause, pc.next) - b := container.New(layout.NewCenterLayout(), buttons) + buttons := container.NewHBox(layout.NewSpacer(), pc.prev, pc.playpause, pc.next, layout.NewSpacer()) c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider) - pc.container = container.NewVBox(c, b) + pc.container = container.New(layout.NewCustomPaddedVBoxLayout(0), c, buttons) return pc }