2024-05-28 19:55:51 -06:00
|
|
|
package xdgIcons
|
|
|
|
|
|
|
|
import "os"
|
|
|
|
import "fmt"
|
|
|
|
import "log"
|
|
|
|
import "image"
|
|
|
|
import "regexp"
|
|
|
|
import "strings"
|
|
|
|
import _ "image/png"
|
|
|
|
import "git.tebibyte.media/tomo/tomo"
|
|
|
|
import "git.tebibyte.media/tomo/tomo/data"
|
2024-08-22 23:10:54 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo/event"
|
2024-05-28 19:55:51 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
2024-08-10 19:56:03 -06:00
|
|
|
import "git.tebibyte.media/tomo/backend/style"
|
2024-08-19 01:18:37 -06:00
|
|
|
import "git.tebibyte.media/tomo/nasin/internal/util"
|
2024-08-10 19:56:03 -06:00
|
|
|
import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
|
2024-05-28 19:55:51 -06:00
|
|
|
|
|
|
|
type iconTheme struct {
|
|
|
|
xdg xdgIconTheme.Theme
|
2024-08-10 19:56:03 -06:00
|
|
|
fallback style.IconSet
|
2024-08-22 23:10:54 -06:00
|
|
|
texturesSmall map[tomo.Icon] canvas.TextureCloser
|
|
|
|
texturesMedium map[tomo.Icon] canvas.TextureCloser
|
|
|
|
texturesLarge map[tomo.Icon] canvas.TextureCloser
|
2024-05-28 19:55:51 -06:00
|
|
|
}
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, event.Cookie, error) {
|
2024-05-28 19:55:51 -06:00
|
|
|
this := &iconTheme {
|
2024-05-28 20:13:42 -06:00
|
|
|
fallback: fallback,
|
2024-08-22 23:10:54 -06:00
|
|
|
texturesLarge: make(map[tomo.Icon] canvas.TextureCloser),
|
|
|
|
texturesMedium: make(map[tomo.Icon] canvas.TextureCloser),
|
|
|
|
texturesSmall: make(map[tomo.Icon] canvas.TextureCloser),
|
2024-05-28 19:55:51 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
xdg, err := xdgIconTheme.FindThemeWarn(name, path...)
|
2024-08-22 23:10:54 -06:00
|
|
|
if err != nil { return nil, nil, err }
|
2024-05-28 19:55:51 -06:00
|
|
|
this.xdg = xdg
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
return this, this, nil
|
2024-05-28 19:55:51 -06:00
|
|
|
}
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.TextureCloser {
|
2024-05-28 19:55:51 -06:00
|
|
|
switch size {
|
|
|
|
case tomo.IconSizeMedium: return this.texturesMedium
|
|
|
|
case tomo.IconSizeLarge: return this.texturesLarge
|
|
|
|
default: return this.texturesSmall
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.TextureCloser, bool) {
|
2024-05-28 19:55:51 -06:00
|
|
|
// TODO use scaling factor instead of 1
|
|
|
|
// find icon file
|
|
|
|
icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG)
|
|
|
|
if err != nil { return nil, false }
|
|
|
|
|
|
|
|
// open icon file
|
|
|
|
iconFile, err := os.Open(icon.Path)
|
|
|
|
if err != nil {
|
|
|
|
// this failing indicates a broken icon theme
|
2024-06-03 01:04:29 -06:00
|
|
|
log.Printf("nasin: icon file '%s' is inaccessible: %v\n", icon.Path, err)
|
2024-05-28 19:55:51 -06:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
iconImage, _, err := image.Decode(iconFile)
|
|
|
|
if err != nil {
|
|
|
|
// this failing indicates a broken icon theme
|
2024-06-03 01:04:29 -06:00
|
|
|
log.Printf("nasin: icon file '%s' is broken: %v\n", icon.Path, err)
|
2024-05-28 19:55:51 -06:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
return tomo.NewTexture(iconImage), true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture {
|
|
|
|
source := this.selectSource(size)
|
2024-05-28 20:13:42 -06:00
|
|
|
texture, ok := source[icon]
|
|
|
|
if !ok {
|
|
|
|
texture = this.icon(icon, size)
|
|
|
|
source[icon] = texture
|
|
|
|
}
|
|
|
|
|
|
|
|
if texture == nil {
|
|
|
|
return this.fallback.Icon(icon, size)
|
|
|
|
} else {
|
|
|
|
return texture
|
|
|
|
}
|
2024-05-28 19:55:51 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
|
|
|
|
icon := tomo.Icon(mime.String())
|
|
|
|
source := this.selectSource(size)
|
2024-05-28 20:13:42 -06:00
|
|
|
texture, ok := source[icon]
|
|
|
|
if !ok {
|
|
|
|
texture = this.mimeIcon(mime, size)
|
|
|
|
source[icon] = texture
|
|
|
|
}
|
|
|
|
|
|
|
|
if texture == nil {
|
|
|
|
return this.fallback.MimeIcon(mime, size)
|
|
|
|
} else {
|
|
|
|
return texture
|
|
|
|
}
|
2024-05-28 19:55:51 -06:00
|
|
|
}
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
func (this *iconTheme) Close () {
|
|
|
|
closeAllIn := func (mp map[tomo.Icon] canvas.TextureCloser) {
|
|
|
|
for _, texture := range mp {
|
|
|
|
if texture != nil {
|
|
|
|
texture.Close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
closeAllIn(this.texturesSmall)
|
|
|
|
closeAllIn(this.texturesMedium)
|
|
|
|
closeAllIn(this.texturesLarge)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.TextureCloser {
|
2024-05-28 19:55:51 -06:00
|
|
|
if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok {
|
|
|
|
return texture
|
|
|
|
}
|
|
|
|
if texture, ok := this.xdgIcon(XdgIconName(generalizeIcon(icon)), size); ok {
|
|
|
|
return texture
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-08-22 23:10:54 -06:00
|
|
|
func (this *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.TextureCloser {
|
2024-05-28 19:55:51 -06:00
|
|
|
if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok {
|
|
|
|
return texture
|
|
|
|
}
|
2024-08-19 01:18:37 -06:00
|
|
|
if texture, ok := this.xdgIcon(xdgFormatMime(util.GeneralizeXDGIconMimeType(mime)), size); ok {
|
2024-05-28 19:55:51 -06:00
|
|
|
return texture
|
|
|
|
}
|
|
|
|
if texture, ok := this.xdgIcon(xdgFormatMime(data.M("text", "x-generic")), size); ok {
|
|
|
|
return texture
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var kebabMatchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
|
|
|
var kebabMatchAllCaps = regexp.MustCompile("([a-z0-9])([A-Z])")
|
|
|
|
|
|
|
|
// XdgIconName returns the best XDG name for the given icon.
|
|
|
|
func XdgIconName (icon tomo.Icon) string {
|
|
|
|
if name, ok := xdgIconNames[icon]; ok {
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
name := kebabMatchFirstCap.ReplaceAllString(string(icon), "${1}-${2}")
|
|
|
|
name = kebabMatchAllCaps.ReplaceAllString(string(name), "${1}-${2}")
|
|
|
|
return strings.ToLower(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func generalizeIcon (icon tomo.Icon) tomo.Icon {
|
|
|
|
name := string(icon)
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(name, "Application"): return tomo.IconApplication
|
|
|
|
case strings.HasPrefix(name, "Preferences"): return tomo.IconPreferences
|
|
|
|
case strings.HasPrefix(name, "Device"): return tomo.IconDevice
|
|
|
|
case strings.HasPrefix(name, "Hardware"): return tomo.IconHardware
|
|
|
|
case strings.HasPrefix(name, "Storage"): return tomo.IconStorageHardDisk
|
|
|
|
case strings.HasPrefix(name, "Input"): return tomo.IconInputMouse
|
|
|
|
case strings.HasPrefix(name, "Network"): return tomo.IconNetworkWired
|
|
|
|
case strings.HasPrefix(name, "Place"): return tomo.IconPlaceDirectory
|
|
|
|
case strings.HasPrefix(name, "Directory"): return tomo.IconPlaceDirectory
|
|
|
|
case strings.HasPrefix(name, "Trash"): return tomo.IconPlaceTrash
|
|
|
|
case strings.HasPrefix(name, "Help"): return tomo.IconHelpContents
|
|
|
|
}
|
|
|
|
|
|
|
|
switch icon {
|
|
|
|
case tomo.IconCellularSignal0: return tomo.IconWirelessSignal0
|
|
|
|
case tomo.IconCellularSignal1: return tomo.IconWirelessSignal1
|
|
|
|
case tomo.IconCellularSignal2: return tomo.IconWirelessSignal2
|
|
|
|
case tomo.IconCellularSignal3: return tomo.IconWirelessSignal3
|
|
|
|
}
|
|
|
|
|
|
|
|
return icon
|
|
|
|
}
|
|
|
|
|
|
|
|
func xdgFormatMime (mime data.Mime) string {
|
|
|
|
return fmt.Sprintf("%s-%s", mime.Type, mime.Subtype)
|
|
|
|
}
|
|
|
|
|
|
|
|
func iconSizePixels (size tomo.IconSize) int {
|
|
|
|
// TODO: once Tomo has scaling support, take that into account here
|
|
|
|
switch size {
|
|
|
|
case tomo.IconSizeMedium: return 24
|
|
|
|
case tomo.IconSizeLarge: return 48
|
|
|
|
default: return 16
|
|
|
|
}
|
|
|
|
}
|