New Widget: TabFrame

* Added the TabFrame widget with an example program and screenshot
* Button: FixedColor=true to set a consistent background color and not
  worry about it with mouseover/down events.
This commit is contained in:
Noah 2021-07-25 20:53:09 -07:00
parent e7e8b4b2c1
commit 5d16f5d50c
12 changed files with 648 additions and 181 deletions

View File

@ -113,6 +113,7 @@ most complex.
* Place() lets you place child widgets relative to the parent. You can place
it at an exact Point, or against the Top, Left, Bottom or Right sides, or
aligned to the Center (horizontal) or Middle (vertical) of the parent.
[Example](eg/frame-place)
* [x] **Label**: Textual labels for your UI.
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
* Variable binding support: TextVariable or IntVariable can point to a
@ -136,18 +137,20 @@ most complex.
* Works the same as CheckButton and RadioButton but draws a separate
label next to a small check button. Clicking the label will toggle the
state of the checkbox.
* [x] **TabFrame:** a collection of Frames navigated between using a row
of tab buttons along their top edge. [Example](eg/tabframe).
* [x] **Pager**: a series of numbered buttons to use with a paginated UI.
Includes "Forward" and "Next" buttons and buttons for each page number.
* [x] **Window**: a Frame with a title bar Frame on top.
* Can be managed by Supervisor to give Window Manager controls to it
(drag it by its title bar, Close button, window focus, multiple overlapping
windows, and so on).
* [x] **Tooltip**: a mouse hover label attached to a widget.
windows, and so on). [Example](eg/windows)
* [x] **Tooltip**: a mouse hover label attached to a widget. [Example](eg/tooltip)
* [x] **MenuButton**: a button that opens a modal pop-up menu on click.
* [x] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
provides a simple API to add menus and items to it.
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
a modal pop-up by the MenuButton and MenuBar.
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
value from a list of possible values.

View File

