diff --git a/internal/theme/attribute.go b/internal/theme/attribute.go new file mode 100644 index 0000000..3fd4481 --- /dev/null +++ b/internal/theme/attribute.go @@ -0,0 +1,34 @@ +package theme + +import "image" +import "image/color" +import "golang.org/x/image/font" +import "git.tebibyte.media/tomo/tomo" + +// Attr modifies one thing about an Objects's style. +type Attr interface { attr () } + +// AttrColor sets the background color of an Objects. +type AttrColor struct { color.Color } +func (AttrColor) attr () { } +// AttrTexture sets the texture of an Objects to a named texture. +type AttrTexture string +func (AttrTexture) attr () { } +// AttrBorder sets the border of an Objects. +type AttrBorder []tomo.Border +func (AttrBorder) attr () { } +// AttrMinimumSize sets the minimum size of an Objects. +type AttrMinimumSize image.Point +func (AttrMinimumSize) attr () { } +// AttrPadding sets the inner padding of an Objects. +type AttrPadding tomo.Inset +func (AttrPadding) attr () { } +// AttrGap sets the gap between child Objects, if the Object is a ContainerBox. +type AttrGap image.Point +func (AttrGap) attr () { } +// AttrTextColor sets the text color, if the Object is a TextBox. +type AttrTextColor struct { color.Color } +func (AttrTextColor) attr () { } +// AttrFace sets the font face, if the Object is a TextBox. +type AttrFace struct { font.Face } +func (AttrFace) attr () { } diff --git a/internal/theme/missing.go b/internal/theme/missing.go new file mode 100644 index 0000000..641a7ce --- /dev/null +++ b/internal/theme/missing.go @@ -0,0 +1,24 @@ +package theme + +import "image" +import "image/color" + +type missingTexture int + +func (texture missingTexture) ColorModel () color.Model { + return color.RGBAModel +} + +func (texture missingTexture) Bounds () image.Rectangle { + return image.Rect(0, 0, int(texture), int(texture)) +} + +func (texture missingTexture) At (x, y int) color.Color { + x /= 8 + y /= 8 + if (x + y) % 2 == 0 { + return color.RGBA { R: 0xFF, B: 0xFF, A: 0xFF } + } else { + return color.RGBA { A: 0xFF } + } +} diff --git a/internal/theme/theme.go b/internal/theme/theme.go new file mode 100644 index 0000000..214c7d8 --- /dev/null +++ b/internal/theme/theme.go @@ -0,0 +1,197 @@ +// 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.RGBA + + // 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) Color (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 +}