Repository: fyne-io/fyne
File: theme/json.go (Go)

Code before:
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSON(data string) (fyne.Theme, error) {
	return FromJSONReader(strings.NewReader(data))
}

// FromJSONReader returns a Theme created from the given JSON metadata through the reader.
// Any values not present in the data will fall back to the default theme.
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSONReader(r io.Reader) (fyne.Theme, error) {
	return fromJSONWithFallback(r, DefaultTheme())
}

func fromJSONWithFallback(r io.Reader, fallback fyne.Theme) (fyne.Theme, error) {
	var th *schema
	if err := json.NewDecoder(r).Decode(&th); err != nil {
		return fallback, err
	}

	return &jsonTheme{data: th, fallback: fallback}, nil
}

var parsedColors = map[hexColor]color.Color{}

type hexColor string

func (h hexColor) color() (color.Color, error) {
	if parsed, ok := parsedColors[h]; ok {
		return parsed, nil
	}

	data := h
	switch len([]rune(h)) {
	case 8, 6:
	case 9, 7: // remove # prefix
		data = h[1:]
	case 5: // remove # prefix, then double up
		data = h[1:]
		fallthrough
	case 4: // could be rgba or #rgb
		if data[0] == '#' {
			v := []rune(data[1:])
			data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2]})
			break
		}

		v := []rune(data)
		data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2], v[3], v[3]})

Review comment (performance):
This is a global map that is only added to but never deleted from. It will be a memory leak for apps that switch themes. A possible solution could be to scope it to be a member of the jsonTheme struct, so that loading a new JSON theme would start with a new parsedColors map and allow the old one to be GC'ed

Code after:
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSON(data string) (fyne.Theme, error) {
	return FromJSONReader(strings.NewReader(data))
}

// FromJSONReader returns a Theme created from the given JSON metadata through the reader.
// Any values not present in the data will fall back to the default theme.
// If a parse error occurs it will be returned along with a default theme.
//
// Since: 2.2
func FromJSONReader(r io.Reader) (fyne.Theme, error) {
	return fromJSONWithFallback(r, DefaultTheme())
}

func fromJSONWithFallback(r io.Reader, fallback fyne.Theme) (fyne.Theme, error) {
	var th *schema
	if err := json.NewDecoder(r).Decode(&th); err != nil {
		return fallback, err
	}

	return &jsonTheme{data: th, fallback: fallback, parsedColors: map[hexColor]color.Color{}}, nil
}

type hexColor string

func (h hexColor) color(cache map[hexColor]color.Color) (color.Color, error) {
	if parsed, ok := cache[h]; ok {
		return parsed, nil
	}

	data := h
	switch len([]rune(h)) {
	case 8, 6:
	case 9, 7: // remove # prefix
		data = h[1:]
	case 5: // remove # prefix, then double up
		data = h[1:]
		fallthrough
	case 4: // could be rgba or #rgb
		if data[0] == '#' {
			v := []rune(data[1:])
			data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2]})
			break
		}

		v := []rune(data)
		data = hexColor([]rune{v[0], v[0], v[1], v[1], v[2], v[2], v[3], v[3]})
	case 3:
		v := []rune(h)