Place Strategy for Frame Widget
This commit is contained in:
parent
4ba563d48d
commit
0846fe22fc
29
eg/frame-place/README.md
Normal file
29
eg/frame-place/README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Frame Placement Example
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This demonstrates using the Place() method of the MainWindow and Frame to
|
||||||
|
position widgets around the window. The MainWindow itself and two child frames
|
||||||
|
(red and blue) are given the same set of buttons, placed relative to their own
|
||||||
|
parent widget.
|
||||||
|
|
||||||
|
The options for frame placing are:
|
||||||
|
|
||||||
|
* **Point:** Absolute X,Y coordinate relative to parent
|
||||||
|
* **Side:** binding the widget relative to a side of its parent.
|
||||||
|
* Top, Bottom, Left and Right to anchor to a side. In Bottom and Right, the
|
||||||
|
child widget's size is taken into account, so the right edge of the widget
|
||||||
|
would be `Right` pixels from the parent's right edge.
|
||||||
|
* Center and Middle options allow to anchor it to the center horizontally or
|
||||||
|
middle vertically.
|
||||||
|
|
||||||
|
Click any button and the title bar will update to show the name of the
|
||||||
|
button clicked and which parent it belonged to.
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```
|
||||||
|
go run main.go
|
||||||
|
```
|
144
eg/frame-place/main.go
Normal file
144
eg/frame-place/main.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// Example script for using the Place strategy of ui.Frame.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := ui.NewMainWindow("Frame Placement Demo | Click a Button", 800, 600)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.SetBackground(render.White)
|
||||||
|
|
||||||
|
// Create a sub-frame with its own buttons packed within.
|
||||||
|
frame := ui.NewFrame("Blue Frame")
|
||||||
|
frame.Configure(ui.Config{
|
||||||
|
Width: 300,
|
||||||
|
Height: 150,
|
||||||
|
Background: render.DarkBlue,
|
||||||
|
BorderSize: 1,
|
||||||
|
BorderStyle: ui.BorderSunken,
|
||||||
|
})
|
||||||
|
mw.Place(frame, ui.Place{
|
||||||
|
Point: render.NewPoint(80, 80),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create another frame that attaches itself to the bottom right
|
||||||
|
// of the window.
|
||||||
|
frame2 := ui.NewFrame("Red Frame")
|
||||||
|
frame2.Configure(ui.Config{
|
||||||
|
Width: 300,
|
||||||
|
Height: 150,
|
||||||
|
Background: render.DarkRed,
|
||||||
|
})
|
||||||
|
mw.Place(frame2, ui.Place{
|
||||||
|
Right: 80,
|
||||||
|
Bottom: 80,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw rings of buttons around various widgets. The buttons say things
|
||||||
|
// like "Top Left", "Top Center", "Left Middle", "Center" etc. encompassing
|
||||||
|
// all 9 side placement options.
|
||||||
|
CreateButtons(mw, frame)
|
||||||
|
CreateButtons(mw, frame2)
|
||||||
|
CreateButtons(mw, mw.Frame())
|
||||||
|
|
||||||
|
mw.MainLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateButtons creates a set of Placed buttons around all the edges and
|
||||||
|
// center of the parent frame.
|
||||||
|
func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
|
||||||
|
// Draw buttons around the edges of the window.
|
||||||
|
buttons := []struct {
|
||||||
|
Label string
|
||||||
|
Place ui.Place
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Label: "Top Left",
|
||||||
|
Place: ui.Place{
|
||||||
|
Point: render.NewPoint(12, 12),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Top Middle",
|
||||||
|
Place: ui.Place{
|
||||||
|
Top: 12,
|
||||||
|
Center: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Top Right",
|
||||||
|
Place: ui.Place{
|
||||||
|
Top: 12,
|
||||||
|
Right: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Left Middle",
|
||||||
|
Place: ui.Place{
|
||||||
|
Left: 12,
|
||||||
|
Middle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Center",
|
||||||
|
Place: ui.Place{
|
||||||
|
Center: true,
|
||||||
|
Middle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Right Middle",
|
||||||
|
Place: ui.Place{
|
||||||
|
Right: 12,
|
||||||
|
Middle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Bottom Left",
|
||||||
|
Place: ui.Place{
|
||||||
|
Left: 12,
|
||||||
|
Bottom: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Bottom Center",
|
||||||
|
Place: ui.Place{
|
||||||
|
Bottom: 12,
|
||||||
|
Center: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Bottom Right",
|
||||||
|
Place: ui.Place{
|
||||||
|
Bottom: 12,
|
||||||
|
Right: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, setting := range buttons {
|
||||||
|
setting := setting
|
||||||
|
|
||||||
|
button := ui.NewButton(setting.Label, ui.NewLabel(ui.Label{
|
||||||
|
Text: setting.Label,
|
||||||
|
Font: render.Text{
|
||||||
|
FontFilename: "../DejaVuSans.ttf",
|
||||||
|
Size: 12,
|
||||||
|
Color: render.Black,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// When clicked, change the window title to ID this button.
|
||||||
|
button.Handle(ui.Click, func(p render.Point) {
|
||||||
|
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||||
|
})
|
||||||
|
|
||||||
|
parent.Place(button, setting.Place)
|
||||||
|
window.Add(button)
|
||||||
|
}
|
||||||
|
}
|
BIN
eg/frame-place/screenshot.png
Normal file
BIN
eg/frame-place/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
6
frame.go
6
frame.go
|
@ -10,7 +10,10 @@ import (
|
||||||
type Frame struct {
|
type Frame struct {
|
||||||
Name string
|
Name string
|
||||||
BaseWidget
|
BaseWidget
|
||||||
packs map[Side][]packedWidget
|
|
||||||
|
// Widget placement settings.
|
||||||
|
packs map[Side][]packedWidget // Packed widgets
|
||||||
|
placed []placedWidget // Placed widgets
|
||||||
widgets []Widget
|
widgets []Widget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +51,7 @@ func (w *Frame) Children() []Widget {
|
||||||
// Compute the size of the Frame.
|
// Compute the size of the Frame.
|
||||||
func (w *Frame) Compute(e render.Engine) {
|
func (w *Frame) Compute(e render.Engine) {
|
||||||
w.computePacked(e)
|
w.computePacked(e)
|
||||||
|
w.computePlaced(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the Frame.
|
// Present the Frame.
|
||||||
|
|
97
frame_place.go
Normal file
97
frame_place.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Place provides configuration fields for Frame.Place().
|
||||||
|
type Place struct {
|
||||||
|
// X and Y coordinates for explicit location of widget within its parent.
|
||||||
|
// This placement option trumps all others.
|
||||||
|
Point render.Point
|
||||||
|
|
||||||
|
// Place relative to an edge of the window. The widget will stick to the
|
||||||
|
// edge of the window even as it resizes. Options are ignored if Point
|
||||||
|
// is set.
|
||||||
|
Top int
|
||||||
|
Left int
|
||||||
|
Right int
|
||||||
|
Bottom int
|
||||||
|
Center bool
|
||||||
|
Middle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy returns the placement strategy for a Place config struct.
|
||||||
|
// Returns 'Point' if a render.Point is used (even if zero, zero)
|
||||||
|
// Returns 'Side' if the side values are set.
|
||||||
|
func (p Place) Strategy() string {
|
||||||
|
if p.Top != 0 || p.Left != 0 || p.Right != 0 || p.Bottom != 0 || p.Center || p.Middle {
|
||||||
|
return "Side"
|
||||||
|
}
|
||||||
|
return "Point"
|
||||||
|
}
|
||||||
|
|
||||||
|
// placedWidget holds the data for a widget placed in a frame.
|
||||||
|
type placedWidget struct {
|
||||||
|
widget Widget
|
||||||
|
place Place
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place a widget into the frame.
|
||||||
|
func (w *Frame) Place(child Widget, config Place) {
|
||||||
|
w.placed = append(w.placed, placedWidget{
|
||||||
|
widget: child,
|
||||||
|
place: config,
|
||||||
|
})
|
||||||
|
w.widgets = append(w.widgets, child)
|
||||||
|
|
||||||
|
// Adopt the child widget so it can access the Frame.
|
||||||
|
child.SetParent(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// computePlaced processes all the Place layout widgets in the Frame,
|
||||||
|
// determining their X,Y location and whether they need to change.
|
||||||
|
func (w *Frame) computePlaced(e render.Engine) {
|
||||||
|
var (
|
||||||
|
frameSize = w.BoxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, row := range w.placed {
|
||||||
|
// X,Y placement takes priority.
|
||||||
|
switch row.place.Strategy() {
|
||||||
|
case "Point":
|
||||||
|
row.widget.MoveTo(row.place.Point)
|
||||||
|
case "Side":
|
||||||
|
var moveTo render.Point
|
||||||
|
|
||||||
|
// Compute the initial X,Y based on Top, Left, Right, Bottom.
|
||||||
|
if row.place.Left > 0 {
|
||||||
|
moveTo.X = row.place.Left
|
||||||
|
}
|
||||||
|
if row.place.Top > 0 {
|
||||||
|
moveTo.Y = row.place.Top
|
||||||
|
}
|
||||||
|
if row.place.Right > 0 {
|
||||||
|
moveTo.X = frameSize.W - row.widget.Size().W - row.place.Right
|
||||||
|
}
|
||||||
|
if row.place.Bottom > 0 {
|
||||||
|
moveTo.Y = frameSize.H - row.widget.Size().H - row.place.Bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center and Middle aligned values override Left/Right, Top/Bottom
|
||||||
|
// settings respectively.
|
||||||
|
if row.place.Center {
|
||||||
|
moveTo.X = frameSize.W - (w.Size().W / 2) - (row.widget.Size().W / 2)
|
||||||
|
}
|
||||||
|
if row.place.Middle {
|
||||||
|
moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
|
||||||
|
}
|
||||||
|
row.widget.MoveTo(moveTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this widget itself has placed widgets, call its function too.
|
||||||
|
if frame, ok := row.widget.(*Frame); ok {
|
||||||
|
frame.computePlaced(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module git.kirsle.net/go/ui
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d
|
||||||
|
github.com/veandco/go-sdl2 v0.4.1 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect
|
||||||
|
)
|
7
go.sum
Normal file
7
go.sum
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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=
|
||||||
|
github.com/veandco/go-sdl2 v0.4.1 h1:HmSBvVmKWI8LAOeCfTTM8R33rMyPcs6U3o8n325c9Qg=
|
||||||
|
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
49
image.go
49
image.go
|
@ -2,6 +2,9 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -13,8 +16,9 @@ type ImageType string
|
||||||
|
|
||||||
// Supported image formats.
|
// Supported image formats.
|
||||||
const (
|
const (
|
||||||
BMP ImageType = "bmp"
|
BMP ImageType = "bmp"
|
||||||
PNG = "png"
|
PNG = "png"
|
||||||
|
JPEG = "jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Image is a widget that is backed by an image file.
|
// Image is a widget that is backed by an image file.
|
||||||
|
@ -23,6 +27,7 @@ type Image struct {
|
||||||
|
|
||||||
// Configurable fields for the constructor.
|
// Configurable fields for the constructor.
|
||||||
Type ImageType
|
Type ImageType
|
||||||
|
Image image.Image
|
||||||
texture render.Texturer
|
texture render.Texturer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +53,29 @@ func ImageFromTexture(tex render.Texturer) *Image {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageFromFile creates an Image by opening a file from disk.
|
||||||
|
func ImageFromFile(e render.Engine, filename string) (*Image, error) {
|
||||||
|
fh, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := jpeg.Decode(fh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tex, err := e.StoreTexture(filename, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Image{
|
||||||
|
Image: img,
|
||||||
|
texture: tex,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// OpenImage initializes an Image with a given file name.
|
// OpenImage initializes an Image with a given file name.
|
||||||
//
|
//
|
||||||
// The file extension is important and should be a supported ImageType.
|
// The file extension is important and should be a supported ImageType.
|
||||||
|
@ -58,6 +86,10 @@ func OpenImage(e render.Engine, filename string) (*Image, error) {
|
||||||
w.Type = BMP
|
w.Type = BMP
|
||||||
case ".png":
|
case ".png":
|
||||||
w.Type = PNG
|
w.Type = PNG
|
||||||
|
case ".jpg":
|
||||||
|
w.Type = JPEG
|
||||||
|
case ".jpeg":
|
||||||
|
w.Type = JPEG
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
|
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +103,19 @@ func OpenImage(e render.Engine, filename string) (*Image, error) {
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRGBA returns an image.RGBA from the image data.
|
||||||
|
func (w *Image) GetRGBA() *image.RGBA {
|
||||||
|
var bounds = w.Image.Bounds()
|
||||||
|
var rgba = image.NewRGBA(bounds)
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
color := w.Image.At(x, y)
|
||||||
|
rgba.Set(x, y, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
// Compute the widget.
|
// Compute the widget.
|
||||||
func (w *Image) Compute(e render.Engine) {
|
func (w *Image) Compute(e render.Engine) {
|
||||||
w.Resize(w.texture.Size())
|
w.Resize(w.texture.Size())
|
||||||
|
|
|
@ -16,6 +16,12 @@ var (
|
||||||
FPS = 60
|
FPS = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Default width and height for MainWindow.
|
||||||
|
var (
|
||||||
|
DefaultWidth = 640
|
||||||
|
DefaultHeight = 480
|
||||||
|
)
|
||||||
|
|
||||||
// MainWindow is the parent window of a UI application.
|
// MainWindow is the parent window of a UI application.
|
||||||
type MainWindow struct {
|
type MainWindow struct {
|
||||||
Engine render.Engine
|
Engine render.Engine
|
||||||
|
@ -27,11 +33,26 @@ type MainWindow struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMainWindow initializes the MainWindow. You should probably only have one
|
// NewMainWindow initializes the MainWindow. You should probably only have one
|
||||||
// of these per application.
|
// of these per application. Dimensions are the width and height of the window.
|
||||||
func NewMainWindow(title string) (*MainWindow, error) {
|
//
|
||||||
|
// Example: NewMainWindow("Title Bar") // default 640x480 window
|
||||||
|
// NewMainWindow("Title", 800, 600) // both required
|
||||||
|
func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) {
|
||||||
|
var (
|
||||||
|
width = DefaultWidth
|
||||||
|
height = DefaultHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(dimensions) > 0 {
|
||||||
|
if len(dimensions) != 2 {
|
||||||
|
return nil, fmt.Errorf("provide width and height dimensions, like NewMainWindow(title, 800, 600)")
|
||||||
|
}
|
||||||
|
width, height = dimensions[0], dimensions[1]
|
||||||
|
}
|
||||||
|
|
||||||
mw := &MainWindow{
|
mw := &MainWindow{
|
||||||
w: 800,
|
w: width,
|
||||||
h: 600,
|
h: height,
|
||||||
supervisor: NewSupervisor(),
|
supervisor: NewSupervisor(),
|
||||||
loopCallbacks: []func(*event.State){},
|
loopCallbacks: []func(*event.State){},
|
||||||
}
|
}
|
||||||
|
@ -56,6 +77,11 @@ func NewMainWindow(title string) (*MainWindow, error) {
|
||||||
return mw, nil
|
return mw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTitle changes the title of the window.
|
||||||
|
func (mw *MainWindow) SetTitle(title string) {
|
||||||
|
mw.Engine.SetTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
// Add a child widget to the window.
|
// Add a child widget to the window.
|
||||||
func (mw *MainWindow) Add(w Widget) {
|
func (mw *MainWindow) Add(w Widget) {
|
||||||
mw.supervisor.Add(w)
|
mw.supervisor.Add(w)
|
||||||
|
@ -67,6 +93,12 @@ func (mw *MainWindow) Pack(w Widget, pack Pack) {
|
||||||
mw.frame.Pack(w, pack)
|
mw.frame.Pack(w, pack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Place a child widget into the window's default frame.
|
||||||
|
func (mw *MainWindow) Place(w Widget, config Place) {
|
||||||
|
mw.Add(w)
|
||||||
|
mw.frame.Place(w, config)
|
||||||
|
}
|
||||||
|
|
||||||
// Frame returns the window's main frame, if needed.
|
// Frame returns the window's main frame, if needed.
|
||||||
func (mw *MainWindow) Frame() *Frame {
|
func (mw *MainWindow) Frame() *Frame {
|
||||||
return mw.frame
|
return mw.frame
|
||||||
|
|
Loading…
Reference in New Issue
Block a user