Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI (don't merge) #157

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ go.json
gop.json
gop_autogen.go
_gsc
_ui
_demo
_test
haiyang/
feijidazhan/
Expand Down
2 changes: 2 additions & 0 deletions internal/ebitenui/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package ebitenui contains the main UI type that renders a complete user interface.
package ebitenui
9 changes: 9 additions & 0 deletions internal/ebitenui/event/deferred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package event

import internalevent "github.com/goplus/spx/internal/ebitenui/internal/event"

// ExecuteDeferred processes the queue of deferred actions and executes them. This should only be called by UI.
// Additionally, it can be called in unit tests to process events programmatically.
func ExecuteDeferred() {
internalevent.ExecuteDeferred()
}
2 changes: 2 additions & 0 deletions internal/ebitenui/event/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package event contains types to deal with firing and handling events.
package event
104 changes: 104 additions & 0 deletions internal/ebitenui/event/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package event

import internalevent "github.com/goplus/spx/internal/ebitenui/internal/event"

// Event encapsulates an arbitrary event that event handlers may be interested in.
type Event struct {
idCounter uint32
handlers []handler
}

// A HandlerFunc is a function that receives and handles an event. When firing an event using
// Event.Fire, arbitrary event arguments may be passed that are in turn passed on to the handler function.
type HandlerFunc func(args interface{})

// RemoveHandlerFunc is a function that removes a handler from an event.
type RemoveHandlerFunc func()

type handler struct {
id uint32
h HandlerFunc
}

type deferredEvent struct {
event *Event
args interface{}
}

type deferredAddHandler struct {
event *Event
handler handler
}

// AddHandler registers event handler h with e. It returns a function to remove h from e if desired.
func (e *Event) AddHandler(h HandlerFunc) RemoveHandlerFunc {
e.idCounter++

id := e.idCounter

internalevent.AddDeferred(&deferredAddHandler{
event: e,
handler: handler{
id: id,
h: h,
},
})

return func() {
e.removeHandler(id)
}
}

func (e *Event) removeHandler(id uint32) {
index := -1
for i, h := range e.handlers {
if h.id == id {
index = i
break
}
}

if index < 0 {
return
}

e.handlers = append(e.handlers[:index], e.handlers[index+1:]...)
}

// Fire fires an event to all registered handlers. Arbitrary event arguments may be passed
// which are in turn passed on to event handlers.
//
// Events are not fired directly, but are put into a deferred queue. This queue is then
// processed by the UI.
func (e *Event) Fire(args interface{}) {
internalevent.AddDeferred(&deferredEvent{
event: e,
args: args,
})
}

func (e *Event) handle(args interface{}) {
for _, h := range e.handlers {
h.h(args)
}
}

// Do implements DeferredAction.
func (e *deferredEvent) Do() {
e.event.handle(e.args)
}

// Do implements DeferredAction.
func (a *deferredAddHandler) Do() {
a.event.handlers = append(a.event.handlers, a.handler)
}

// AddEventHandlerOneShot registers event handler h with e. When e fires an event, h is removed from e immediately.
func AddEventHandlerOneShot(e *Event, h HandlerFunc) {
var r RemoveHandlerFunc
rh := func(args interface{}) {
r()
h(args)
}
r = e.AddHandler(rh)
}
121 changes: 121 additions & 0 deletions internal/ebitenui/examples/_textinput/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"image/color"
"log"
"strconv"

_ "image/png"

"github.com/goplus/spx/internal/ebitenui"
"github.com/goplus/spx/internal/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/font"

xfont "github.com/goplus/spx/internal/gdi/font"
)

type game struct {
ui *ebitenui.UI
}

// Layout implements Game.
func (g *game) Layout(outsideWidth int, outsideHeight int) (int, int) {
return outsideWidth, outsideHeight
}

// Update implements Game.
func (g *game) Update() error {
// update the UI
g.ui.Update()
return nil
}

// Draw implements Ebiten's Draw method.
func (g *game) Draw(screen *ebiten.Image) {
// draw the UI onto the screen
g.ui.Draw(screen)
}

func newPageContentContainer() *widget.Container {
return widget.NewContainer(
widget.ContainerOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
StretchHorizontal: true,
})),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Spacing(10),
)))
}

