diff --git a/button.go b/button.go index 6e52db2..68d427a 100644 --- a/button.go +++ b/button.go @@ -5,13 +5,14 @@ import ( "fmt" "git.kirsle.net/go/render" - "git.kirsle.net/go/ui/theme" + "git.kirsle.net/go/ui/style" ) // Button is a clickable button. type Button struct { BaseWidget child Widget + style *style.Button // Private options. hovering bool @@ -22,27 +23,28 @@ type Button struct { func NewButton(name string, child Widget) *Button { w := &Button{ child: child, + style: &style.DefaultButton, } w.IDFunc(func() string { return fmt.Sprintf("Button<%s>", name) }) - w.Configure(Config{ - BorderSize: 2, - BorderStyle: BorderRaised, - OutlineSize: 1, - OutlineColor: theme.ButtonOutlineColor, - Background: theme.ButtonBackgroundColor, - }) + w.SetStyle(Theme.Button) w.Handle(MouseOver, func(e EventData) error { 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 }) w.Handle(MouseOut, func(e EventData) error { 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 }) @@ -53,13 +55,34 @@ func NewButton(name string, child Widget) *Button { }) w.Handle(MouseUp, func(e EventData) error { w.clicked = false - w.SetBorderStyle(BorderRaised) + w.SetBorderStyle(BorderStyle(w.style.BorderStyle)) return nil }) 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. func (w *Button) Children() []Widget { return []Widget{w.child} diff --git a/eg/themes/Makefile b/eg/themes/Makefile new file mode 100644 index 0000000..b5b7bbb --- /dev/null +++ b/eg/themes/Makefile @@ -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 diff --git a/eg/themes/main.go b/eg/themes/main.go new file mode 100644 index 0000000..d7cebfe --- /dev/null +++ b/eg/themes/main.go @@ -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, + }) +} diff --git a/eg/themes/main_wasm.go b/eg/themes/main_wasm.go new file mode 100644 index 0000000..f866519 --- /dev/null +++ b/eg/themes/main_wasm.go @@ -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, + }) +} diff --git a/label.go b/label.go index cb3cee4..4353af2 100644 --- a/label.go +++ b/label.go @@ -5,6 +5,7 @@ import ( "strings" "git.kirsle.net/go/render" + "git.kirsle.net/go/ui/style" ) // DefaultFont is the default font settings used for a Label. @@ -23,6 +24,7 @@ type Label struct { IntVariable *int Font render.Text + style *style.Label width int height int lineHeight int @@ -36,6 +38,7 @@ func NewLabel(c Label) *Label { IntVariable: c.IntVariable, Font: DefaultFont, } + w.SetStyle(Theme.Label) if !c.Font.IsZero() { w.Font = c.Font } @@ -45,6 +48,17 @@ func NewLabel(c Label) *Label { 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 // available or else the Text attribute instead. func (w *Label) text() render.Text { diff --git a/style/button.go b/style/button.go new file mode 100644 index 0000000..e29fe72 --- /dev/null +++ b/style/button.go @@ -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 +} diff --git a/style/style.go b/style/style.go new file mode 100644 index 0000000..5e7880a --- /dev/null +++ b/style/style.go @@ -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" +) diff --git a/theme.go b/theme.go new file mode 100644 index 0000000..51e0668 --- /dev/null +++ b/theme.go @@ -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 diff --git a/theme/theme.go b/theme/theme.go index 33ce4b4..d0df939 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -1,6 +1,9 @@ package theme -import "git.kirsle.net/go/render" +import ( + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui/style" +) // Color schemes. var ( @@ -10,3 +13,64 @@ var ( 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, + }, +} diff --git a/tooltip.go b/tooltip.go index 5cee0d9..1c73d0e 100644 --- a/tooltip.go +++ b/tooltip.go @@ -5,6 +5,7 @@ import ( "strings" "git.kirsle.net/go/render" + "git.kirsle.net/go/ui/style" ) func init() { @@ -20,6 +21,7 @@ type Tooltip struct { TextVariable *string // String pointer instead of text. Edge Edge // side to display tooltip on + style *style.Tooltip target Widget lineHeight int font render.Text @@ -74,9 +76,22 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip { return fmt.Sprintf(`Tooltip<"%s">`, w.Value()) }) + w.SetStyle(Theme.Tooltip) + 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 // configured Text or the TextVariable pointer. func (w *Tooltip) Value() string { diff --git a/window.go b/window.go index 6ece7a8..5e5d673 100644 --- a/window.go +++ b/window.go @@ -4,6 +4,7 @@ import ( "fmt" "git.kirsle.net/go/render" + "git.kirsle.net/go/ui/style" ) // Window is a frame with a title bar. @@ -19,6 +20,7 @@ type Window struct { InactiveTitleForeground render.Color // Private widgets. + style *style.Window body *Frame titleBar *Frame 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. titleBar, titleLabel := w.setupTitleBar() w.body.Pack(titleBar, Pack{ @@ -87,9 +83,32 @@ func NewWindow(title string) *Window { // Set up parent/child relationships w.body.SetParent(w) + w.SetStyle(Theme.Window) + 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. func (w *Window) setupTitleBar() (*Frame, *Label) { frame := NewFrame("Titlebar for Window: " + w.Title) @@ -260,12 +279,12 @@ func (w *Window) SetFocus(v bool) { // Update the title bar colors. var ( - bg = w.ActiveTitleBackground - fg = w.ActiveTitleForeground + bg = w.style.ActiveTitleBackground + fg = w.style.ActiveTitleForeground ) if !w.focused { - bg = w.InactiveTitleBackground - fg = w.InactiveTitleForeground + bg = w.style.InactiveTitleBackground + fg = w.style.InactiveTitleForeground } w.titleBar.SetBackground(bg) w.titleLabel.Font.Color = fg