WIP Themes Support

This commit is contained in:
Noah 2020-06-17 18:04:18 -07:00
parent 4206330398
commit e675ead0ed
11 changed files with 534 additions and 22 deletions

View File

@ -5,13 +5,14 @@ import (
"fmt" "fmt"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui/theme" "git.kirsle.net/go/ui/style"
) )
// Button is a clickable button. // Button is a clickable button.
type Button struct { type Button struct {
BaseWidget BaseWidget
child Widget child Widget
style *style.Button
// Private options. // Private options.
hovering bool hovering bool
@ -22,27 +23,28 @@ type Button struct {
func NewButton(name string, child Widget) *Button { func NewButton(name string, child Widget) *Button {
w := &Button{ w := &Button{
child: child, child: child,
style: &style.DefaultButton,
} }
w.IDFunc(func() string { w.IDFunc(func() string {
return fmt.Sprintf("Button<%s>", name) return fmt.Sprintf("Button<%s>", name)
}) })
w.Configure(Config{ w.SetStyle(Theme.Button)
BorderSize: 2,
BorderStyle: BorderRaised,
OutlineSize: 1,
OutlineColor: theme.ButtonOutlineColor,
Background: theme.ButtonBackgroundColor,
})
w.Handle(MouseOver, func(e EventData) error { w.Handle(MouseOver, func(e EventData) error {
w.hovering = true w.hovering = true
w.SetBackground(theme.ButtonHoverColor) w.SetBackground(w.style.HoverBackground)
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.HoverForeground
}
return nil return nil
}) })
w.Handle(MouseOut, func(e EventData) error { w.Handle(MouseOut, func(e EventData) error {
w.hovering = false w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor) w.SetBackground(w.style.Background)
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.Foreground
}
return nil return nil
}) })
@ -53,13 +55,34 @@ func NewButton(name string, child Widget) *Button {
}) })
w.Handle(MouseUp, func(e EventData) error { w.Handle(MouseUp, func(e EventData) error {
w.clicked = false w.clicked = false
w.SetBorderStyle(BorderRaised) w.SetBorderStyle(BorderStyle(w.style.BorderStyle))
return nil return nil
}) })
return w return w
} }
// SetStyle sets the button style.
func (w *Button) SetStyle(v *style.Button) {
if v == nil {
v = &style.DefaultButton
}
w.style = v
w.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderStyle(w.style.BorderStyle),
OutlineSize: w.style.OutlineSize,
OutlineColor: w.style.OutlineColor,
Background: w.style.Background,
})
// If the child is a Label, apply the foreground color.
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.Foreground
}
}
// Children returns the button's child widget. // Children returns the button's child widget.
func (w *Button) Children() []Widget { func (w *Button) Children() []Widget {
return []Widget{w.child} return []Widget{w.child}

11
eg/themes/Makefile Normal file
View File

@ -0,0 +1,11 @@
.PHONY: run
run:
go run main.go
.PHONY: wasm
wasm:
GOOS=js GOARCH=wasm go build -v -o windows.wasm main_wasm.go
.PHONY: wasm-serve
wasm-serve: wasm
../wasm-common/serve.sh

107
eg/themes/main.go Normal file
View File

@ -0,0 +1,107 @@
package main
import (
"os"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/render/sdl"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/theme"
)
// Program globals.
var (
// Size of the MainWindow.
Width = 1024
Height = 768
)
func init() {
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
}
func main() {
mw, err := ui.NewMainWindow("Theme Demo", Width, Height)
if err != nil {
panic(err)
}
// Menu bar.
menu := ui.NewMenuBar("Main Menu")
file := menu.AddMenu("Select Theme")
file.AddItem("Default", func() {
addWindow(mw, theme.Default)
})
file.AddItem("DefaultFlat", func() {
addWindow(mw, theme.DefaultFlat)
})
file.AddItem("DefaultDark", func() {
addWindow(mw, theme.DefaultDark)
})
menu.Supervise(mw.Supervisor())
menu.Compute(mw.Engine)
mw.Pack(menu, menu.PackTop())
mw.SetBackground(render.White)
mw.OnLoop(func(e *event.State) {
if e.Escape {
os.Exit(0)
}
})
mw.MainLoop()
}
// Add a new child window.
func addWindow(mw *ui.MainWindow, theme theme.Theme) {
ui.Theme = theme
win1 := ui.NewWindow(theme.Name)
win1.SetButtons(ui.CloseButton)
win1.Configure(ui.Config{
Width: 320,
Height: 240,
})
win1.Compute(mw.Engine)
win1.Supervise(mw.Supervisor())
// Draw a label.
label := ui.NewLabel(ui.Label{
Text: theme.Name,
})
win1.Place(label, ui.Place{
Top: 10,
Left: 10,
})
// Add a button with tooltip.
btn2 := ui.NewButton(theme.Name+":Button2", ui.NewLabel(ui.Label{
Text: "Button",
}))
btn2.Handle(ui.Click, func(ed ui.EventData) error {
return nil
})
mw.Add(btn2)
win1.Place(btn2, ui.Place{
Top: 10,
Right: 10,
})
ui.NewTooltip(btn2, ui.Tooltip{
Text: "Hello world!",
Edge: ui.Bottom,
})
// Add a checkbox.
var b bool
cb := ui.NewCheckbox("Checkbox", &b, ui.NewLabel(ui.Label{
Text: "Check me!",
}))
mw.Add(cb)
win1.Place(cb, ui.Place{
Top: 30,
Left: 10,
})
}

169
eg/themes/main_wasm.go Normal file
View File

@ -0,0 +1,169 @@
// +build js,wasm
// WebAssembly version of the window manager demo.
// To build: make wasm
// To test: make wasm-serve
package main
import (
"fmt"
"time"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/canvas"
"git.kirsle.net/go/ui"
)
// Program globals.
var (
ThrottleFPS = 1000 / 60
// Size of the MainWindow.
Width = 1024
Height = 768
// Cascade offset for creating multiple windows.
Cascade = render.NewPoint(10, 32)
CascadeStep = render.NewPoint(24, 24)
CascadeLoops = 1
// Colors for each window created.
WindowColors = []render.Color{
render.Blue,
render.Red,
render.DarkYellow,
render.DarkGreen,
render.DarkCyan,
render.DarkBlue,
render.DarkRed,
}
WindowID int
OpenWindows int
)
func main() {
mw, err := canvas.New("canvas")
if err != nil {
panic(err)
}
// Bind DOM event handlers.
mw.AddEventListeners()
supervisor := ui.NewSupervisor()
frame := ui.NewFrame("Main Frame")
frame.Resize(render.NewRect(mw.WindowSize()))
frame.Compute(mw)
_, height := mw.WindowSize()
lbl := ui.NewLabel(ui.Label{
Text: "Window Manager Demo",
Font: render.Text{
FontFilename: "DejaVuSans.ttf",
Size: 32,
Color: render.SkyBlue,
Shadow: render.SkyBlue.Darken(60),
},
})
lbl.Compute(mw)
lbl.MoveTo(render.NewPoint(
20,
height-lbl.Size().H-20,
))
// Menu bar.
menu := ui.NewMenuBar("Main Menu")
file := menu.AddMenu("Options")
file.AddItem("New window", func() {
addWindow(mw, frame, supervisor)
})
file.AddItem("Close all windows", func() {
OpenWindows -= supervisor.CloseAllWindows()
})
menu.Supervise(supervisor)
menu.Compute(mw)
frame.Pack(menu, menu.PackTop())
// Add some windows to play with.
addWindow(mw, frame, supervisor)
addWindow(mw, frame, supervisor)
for {
mw.Clear(render.RGBA(255, 255, 200, 255))
start := time.Now()
ev, err := mw.Poll()
if err != nil {
panic(err)
}
frame.Present(mw, frame.Point())
lbl.Present(mw, lbl.Point())
supervisor.Loop(ev)
supervisor.Present(mw)
var delay uint32
elapsed := time.Now().Sub(start)
tmp := elapsed / time.Millisecond
if ThrottleFPS-int(tmp) > 0 {
delay = uint32(ThrottleFPS - int(tmp))
}
mw.Delay(delay)
}
}
// Add a new child window.
func addWindow(engine render.Engine, parent *ui.Frame, sup *ui.Supervisor) {
var (
color = WindowColors[WindowID%len(WindowColors)]
title = fmt.Sprintf("Window %d", WindowID+1)
)
WindowID++
win1 := ui.NewWindow(title)
win1.SetButtons(ui.CloseButton)
win1.ActiveTitleBackground = color
win1.InactiveTitleBackground = color.Darken(60)
win1.InactiveTitleForeground = render.Grey
win1.Configure(ui.Config{
Width: 320,
Height: 240,
})
win1.Compute(engine)
win1.Supervise(sup)
// Re-open a window when the last one is closed.
OpenWindows++
win1.Handle(ui.CloseWindow, func(ed ui.EventData) error {
OpenWindows--
if OpenWindows <= 0 {
addWindow(engine, parent, sup)
}
return nil
})
// Default placement via cascade.
win1.MoveTo(Cascade)
Cascade.Add(CascadeStep)
if Cascade.Y > Height-240-64 {
CascadeLoops++
Cascade.Y = 24
Cascade.X = 24 * CascadeLoops
}
// Add a window duplicator button.
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
Text: "New Window",
}))
btn2.Handle(ui.Click, func(ed ui.EventData) error {
addWindow(engine, parent, sup)
return nil
})
sup.Add(btn2)
win1.Place(btn2, ui.Place{
Top: 10,
Right: 10,
})
}

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
) )
// DefaultFont is the default font settings used for a Label. // DefaultFont is the default font settings used for a Label.
@ -23,6 +24,7 @@ type Label struct {
IntVariable *int IntVariable *int
Font render.Text Font render.Text
style *style.Label
width int width int
height int height int
lineHeight int lineHeight int
@ -36,6 +38,7 @@ func NewLabel(c Label) *Label {
IntVariable: c.IntVariable, IntVariable: c.IntVariable,
Font: DefaultFont, Font: DefaultFont,
} }
w.SetStyle(Theme.Label)
if !c.Font.IsZero() { if !c.Font.IsZero() {
w.Font = c.Font w.Font = c.Font
} }
@ -45,6 +48,17 @@ func NewLabel(c Label) *Label {
return w return w
} }
// SetStyle sets the label's default style.
func (w *Label) SetStyle(v *style.Label) {
if v == nil {
v = &style.DefaultLabel
}
w.style = v
w.SetBackground(w.style.Background)
w.Font.Color = w.style.Foreground
}
// text returns the label's displayed text, coming from the TextVariable if // text returns the label's displayed text, coming from the TextVariable if
// available or else the Text attribute instead. // available or else the Text attribute instead.
func (w *Label) text() render.Text { func (w *Label) text() render.Text {

71
style/button.go Normal file
View File

@ -0,0 +1,71 @@
// Package style provides style definitions for UI components.
package style
import "git.kirsle.net/go/render"
// Default styles for widgets without a theme.
var (
DefaultWindow = Window{
ActiveTitleBackground: render.Blue,
ActiveTitleForeground: render.White,
InactiveTitleBackground: render.DarkGrey,
InactiveTitleForeground: render.Grey,
ActiveBackground: render.Grey,
InactiveBackground: render.Grey,
}
DefaultLabel = Label{
Background: render.Invisible,
Foreground: render.Black,
}
DefaultButton = Button{
Background: render.RGBA(200, 200, 200, 255),
Foreground: render.Black,
OutlineColor: render.Black,
OutlineSize: 1,
HoverBackground: render.RGBA(200, 255, 255, 255),
HoverForeground: render.Black,
BorderStyle: BorderRaised,
BorderSize: 2,
}
DefaultTooltip = Tooltip{
Background: render.RGBA(0, 0, 0, 230),
Foreground: render.White,
}
)
// Window style configuration.
type Window struct {
ActiveTitleBackground render.Color
ActiveTitleForeground render.Color
ActiveBackground render.Color
InactiveTitleBackground render.Color
InactiveTitleForeground render.Color
InactiveBackground render.Color
}
// Label style configuration.
type Label struct {
Background render.Color
Foreground render.Color
}
// Button style configuration.
type Button struct {
Background render.Color
Foreground render.Color // Labels only
OutlineColor render.Color
OutlineSize int
HoverBackground render.Color
HoverForeground render.Color
BorderStyle BorderStyle
BorderSize int
}
// Tooltip style configuration.
type Tooltip struct {
Background render.Color
Foreground render.Color
}

13
style/style.go Normal file
View File

@ -0,0 +1,13 @@
// Package style provides style definitions for UI components.
package style
// BorderStyle options for widget.SetBorderStyle()
type BorderStyle string
// Styles for a widget border.
const (
BorderNone BorderStyle = ""
BorderSolid BorderStyle = "solid"
BorderRaised = "raised"
BorderSunken = "sunken"
)

6
theme.go Normal file
View File

@ -0,0 +1,6 @@
package ui
import "git.kirsle.net/go/ui/theme"
// Theme sets the default theme used when creating new widgets.
var Theme = theme.Default

View File

@ -1,6 +1,9 @@
package theme package theme
import "git.kirsle.net/go/render" import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
)
// Color schemes. // Color schemes.
var ( var (
@ -10,3 +13,64 @@ var (
BorderColorOffset = 40 BorderColorOffset = 40
) )
// Theme is a collection of styles for various built-in widgets.
type Theme struct {
Name string
Window *style.Window
Label *style.Label
Button *style.Button
Tooltip *style.Tooltip
}
// Default theme.
var Default = Theme{
Name: "Default",
Label: &style.DefaultLabel,
Button: &style.DefaultButton,
Tooltip: &style.DefaultTooltip,
}
// DefaultFlat is a flat version of the default theme.
var DefaultFlat = Theme{
Name: "DefaultFlat",
Button: &style.Button{
Background: style.DefaultButton.Background,
Foreground: style.DefaultButton.Foreground,
OutlineColor: style.DefaultButton.OutlineColor,
OutlineSize: 1,
HoverBackground: style.DefaultButton.HoverBackground,
HoverForeground: style.DefaultButton.HoverForeground,
BorderStyle: style.BorderSolid,
BorderSize: 2,
},
}
// DefaultDark is a dark version of the default theme.
var DefaultDark = Theme{
Name: "DefaultDark",
Label: &style.Label{
Foreground: render.Grey,
},
Window: &style.Window{
ActiveTitleBackground: render.Red,
ActiveTitleForeground: render.White,
InactiveTitleBackground: render.DarkGrey,
InactiveTitleForeground: render.Grey,
ActiveBackground: render.Black,
InactiveBackground: render.Black,
},
Button: &style.Button{
Background: render.Black,
Foreground: render.Grey,
OutlineColor: render.DarkGrey,
OutlineSize: 1,
HoverBackground: render.Grey,
BorderStyle: style.BorderRaised,
BorderSize: 2,
},
Tooltip: &style.Tooltip{
Background: render.RGBA(60, 60, 60, 230),
Foreground: render.Cyan,
},
}

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
) )
func init() { func init() {
@ -20,6 +21,7 @@ type Tooltip struct {
TextVariable *string // String pointer instead of text. TextVariable *string // String pointer instead of text.
Edge Edge // side to display tooltip on Edge Edge // side to display tooltip on
style *style.Tooltip
target Widget target Widget
lineHeight int lineHeight int
font render.Text font render.Text
@ -74,9 +76,22 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
return fmt.Sprintf(`Tooltip<"%s">`, w.Value()) return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
}) })
w.SetStyle(Theme.Tooltip)
return w return w
} }
// SetStyle sets the tooltip's default style.
func (w *Tooltip) SetStyle(v *style.Tooltip) {
if v == nil {
v = &style.DefaultTooltip
}
w.style = v
w.SetBackground(w.style.Background)
w.font.Color = w.style.Foreground
}
// Value returns the current text displayed in the tooltop, whether from the // Value returns the current text displayed in the tooltop, whether from the
// configured Text or the TextVariable pointer. // configured Text or the TextVariable pointer.
func (w *Tooltip) Value() string { func (w *Tooltip) Value() string {

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
) )
// Window is a frame with a title bar. // Window is a frame with a title bar.
@ -19,6 +20,7 @@ type Window struct {
InactiveTitleForeground render.Color InactiveTitleForeground render.Color
// Private widgets. // Private widgets.
style *style.Window
body *Frame body *Frame
titleBar *Frame titleBar *Frame
titleLabel *Label titleLabel *Label
@ -58,12 +60,6 @@ func NewWindow(title string) *Window {
) )
}) })
w.body.Configure(Config{
Background: render.Grey,
BorderSize: 2,
BorderStyle: BorderRaised,
})
// Title bar widget. // Title bar widget.
titleBar, titleLabel := w.setupTitleBar() titleBar, titleLabel := w.setupTitleBar()
w.body.Pack(titleBar, Pack{ w.body.Pack(titleBar, Pack{
@ -87,9 +83,32 @@ func NewWindow(title string) *Window {
// Set up parent/child relationships // Set up parent/child relationships
w.body.SetParent(w) w.body.SetParent(w)
w.SetStyle(Theme.Window)
return w return w
} }
// SetStyle sets the window style.
func (w *Window) SetStyle(v *style.Window) {
if v == nil {
v = &style.DefaultWindow
}
w.style = v
w.body.Configure(Config{
Background: w.style.ActiveBackground,
BorderSize: 2,
BorderStyle: BorderRaised,
})
if w.focused {
w.titleBar.SetBackground(w.style.ActiveTitleBackground)
w.titleLabel.Font.Color = w.style.ActiveTitleForeground
} else {
w.titleBar.SetBackground(w.style.InactiveTitleBackground)
w.titleLabel.Font.Color = w.style.InactiveTitleForeground
}
}
// setupTitlebar creates the title bar frame of the window. // setupTitlebar creates the title bar frame of the window.
func (w *Window) setupTitleBar() (*Frame, *Label) { func (w *Window) setupTitleBar() (*Frame, *Label) {
frame := NewFrame("Titlebar for Window: " + w.Title) frame := NewFrame("Titlebar for Window: " + w.Title)
@ -260,12 +279,12 @@ func (w *Window) SetFocus(v bool) {
// Update the title bar colors. // Update the title bar colors.
var ( var (
bg = w.ActiveTitleBackground bg = w.style.ActiveTitleBackground
fg = w.ActiveTitleForeground fg = w.style.ActiveTitleForeground
) )
if !w.focused { if !w.focused {
bg = w.InactiveTitleBackground bg = w.style.InactiveTitleBackground
fg = w.InactiveTitleForeground fg = w.style.InactiveTitleForeground
} }
w.titleBar.SetBackground(bg) w.titleBar.SetBackground(bg)
w.titleLabel.Font.Color = fg w.titleLabel.Font.Color = fg