@ -11,9 +11,14 @@ import (
// Button is a clickable button.
type Button struct {
BaseWidget
Name string
child Widget
style *style.Button
// Set this true to hard-set a color for this button;
// it will not adjust on mouse-over or press.
FixedColor bool
// Private options.
hovering bool
clicked bool
@ -22,28 +27,33 @@ type Button struct {
// NewButton creates a new Button.
func NewButton(name string, child Widget) *Button {
w := &Button{
Name: name,
child: child,
style: &style.DefaultButton,
}
w.IDFunc(func() string {
return fmt.Sprintf("Button<%s>", name)
return fmt.Sprintf("Button<%s>", w.Name)
})
w.SetStyle(Theme.Button)
w.Handle(MouseOver, func(e EventData) error {
w.hovering = true
w.SetBackground(w.style.HoverBackground)
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.HoverForeground
if !w.FixedColor {
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(w.style.Background)
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.Foreground
if !w.FixedColor {
w.SetBackground(w.style.Background)
if label, ok := w.child.(*Label); ok {
label.Font.Color = w.style.Foreground
}
}
return nil
})
@ -83,6 +93,11 @@ func (w *Button) SetStyle(v *style.Button) {
}
}
// GetStyle gets the button style.
func (w *Button) GetStyle() *style.Button {
return w.style
}
// Children returns the button's child widget.
func (w *Button) Children() []Widget {
return []Widget{w.child}

View File

@ -14,3 +14,5 @@ screenshot and description:
* [Menus](menus/): demonstrates various Menu Buttons and a Menu Bar.
* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI
themes as part of experimental theming support.
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
with tabbed interfaces.

11
eg/tabframe/README.md Normal file
View File

@ -0,0 +1,11 @@
# TabFrame Demo
![Screenshot](screenshot.png)
This demo shows off the TabFrame widget, in multiple copies of a
pop-up Window widget.
## Running It
From your terminal, just type `go run main.go` from this
example's directory.

267
eg/tabframe/main.go Normal file
View File

@ -0,0 +1,267 @@
package main
// See the MakeTabFrame() function just below for the meat of this example.
import (
"fmt"
"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
// 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
TabFont = render.Text{
Size: 10,
Color: render.Black,
Padding: 4,
}
)
func init() {
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
}
// MakeTabFrame is the example use of the TabFrame widget.
// The rest of this file is basically a copy of the eg/windows
// demo, except each window embeds the TabFrame.
func MakeTabFrame(mw *ui.MainWindow) *ui.TabFrame {
notebook := ui.NewTabFrame("Example")
// AddTab gives you the Frame to populate for that tab.
// First Tab contents.
tab1 := notebook.AddTab("Tab 1", ui.NewLabel(ui.Label{
Text: "First Tab",
Font: TabFont,
}))
{
label := ui.NewLabel(ui.Label{
Text: "Hello world",
Font: render.Text{
Size: 24,
Color: render.SkyBlue,
},
})
tab1.Pack(label, ui.Pack{
Side: ui.N,
})
label2 := ui.NewLabel(ui.Label{
Text: "This is the text content of the first\n" +
"of the three tab frames.",
Font: render.Text{
Size: 10,
Color: render.SkyBlue.Darken(40),
},
})
tab1.Pack(label2, ui.Pack{
Side: ui.N,
PadY: 8,
})
}
// Second Tab.
tab2 := notebook.AddTab("Tab 2", ui.NewLabel(ui.Label{
Text: "Second",
Font: TabFont,
}))
{
label := ui.NewLabel(ui.Label{
Text: "Goodbye Mars",
Font: render.Text{
Size: 24,
Color: render.Orange,
},
})
tab2.Pack(label, ui.Pack{
Side: ui.N,
})
label2 := ui.NewLabel(ui.Label{
Text: "This is the text content of the second\n" +
"of the three tab frames.\n\nIt has longer text\nin it!",
Font: render.Text{
Size: 10,
Color: render.Orange.Darken(20),
},
})
tab2.Pack(label2, ui.Pack{
Side: ui.N,
PadY: 8,
})
}
// Third Tab.
tab3 := notebook.AddTab("Tab 3", ui.NewLabel(ui.Label{
Text: "Third",
Font: TabFont,
}))
{
label := ui.NewLabel(ui.Label{
Text: "The Third Tab",
Font: render.Text{
Size: 24,
Color: render.Pink,
},
})
tab3.Pack(label, ui.Pack{
Side: ui.N,
})
label2 := ui.NewLabel(ui.Label{
Text: "This is the text content of the third\n" +
"of the tab frames.",
Font: render.Text{
Size: 10,
Color: render.Pink.Darken(40),
},
})
tab3.Pack(label2, ui.Pack{
Side: ui.N,
PadY: 8,
})
}
notebook.Supervise(mw.Supervisor())
// notebook.SetBackground(render.DarkGrey)
return notebook
}
func main() {
mw, err := ui.NewMainWindow("TabFrame Example", Width, Height)
if err != nil {
panic(err)
}
// Dark theme.
// ui.Theme = theme.DefaultDark
// Menu bar.
menu := ui.NewMenuBar("Main Menu")
file := menu.AddMenu("UI Theme")
file.AddItem("Default", func() {
ui.Theme = theme.Default
addWindow(mw)
})
file.AddItem("DefaultFlat", func() {
ui.Theme = theme.DefaultFlat
addWindow(mw)
})
file.AddItem("DefaultDark", func() {
ui.Theme = theme.DefaultDark
addWindow(mw)
})
file.AddSeparator()
file.AddItem("Close all windows", func() {
OpenWindows -= mw.Supervisor().CloseAllWindows()
})
menu.Supervise(mw.Supervisor())
menu.Compute(mw.Engine)
mw.Pack(menu, menu.PackTop())
// Add some windows to play with.
addWindow(mw)
addWindow(mw)
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) {
var (
color = WindowColors[WindowID%len(WindowColors)]
title = fmt.Sprintf("Window %d (%s)", WindowID+1, ui.Theme.Name)
)
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(mw.Engine)
win1.Supervise(mw.Supervisor())
// 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(mw)
}
return nil
})
// Default placement via cascade.
win1.MoveTo(Cascade)
Cascade.Add(CascadeStep)
if Cascade.Y > Height-240-64 {
CascadeLoops++
Cascade.Y = 32
Cascade.X = 24 * CascadeLoops
}
// Add the TabFrame.
tabframe := MakeTabFrame(mw)
win1.Pack(tabframe, ui.Pack{
Side: ui.W,
Expand: true,
})
// 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(mw)
return nil
})
btn2.Compute(mw.Engine)
mw.Add(btn2)
win1.Compute(mw.Engine)
win1.Place(btn2, ui.Place{
Bottom: 12,
Right: 12,
})
}

