Add icon lookup
This commit is contained in:
parent
e9ff323f69
commit
19b442598d
@ -6,6 +6,7 @@ import "io"
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
import "log"
|
import "log"
|
||||||
import "image"
|
import "image"
|
||||||
|
import "strings"
|
||||||
import "path/filepath"
|
import "path/filepath"
|
||||||
import "git.tebibyte.media/tomo/xdg/basedir"
|
import "git.tebibyte.media/tomo/xdg/basedir"
|
||||||
import "git.tebibyte.media/tomo/xdg/key-value"
|
import "git.tebibyte.media/tomo/xdg/key-value"
|
||||||
@ -34,6 +35,8 @@ const (
|
|||||||
// ErrCircularDependency indicates that an icon theme depends on one of
|
// ErrCircularDependency indicates that an icon theme depends on one of
|
||||||
// its dependents.
|
// its dependents.
|
||||||
ErrCircularDependency = iconThemeError("circular dependency")
|
ErrCircularDependency = iconThemeError("circular dependency")
|
||||||
|
// ErrIconNotFound indicates that an icon could not be found.
|
||||||
|
ErrIconNotFound = iconThemeError("icon not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ThemeDirs returns the set of directories in which themes should be looked for
|
// ThemeDirs returns the set of directories in which themes should be looked for
|
||||||
@ -298,6 +301,66 @@ type Theme struct {
|
|||||||
Example string
|
Example string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindIcon finds the best icon of the given name for the given size and scale.
|
||||||
|
// If a list of supported filetypes is not given, { PNG, SVG, XPM } is used
|
||||||
|
// instead.
|
||||||
|
func (theme Theme) FindIcon (name string, size, scale int, supported ...IconFileType) (Icon, error) {
|
||||||
|
// this method is equivalent to FindIconHelper
|
||||||
|
if len(supported) == 0 { supported = []IconFileType{ PNG, SVG, XPM } }
|
||||||
|
|
||||||
|
// try to find icon in this theme first
|
||||||
|
icon, err := theme.findIconHere(name, size, scale, supported...)
|
||||||
|
if err == nil { return icon, nil }
|
||||||
|
|
||||||
|
// if that didn't work, recurse through parent themes
|
||||||
|
for _, parent := range theme.Inherits {
|
||||||
|
icon, err := parent.FindIcon(name, size, scale, supported...)
|
||||||
|
if err == nil { return icon, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon { }, ErrIconNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme Theme) findIconHere (name string, size, scale int, supported ...IconFileType) (Icon, error) {
|
||||||
|
// this method is equivalent to LookupIcon
|
||||||
|
|
||||||
|
// stage 1: attempt to find an exact match
|
||||||
|
for _, directory := range theme.Directories {
|
||||||
|
if directory.Fits(size, scale) {
|
||||||
|
for _, path := range directory.Paths {
|
||||||
|
for _, ty := range supported {
|
||||||
|
icon, err := parseIcon(filepath.Join(path, ty.Extend(name)))
|
||||||
|
if err == nil { return icon, err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 2: find the best icon matching the name
|
||||||
|
minimalDistance := int((^uint(0)) >> 1)
|
||||||
|
closestFilename := ""
|
||||||
|
for _, directory := range theme.Directories {
|
||||||
|
sizeDistance := directory.SizeDistance(size, scale)
|
||||||
|
if sizeDistance < minimalDistance {
|
||||||
|
for _, path := range directory.Paths {
|
||||||
|
for _, ty := range supported {
|
||||||
|
iconFilename := filepath.Join(path, ty.Extend(name))
|
||||||
|
if _, err := os.Stat(iconFilename); err == nil {
|
||||||
|
closestFilename = iconFilename
|
||||||
|
minimalDistance = sizeDistance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if closestFilename != "" {
|
||||||
|
icon, err := parseIcon(closestFilename)
|
||||||
|
if err == nil { return icon, err }
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon { }, ErrIconNotFound
|
||||||
|
}
|
||||||
|
|
||||||
// Directory represents a directory of an icon theme which directly contains
|
// Directory represents a directory of an icon theme which directly contains
|
||||||
// icons.
|
// icons.
|
||||||
type Directory struct {
|
type Directory struct {
|
||||||
@ -312,6 +375,79 @@ type Directory struct {
|
|||||||
Threshold int
|
Threshold int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fits returns whether icons in the directory fit the given size and scale.
|
||||||
|
func (directory Directory) Fits (size, scale int) bool {
|
||||||
|
// this method is equivalent to DirectoryMatchesSize
|
||||||
|
if directory.Scale != scale { return false }
|
||||||
|
switch directory.Type {
|
||||||
|
case IconSizeTypeFixed:
|
||||||
|
return directory.Size == size
|
||||||
|
case IconSizeTypeScalable:
|
||||||
|
return directory.MinSize <= size && size <= directory.MaxSize
|
||||||
|
case IconSizeTypeThreshold:
|
||||||
|
return directory.Size - directory.Threshold <=
|
||||||
|
size && size <=
|
||||||
|
directory.Size + directory.Threshold
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeDistance returns how close the size of this directory's icons is to the
|
||||||
|
// given size. A smaller number is closer.
|
||||||
|
func (directory Directory) SizeDistance (size, scale int) int {
|
||||||
|
// this method is equivalent to DirectorySizeDistance
|
||||||
|
// TODO: the pseudocode for DirectorySizeDistance in the spec seems
|
||||||
|
// incredibly wrong, and the code here has been altered to correct for
|
||||||
|
// that. figure out why the spec is the way it is.
|
||||||
|
scaledSize := size * scale
|
||||||
|
scaledDir := directory.Size * directory.Scale
|
||||||
|
scaledMin := directory.MinSize * directory.Scale
|
||||||
|
scaledMax := directory.MaxSize * directory.Scale
|
||||||
|
threshMin := (directory.Size - directory.Threshold) * directory.Scale
|
||||||
|
threshMax := (directory.Size + directory.Threshold) * directory.Scale
|
||||||
|
|
||||||
|
switch directory.Type {
|
||||||
|
case IconSizeTypeFixed:
|
||||||
|
difference := scaledDir - scaledSize
|
||||||
|
if difference > 0 {
|
||||||
|
return difference
|
||||||
|
} else {
|
||||||
|
return -difference
|
||||||
|
}
|
||||||
|
|
||||||
|
case IconSizeTypeScalable:
|
||||||
|
if scaledSize < scaledMin { return scaledMin - scaledSize }
|
||||||
|
if scaledSize > scaledMax { return scaledSize - scaledMax }
|
||||||
|
|
||||||
|
case IconSizeTypeThreshold:
|
||||||
|
if scaledSize < threshMin { return threshMin - scaledSize }
|
||||||
|
if scaledSize > threshMax { return scaledSize - threshMin }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IconFileType specifies the file type of an icon.
|
||||||
|
type IconFileType uint; const (
|
||||||
|
PNG IconFileType = iota
|
||||||
|
SVG
|
||||||
|
XPM
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns a string representation of the file type.
|
||||||
|
func (ty IconFileType) String () string {
|
||||||
|
switch ty {
|
||||||
|
case PNG: return ".png"
|
||||||
|
case SVG: return ".svg"
|
||||||
|
case XPM: return ".xpm"
|
||||||
|
default: return fmt.Sprintf("IconFileType(%d)", ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend applies the file extension to a file name.
|
||||||
|
func (ty IconFileType) Extend (name string) string {
|
||||||
|
return name + ty.String()
|
||||||
|
}
|
||||||
|
|
||||||
// IconSizeType specifies how an icon can be scaled. Fixed icons cannot be
|
// IconSizeType specifies how an icon can be scaled. Fixed icons cannot be
|
||||||
// scaled, scalable icons can be scaled to any size within a min and max range,
|
// scaled, scalable icons can be scaled to any size within a min and max range,
|
||||||
// and threshold icons can be scaled to any size within a certain distance of
|
// and threshold icons can be scaled to any size within a certain distance of
|
||||||
@ -343,9 +479,65 @@ func IconSizeTypeFromString (value string) (IconSizeType, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon represents data in <icon-name>.icon files
|
// Icon represents an icon, and its associated data (if applicable).
|
||||||
type Icon struct {
|
type Icon struct {
|
||||||
DisplayName string
|
Path string
|
||||||
|
FileType IconFileType
|
||||||
|
DisplayName keyValue.Entry
|
||||||
EmbeddedTextRectangle image.Rectangle
|
EmbeddedTextRectangle image.Rectangle
|
||||||
AttachPoints []image.Point
|
AttachPoints []image.Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIcon (path string) (Icon, error) {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
dataPath := strings.TrimSuffix(path, ext) + ".icon"
|
||||||
|
|
||||||
|
if data, err := parseIconData(dataPath); err != nil {
|
||||||
|
return data, nil
|
||||||
|
} else {
|
||||||
|
return Icon {
|
||||||
|
Path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIconData (path string) (Icon, error) {
|
||||||
|
reader, err := os.Open(path)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
file, err := keyValue.Parse(reader)
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
|
||||||
|
iconDataGroup, ok := file["Icon Data"]
|
||||||
|
if !ok { return Icon { }, ErrGroupMissing }
|
||||||
|
|
||||||
|
icon := Icon { }
|
||||||
|
|
||||||
|
// DisplayName (optional)
|
||||||
|
if entry, ok := iconDataGroup["DisplayName"]; ok {
|
||||||
|
icon.DisplayName, err = entry.Unescape()
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedTextRectangle (optional)
|
||||||
|
if entry, ok := iconDataGroup["EmbeddedTextRectangle"]; ok {
|
||||||
|
embeddedTextRectangle, err := keyValue.ParseMultipleComma(keyValue.ParseInteger, entry.Value)
|
||||||
|
if len(embeddedTextRectangle) == 4 {
|
||||||
|
icon.EmbeddedTextRectangle = image.Rect (
|
||||||
|
embeddedTextRectangle[0],
|
||||||
|
embeddedTextRectangle[1],
|
||||||
|
embeddedTextRectangle[2],
|
||||||
|
embeddedTextRectangle[3])
|
||||||
|
if err != nil { return Icon { }, err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO figure out what the actual fuck the spec is talking about with
|
||||||
|
// regard to AttatchPoints. What even the fuck is that supposed to mean.
|
||||||
|
// Ohhhh a list of points separated by |. Mf hoe what do you define as
|
||||||
|
// a point.
|
||||||
|
|
||||||
|
return icon, nil
|
||||||
|
}
|
||||||
|
@ -283,8 +283,6 @@ func (entry Entry) Unescape () (Entry, error) {
|
|||||||
return localizedValue, nil
|
return localizedValue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO have functions to parse/validate all data types
|
|
||||||
|
|
||||||
// ParseString parses a value of type string.
|
// ParseString parses a value of type string.
|
||||||
// Values of type string may contain all ASCII characters except for control
|
// Values of type string may contain all ASCII characters except for control
|
||||||
// characters.
|
// characters.
|
||||||
|
Loading…
Reference in New Issue
Block a user