Place Strategy for Frame Widget

This commit is contained in:
Noah 2020-03-08 22:07:46 -07:00
parent 4ba563d48d
commit 0846fe22fc
9 changed files with 374 additions and 7 deletions

29
eg/frame-place/README.md Normal file
View File

@ -0,0 +1,29 @@
# Frame Placement Example
![Screenshot](screenshot.png)
## 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
View 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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -10,7 +10,10 @@ import (
type Frame struct {
Name string
BaseWidget
packs map[Side][]packedWidget
// Widget placement settings.
packs map[Side][]packedWidget // Packed widgets
placed []placedWidget // Placed widgets
widgets []Widget
}
@ -48,6 +51,7 @@ func (w *Frame) Children() []Widget {
// Compute the size of the Frame.
func (w *Frame) Compute(e render.Engine) {
w.computePacked(e)
w.computePlaced(e)
}
// Present the Frame.

97
frame_place.go Normal file
View 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
View 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
View 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=

View File

@ -2,6 +2,9 @@ package ui
import (
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strings"
@ -13,8 +16,9 @@ type ImageType string
// Supported image formats.
const (
BMP ImageType = "bmp"
PNG = "png"
BMP ImageType = "bmp"
PNG = "png"
JPEG = "jpg"
)
// Image is a widget that is backed by an image file.
@ -23,6 +27,7 @@ type Image struct {
// Configurable fields for the constructor.
Type ImageType
Image image.Image
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.
//
// 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
case ".png":
w.Type = PNG
case ".jpg":
w.Type = JPEG
case ".jpeg":
w.Type = JPEG
default:
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
}
// 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.
func (w *Image) Compute(e render.Engine) {
w.Resize(w.texture.Size())

View File

@ -16,6 +16,12 @@ var (
FPS = 60
)
// Default width and height for MainWindow.
var (
DefaultWidth = 640
DefaultHeight = 480
)
// MainWindow is the parent window of a UI application.
type MainWindow struct {
Engine render.Engine
@ -27,11 +33,26 @@ type MainWindow struct {
}
// NewMainWindow initializes the MainWindow. You should probably only have one
// of these per application.
func NewMainWindow(title string) (*MainWindow, error) {
// of these per application. Dimensions are the width and height of the window.
//
// 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{
w: 800,
h: 600,
w: width,
h: height,
supervisor: NewSupervisor(),
loopCallbacks: []func(*event.State){},
}
@ -56,6 +77,11 @@ func NewMainWindow(title string) (*MainWindow, error) {
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.
func (mw *MainWindow) Add(w Widget) {
mw.supervisor.Add(w)
@ -67,6 +93,12 @@ func (mw *MainWindow) Pack(w Widget, pack 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.
func (mw *MainWindow) Frame() *Frame {
return mw.frame