diff --git a/icon-theme/icon-theme.go b/icon-theme/icon-theme.go index bd428a0..6915316 100644 --- a/icon-theme/icon-theme.go +++ b/icon-theme/icon-theme.go @@ -6,6 +6,7 @@ import "io" import "fmt" import "log" import "image" +import "strings" import "path/filepath" import "git.tebibyte.media/tomo/xdg/basedir" import "git.tebibyte.media/tomo/xdg/key-value" @@ -34,6 +35,8 @@ const ( // ErrCircularDependency indicates that an icon theme depends on one of // its dependents. 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 @@ -298,6 +301,66 @@ type Theme struct { 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 // icons. type Directory struct { @@ -312,6 +375,79 @@ type Directory struct { 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 // 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 @@ -343,9 +479,65 @@ func IconSizeTypeFromString (value string) (IconSizeType, error) { } } -// Icon represents data in .icon files +// Icon represents an icon, and its associated data (if applicable). type Icon struct { - DisplayName string + Path string + FileType IconFileType + DisplayName keyValue.Entry EmbeddedTextRectangle image.Rectangle 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 +} diff --git a/key-value/key-value.go b/key-value/key-value.go index ab18e93..1f8975a 100644 --- a/key-value/key-value.go +++ b/key-value/key-value.go @@ -283,8 +283,6 @@ func (entry Entry) Unescape () (Entry, error) { return localizedValue, nil } -// TODO have functions to parse/validate all data types - // ParseString parses a value of type string. // Values of type string may contain all ASCII characters except for control // characters.