diff --git a/eg/frame-place/README.md b/eg/frame-place/README.md new file mode 100644 index 0000000..35060d2 --- /dev/null +++ b/eg/frame-place/README.md @@ -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 +``` diff --git a/eg/frame-place/main.go b/eg/frame-place/main.go new file mode 100644 index 0000000..8cf63c6 --- /dev/null +++ b/eg/frame-place/main.go @@ -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) + } +} diff --git a/eg/frame-place/screenshot.png b/eg/frame-place/screenshot.png new file mode 100644 index 0000000..35a2ad0 Binary files /dev/null and b/eg/frame-place/screenshot.png differ diff --git a/frame.go b/frame.go index cc2fca9..90bcbf5 100644 --- a/frame.go +++ b/frame.go @@ -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. diff --git a/frame_place.go b/frame_place.go new file mode 100644 index 0000000..2b2c00d --- /dev/null +++ b/frame_place.go @@ -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) + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..630c5e6 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..044cc79 --- /dev/null +++ b/go.sum @@ -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= diff --git a/image.go b/image.go index d58e740..4ad251b 100644 --- a/image.go +++ b/image.go @@ -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()) diff --git a/main_window.go b/main_window.go index 3678c9d..d90faa4 100644 --- a/main_window.go +++ b/main_window.go @@ -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