BIN
eg/tabframe/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -54,6 +54,8 @@ func (w *Frame) Place(child Widget, config Place) {
func (w *Frame) computePlaced(e render.Engine) {
var (
frameSize = w.BoxSize()
// maxWidth int
// maxHeight int
)
for _, row := range w.placed {

8
go.mod
View File

@ -1,9 +1,9 @@
module git.kirsle.net/go/ui
go 1.13
go 1.16
require (
git.kirsle.net/go/render v0.0.0-20210104010442-b4a1979a8ba1
github.com/veandco/go-sdl2 v0.4.7 // indirect
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e // indirect
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782
github.com/veandco/go-sdl2 v0.4.8 // indirect
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
)

165
go.sum
View File

@ -1,166 +1,17 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d h1:vErak6oVRT2dosyQzcwkjXyWQ2NRIVL8q9R8NOUTtsg=
git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d/go.mod h1:ywZtC+zE2SpeObfkw0OvG01pWHQadsVQ4WDKOYzaejc=
git.kirsle.net/go/render v0.0.0-20210104010442-b4a1979a8ba1 h1:wGQLjBnWvqx7rU43yFG8ow4rnYqUMUwqorEkxdPaJ6Q=
git.kirsle.net/go/render v0.0.0-20210104010442-b4a1979a8ba1/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jinzhu/gorm v1.9.9/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kirsle/blog v0.0.0-20191022175051-d78814b9c99b/go.mod h1:D6I6jquqhLewuhrVt9Q3p97r2YCC7CA4DgNv14+WS8k=
github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292/go.mod h1:0KaOvOX8s5YINMREeyTILsuU0wkmnKQQTy99e/2oDGc=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10=
github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/veandco/go-sdl2 v0.4.1 h1:HmSBvVmKWI8LAOeCfTTM8R33rMyPcs6U3o8n325c9Qg=
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782 h1:Ko+NvZxmJbW+M1dA2jCSnV6qSpkLoZASramE6ltlf/s=
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
github.com/veandco/go-sdl2 v0.4.7 h1:VfpCM+LfEGDbHdByglCo2bcBsevjFvzl8W0f6VLNitg=
github.com/veandco/go-sdl2 v0.4.7/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU=
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

282
tabframe.go Normal file
View File

@ -0,0 +1,282 @@
package ui
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
"git.kirsle.net/go/ui/theme"
)
// TabFrame is a tabbed notebook of multiple frames showing
// tab names along the top and clicking them reveals each
// named tab.
type TabFrame struct {
Name string
Frame
supervisor *Supervisor
style *style.Button
// Child widgets.
header *Frame
content *Frame
tabButtons []*Button
tabFrames []*Frame
currentTabKey string
}
// NewTabFrame creates a new Frame.
func NewTabFrame(name string) *TabFrame {
w := &TabFrame{
Name: name,
style: Theme.TabFrame,
header: NewFrame(name + " Header"),
content: NewFrame(name + " Content"),
tabButtons: []*Button{},
tabFrames: []*Frame{},
}
// Initialize the root frame of this widget.
w.Frame.Setup()
// Pack the high-level layout into the root frame.
// Only root needs to Present for this widget.
w.Frame.Pack(w.header, Pack{
Side: N,
FillX: true,
})
w.Frame.Pack(w.content, Pack{
Side: N,
Fill: true,
Expand: true,
})
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
w.IDFunc(func() string {
return fmt.Sprintf("TabFrame<%s>",
name,
)
})
return w
}
// AddTab creates a new content tab. The key is a unique identifier
// for the tab and is how the TabFrame knows which tab is selected.
//
// The child widget would probably be a Label or Image but could be
// any other kind of widget.
//
// The first tab added becomes the selected tab by default.
func (w *TabFrame) AddTab(key string, child Widget) *Frame {
// Create the tab button for this tab.
button := NewButton(key, child)
button.SetStyle(w.style)
button.FixedColor = true
button.SetOutlineSize(0)
button.SetBorderSize(1)
w.setButtonStyle(button, len(w.tabButtons) == 0)
w.header.Pack(button, Pack{
Side: W,
})
button.Handle(MouseDown, func(ed EventData) error {
w.SetTab(key)
return nil
})
// Create the frame of the tab's body.
frame := NewFrame(key)
frame.Configure(Config{
BorderSize: w.style.BorderSize,
Background: w.style.Background,
BorderStyle: BorderRaised,
})
if len(w.tabFrames) > 0 {
frame.Hide()
}
// Pack this frame into the content part of the widget.
w.content.Pack(frame, Pack{
Side: N,
FillX: true,
})
w.tabButtons = append(w.tabButtons, button)
w.tabFrames = append(w.tabFrames, frame)
return frame
}
// set the tab style between active and inactive
func (w *TabFrame) setButtonStyle(button *Button, active bool) {
var style = button.GetStyle()
if active {
button.SetBackground(style.Background)
button.SetBorderStyle(BorderRaised)
if label, ok := button.child.(*Label); ok {
label.Font.Color = style.Foreground
}
} else {
button.SetBackground(style.Background.Darken(theme.BorderColorOffset))
button.SetBorderStyle(BorderSolid)
if label, ok := button.child.(*Label); ok {
label.Font.Color = style.Foreground
}
}
}
// SetTab changes the selected tab to the new value. If the
// tab doesn't exist, the first tab is selected.
func (w *TabFrame) SetTab(key string) bool {
var found bool
for i, frame := range w.tabFrames {
button := w.tabButtons[i]
if frame.Name == key {
frame.Show()
w.setButtonStyle(button, true)
w.currentTabKey = key
found = true
} else {
frame.Hide()
w.setButtonStyle(button, false)
}
}
if !found && len(w.tabFrames) > 0 {
w.tabFrames[0].Show()
w.currentTabKey = w.tabFrames[0].Name
}
return found
}
// Supervise activates the tab frame using your supervisor. If you
// don't call this, the tab buttons won't be clickable!
//
// Call this AFTER adding all tabs. This function calls Supervisor.Add
// on all tab buttons.
func (w *TabFrame) Supervise(supervisor *Supervisor) {
for _, button := range w.tabButtons {
supervisor.Add(button)
}
}
// SetStyle controls the visual styling of the tab button bar.
func (w *TabFrame) SetStyle(style *style.Button) {
w.style = style
for _, button := range w.tabButtons {
button.SetStyle(style)
w.setButtonStyle(button, !button.Hidden())
}
}
// Compute the size of the Frame.
func (w *TabFrame) Compute(e render.Engine) {
// Compute all the child frames.
w.Frame.Compute(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the Frame.
func (w *TabFrame) Present(e render.Engine, P render.Point) {
if w.Hidden() {
return
}
var (
S = w.Size()
)
// Draw the widget's border and everything.
w.DrawBox(e, P)
// Draw the background color.
e.DrawBox(w.Background(), render.Rect{
X: P.X + w.BoxThickness(1),
Y: P.Y + w.BoxThickness(1),
W: S.W - w.BoxThickness(2),
H: S.H - w.BoxThickness(2),
})
// Present the root frame.
w.Frame.Present(e, P)
// Draw the borders over the tabs.
w.presentBorders(e, P)
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}
/*
presentBorders handles drawing the borders around tab buttons.
The tabs are simple Button widgets but drawn with no borders. Instead,
borders are painted on post-hoc in the Present function.
*/
func (w *TabFrame) presentBorders(e render.Engine, P render.Point) {
if len(w.tabButtons) == 0 {
return
}
// Prep some variables.
var (
// The 1st and last tab button widgets.
first = w.tabButtons[0]
last = w.tabButtons[len(w.tabButtons)-1]
topLeft = AbsolutePosition(first)
bottomRight = AbsolutePosition(last)
// The absolute bounding box of the tabs part of the UI,
// from the top-left corner of Tab #1 to the bottom-right
// corner of the final tab.
bounding = render.Rect{
X: P.X, //topLeft.X + first.BoxThickness(4),
Y: P.Y, //topLeft.Y + first.BoxThickness(4),
W: bottomRight.X + last.Size().W - topLeft.X,
H: bottomRight.Y + last.Size().H - topLeft.Y,
}
// The very bottom edge of the whole tab bar,
// to overlap the BorderSize=1 along their buttons.
bottomLine = []render.Point{
render.NewPoint(P.X+1, bounding.Y+bounding.H-1),
render.NewPoint(bounding.X+bounding.W-1, bounding.Y+bounding.H-1),
}
)
// Draw a shadow border on all the inactive tabs' right edges,
// so they don't all blend together in solid grey.
// Note: the active button has a BorderSize=1 and others are 0.
for i, button := range w.tabButtons {
if button.Name != w.currentTabKey {
// If it immediately precedes the current tab, do not draw the line,
// it would cover the highlight color of the current tab's button.
if i+1 < len(w.tabButtons) && w.tabButtons[i+1].Name == w.currentTabKey {
continue
}
var (
abs = AbsolutePosition(button)
size = button.BoxSize()
points = []render.Point{
render.NewPoint(abs.X+size.W-1, abs.Y+2),
render.NewPoint(abs.X+size.W-1, abs.Y+size.H-2),
}
)
e.DrawLine(button.Background().Darken(theme.BorderColorOffset), points[0], points[1])
}
}
// Erase the button edge from all tabs.
e.DrawLine(w.style.Background, bottomLine[0], bottomLine[1])
e.DrawBox(w.style.Background, render.Rect{
X: bottomLine[0].X + 1,
Y: bottomLine[0].Y,
W: bounding.W - 2,
H: 3,
})
}

View File

@ -17,19 +17,21 @@ var (
// 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
Name string
Window *style.Window
Label *style.Label
Button *style.Button
Tooltip *style.Tooltip
TabFrame *style.Button
}
// Default theme.
var Default = Theme{
Name: "Default",
Label: &style.DefaultLabel,
Button: &style.DefaultButton,
Tooltip: &style.DefaultTooltip,
Name: "Default",
Label: &style.DefaultLabel,
Button: &style.DefaultButton,
Tooltip: &style.DefaultTooltip,
TabFrame: &style.DefaultButton,
}
// DefaultFlat is a flat version of the default theme.
@ -45,6 +47,16 @@ var DefaultFlat = Theme{
BorderStyle: style.BorderSolid,
BorderSize: 2,
},
TabFrame: &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.
@ -74,4 +86,13 @@ var DefaultDark = Theme{
Background: render.RGBA(60, 60, 60, 230),
Foreground: render.Cyan,
},
TabFrame: &style.Button{
Background: render.DarkGrey,
Foreground: render.Grey,
OutlineColor: render.DarkGrey,
OutlineSize: 1,
HoverBackground: render.Grey,
BorderStyle: style.BorderRaised,
BorderSize: 2,
},
}

View File

@ -341,7 +341,7 @@ func (w *Window) Pack(child Widget, config ...Pack) {
// Place a child widget into the window's main frame.
func (w *Window) Place(child Widget, config Place) {
w.content.Place(child, config)
w.body.Place(child, config)
}
// TitleBar returns the title bar widgets.
@ -356,6 +356,14 @@ func (w *Window) Configure(C Config) {
w.body.Configure(C)
// Don't pass dimensions down any further than the body.
// TODO: this causes the content frame to compute its size
// dynamically based on Packed widgets, but if using Place on
// your window, the content frame doesn't know a size by which
// to place the child relative to (Frame has size 0x0).
// Commenting out these two lines causes windows to render very
// incorrectly (child frame content flying off the window bottom).
// In the meantime, Window.Place intercepts it and draws it onto
// the parent window directly so it works how you expect.
C.Width = 0
C.Height = 0
w.content.Configure(C)
@ -366,6 +374,11 @@ func (w *Window) ConfigureTitle(C Config) {
w.titleBar.Configure(C)
}
// ContentFrame returns the main content Frame of this window.
func (w *Window) ContentFrame() *Frame {
return w.content
}
// Compute the window.
func (w *Window) Compute(e render.Engine) {
w.engine = e // hang onto it in case of maximize