add icon buttons to bottom of GridViewItem when hovered
This commit is contained in:
@@ -33,6 +33,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
GridViewIconColor color.Color = color.White
|
||||
GridViewHoveredIconColor color.Color = darkenColor(GridViewIconColor, 0.05)
|
||||
|
||||
AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg)
|
||||
ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg)
|
||||
RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
|
||||
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package util
|
||||
|
||||
// Contents of this file come from the fyne internal SVG package
|
||||
// Once https://github.com/fyne-io/fyne/pull/5345 is available in main,
|
||||
// this file can be retired, and the ColorizeSVG func can be replaced with
|
||||
// `canvas.ColorizeSVG`
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ColorizeSVG creates a new SVG from a given one by replacing all fill colors by the given color.
|
||||
func ColorizeSVG(src []byte, clr color.Color) ([]byte, error) {
|
||||
rdr := bytes.NewReader(src)
|
||||
s, err := svgFromXML(rdr)
|
||||
if err != nil {
|
||||
return src, fmt.Errorf("could not load SVG, falling back to static content: %v", err)
|
||||
}
|
||||
if err := s.replaceFillColor(clr); err != nil {
|
||||
return src, fmt.Errorf("could not replace fill color, falling back to static content: %v", err)
|
||||
}
|
||||
colorized, err := xml.Marshal(s)
|
||||
if err != nil {
|
||||
return src, fmt.Errorf("could not marshal svg, falling back to static content: %v", err)
|
||||
}
|
||||
return colorized, nil
|
||||
}
|
||||
|
||||
// svg holds the unmarshaled XML from a Scalable Vector Graphic
|
||||
type svg struct {
|
||||
XMLName xml.Name `xml:"svg"`
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
Width string `xml:"width,attr,omitempty"`
|
||||
Height string `xml:"height,attr,omitempty"`
|
||||
ViewBox string `xml:"viewBox,attr,omitempty"`
|
||||
Paths []*pathObj `xml:"path"`
|
||||
Rects []*rectObj `xml:"rect"`
|
||||
Circles []*circleObj `xml:"circle"`
|
||||
Ellipses []*ellipseObj `xml:"ellipse"`
|
||||
Polygons []*polygonObj `xml:"polygon"`
|
||||
Groups []*objGroup `xml:"g"`
|
||||
}
|
||||
|
||||
type pathObj struct {
|
||||
XMLName xml.Name `xml:"path"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
D string `xml:"d,attr"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
}
|
||||
|
||||
type rectObj struct {
|
||||
XMLName xml.Name `xml:"rect"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
X string `xml:"x,attr,omitempty"`
|
||||
Y string `xml:"y,attr,omitempty"`
|
||||
Width string `xml:"width,attr,omitempty"`
|
||||
Height string `xml:"height,attr,omitempty"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
}
|
||||
|
||||
type circleObj struct {
|
||||
XMLName xml.Name `xml:"circle"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
CX string `xml:"cx,attr,omitempty"`
|
||||
CY string `xml:"cy,attr,omitempty"`
|
||||
R string `xml:"r,attr,omitempty"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
}
|
||||
|
||||
type ellipseObj struct {
|
||||
XMLName xml.Name `xml:"ellipse"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
CX string `xml:"cx,attr,omitempty"`
|
||||
CY string `xml:"cy,attr,omitempty"`
|
||||
RX string `xml:"rx,attr,omitempty"`
|
||||
RY string `xml:"ry,attr,omitempty"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
}
|
||||
|
||||
type polygonObj struct {
|
||||
XMLName xml.Name `xml:"polygon"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
FillOpacity string `xml:"fill-opacity,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
Points string `xml:"points,attr"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
}
|
||||
|
||||
type objGroup struct {
|
||||
XMLName xml.Name `xml:"g"`
|
||||
ID string `xml:"id,attr,omitempty"`
|
||||
Fill string `xml:"fill,attr,omitempty"`
|
||||
Stroke string `xml:"stroke,attr,omitempty"`
|
||||
StrokeWidth string `xml:"stroke-width,attr,omitempty"`
|
||||
StrokeLineCap string `xml:"stroke-linecap,attr,omitempty"`
|
||||
StrokeLineJoin string `xml:"stroke-linejoin,attr,omitempty"`
|
||||
StrokeDashArray string `xml:"stroke-dasharray,attr,omitempty"`
|
||||
Transform string `xml:"transform,attr,omitempty"`
|
||||
Paths []*pathObj `xml:"path"`
|
||||
Circles []*circleObj `xml:"circle"`
|
||||
Ellipses []*ellipseObj `xml:"ellipse"`
|
||||
Rects []*rectObj `xml:"rect"`
|
||||
Polygons []*polygonObj `xml:"polygon"`
|
||||
Groups []*objGroup `xml:"g"`
|
||||
}
|
||||
|
||||
func replacePathsFill(paths []*pathObj, hexColor string, opacity string) {
|
||||
for _, path := range paths {
|
||||
if path.Fill != "none" {
|
||||
path.Fill = hexColor
|
||||
path.FillOpacity = opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceRectsFill(rects []*rectObj, hexColor string, opacity string) {
|
||||
for _, rect := range rects {
|
||||
if rect.Fill != "none" {
|
||||
rect.Fill = hexColor
|
||||
rect.FillOpacity = opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceCirclesFill(circles []*circleObj, hexColor string, opacity string) {
|
||||
for _, circle := range circles {
|
||||
if circle.Fill != "none" {
|
||||
circle.Fill = hexColor
|
||||
circle.FillOpacity = opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceEllipsesFill(ellipses []*ellipseObj, hexColor string, opacity string) {
|
||||
for _, ellipse := range ellipses {
|
||||
if ellipse.Fill != "none" {
|
||||
ellipse.Fill = hexColor
|
||||
ellipse.FillOpacity = opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replacePolygonsFill(polys []*polygonObj, hexColor string, opacity string) {
|
||||
for _, poly := range polys {
|
||||
if poly.Fill != "none" {
|
||||
poly.Fill = hexColor
|
||||
poly.FillOpacity = opacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceGroupObjectFill(groups []*objGroup, hexColor string, opacity string) {
|
||||
for _, grp := range groups {
|
||||
replaceCirclesFill(grp.Circles, hexColor, opacity)
|
||||
replaceEllipsesFill(grp.Ellipses, hexColor, opacity)
|
||||
replacePathsFill(grp.Paths, hexColor, opacity)
|
||||
replaceRectsFill(grp.Rects, hexColor, opacity)
|
||||
replacePolygonsFill(grp.Polygons, hexColor, opacity)
|
||||
replaceGroupObjectFill(grp.Groups, hexColor, opacity)
|
||||
}
|
||||
}
|
||||
|
||||
// replaceFillColor alters an svg objects fill color. Note that if an svg with multiple fill
|
||||
// colors is being operated upon, all fills will be converted to a single color. Mostly used
|
||||
// to recolor Icons to match the theme's IconColor.
|
||||
func (s *svg) replaceFillColor(color color.Color) error {
|
||||
hexColor, opacity := colorToHexAndOpacity(color)
|
||||
replacePathsFill(s.Paths, hexColor, opacity)
|
||||
replaceRectsFill(s.Rects, hexColor, opacity)
|
||||
replaceCirclesFill(s.Circles, hexColor, opacity)
|
||||
replaceEllipsesFill(s.Ellipses, hexColor, opacity)
|
||||
replacePolygonsFill(s.Polygons, hexColor, opacity)
|
||||
replaceGroupObjectFill(s.Groups, hexColor, opacity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func svgFromXML(reader io.Reader) (*svg, error) {
|
||||
var s svg
|
||||
bSlice, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(bSlice, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func colorToHexAndOpacity(color color.Color) (hexStr, aStr string) {
|
||||
r, g, b, a := toNRGBA(color)
|
||||
cBytes := []byte{byte(r), byte(g), byte(b)}
|
||||
hexStr, aStr = "#"+hex.EncodeToString(cBytes), strconv.FormatFloat(float64(a)/0xff, 'f', 6, 64)
|
||||
return
|
||||
}
|
||||
|
||||
// toNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA().
|
||||
func toNRGBA(c color.Color) (r, g, b, a int) {
|
||||
// We use UnmultiplyAlpha with RGBA, RGBA64, and unrecognized implementations of Color.
|
||||
// It works for all Colors whose RGBA() method is implemented according to spec, but is only necessary for those.
|
||||
// Only RGBA and RGBA64 have components which are already premultiplied.
|
||||
switch col := c.(type) {
|
||||
// NRGBA and NRGBA64 are not premultiplied
|
||||
case color.NRGBA:
|
||||
r = int(col.R)
|
||||
g = int(col.G)
|
||||
b = int(col.B)
|
||||
a = int(col.A)
|
||||
case *color.NRGBA:
|
||||
r = int(col.R)
|
||||
g = int(col.G)
|
||||
b = int(col.B)
|
||||
a = int(col.A)
|
||||
case color.NRGBA64:
|
||||
r = int(col.R) >> 8
|
||||
g = int(col.G) >> 8
|
||||
b = int(col.B) >> 8
|
||||
a = int(col.A) >> 8
|
||||
case *color.NRGBA64:
|
||||
r = int(col.R) >> 8
|
||||
g = int(col.G) >> 8
|
||||
b = int(col.B) >> 8
|
||||
a = int(col.A) >> 8
|
||||
// Gray and Gray16 have no alpha component
|
||||
case *color.Gray:
|
||||
r = int(col.Y)
|
||||
g = int(col.Y)
|
||||
b = int(col.Y)
|
||||
a = 0xff
|
||||
case color.Gray:
|
||||
r = int(col.Y)
|
||||
g = int(col.Y)
|
||||
b = int(col.Y)
|
||||
a = 0xff
|
||||
case *color.Gray16:
|
||||
r = int(col.Y) >> 8
|
||||
g = int(col.Y) >> 8
|
||||
b = int(col.Y) >> 8
|
||||
a = 0xff
|
||||
case color.Gray16:
|
||||
r = int(col.Y) >> 8
|
||||
g = int(col.Y) >> 8
|
||||
b = int(col.Y) >> 8
|
||||
a = 0xff
|
||||
// Alpha and Alpha16 contain only an alpha component.
|
||||
case color.Alpha:
|
||||
r = 0xff
|
||||
g = 0xff
|
||||
b = 0xff
|
||||
a = int(col.A)
|
||||
case *color.Alpha:
|
||||
r = 0xff
|
||||
g = 0xff
|
||||
b = 0xff
|
||||
a = int(col.A)
|
||||
case color.Alpha16:
|
||||
r = 0xff
|
||||
g = 0xff
|
||||
b = 0xff
|
||||
a = int(col.A) >> 8
|
||||
case *color.Alpha16:
|
||||
r = 0xff
|
||||
g = 0xff
|
||||
b = 0xff
|
||||
a = int(col.A) >> 8
|
||||
default: // RGBA, RGBA64, and unknown implementations of Color
|
||||
r, g, b, a = unmultiplyAlpha(c)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication.
|
||||
// It is only used by ToRGBA.
|
||||
func unmultiplyAlpha(c color.Color) (r, g, b, a int) {
|
||||
red, green, blue, alpha := c.RGBA()
|
||||
if alpha != 0 && alpha != 0xffff {
|
||||
red = (red * 0xffff) / alpha
|
||||
green = (green * 0xffff) / alpha
|
||||
blue = (blue * 0xffff) / alpha
|
||||
}
|
||||
// Convert from range 0-65535 to range 0-255
|
||||
r = int(red >> 8)
|
||||
g = int(green >> 8)
|
||||
b = int(blue >> 8)
|
||||
a = int(alpha >> 8)
|
||||
return
|
||||
}
|
||||
@@ -29,6 +29,10 @@ type coverImage struct {
|
||||
|
||||
Im *ImagePlaceholder
|
||||
playbtn *canvas.Image
|
||||
favoriteButton *canvas.Image
|
||||
moreButton *canvas.Image
|
||||
prevTheme fyne.ThemeVariant
|
||||
bottomPanel *fyne.Container
|
||||
mouseInsideBtn bool
|
||||
OnPlay func()
|
||||
OnShowPage func()
|
||||
@@ -38,9 +42,43 @@ type coverImage struct {
|
||||
var (
|
||||
playBtnSize = fyne.NewSize(60, 60)
|
||||
playBtnHoveredSize = fyne.NewSize(65, 65)
|
||||
|
||||
resourcesInitted bool
|
||||
heartFilledResource fyne.Resource
|
||||
heartFilledHoveredResource fyne.Resource
|
||||
heartUnfilledResource fyne.Resource
|
||||
heartUnfilledHoveredResource fyne.Resource
|
||||
moreVerticalResource fyne.Resource
|
||||
moreVerticalHoveredResource fyne.Resource
|
||||
inlineIconSize float32
|
||||
)
|
||||
|
||||
func initResources() {
|
||||
if resourcesInitted {
|
||||
return
|
||||
}
|
||||
resourcesInitted = true
|
||||
|
||||
inlineIconSize = fyne.CurrentApp().Settings().Theme().Size(theme.SizeNameInlineIcon)
|
||||
|
||||
// TODO: replace util.ColorizeSVG with Fyne's canvas.ColorizeSVG once
|
||||
// https://github.com/fyne-io/fyne/pull/5345 is available in Fyne
|
||||
c, _ := util.ColorizeSVG(myTheme.NotFavoriteIcon.Content(), myTheme.GridViewIconColor)
|
||||
heartUnfilledResource = fyne.NewStaticResource("gridviewnotfavorite", c)
|
||||
c, _ = util.ColorizeSVG(myTheme.NotFavoriteIcon.Content(), myTheme.GridViewHoveredIconColor)
|
||||
heartUnfilledHoveredResource = fyne.NewStaticResource("gridviewnotfavorite_hover", c)
|
||||
c, _ = util.ColorizeSVG(myTheme.FavoriteIcon.Content(), myTheme.GridViewIconColor)
|
||||
heartFilledResource = fyne.NewStaticResource("gridviewfavorite", c)
|
||||
c, _ = util.ColorizeSVG(myTheme.FavoriteIcon.Content(), myTheme.GridViewHoveredIconColor)
|
||||
heartFilledHoveredResource = fyne.NewStaticResource("gridviewfavorite_hover", c)
|
||||
c, _ = util.ColorizeSVG(theme.MoreVerticalIcon().Content(), myTheme.GridViewIconColor)
|
||||
moreVerticalResource = fyne.NewStaticResource("gridviewmore", c)
|
||||
c, _ = util.ColorizeSVG(theme.MoreVerticalIcon().Content(), myTheme.GridViewHoveredIconColor)
|
||||
moreVerticalHoveredResource = fyne.NewStaticResource("gridviewmore_hover", c)
|
||||
}
|
||||
|
||||
func newCoverImage(placeholderResource fyne.Resource) *coverImage {
|
||||
initResources()
|
||||
c := &coverImage{}
|
||||
c.Im = NewImagePlaceholder(placeholderResource, 200)
|
||||
c.Im.OnTapped = c.Tapped
|
||||
@@ -49,13 +87,43 @@ func newCoverImage(placeholderResource fyne.Resource) *coverImage {
|
||||
c.playbtn = &canvas.Image{FillMode: canvas.ImageFillContain, Resource: res.ResPlaybuttonPng}
|
||||
c.playbtn.SetMinSize(playBtnSize)
|
||||
c.playbtn.Hidden = true
|
||||
|
||||
c.favoriteButton = canvas.NewImageFromResource(heartUnfilledResource)
|
||||
c.favoriteButton.SetMinSize(fyne.NewSquareSize(inlineIconSize))
|
||||
c.moreButton = canvas.NewImageFromResource(moreVerticalResource)
|
||||
c.moreButton.SetMinSize(fyne.NewSquareSize(inlineIconSize))
|
||||
c.bottomPanel = container.NewStack(
|
||||
canvas.NewVerticalGradient(color.Transparent, color.Black),
|
||||
container.NewVBox(
|
||||
layout.NewSpacer(), // keep the HBox pushed down
|
||||
container.NewHBox(
|
||||
layout.NewSpacer(),
|
||||
c.favoriteButton,
|
||||
c.moreButton,
|
||||
util.NewHSpace(0),
|
||||
),
|
||||
container.New(
|
||||
layout.NewCustomPaddedLayout(0, theme.Padding()*2, 0, 0),
|
||||
layout.NewSpacer(),
|
||||
),
|
||||
),
|
||||
)
|
||||
c.bottomPanel.Hidden = true
|
||||
|
||||
c.ExtendBaseWidget(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *coverImage) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(
|
||||
container.NewStack(c.Im, container.NewCenter(c.playbtn)),
|
||||
container.NewStack(
|
||||
c.Im,
|
||||
container.NewCenter(c.playbtn),
|
||||
container.NewGridWithRows(2,
|
||||
layout.NewSpacer(),
|
||||
c.bottomPanel,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,12 +151,14 @@ func (c *coverImage) TappedSecondary(e *fyne.PointEvent) {
|
||||
|
||||
func (a *coverImage) MouseIn(*desktop.MouseEvent) {
|
||||
a.playbtn.Hidden = false
|
||||
a.bottomPanel.Hidden = false
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
func (a *coverImage) MouseOut() {
|
||||
a.mouseInsideBtn = false
|
||||
a.playbtn.Hidden = true
|
||||
a.bottomPanel.Hidden = true
|
||||
a.Refresh()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user