Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions theme/icons.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package theme

import (
"bytes"
"image"
"image/color"
_ "image/jpeg" // register JPEG decoder so DisabledResource can desaturate JPEG icons
"image/png"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/svg"
Expand Down Expand Up @@ -838,9 +842,57 @@ func (res *DisabledResource) Name() string {
return "disabled_" + unwrapResource(res.source).Name()
}

// Content returns the disabled style content of the correct resource for the current theme
// ITU-R BT.601 luma coefficients, scaled by 1000 for integer math.
const (
lumaWeightR = 299
lumaWeightG = 587
lumaWeightB = 114
lumaScale = 1000
)

// Content returns the disabled style content of the correct resource for the current theme.
// SVG resources are recolored with the theme's disabled color; bitmap resources (PNG, JPEG, ...)
// are desaturated to greyscale.
func (res *DisabledResource) Content() []byte {
return colorizeLogError(unwrapResource(res.source).Content(), Color(ColorNameDisabled))
src := unwrapResource(res.source)
content := src.Content()
if svg.IsResourceSVG(src) {
return colorizeLogError(content, Color(ColorNameDisabled))
}
out, err := desaturate(content)
if err != nil {
fyne.LogError("Failed to desaturate bitmap for disabled state", err)
return content
}
return out
}

// desaturate returns a PNG-encoded greyscale copy of the given image bytes,
// preserving the alpha channel.
func desaturate(src []byte) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(src))
if err != nil {
return nil, err
}
bounds := img.Bounds()
gray := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// Convert via NRGBA so the luminance is computed on unpremultiplied
// channels — otherwise partially-transparent pixels go too dark.
n := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
lum := (lumaWeightR*uint32(n.R) + lumaWeightG*uint32(n.G) + lumaWeightB*uint32(n.B)) / lumaScale
if lum > 255 {
lum = 255
}
gray.SetNRGBA(x, y, color.NRGBA{R: uint8(lum), G: uint8(lum), B: uint8(lum), A: n.A})
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, gray); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color.
Expand Down
4 changes: 1 addition & 3 deletions widget/button.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
col "fyne.io/fyne/v2/internal/color"
"fyne.io/fyne/v2/internal/svg"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
Expand Down Expand Up @@ -419,8 +418,7 @@ func (r *buttonRenderer) updateIconAndText() {
r.icon.FillMode = canvas.ImageFillContain
r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon})
}
// TODO support disabling bitmap resource not just SVG
if r.button.Disabled() && svg.IsResourceSVG(icon) {
if r.button.Disabled() {
icon = theme.NewDisabledResource(icon)
}
r.icon.Resource = icon
Expand Down
19 changes: 19 additions & 0 deletions widget/button_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ func TestButton_DisabledIconChangedDirectly(t *testing.T) {
assert.Equal(t, render.icon.Resource.Name(), fmt.Sprintf("disabled_%v", searchBaseName))
}

func TestButton_DisabledBitmapIcon(t *testing.T) {
pngIcon := fyne.NewStaticResource("fyne.png", iconData)
button := NewButtonWithIcon("Test", pngIcon, nil)
render := test.TempWidgetRenderer(t, button).(*buttonRenderer)

// While enabled the original bitmap resource is rendered untouched.
assert.Equal(t, pngIcon, render.icon.Resource)

// When disabled the resource is wrapped so DisabledResource.Content()
// returns a desaturated PNG copy — the icon is recolored, not faded.
button.Disable()
assert.True(t, strings.HasPrefix(render.icon.Resource.Name(), "disabled_"),
"disabled icon should be wrapped: %s", render.icon.Resource.Name())

// Re-enabling restores the original resource reference.
button.Enable()
assert.Equal(t, pngIcon, render.icon.Resource)
}

func TestButton_Focus(t *testing.T) {
tapped := false
button := NewButton("Test", func() {
Expand Down
Loading