// Package theme provides a data-driven theme implementation. package theme import "image" import "image/color" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/theme" import "git.tebibyte.media/tomo/tomo/canvas" // Theme allows the use of data to define a visual style. type Theme struct { // Textures maps texture names to image textures. Textures map[string] image.Image textures map[string] canvas.TextureCloser // private texture cache missing canvas.TextureCloser // cache for "missing" texture // Default lists style attributes that apply to all objects, which are // overridden by attributes in the Rules map. Default Rule // Rules determines which styles get applied to which Objects. Rules map[theme.Role] Rule // Colors maps theme.Color values to color.RGBA values. Colors map[theme.Color] color.Color // This type does not handle icons, and as such, a special icon theme // must be separately specified. IconTheme } // IconTheme implements the part of theme.Theme that handles icons. type IconTheme interface { // Icon returns a texture of the corresponding icon ID. Icon (theme.Icon, theme.IconSize) canvas.Texture // MimeIcon returns an icon corresponding to a MIME type. MimeIcon (data.Mime, theme.IconSize) canvas.Texture } // Rule describes under what circumstances should certain style attributes be // active. type Rule struct { Default []Attr Hovered []Attr Pressed []Attr } func (this *Theme) execute (object tomo.Object, attrs ...Attr) { box := object.GetBox() for _, attr := range attrs { switch attr := attr.(type) { case AttrColor: box.SetColor(attr.Color) case AttrTexture: box.SetTexture(this.texture(string(attr))) case AttrBorder: box.SetBorder([]tomo.Border(attr)...) case AttrMinimumSize: box.SetMinimumSize(image.Point(attr)) case AttrPadding: box.SetPadding(tomo.Inset(attr)) case AttrGap: if box, ok := box.(tomo.ContainerBox); ok { box.SetGap(image.Point(attr)) } case AttrTextColor: if box, ok := box.(tomo.TextBox); ok { box.SetTextColor(attr.Color) } case AttrFace: if box, ok := box.(tomo.TextBox); ok { box.SetFace(attr) } } } } func (this *Theme) texture (name string) canvas.Texture { this.ensureTextureCache() if texture, ok := this.textures[name]; ok { return texture } if this.Textures == nil { if source, ok := this.Textures[name]; ok { texture := tomo.NewTexture(source) this.textures[name] = texture return texture } } return this.missingTexture() } func (this *Theme) missingTexture () canvas.Texture { if this.missing == nil { this.missing = tomo.NewTexture(missingTexture(16)) } return this.missing } func (this *Theme) ensureTextureCache () { if this.textures == nil { this.textures = make(map[string] canvas.TextureCloser) } } func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie { pressed := false hovered := false box := object.GetBox() var rule Rule if this.Rules != nil { rule, _ = this.Rules[role] } updateStyle := func () { // hover styles override default styles, pressed styles // override hovered styles, and specific styles override default // styles. // default this.execute(object, this.Default.Default...) if hovered { this.execute(object, this.Default.Hovered...) } if pressed { this.execute(object, this.Default.Pressed...) } // specific this.execute(object, rule.Default...) if hovered { this.execute(object, rule.Hovered...) } if pressed { this.execute(object, rule.Pressed...) } } return event.MultiCookie ( box.OnFocusEnter(updateStyle), box.OnFocusLeave(updateStyle), box.OnMouseDown(func (button input.Button) { pressed = true updateStyle() }), box.OnMouseUp(func (button input.Button) { pressed = false updateStyle() }), box.OnMouseEnter(func () { hovered = true updateStyle() }), box.OnMouseLeave(func () { hovered = false updateStyle() })) } func (this *Theme) RGBA (c theme.Color) (r, g, b, a uint32) { if this.Colors == nil { return 0xFFFF, 0, 0xFFFF, 0xFFFF } color, ok := this.Colors[c] if !ok { return 0xFFFF, 0, 0xFFFF, 0xFFFF } return color.RGBA() } func (this *Theme) Icon (icon theme.Icon, size theme.IconSize) canvas.Texture { if this.IconTheme == nil { return this.missingTexture() } else { return this.IconTheme.Icon(icon, size) } } func (this *Theme) MimeIcon (mime data.Mime, size theme.IconSize) canvas.Texture { if this.IconTheme == nil { return this.missingTexture() } else { return this.IconTheme.MimeIcon(mime, size) } } // Close closes all cached textures this theme has open. Do not call this while // the theme is in use. func (this *Theme) Close () error { this.missing.Close() this.missing = nil for _, texture := range this.textures { texture.Close() } this.textures = nil return nil }