func hexToColor(h string) color.Color {
u, err := strconv.ParseUint(h, 16, 0)
if err != nil {
panic(err)
}

return color.RGBA{
R: uint8(u & 0xff0000 >> 16),
G: uint8(u & 0xff00 >> 8),
B: uint8(u & 0xff),
A: 255,
}
}

const (
textIdleColor = "dff4ff"
textDisabledColor = "5a7a91"
textInputCaretColor = "e7c34b"
textInputDisabledCaretColor = "766326"
)

func main() {
// construct a new container that serves as the root of the UI hierarchy
rootContainer := newPageContentContainer()

color := &widget.TextInputColor{
Idle: hexToColor(textIdleColor),
Disabled: hexToColor(textDisabledColor),
Caret: hexToColor(textInputCaretColor),
DisabledCaret: hexToColor(textInputDisabledCaretColor),
}
const dpi = 72
defaultFont := xfont.NewDefault(&xfont.Options{
Size: 17,
DPI: dpi,
Hinting: font.HintingFull,
})
t := widget.NewTextInput(
widget.TextInputOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.TextInputOpts.Padding(widget.Insets{
Left: 13,
Right: 13,
Top: 7,
Bottom: 7,
}),
widget.TextInputOpts.Color(color),
widget.TextInputOpts.Face(defaultFont),
widget.TextInputOpts.CaretOpts(
widget.CaretOpts.Size(defaultFont, 2),
),
widget.TextInputOpts.Placeholder("Enter text here"),
)

rootContainer.AddChild(t)

// construct the UI
ui := ebitenui.UI{
Container: rootContainer,
}
game := game{
ui: &ui,
}

// run Ebiten main loop
err := ebiten.RunGame(&game)
if err != nil {
log.Println(err)
}
}
67 changes: 67 additions & 0 deletions internal/ebitenui/image/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package image

import "github.com/hajimehoshi/ebiten/v2"

// BufferedImage is a wrapper for an Ebiten Image that helps with caching the Image.
// As long as Width and Height stay the same, no new Image will be created.
type BufferedImage struct {
Width int
Height int

image *ebiten.Image
}

// MaskedRenderBuffer is a helper to draw images using a mask.
type MaskedRenderBuffer struct {
renderBuf *BufferedImage
maskedBuf *BufferedImage
}

// DrawFunc is a function that draws something into buf.
type DrawFunc func(buf *ebiten.Image)

// Image returns the internal Ebiten Image. If b.Width or b.Height have changed, a new Image
// will be created and returned, otherwise the cached Image will be returned.
func (b *BufferedImage) Image() *ebiten.Image {
w, h := -1, -1
if b.image != nil {
w, h = b.image.Size()
}

if b.image == nil || b.Width != w || b.Height != h {
b.image = ebiten.NewImage(b.Width, b.Height)
}

return b.image
}

// NewMaskedRenderBuffer returns a new MaskedRenderBuffer.
func NewMaskedRenderBuffer() *MaskedRenderBuffer {
return &MaskedRenderBuffer{
renderBuf: &BufferedImage{},
maskedBuf: &BufferedImage{},
}
}

// Draw calls d to draw onto screen, using the mask drawn by dm. The buffer images passed
// to d and dm are of the same size as screen.
func (m *MaskedRenderBuffer) Draw(screen *ebiten.Image, d DrawFunc, dm DrawFunc) {
w, h := screen.Size()

m.renderBuf.Width, m.renderBuf.Height = w, h
renderBuf := m.renderBuf.Image()
renderBuf.Clear()

m.maskedBuf.Width, m.maskedBuf.Height = w, h
maskedBuf := m.maskedBuf.Image()
maskedBuf.Clear()

d(renderBuf)
dm(maskedBuf)

maskedBuf.DrawImage(renderBuf, &ebiten.DrawImageOptions{
CompositeMode: ebiten.CompositeModeSourceIn,
})

screen.DrawImage(maskedBuf, nil)
}
3 changes: 3 additions & 0 deletions internal/ebitenui/image/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package image contains types to deal with nine-slice images, buffered (cached) images, as well as
// drawing using masks.
package image
Loading