Added SDL backend.

Added Action{,s}. List of actions that can be used to defer cleanup code (see NewRenderer implementations).
Added TextInputEvent (replaces the old KeyPressEvent) and added to new events KeyDown & KeyUp.
Added VSync to NewRendererOptions.
Removed IconScale from button.
Added ImageSource interface that replaces the Image/Texture method on the Texture interface. This makes converting back a texture to an image optional (since this is atypical for a hardware texture for instance).
Added new KeyModifier: OSCommand (Windows/Command key).
Added KeyState that can keep the state of keys (pressed or not).
Added KeyEnter, representing the Enter key.
Changed signatures of CreateTexture methods in Renderer.
Changed signatures of icon related method (removed factories).
Basic example now depends on sdlgui.
This commit is contained in:
Sander Schobers 2020-05-15 09:20:44 +02:00
parent f618c55b25
commit cdfb863ab0
29 changed files with 1471 additions and 131 deletions

37
action.go Normal file
View File

@ -0,0 +1,37 @@
package zntg
type Action func()
func (a Action) Err() ActionErr {
return func() error {
a()
return nil
}
}
type ActionErr func() error
type Actions []ActionErr
func (a Actions) Add(fn Action) Actions {
return a.AddErr(fn.Err())
}
func (a Actions) AddErr(fn ActionErr) Actions {
return append(a, fn)
}
func (a Actions) Do() {
for _, a := range a {
a()
}
}
func (a Actions) DoErr() error {
for _, a := range a {
if err := a(); err != nil {
return err
}
}
return nil
}

View File

@ -97,6 +97,8 @@ func key(key allg5.Key) ui.Key {
return ui.KeyE return ui.KeyE
case allg5.KeyEnd: case allg5.KeyEnd:
return ui.KeyEnd return ui.KeyEnd
case allg5.KeyEnter:
return ui.KeyEnter
case allg5.KeyEquals: case allg5.KeyEquals:
return ui.KeyEquals return ui.KeyEquals
case allg5.KeyEscape: case allg5.KeyEscape:

View File

@ -4,6 +4,9 @@ import (
"image" "image"
"image/color" "image/color"
"math" "math"
"unicode"
"opslag.de/schobers/zntg"
"opslag.de/schobers/allg5" "opslag.de/schobers/allg5"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
@ -13,20 +16,25 @@ import (
var _ ui.Renderer = &Renderer{} var _ ui.Renderer = &Renderer{}
func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) { func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
var clean zntg.Actions
defer func() { clean.Do() }()
var err = allg5.Init(allg5.InitAll) var err = allg5.Init(allg5.InitAll)
if err != nil { if err != nil {
return nil, err return nil, err
} }
disp, err := allg5.NewDisplay(w, h, opts) disp, err := allg5.NewDisplay(w, h, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
clean = clean.Add(disp.Destroy)
eq, err := allg5.NewEventQueue() eq, err := allg5.NewEventQueue()
if err != nil { if err != nil {
disp.Destroy()
return nil, err return nil, err
} }
clean = clean.Add(eq.Destroy)
user := allg5.NewUserEventSource() user := allg5.NewUserEventSource()
eq.RegisterKeyboard() eq.RegisterKeyboard()
@ -34,7 +42,12 @@ func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
eq.RegisterDisplay(disp) eq.RegisterDisplay(disp)
eq.RegisterUserEvents(user) eq.RegisterUserEvents(user)
return &Renderer{disp, eq, nil, map[string]*font{}, user, ui.MouseCursorDefault, ui.MouseCursorDefault}, nil allg5.CaptureNewBitmapFlags().Mutate(func(m allg5.FlagMutation) {
m.Set(allg5.NewBitmapFlagMinLinear)
})
clean = nil
return &Renderer{disp, eq, nil, map[string]*font{}, user, ui.KeyState{}, ui.KeyModifierNone, ui.MouseCursorDefault}, nil
} }
// Renderer implements ui.Renderer using Allegro 5. // Renderer implements ui.Renderer using Allegro 5.
@ -44,8 +57,10 @@ type Renderer struct {
unh func(allg5.Event) unh func(allg5.Event)
ft map[string]*font ft map[string]*font
user *allg5.UserEventSource user *allg5.UserEventSource
keys ui.KeyState
modifiers ui.KeyModifier
cursor ui.MouseCursor cursor ui.MouseCursor
newCursor ui.MouseCursor
} }
// Renderer implementation (events) // Renderer implementation (events)
@ -53,11 +68,14 @@ type Renderer struct {
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) { func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
r.disp.Flip() r.disp.Flip()
r.newCursor = ui.MouseCursorDefault
var ev = eventWait(r.eq, wait) var ev = eventWait(r.eq, wait)
if ev == nil { if ev == nil {
return return
} }
cursor := r.cursor
r.cursor = ui.MouseCursorDefault
var unhandled bool
for ev != nil { for ev != nil {
switch e := ev.(type) { switch e := ev.(type) {
case *allg5.DisplayCloseEvent: case *allg5.DisplayCloseEvent:
@ -65,7 +83,21 @@ func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
case *allg5.DisplayResizeEvent: case *allg5.DisplayResizeEvent:
t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: geom.RectF32(float32(e.X), float32(e.Y), float32(e.X+e.Width), float32(e.Y+e.Height))}) t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: geom.RectF32(float32(e.X), float32(e.Y), float32(e.X+e.Width), float32(e.Y+e.Height))})
case *allg5.KeyCharEvent: case *allg5.KeyCharEvent:
t.Handle(&ui.KeyPressEvent{EventBase: eventBase(e), Key: key(e.KeyCode), Modifiers: keyModifiers(e.Modifiers), Character: e.UnicodeCharacter}) if r.modifiers&ui.KeyModifierControl == ui.KeyModifierNone && !unicode.IsControl(e.UnicodeCharacter) {
t.Handle(&ui.TextInputEvent{EventBase: eventBase(e), Character: e.UnicodeCharacter})
} else {
unhandled = true
}
case *allg5.KeyDownEvent:
key := key(e.KeyCode)
r.keys[key] = true
r.modifiers = r.keys.Modifiers()
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: key, Modifiers: r.modifiers})
case *allg5.KeyUpEvent:
key := key(e.KeyCode)
r.keys[key] = false
r.modifiers = r.keys.Modifiers()
t.Handle(&ui.KeyUpEvent{EventBase: eventBase(e), Key: key, Modifiers: r.modifiers})
case *allg5.MouseButtonDownEvent: case *allg5.MouseButtonDownEvent:
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)}) t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseButtonUpEvent: case *allg5.MouseButtonUpEvent:
@ -82,12 +114,12 @@ func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
if r.unh != nil { if r.unh != nil {
r.unh(e) r.unh(e)
} }
unhandled = true
} }
ev = r.eq.Get() ev = r.eq.Get()
} }
if r.newCursor != r.cursor { if !unhandled && cursor != r.cursor {
r.cursor = r.newCursor
switch r.cursor { switch r.cursor {
case ui.MouseCursorNone: case ui.MouseCursorNone:
r.disp.SetMouseCursor(allg5.MouseCursorNone) r.disp.SetMouseCursor(allg5.MouseCursorNone)
@ -130,32 +162,47 @@ func (r *Renderer) Clear(c color.Color) {
allg5.ClearToColor(newColor(c)) allg5.ClearToColor(newColor(c))
} }
func (r *Renderer) CreateTexture(im image.Image) (ui.Texture, error) { func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
im, err := source.CreateImage()
if err != nil {
return nil, err
}
bmp, err := allg5.NewBitmapFromImage(im, true) bmp, err := allg5.NewBitmapFromImage(im, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &texture{bmp}, nil if keepSource {
return &texture{bmp, source}, nil
}
return &texture{bmp, nil}, nil
} }
func (r *Renderer) CreateTexturePath(path string) (ui.Texture, error) { func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) {
return r.createTexture(source, true)
}
func (r *Renderer) CreateTextureGo(im image.Image, source bool) (ui.Texture, error) {
return r.createTexture(ui.ImageGoSource{im}, true)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
bmp, err := allg5.LoadBitmap(path) bmp, err := allg5.LoadBitmap(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &texture{bmp}, nil return &texture{bmp, nil}, nil
} }
func (r *Renderer) CreateTextureSize(w, h float32) (ui.Texture, error) { func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) {
bmp, err := allg5.NewVideoBitmap(int(w), int(h)) bmp, err := allg5.NewVideoBitmap(int(w), int(h))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &texture{bmp}, nil return &texture{bmp, nil}, nil
} }
func (r *Renderer) DefaultTarget() ui.Texture { func (r *Renderer) DefaultTarget() ui.Texture {
return &texture{r.disp.Target()} return &texture{r.disp.Target(), nil}
} }
func (r *Renderer) Display() *allg5.Display { return r.disp } func (r *Renderer) Display() *allg5.Display { return r.disp }
@ -191,7 +238,7 @@ func (r *Renderer) Font(name string) ui.Font {
func (r *Renderer) mustGetBitmap(t ui.Texture) *allg5.Bitmap { func (r *Renderer) mustGetBitmap(t ui.Texture) *allg5.Bitmap {
texture, ok := t.(*texture) texture, ok := t.(*texture)
if !ok { if !ok {
panic("image must be created on same renderer") panic("texture must be created on same renderer")
} }
return texture.bmp return texture.bmp
} }
@ -244,7 +291,7 @@ func (r *Renderer) SetIcon(texture ui.Texture) {
} }
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.newCursor = c r.cursor = c
} }
func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) { func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) {
@ -256,7 +303,7 @@ func (r *Renderer) SetWindowTitle(t string) {
} }
func (r *Renderer) Target() ui.Texture { func (r *Renderer) Target() ui.Texture {
return &texture{allg5.CurrentTarget()} return &texture{allg5.CurrentTarget(), nil}
} }
func (r *Renderer) text(p geom.PointF32, font string, c color.Color, t string, align allg5.HorizontalAlignment) { func (r *Renderer) text(p geom.PointF32, font string, c color.Color, t string, align allg5.HorizontalAlignment) {

View File

@ -18,6 +18,7 @@ func (f rendererFactory) New(title string, width, height int) (ui.Renderer, erro
func (f rendererFactory) NewOptions(title string, width, height int, opts ui.NewRendererOptions) (ui.Renderer, error) { func (f rendererFactory) NewOptions(title string, width, height int, opts ui.NewRendererOptions) (ui.Renderer, error) {
renderer, err := NewRenderer(width, height, allg5.NewDisplayOptions{ renderer, err := NewRenderer(width, height, allg5.NewDisplayOptions{
Resizable: opts.Resizable, Resizable: opts.Resizable,
Vsync: opts.VSync,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -8,21 +8,27 @@ import (
) )
var _ ui.Texture = &texture{} var _ ui.Texture = &texture{}
var _ ui.ImageSource = &texture{}
type texture struct { type texture struct {
bmp *allg5.Bitmap bmp *allg5.Bitmap
source ui.ImageSource
} }
func (t *texture) Destroy() { func (t *texture) Destroy() error {
t.bmp.Destroy() t.bmp.Destroy()
return nil
} }
func (t *texture) Height() float32 { func (t *texture) Height() float32 {
return float32(t.bmp.Height()) return float32(t.bmp.Height())
} }
func (t *texture) Texture() image.Image { func (t *texture) CreateImage() (image.Image, error) {
return t.bmp.Image() if t.source == nil {
return t.bmp.Image(), nil
}
return t.source.CreateImage()
} }
func (t *texture) Width() float32 { func (t *texture) Width() float32 {

11
sdlui/color.go Normal file
View File

@ -0,0 +1,11 @@
package sdlui
import (
"image/color"
"github.com/veandco/go-sdl2/sdl"
)
func ColorSDL(c color.Color) sdl.Color {
return sdl.Color(color.RGBAModel.Convert(c).(color.RGBA))
}

526
sdlui/events.go Normal file
View File

@ -0,0 +1,526 @@
package sdlui
import (
"github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/zntg/ui"
)
func eventBase(e sdl.Event) ui.EventBase {
return ui.EventBase{StampInSeconds: .001 * float64(e.GetTimestamp())}
}
func key(code sdl.Keycode) ui.Key {
switch code {
case sdl.K_UNKNOWN:
return ui.KeyNone
case sdl.K_RETURN:
return ui.KeyEnter
case sdl.K_ESCAPE:
return ui.KeyEscape
case sdl.K_BACKSPACE:
return ui.KeyBackspace
case sdl.K_TAB:
return ui.KeyTab
case sdl.K_SPACE:
return ui.KeySpace
// case sdl.K_EXCLAIM:
// return ui.KeyNone
// case sdl.K_QUOTEDBL:
// return ui.KeyNone
// case sdl.K_HASH:
// return ui.KeyNone
// case sdl.K_PERCENT:
// return ui.KeyNone
// case sdl.K_DOLLAR:
// return ui.KeyNone
// case sdl.K_AMPERSAND:
// return ui.KeyNone
case sdl.K_QUOTE:
return ui.KeyQuote
// case sdl.K_LEFTPAREN:
// return ui.KeyNone
// case sdl.K_RIGHTPAREN:
// return ui.KeyNone
case sdl.K_ASTERISK:
return ui.KeyPadAsterisk
case sdl.K_PLUS:
return ui.KeyPadPlus
case sdl.K_COMMA:
return ui.KeyComma
case sdl.K_MINUS:
return ui.KeyMinus
case sdl.K_PERIOD:
return ui.KeyFullstop
case sdl.K_SLASH:
return ui.KeySlash
case sdl.K_0:
return ui.Key0
case sdl.K_1:
return ui.Key1
case sdl.K_2:
return ui.Key2
case sdl.K_3:
return ui.Key3
case sdl.K_4:
return ui.Key4
case sdl.K_5:
return ui.Key5
case sdl.K_6:
return ui.Key6
case sdl.K_7:
return ui.Key7
case sdl.K_8:
return ui.Key8
case sdl.K_9:
return ui.Key9
// case sdl.K_COLON:
// return ui.KeyNone
case sdl.K_SEMICOLON:
return ui.KeySemicolon
// case sdl.K_LESS:
// return ui.KeyNone
case sdl.K_EQUALS:
return ui.KeyEquals
// case sdl.K_GREATER:
// return ui.KeyNone
// case sdl.K_QUESTION:
// return ui.KeyNone
// case sdl.K_AT:
// return ui.KeyNone
case sdl.K_LEFTBRACKET:
return ui.KeyOpenBrace
case sdl.K_BACKSLASH:
return ui.KeyBackslash
case sdl.K_RIGHTBRACKET:
return ui.KeyCloseBrace
// case sdl.K_CARET:
// return ui.KeyNone
// case sdl.K_UNDERSCORE:
// return ui.KeyNone
case sdl.K_BACKQUOTE:
return ui.KeyBacktick
case sdl.K_a:
return ui.KeyA
case sdl.K_b:
return ui.KeyB
case sdl.K_c:
return ui.KeyC
case sdl.K_d:
return ui.KeyD
case sdl.K_e:
return ui.KeyE
case sdl.K_f:
return ui.KeyF
case sdl.K_g:
return ui.KeyG
case sdl.K_h:
return ui.KeyH
case sdl.K_i:
return ui.KeyI
case sdl.K_j:
return ui.KeyJ
case sdl.K_k:
return ui.KeyK
case sdl.K_l:
return ui.KeyL
case sdl.K_m:
return ui.KeyM
case sdl.K_n:
return ui.KeyN
case sdl.K_o:
return ui.KeyO
case sdl.K_p:
return ui.KeyP
case sdl.K_q:
return ui.KeyQ
case sdl.K_r:
return ui.KeyR
case sdl.K_s:
return ui.KeyS
case sdl.K_t:
return ui.KeyT
case sdl.K_u:
return ui.KeyU
case sdl.K_v:
return ui.KeyV
case sdl.K_w:
return ui.KeyW
case sdl.K_x:
return ui.KeyX
case sdl.K_y:
return ui.KeyY
case sdl.K_z:
return ui.KeyZ
case sdl.K_CAPSLOCK:
return ui.KeyCapsLock
case sdl.K_F1:
return ui.KeyF1
case sdl.K_F2:
return ui.KeyF2
case sdl.K_F3:
return ui.KeyF3
case sdl.K_F4:
return ui.KeyF4
case sdl.K_F5:
return ui.KeyF5
case sdl.K_F6:
return ui.KeyF6
case sdl.K_F7:
return ui.KeyF7
case sdl.K_F8:
return ui.KeyF8
case sdl.K_F9:
return ui.KeyF9
case sdl.K_F10:
return ui.KeyF10
case sdl.K_F11:
return ui.KeyF11
case sdl.K_F12:
return ui.KeyF12
case sdl.K_PRINTSCREEN:
return ui.KeyPrintScreen
case sdl.K_SCROLLLOCK:
return ui.KeyScrollLock
case sdl.K_PAUSE:
return ui.KeyPause
case sdl.K_INSERT:
return ui.KeyInsert
case sdl.K_HOME:
return ui.KeyHome
case sdl.K_PAGEUP:
return ui.KeyPageUp
case sdl.K_DELETE:
return ui.KeyDelete
case sdl.K_END:
return ui.KeyEnd
case sdl.K_PAGEDOWN:
return ui.KeyPageDown
case sdl.K_RIGHT:
return ui.KeyRight
case sdl.K_LEFT:
return ui.KeyLeft
case sdl.K_DOWN:
return ui.KeyDown
case sdl.K_UP:
return ui.KeyUp
// case sdl.K_NUMLOCKCLEAR:
// return ui.KeyNone
case sdl.K_KP_DIVIDE:
return ui.KeyPadSlash
case sdl.K_KP_MULTIPLY:
return ui.KeyPadAsterisk
case sdl.K_KP_MINUS:
return ui.KeyPadMinus
case sdl.K_KP_PLUS:
return ui.KeyPadPlus
case sdl.K_KP_ENTER:
return ui.KeyPadEnter
case sdl.K_KP_1:
return ui.KeyPad1
case sdl.K_KP_2:
return ui.KeyPad2
case sdl.K_KP_3:
return ui.KeyPad3
case sdl.K_KP_4:
return ui.KeyPad4
case sdl.K_KP_5:
return ui.KeyPad5
case sdl.K_KP_6:
return ui.KeyPad6
case sdl.K_KP_7:
return ui.KeyPad7
case sdl.K_KP_8:
return ui.KeyPad8
case sdl.K_KP_9:
return ui.KeyPad9
case sdl.K_KP_0:
return ui.KeyPad0
// case sdl.K_KP_PERIOD:
// return ui.KeyNone
// case sdl.K_APPLICATION:
// return ui.KeyNone
// case sdl.K_POWER:
// return ui.KeyNone
case sdl.K_KP_EQUALS:
return ui.KeyPadEquals
// case sdl.K_F13:
// return ui.KeyNone
// case sdl.K_F14:
// return ui.KeyNone
// case sdl.K_F15:
// return ui.KeyNone
// case sdl.K_F16:
// return ui.KeyNone
// case sdl.K_F17:
// return ui.KeyNone
// case sdl.K_F18:
// return ui.KeyNone
// case sdl.K_F19:
// return ui.KeyNone
// case sdl.K_F20:
// return ui.KeyNone
// case sdl.K_F21:
// return ui.KeyNone
// case sdl.K_F22:
// return ui.KeyNone
// case sdl.K_F23:
// return ui.KeyNone
// case sdl.K_F24:
// return ui.KeyNone
// case sdl.K_EXECUTE:
// return ui.KeyNone
// case sdl.K_HELP:
// return ui.KeyNone
case sdl.K_MENU:
return ui.KeyNone
case sdl.K_SELECT:
return ui.KeyNone
case sdl.K_STOP:
return ui.KeyNone
case sdl.K_AGAIN:
return ui.KeyNone
case sdl.K_UNDO:
return ui.KeyNone
case sdl.K_CUT:
return ui.KeyNone
case sdl.K_COPY:
return ui.KeyNone
case sdl.K_PASTE:
return ui.KeyNone
case sdl.K_FIND:
return ui.KeyNone
case sdl.K_MUTE:
return ui.KeyNone
case sdl.K_VOLUMEUP:
return ui.KeyVolumeUp
case sdl.K_VOLUMEDOWN:
return ui.KeyVolumeDown
// case sdl.K_KP_COMMA:
// return ui.KeyNone
// case sdl.K_KP_EQUALSAS400:
// return ui.KeyNone
// case sdl.K_ALTERASE:
// return ui.KeyNone
// case sdl.K_SYSREQ:
// return ui.KeyNone
// case sdl.K_CANCEL:
// return ui.KeyNone
// case sdl.K_CLEAR:
// return ui.KeyNone
// case sdl.K_PRIOR:
// return ui.KeyNone
// case sdl.K_RETURN2:
// return ui.KeyNone
// case sdl.K_SEPARATOR:
// return ui.KeyNone
// case sdl.K_OUT:
// return ui.KeyNone
// case sdl.K_OPER:
// return ui.KeyNone
// case sdl.K_CLEARAGAIN:
// return ui.KeyNone
// case sdl.K_CRSEL:
// return ui.KeyNone
// case sdl.K_EXSEL:
// return ui.KeyNone
// case sdl.K_KP_00:
// return ui.KeyNone
// case sdl.K_KP_000:
// return ui.KeyNone
// case sdl.K_THOUSANDSSEPARATOR:
// return ui.KeyNone
// case sdl.K_DECIMALSEPARATOR:
// return ui.KeyNone
// case sdl.K_CURRENCYUNIT:
// return ui.KeyNone
// case sdl.K_CURRENCYSUBUNIT:
// return ui.KeyNone
// case sdl.K_KP_LEFTPAREN:
// return ui.KeyNone
// case sdl.K_KP_RIGHTPAREN:
// return ui.KeyNone
case sdl.K_KP_LEFTBRACE:
return ui.KeyOpenBrace // generic equivalent
case sdl.K_KP_RIGHTBRACE:
return ui.KeyCloseBrace // generic equivalent
case sdl.K_KP_TAB:
return ui.KeyTab // generic equivalent
case sdl.K_KP_BACKSPACE:
return ui.KeyBackspace // generic equivalent
case sdl.K_KP_A:
return ui.KeyA // generic equivalent
case sdl.K_KP_B:
return ui.KeyB // generic equivalent
case sdl.K_KP_C:
return ui.KeyC // generic equivalent
case sdl.K_KP_D:
return ui.KeyD // generic equivalent
case sdl.K_KP_E:
return ui.KeyE // generic equivalent
case sdl.K_KP_F:
return ui.KeyF // generic equivalent
// case sdl.K_KP_XOR:
// return ui.KeyNone
// case sdl.K_KP_POWER:
// return ui.KeyNone
// case sdl.K_KP_PERCENT:
// return ui.KeyNone
// case sdl.K_KP_LESS:
// return ui.KeyNone
// case sdl.K_KP_GREATER:
// return ui.KeyNone
// case sdl.K_KP_AMPERSAND:
// return ui.KeyNone
// case sdl.K_KP_DBLAMPERSAND:
// return ui.KeyNone
// case sdl.K_KP_VERTICALBAR:
// return ui.KeyNone
// case sdl.K_KP_DBLVERTICALBAR:
// return ui.KeyNone
// case sdl.K_KP_COLON:
// return ui.KeyNone
// case sdl.K_KP_HASH:
// return ui.KeyNone
case sdl.K_KP_SPACE:
return ui.KeySpace // generic equivalent
// case sdl.K_KP_AT:
// return ui.KeyNone
// case sdl.K_KP_EXCLAM:
// return ui.KeyNone
// case sdl.K_KP_MEMSTORE:
// return ui.KeyNone
// case sdl.K_KP_MEMRECALL:
// return ui.KeyNone
// case sdl.K_KP_MEMCLEAR:
// return ui.KeyNone
// case sdl.K_KP_MEMADD:
// return ui.KeyNone
// case sdl.K_KP_MEMSUBTRACT:
// return ui.KeyNone
// case sdl.K_KP_MEMMULTIPLY:
// return ui.KeyNone
// case sdl.K_KP_MEMDIVIDE:
// return ui.KeyNone
// case sdl.K_KP_PLUSMINUS:
// return ui.KeyNone
// case sdl.K_KP_CLEAR:
// return ui.KeyNone
// case sdl.K_KP_CLEARENTRY:
// return ui.KeyNone
// case sdl.K_KP_BINARY:
// return ui.KeyNone
// case sdl.K_KP_OCTAL:
// return ui.KeyNone
// case sdl.K_KP_DECIMAL:
// return ui.KeyNone
// case sdl.K_KP_HEXADECIMAL:
// return ui.KeyNone
case sdl.K_LCTRL:
return ui.KeyLeftControl
case sdl.K_LSHIFT:
return ui.KeyLeftShift
case sdl.K_LALT:
return ui.KeyAlt
case sdl.K_LGUI:
return ui.KeyLeftWin
case sdl.K_RCTRL:
return ui.KeyRightControl
case sdl.K_RSHIFT:
return ui.KeyRightShift
case sdl.K_RALT:
return ui.KeyAltGr
case sdl.K_RGUI:
return ui.KeyRightWin
// case sdl.K_MODE:
// return ui.KeyNone
// case sdl.K_AUDIONEXT:
// return ui.KeyNone
// case sdl.K_AUDIOPREV:
// return ui.KeyNone
// case sdl.K_AUDIOSTOP:
// return ui.KeyNone
// case sdl.K_AUDIOPLAY:
// return ui.KeyNone
// case sdl.K_AUDIOMUTE:
// return ui.KeyNone
// case sdl.K_MEDIASELECT:
// return ui.KeyNone
// case sdl.K_WWW:
// return ui.KeyNone
// case sdl.K_MAIL:
// return ui.KeyNone
// case sdl.K_CALCULATOR:
// return ui.KeyNone
// case sdl.K_COMPUTER:
// return ui.KeyNone
// case sdl.K_AC_SEARCH:
// return ui.KeyNone
// case sdl.K_AC_HOME:
// return ui.KeyNone
// case sdl.K_AC_BACK:
// return ui.KeyNone
// case sdl.K_AC_FORWARD:
// return ui.KeyNone
// case sdl.K_AC_STOP:
// return ui.KeyNone
// case sdl.K_AC_REFRESH:
// return ui.KeyNone
// case sdl.K_AC_BOOKMARKS:
// return ui.KeyNone
// case sdl.K_BRIGHTNESSDOWN:
// return ui.KeyNone
// case sdl.K_BRIGHTNESSUP:
// return ui.KeyNone
// case sdl.K_DISPLAYSWITCH:
// return ui.KeyNone
// case sdl.K_KBDILLUMTOGGLE:
// return ui.KeyNone
// case sdl.K_KBDILLUMDOWN:
// return ui.KeyNone
// case sdl.K_KBDILLUMUP:
// return ui.KeyNone
// case sdl.K_EJECT:
// return ui.KeyNone
// case sdl.K_SLEEP:
// return ui.KeyNone
default:
return ui.KeyNone
}
}
func keyModifiers(mod uint16) ui.KeyModifier {
var modifiers ui.KeyModifier
if mod&uint16(sdl.KMOD_ALT|sdl.KMOD_LALT) != 0 {
modifiers |= ui.KeyModifierAlt
}
if mod&uint16(sdl.KMOD_CTRL|sdl.KMOD_LCTRL) != 0 {
modifiers |= ui.KeyModifierControl
}
if mod&uint16(sdl.KMOD_SHIFT|sdl.KMOD_LSHIFT) != 0 {
modifiers |= ui.KeyModifierShift
}
if mod&uint16(sdl.KMOD_GUI|sdl.KMOD_LGUI) != 0 {
modifiers |= ui.KeyModifierOSCommand
}
return modifiers
}
func mouseButton(b uint8) ui.MouseButton {
switch b {
case sdl.BUTTON_LEFT:
return ui.MouseButtonLeft
case sdl.BUTTON_MIDDLE:
return ui.MouseButtonMiddle
case sdl.BUTTON_RIGHT:
return ui.MouseButtonRight
}
return ui.MouseButtonLeft
}
func mouseEvent(e sdl.Event, x, y int32) ui.MouseEvent {
return ui.MouseEvent{
X: float32(x),
Y: float32(y),
EventBase: eventBase(e),
}
}

13
sdlui/events_test.go Normal file
View File

@ -0,0 +1,13 @@
package sdlui
import (
"testing"
"github.com/stretchr/testify/assert"
"opslag.de/schobers/zntg/ui"
)
func TestKeyModifiers(t *testing.T) {
var mod uint16 = 4097
assert.Equal(t, ui.KeyModifier(ui.KeyModifierShift), keyModifiers(mod))
}

24
sdlui/font.go Normal file
View File

@ -0,0 +1,24 @@
package sdlui
import (
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/geom"
)
type Font struct {
*ttf.Font
}
func (f *Font) Height() float32 {
return float32(f.Font.Height())
}
func (f *Font) Measure(t string) geom.RectangleF32 {
w, h, _ := f.SizeUTF8(t)
return geom.RectF32(0, 0, float32(w), float32(h))
}
func (f *Font) WidthOf(t string) float32 {
w, _, _ := f.SizeUTF8(t)
return float32(w)
}

16
sdlui/image.go Normal file
View File

@ -0,0 +1,16 @@
package sdlui
import (
"image"
"image/draw"
)
func RGBAImage(m image.Image) *image.RGBA {
rgba, ok := m.(*image.RGBA)
if ok {
return rgba
}
rgba = image.NewRGBA(m.Bounds())
draw.Draw(rgba, rgba.Bounds(), m, image.ZP, draw.Over)
return rgba
}

38
sdlui/rectangle.go Normal file
View File

@ -0,0 +1,38 @@
package sdlui
import (
"github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/geom"
)
func Rect(x, y, w, h int32) sdl.Rect {
return sdl.Rect{X: x, Y: y, W: w, H: h}
}
func RectAbs(x1, y1, x2, y2 int32) sdl.Rect {
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
return Rect(x1, y1, x2-x1, y2-y1)
}
func RectAbsPtr(x1, y1, x2, y2 int32) *sdl.Rect {
rect := RectAbs(x1, y1, x2, y2)
return &rect
}
func RectPtr(x, y, w, h int32) *sdl.Rect {
return &sdl.Rect{X: x, Y: y, W: w, H: h}
}
func SDLRectangle(r geom.RectangleF32) sdl.Rect {
return sdl.Rect{X: int32(r.Min.X), Y: int32(r.Min.Y), W: int32(r.Dx()), H: int32(r.Dy())}
}
func SDLRectanglePtr(r geom.RectangleF32) *sdl.Rect {
rect := SDLRectangle(r)
return &rect
}

421
sdlui/renderer.go Normal file
View File

@ -0,0 +1,421 @@
package sdlui
import (
"errors"
"image"
"image/color"
_ "image/jpeg" // add JPEG for CreateSurfacePath
_ "image/png" // add PNG for CreateSurfacePath
"math"
"unsafe"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
var errNotImplemented = errors.New(`not implemented`)
type Renderer struct {
window *sdl.Window
renderer *sdl.Renderer
refresh uint32
fonts map[string]*Font
cursor ui.MouseCursor
cursors map[sdl.SystemCursor]*sdl.Cursor
}
var _ ui.Renderer = &Renderer{}
var _ ui.Texture = &Renderer{}
type NewRendererOptions struct {
Location sdl.Point
Resizable bool
VSync bool
}
func NewRenderer(title string, width, height int32, opts NewRendererOptions) (*Renderer, error) {
var clean zntg.Actions
defer func() { clean.Do() }()
if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil {
return nil, err
}
clean = clean.Add(sdl.Quit)
if err := ttf.Init(); err != nil {
return nil, err
}
clean = clean.Add(ttf.Quit)
if opts.VSync {
sdl.SetHint(sdl.HINT_RENDER_VSYNC, "1")
}
sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1")
windowFlags := uint32(sdl.WINDOW_SHOWN)
if opts.Resizable {
windowFlags |= sdl.WINDOW_RESIZABLE
}
window, err := sdl.CreateWindow(title, opts.Location.X, opts.Location.Y, width, height, windowFlags)
if err != nil {
return nil, err
}
clean = clean.AddErr(window.Destroy)
rendererFlags := uint32(sdl.RENDERER_ACCELERATED)
if opts.VSync {
rendererFlags |= sdl.RENDERER_PRESENTVSYNC
}
renderer, err := sdl.CreateRenderer(window, -1, rendererFlags)
if err != nil {
return nil, err
}
renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
clean = clean.AddErr(renderer.Destroy)
refresh := sdl.RegisterEvents(1)
if refresh == math.MaxUint32 {
return nil, errors.New("couldn't register user event")
}
clean = nil
return &Renderer{
window: window,
renderer: renderer,
refresh: refresh,
fonts: map[string]*Font{},
cursors: map[sdl.SystemCursor]*sdl.Cursor{},
}, nil
}
// Events
func (r *Renderer) WindowBounds() geom.RectangleF32 {
x, y := r.window.GetPosition()
w, h := r.window.GetSize()
return geom.RectF32(float32(x), float32(y), float32(x+w), float32(y+h))
}
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
r.renderer.Present()
waitOrPoll := func() sdl.Event {
if wait {
return sdl.WaitEvent()
}
return sdl.PollEvent()
}
cursor := r.cursor
for event := waitOrPoll(); event != nil; event = sdl.PollEvent() {
r.cursor = ui.MouseCursorDefault
var unhandled bool
// TODO: simulate ui.MouseEnter & ui.MouseLeave?
switch e := event.(type) {
case *sdl.WindowEvent:
switch e.Event {
case sdl.WINDOWEVENT_CLOSE:
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
case sdl.WINDOWEVENT_RESIZED:
t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: r.WindowBounds()})
}
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: key(e.Keysym.Sym), Modifiers: keyModifiers(e.Keysym.Mod)})
} else if e.Type == sdl.KEYUP {
t.Handle(&ui.KeyDownEvent{EventBase: eventBase(e), Key: 0, Modifiers: 0})
} else {
unhandled = true
}
case *sdl.TextInputEvent:
if e.Type == sdl.TEXTINPUT {
text := e.GetText()
for _, character := range text {
t.Handle(&ui.TextInputEvent{
EventBase: eventBase(e),
Character: character,
})
}
} else {
unhandled = true
}
case *sdl.MouseButtonEvent:
if e.Type == sdl.MOUSEBUTTONDOWN {
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e, e.X, e.Y), Button: mouseButton(e.Button)})
} else {
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e, e.X, e.Y), Button: mouseButton(e.Button)})
}
case *sdl.MouseMotionEvent:
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}, MouseWheel: 0})
case *sdl.UserEvent:
if r.refresh == e.Type {
t.Handle(&ui.RefreshEvent{EventBase: eventBase(e)})
} else {
unhandled = true
}
default:
unhandled = true // not handled by EventTarget.Handle
}
if unhandled {
r.cursor = cursor
}
}
if r.cursor != cursor {
switch r.cursor {
case ui.MouseCursorDefault:
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_ARROW))
case ui.MouseCursorNotAllowed:
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_NO))
case ui.MouseCursorPointer:
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_HAND))
case ui.MouseCursorText:
sdl.SetCursor(r.SystemCursor(sdl.SYSTEM_CURSOR_IBEAM))
}
}
}
func (r *Renderer) Refresh() {
windowID, _ := r.window.GetID()
e := &sdl.UserEvent{
Type: r.refresh,
WindowID: windowID,
}
sdl.PushEvent(e)
}
// Lifetime
func (r *Renderer) Destroy() error {
for _, f := range r.fonts {
f.Close()
}
r.renderer.Destroy()
r.window.Destroy()
ttf.Quit()
sdl.Quit()
return nil
}
// Drawing
func (r *Renderer) Clear(c color.Color) {
if c == color.Transparent {
return
}
r.SetDrawColorGo(c)
r.renderer.Clear()
}
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
m, err := source.CreateImage()
if err != nil {
return nil, err
}
rgba := RGBAImage(m)
width := int32(rgba.Bounds().Dx())
height := int32(rgba.Bounds().Dy())
surface, err := sdl.CreateRGBSurfaceWithFormatFrom(
unsafe.Pointer(&rgba.Pix[0]),
width, height, 32, int32(rgba.Stride), sdl.PIXELFORMAT_ABGR8888)
if err != nil {
return nil, err
}
defer surface.Free()
texture, err := r.renderer.CreateTextureFromSurface(surface)
if err != nil {
return nil, err
}
if keepSource {
return &TextureImageSource{&Texture{texture}, source}, nil
}
return &Texture{texture}, nil
}
func (r *Renderer) CreateTexture(source ui.ImageSource) (ui.Texture, error) {
return r.createTexture(source, true)
}
func (r *Renderer) CreateTextureGo(m image.Image, source bool) (ui.Texture, error) {
return r.createTexture(ui.ImageGoSource{Image: m}, source)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
return r.createTexture(ui.ImageFileSource(path), source)
}
func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) {
format, err := r.window.GetPixelFormat()
if err != nil {
return nil, err
}
texture, err := r.renderer.CreateTexture(format, sdl.TEXTUREACCESS_TARGET, int32(w), int32(h))
if err != nil {
return nil, err
}
texture.SetBlendMode(sdl.BLENDMODE_BLEND)
return &Texture{texture}, nil
}
func (r *Renderer) DefaultTarget() ui.Texture { return r }
func (r *Renderer) DrawTexture(t ui.Texture, p geom.PointF32) {
r.DrawTextureOptions(t, p, ui.DrawOptions{})
}
func (r *Renderer) DrawTextureOptions(t ui.Texture, p geom.PointF32, opts ui.DrawOptions) {
texture, ok := t.(sdlTexture)
if !ok {
return
}
if opts.Tint != nil {
texture.SetColor(opts.Tint)
}
width, height, err := texture.Size()
if err != nil {
return
}
dst := RectPtr(int32(p.X), int32(p.Y), width, height)
if opts.Scale != nil {
dst.W = int32(float32(width) * opts.Scale.X)
dst.H = int32(float32(height) * opts.Scale.Y)
}
r.renderer.Copy(texture.Native(), RectPtr(0, 0, width, height), dst)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
r.SetDrawColorGo(c)
r.renderer.FillRect(SDLRectanglePtr(rect))
}
func (r *Renderer) Font(name string) ui.Font {
return r.fonts[name]
}
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
r.SetDrawColorGo(c)
if rect.Dx() == 0 { // SDL doesn't draw a 1 px wide line when Dx() == 0 && thickness == 1
offset := int32(rect.Min.X - .5*thickness)
for thick := int32(thickness); thick > 0; thick-- {
r.renderer.DrawLine(offset, int32(rect.Min.Y), offset, int32(rect.Max.Y))
offset++
}
} else if rect.Dy() == 0 {
offset := int32(rect.Min.Y - .5*thickness)
for thick := int32(thickness); thick > 0; thick-- {
r.renderer.DrawLine(int32(rect.Min.X), offset, int32(rect.Max.X), offset)
offset++
}
} else {
for thick := int32(thickness); thick > 0; thick-- {
r.renderer.DrawRect(SDLRectanglePtr(rect))
rect = rect.Inset(1)
}
}
}
func (r *Renderer) RegisterFont(name, path string, size int) error {
font, err := ttf.OpenFont(path, size)
if err != nil {
return err
}
r.fonts[name] = &Font{font}
return nil
}
func (r *Renderer) RenderTo(t ui.Texture) {
texture, ok := t.(sdlTexture)
if ok {
err := r.renderer.SetRenderTarget(texture.Native())
if err != nil {
panic(err)
}
} else {
r.RenderToDisplay()
}
}
func (r *Renderer) RenderToDisplay() {
r.renderer.SetRenderTarget(nil)
}
func (r *Renderer) SetDrawColor(c sdl.Color) {
r.renderer.SetDrawColor(c.R, c.G, c.B, c.A)
}
func (r *Renderer) SetDrawColorGo(c color.Color) {
r.SetDrawColor(ColorSDL(c))
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { r.cursor = c }
func (r *Renderer) Size() geom.PointF32 {
w, h, err := r.renderer.GetOutputSize()
if err != nil {
return geom.PtF32(geom.NaN32(), geom.NaN32())
}
return geom.PtF32(float32(w), float32(h))
}
func (r *Renderer) SystemCursor(id sdl.SystemCursor) *sdl.Cursor {
if cursor, ok := r.cursors[id]; ok {
return cursor
}
cursor := sdl.CreateSystemCursor(id)
r.cursors[id] = cursor
return cursor
}
func (r *Renderer) Target() ui.Texture {
target := r.renderer.GetRenderTarget()
if target == nil {
return r
}
return &Texture{target}
}
func (r *Renderer) Text(p geom.PointF32, font string, color color.Color, text string) {
f := r.Font(font).(*Font)
surface, err := f.RenderUTF8Blended(text, ColorSDL(color))
if err != nil {
return
}
defer surface.Free()
texture, err := r.renderer.CreateTextureFromSurface(surface)
if err != nil {
return
}
defer texture.Destroy()
r.DrawTexture(&Texture{texture}, p)
}
func (r *Renderer) TextAlign(p geom.PointF32, font string, color color.Color, text string, align ui.HorizontalAlignment) {
switch align {
case ui.AlignLeft:
r.Text(p, font, color, text)
case ui.AlignCenter:
width := r.Font(font).(*Font).WidthOf(text)
r.Text(p.Add2D(-.5*width, 0), font, color, text)
case ui.AlignRight:
width := r.Font(font).(*Font).WidthOf(text)
r.Text(p.Add2D(-width, 0), font, color, text)
}
}
// Texture
func (r *Renderer) Image() image.Image { return nil }
func (r *Renderer) Height() float32 { return r.Size().Y }
func (r *Renderer) Width() float32 { return r.Size().X }

24
sdlui/rendererfactory.go Normal file
View File

@ -0,0 +1,24 @@
package sdlui
import (
"github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/zntg/ui"
)
func init() {
ui.SetRendererFactory(&rendererFactory{})
}
type rendererFactory struct{}
func (f rendererFactory) New(title string, width, height int) (ui.Renderer, error) {
return f.NewOptions(title, width, height, ui.NewRendererOptions{Resizable: true})
}
func (f rendererFactory) NewOptions(title string, width, height int, opts ui.NewRendererOptions) (ui.Renderer, error) {
return NewRenderer(title, int32(width), int32(height), NewRendererOptions{
Location: sdl.Point{X: sdl.WINDOWPOS_UNDEFINED, Y: sdl.WINDOWPOS_UNDEFINED},
Resizable: opts.Resizable,
VSync: opts.VSync,
})
}

65
sdlui/texture.go Normal file
View File

@ -0,0 +1,65 @@
package sdlui
import (
"image"
"image/color"
"github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
type sdlTexture interface {
Native() *sdl.Texture
SetColor(color.Color)
Size() (int32, int32, error)
}
type Texture struct {
*sdl.Texture
}
var _ ui.Texture = &Texture{}
func (t *Texture) Height() float32 {
_, _, _, height, err := t.Texture.Query()
if err != nil {
return geom.NaN32()
}
return float32(height)
}
func (t *Texture) Native() *sdl.Texture { return t.Texture }
func (t *Texture) SetColor(c color.Color) {
color := ColorSDL(c)
t.SetColorMod(color.R, color.G, color.B)
}
func (t *Texture) Size() (int32, int32, error) {
_, _, width, height, err := t.Texture.Query()
if err != nil {
return 0, 0, err
}
return width, height, err
}
func (t *Texture) Width() float32 {
_, _, width, _, err := t.Texture.Query()
if err != nil {
return geom.NaN32()
}
return float32(width)
}
var _ ui.ImageSource = &TextureImageSource{}
type TextureImageSource struct {
*Texture
source ui.ImageSource
}
func (s TextureImageSource) CreateImage() (image.Image, error) {
return s.source.CreateImage()
}

View File

@ -18,7 +18,7 @@ func (b *Buffer) Update(ctx Context, size geom.PointF32) error {
b.texture = nil b.texture = nil
b.size = geom.ZeroPtF32 b.size = geom.ZeroPtF32
} }
texture, err := ctx.Renderer().CreateTextureSize(size.X, size.Y) texture, err := ctx.Renderer().CreateTextureTarget(size.X, size.Y)
if err != nil { if err != nil {
return err return err
} }

View File

@ -11,7 +11,6 @@ type Button struct {
HoverColor color.Color HoverColor color.Color
Icon Texture Icon Texture
IconScale float32
Text string Text string
Type ButtonType Type ButtonType
} }
@ -43,7 +42,7 @@ func (b *Button) desiredSize(ctx Context) geom.PointF32 {
w += pad + font.WidthOf(b.Text) w += pad + font.WidthOf(b.Text)
} }
if b.Icon != nil && b.Icon.Height() > 0 { if b.Icon != nil && b.Icon.Height() > 0 {
iconW := b.scale(b.Icon.Width() * h / b.Icon.Height()) iconW := b.Icon.Width() * h / b.Icon.Height()
w += pad + iconW w += pad + iconW
} }
if w == 0 { if w == 0 {
@ -91,13 +90,6 @@ func (b *Button) fillColor(p *Palette) color.Color {
return nil return nil
} }
func (b *Button) scale(f float32) float32 {
if b.IconScale == 0 {
return f
}
return b.IconScale * f
}
func (b *Button) textColor(p *Palette) color.Color { func (b *Button) textColor(p *Palette) color.Color {
if b.Font.Color != nil { if b.Font.Color != nil {
return b.Font.Color return b.Font.Color
@ -131,18 +123,21 @@ func (b *Button) Render(ctx Context) {
var pad = style.Dimensions.TextPadding var pad = style.Dimensions.TextPadding
bounds = bounds.Inset(pad) bounds = bounds.Inset(pad)
boundsH := bounds.Dy()
pos := bounds.Min pos := bounds.Min
if b.Icon != nil && b.Icon.Height() > 0 { if b.Icon != nil && b.Icon.Height() > 0 {
icon, _ := ctx.Textures().ScaledHeight(b.Icon, b.scale(bounds.Dy())) icon, _ := ctx.Textures().ScaledHeight(b.Icon, boundsH) // try to pre-scale icon
if icon != nil { if icon == nil { // let the renderer scale
ctx.Renderer().DrawTextureOptions(icon, geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-icon.Height())), DrawOptions{Tint: textColor}) icon = b.Icon
pos.X += icon.Width() + pad
} }
scale, iconWidth := ScaleToHeight(SizeOfTexture(icon), boundsH)
ctx.Renderer().DrawTextureOptions(icon, geom.PtF32(pos.X, pos.Y), DrawOptions{Tint: textColor, Scale: scale})
pos.X += iconWidth + pad
} }
if len(b.Text) != 0 { if len(b.Text) != 0 {
var fontName = b.FontName(ctx) var fontName = b.FontName(ctx)
var font = ctx.Renderer().Font(fontName) var font = ctx.Renderer().Font(fontName)
ctx.Renderer().Text(geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-font.Height())), fontName, textColor, b.Text) ctx.Renderer().Text(geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fontName, textColor, b.Text)
} }
if b.Type == ButtonTypeOutlined { if b.Type == ButtonTypeOutlined {

View File

@ -30,8 +30,9 @@ func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
if len(c.Text) != 0 { if len(c.Text) != 0 {
w += pad + font.WidthOf(c.Text) w += pad + font.WidthOf(c.Text)
} }
icon, _ := ctx.Textures().ScaledHeight(c.getOrCreateNormalIcon(ctx), h) icon := c.getOrCreateNormalIcon(ctx)
w += pad + icon.Width() _, iconWidth := ScaleToHeight(SizeOfTexture(icon), h)
w += pad + iconWidth
return geom.PtF32(w+pad, pad+h+pad) return geom.PtF32(w+pad, pad+h+pad)
} }
@ -48,47 +49,32 @@ func (c *Checkbox) getOrCreateNormalIcon(ctx Context) Texture {
return GetOrCreateIcon(ctx, "ui-default-checkbox", c.normalIcon) return GetOrCreateIcon(ctx, "ui-default-checkbox", c.normalIcon)
} }
func (c *Checkbox) iconBorder() geom.PolygonF32 { var checkBoxIconBorder = geom.PolF32(
return geom.PolF32(
geom.PtF32(48, 80), geom.PtF32(48, 80),
geom.PtF32(400, 80), geom.PtF32(400, 80),
geom.PtF32(400, 432), geom.PtF32(400, 432),
geom.PtF32(48, 432), geom.PtF32(48, 432),
) )
}
func (c *Checkbox) checkMark() geom.PointsF32 { var checkBoxCheckMark = geom.PointsF32{
return geom.PointsF32{
geom.PtF32(96, 256), geom.PtF32(96, 256),
geom.PtF32(180, 340), geom.PtF32(180, 340),
geom.PtF32(340, 150), geom.PtF32(340, 150),
}
} }
func (c *Checkbox) hoverIcon() ImagePixelTestFn { func (c *Checkbox) hoverIcon(pt geom.PointF32) bool {
border := c.iconBorder() return (pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)) || pt.DistanceToLines(checkBoxCheckMark) < 24
check := c.checkMark()
return func(pt geom.PointF32) bool {
return (pt.DistanceToPolygon(border) < 48 && !pt.InPolygon(border)) || pt.DistanceToLines(check) < 24
}
} }
func (c *Checkbox) normalIcon() ImagePixelTestFn { func (c *Checkbox) normalIcon(pt geom.PointF32) bool {
border := c.iconBorder() return pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)
return func(pt geom.PointF32) bool {
return pt.DistanceToPolygon(border) < 48 && !pt.InPolygon(border)
}
} }
func (c *Checkbox) selectedIcon() ImagePixelTestFn { func (c *Checkbox) selectedIcon(pt geom.PointF32) bool {
border := c.iconBorder() if pt.DistanceToPolygon(checkBoxIconBorder) < 48 || pt.InPolygon(checkBoxIconBorder) {
check := c.checkMark() return pt.DistanceToLines(checkBoxCheckMark) > 24
return func(pt geom.PointF32) bool {
if pt.DistanceToPolygon(border) < 48 || pt.InPolygon(border) {
return pt.DistanceToLines(check) > 24
} }
return false return false
}
} }
func (c *Checkbox) DesiredSize(ctx Context) geom.PointF32 { return c.desiredSize(ctx) } func (c *Checkbox) DesiredSize(ctx Context) geom.PointF32 { return c.desiredSize(ctx) }
@ -124,19 +110,25 @@ func (c *Checkbox) Render(ctx Context) {
var pad = style.Dimensions.TextPadding var pad = style.Dimensions.TextPadding
bounds = bounds.Inset(pad) bounds = bounds.Inset(pad)
boundsH := bounds.Dy()
pos := bounds.Min pos := bounds.Min
icon, _ := ctx.Textures().ScaledHeight(c.icon(ctx), bounds.Dy()) icon := c.icon(ctx)
if icon != nil { if icon != nil {
iconColor := fore iconColor := fore
if c.Selected && c.Font.Color == nil { if c.Selected && c.Font.Color == nil {
iconColor = palette.Primary iconColor = palette.Primary
} }
ctx.Renderer().DrawTextureOptions(icon, geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-icon.Height())), DrawOptions{Tint: iconColor}) scaledIcon, _ := ctx.Textures().ScaledHeight(icon, boundsH) // try to pre-scale icon
pos.X += icon.Width() + pad if scaledIcon == nil { // let the renderer scale
scaledIcon = icon
}
scale, iconWidth := ScaleToHeight(SizeOfTexture(scaledIcon), boundsH)
ctx.Renderer().DrawTextureOptions(scaledIcon, geom.PtF32(pos.X, pos.Y), DrawOptions{Tint: iconColor, Scale: scale})
pos.X += iconWidth + pad
} }
if len(c.Text) != 0 { if len(c.Text) != 0 {
var fontName = c.FontName(ctx) var fontName = c.FontName(ctx)
var font = ctx.Renderer().Font(fontName) var font = ctx.Renderer().Font(fontName)
ctx.Renderer().Text(geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-font.Height())), fontName, fore, c.Text) ctx.Renderer().Text(geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fontName, fore, c.Text)
} }
} }

View File

@ -45,6 +45,7 @@ func (c *context) Renderer() Renderer { return c.r }
func (c *context) Style() *Style { return c.style } func (c *context) Style() *Style { return c.style }
// Handle implement EventTarget // Handle implement EventTarget
func (c *context) Handle(e Event) { func (c *context) Handle(e Event) {
switch e.(type) { switch e.(type) {
case *DisplayCloseEvent: case *DisplayCloseEvent:

View File

@ -10,3 +10,11 @@ type DrawOptions struct {
Tint color.Color Tint color.Color
Scale *geom.PointF32 Scale *geom.PointF32
} }
func ScaleToHeight(size geom.PointF32, height float32) (*geom.PointF32, float32) {
if size.Y == height {
return nil, size.X
}
factor := height / size.Y
return &geom.PointF32{X: factor, Y: factor}, factor * size.X
}

View File

@ -30,13 +30,19 @@ const (
KeyModifierShift = 1 << iota KeyModifierShift = 1 << iota
KeyModifierControl KeyModifierControl
KeyModifierAlt KeyModifierAlt
KeyModifierOSCommand
) )
type KeyPressEvent struct { type KeyDownEvent struct {
EventBase
Key Key
Modifiers KeyModifier
}
type KeyUpEvent struct {
EventBase EventBase
Key Key Key Key
Modifiers KeyModifier Modifiers KeyModifier
Character rune
} }
type MouseButton int type MouseButton int
@ -82,3 +88,8 @@ type MouseMoveEvent struct {
type RefreshEvent struct { type RefreshEvent struct {
EventBase EventBase
} }
type TextInputEvent struct {
EventBase
Character rune
}

View File

@ -4,7 +4,8 @@ import (
"image/color" "image/color"
"log" "log"
_ "opslag.de/schobers/zntg/allg5ui" // import the renderer for the UI _ "opslag.de/schobers/zntg/sdlui" // import the renderer for the UI
// _ "opslag.de/schobers/zntg/allg5ui" // import the renderer for the UI
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
@ -73,7 +74,7 @@ func run() error {
if err != nil { if err != nil {
return err return err
} }
plus, err := render.CreateTexturePath("../resources/images/plus.png") plus, err := render.CreateTexturePath("../resources/images/plus.png", true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -7,11 +7,30 @@ import (
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
) )
type ImagePixelTestFn func(geom.PointF32) bool type AlphaPixelImageSource struct {
ImageAlphaPixelTestFn
Size geom.Point
}
func (s *AlphaPixelImageSource) CreateImage() (image.Image, error) {
return DrawImageAlpha(s.Size, s.ImageAlphaPixelTestFn), nil
}
type ImageAlphaPixelTestFn func(geom.PointF32) uint8 type ImageAlphaPixelTestFn func(geom.PointF32) uint8
func createTexture(ctx Context, image image.Image) Texture { func (f ImageAlphaPixelTestFn) CreateImageSource(size geom.Point) ImageSource {
texture, err := ctx.Renderer().CreateTexture(image) return &AlphaPixelImageSource{f, size}
}
type ImagePixelTestFn func(geom.PointF32) bool
func (f ImagePixelTestFn) CreateImageSource(size geom.Point) ImageSource {
return &PixelImageSource{f, size}
}
func createTexture(ctx Context, source ImageSource) Texture {
texture, err := ctx.Renderer().CreateTexture(source)
if err != nil { if err != nil {
return nil return nil
} }
@ -19,18 +38,15 @@ func createTexture(ctx Context, image image.Image) Texture {
} }
func CreateIcon(ctx Context, test ImagePixelTestFn) Texture { func CreateIcon(ctx Context, test ImagePixelTestFn) Texture {
icon := DrawIcon(test) return createTexture(ctx, test.CreateImageSource(IconSize()))
return createTexture(ctx, icon)
} }
func CreateTexture(ctx Context, size geom.Point, test ImagePixelTestFn) Texture { func CreateTexture(ctx Context, size geom.Point, test ImagePixelTestFn) Texture {
image := DrawImage(size, test) return createTexture(ctx, test.CreateImageSource(size))
return createTexture(ctx, image)
} }
func CreateTextureAlpha(ctx Context, size geom.Point, test ImageAlphaPixelTestFn) Texture { func CreateTextureAlpha(ctx Context, size geom.Point, test ImageAlphaPixelTestFn) Texture {
image := DrawImageAlpha(size, test) return createTexture(ctx, test.CreateImageSource(size))
return createTexture(ctx, image)
} }
func DrawIcon(test ImagePixelTestFn) image.Image { func DrawIcon(test ImagePixelTestFn) image.Image {
@ -66,20 +82,28 @@ func DrawImageAlpha(size geom.Point, test ImageAlphaPixelTestFn) image.Image {
return icon return icon
} }
func GetOrCreateIcon(ctx Context, name string, testFactory func() ImagePixelTestFn) Texture { func GetOrCreateIcon(ctx Context, name string, test ImagePixelTestFn) Texture {
texture := ctx.Textures().Texture(name) texture := ctx.Textures().Texture(name)
if texture != nil { if texture != nil {
return texture return texture
} }
test := testFactory() texture, err := ctx.Textures().CreateTexture(name, test.CreateImageSource(IconSize()))
texture = CreateIcon(ctx, test) if err != nil {
if texture == nil {
return nil return nil
} }
ctx.Textures().AddTexture(name, texture)
return texture return texture
} }
func IconSize() geom.Point { func IconSize() geom.Point {
return geom.Pt(448, 512) return geom.Pt(448, 512)
} }
type PixelImageSource struct {
ImagePixelTestFn
Size geom.Point
}
func (s *PixelImageSource) CreateImage() (image.Image, error) {
return DrawImage(s.Size, s.ImagePixelTestFn), nil
}

37
ui/imagesource.go Normal file
View File

@ -0,0 +1,37 @@
package ui
import (
"image"
"os"
)
type ImageSource interface {
CreateImage() (image.Image, error)
}
type ImageFileSource string
var _ ImageSource = ImageFileSource("")
func (s ImageFileSource) CreateImage() (image.Image, error) {
f, err := os.Open(string(s))
if err != nil {
return nil, err
}
defer f.Close()
m, _, err := image.Decode(f)
if err != nil {
return nil, err
}
return m, nil
}
type ImageGoSource struct {
image.Image
}
var _ ImageSource = ImageGoSource{}
func (s ImageGoSource) CreateImage() (image.Image, error) {
return s.Image, nil
}

View File

@ -48,6 +48,7 @@ const (
KeyDPadUp KeyDPadUp
KeyE KeyE
KeyEnd KeyEnd
KeyEnter
KeyEquals KeyEquals
KeyEscape KeyEscape
KeyF KeyF
@ -135,3 +136,22 @@ const (
KeyY KeyY
KeyZ KeyZ
) )
type KeyState map[Key]bool
func (s KeyState) Modifiers() KeyModifier {
var mods KeyModifier
if s[KeyAlt] || s[KeyAltGr] {
mods |= KeyModifierAlt
}
if s[KeyLeftControl] || s[KeyRightControl] {
mods |= KeyModifierControl
}
if s[KeyLeftShift] || s[KeyRightShift] {
mods |= KeyModifierShift
}
if s[KeyLeftWin] || s[KeyRightWin] || s[KeyCommand] {
mods |= KeyModifierOSCommand
}
return mods
}

View File

@ -17,9 +17,10 @@ type Renderer interface {
// Drawing // Drawing
Clear(c color.Color) Clear(c color.Color)
CreateTexture(m image.Image) (Texture, error) CreateTexture(m ImageSource) (Texture, error)
CreateTexturePath(path string) (Texture, error) CreateTextureGo(m image.Image, source bool) (Texture, error)
CreateTextureSize(w, h float32) (Texture, error) CreateTexturePath(path string, source bool) (Texture, error)
CreateTextureTarget(w, h float32) (Texture, error)
DefaultTarget() Texture DefaultTarget() Texture
DrawTexture(t Texture, p geom.PointF32) DrawTexture(t Texture, p geom.PointF32)
DrawTextureOptions(t Texture, p geom.PointF32, opts DrawOptions) DrawTextureOptions(t Texture, p geom.PointF32, opts DrawOptions)

View File

@ -28,4 +28,5 @@ func SetRendererFactory(factory RendererFactory) {
type NewRendererOptions struct { type NewRendererOptions struct {
Location *geom.PointF32 Location *geom.PointF32
Resizable bool Resizable bool
VSync bool
} }

View File

@ -2,7 +2,6 @@ package ui
import ( import (
"fmt" "fmt"
"image/color"
"strings" "strings"
"time" "time"
"unicode" "unicode"
@ -144,7 +143,7 @@ func (b *TextBox) Handle(ctx Context, e Event) {
b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent) b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
b.Selection.End = b.Selection.Caret b.Selection.End = b.Selection.Caret
} }
case *KeyPressEvent: case *KeyDownEvent:
if !b.Focus { if !b.Focus {
break break
} }
@ -217,15 +216,13 @@ func (b *TextBox) Handle(ctx Context, e Event) {
case KeyX: case KeyX:
DefaultClipboard.WriteText(b.cut()) DefaultClipboard.WriteText(b.cut())
} }
default: }
if e.Modifiers == KeyModifierNone || e.Modifiers&KeyModifierShift == KeyModifierShift { case *TextInputEvent:
caret := b.Selection.Caret caret := b.Selection.Caret
b.Text = fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:]) b.Text = fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:])
b.Selection.Caret = caret + 1 b.Selection.Caret = caret + 1
b.Selection.SetSelectionToCaret() b.Selection.SetSelectionToCaret()
} }
}
}
if b.over { if b.over {
ctx.Renderer().SetMouseCursor(MouseCursorText) ctx.Renderer().SetMouseCursor(MouseCursorText)
} }
@ -242,7 +239,11 @@ func (b *TextBox) Render(ctx Context) {
var caretWidth float32 = 1 var caretWidth float32 = 1
b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) { b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) {
var renderer = ctx.Renderer() var renderer = ctx.Renderer()
renderer.Clear(color.Transparent) back := b.Background
if back == nil {
back = ctx.Style().Palette.Background
}
renderer.Clear(back)
if b.Selection.Start != b.Selection.End { if b.Selection.Start != b.Selection.End {
left, right := renderer.Font(f).WidthOf(b.Text[:b.Selection.Start]), renderer.Font(f).WidthOf(b.Text[:b.Selection.End]) left, right := renderer.Font(f).WidthOf(b.Text[:b.Selection.Start]), renderer.Font(f).WidthOf(b.Text[:b.Selection.End])
renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight) renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight)

View File

@ -1,10 +1,11 @@
package ui package ui
import "image" import "opslag.de/schobers/geom"
type Texture interface { type Texture interface {
Destroy() Destroy() error
Height() float32 Height() float32
Texture() image.Image
Width() float32 Width() float32
} }
func SizeOfTexture(t Texture) geom.PointF32 { return geom.PtF32(t.Width(), t.Height()) }

View File

@ -7,15 +7,21 @@ import (
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
) )
type CreateImageFn func() (image.Image, error)
func ScaleTexture(render Renderer, texture Texture, scale float32) Texture { func ScaleTexture(render Renderer, texture Texture, scale float32) Texture {
w := uint(texture.Width() * scale) w := uint(texture.Width() * scale)
if w == 0 { if w == 0 {
return nil return nil
} }
scaled := resize.Resize(w, 0, texture.Texture(), resize.Bilinear) source, ok := texture.(ImageSource)
res, err := render.CreateTexture(scaled) if !ok {
return nil
}
image, err := source.CreateImage()
if err != nil {
return nil
}
scaled := resize.Resize(w, 0, image, resize.Bilinear)
res, err := render.CreateTextureGo(scaled, false)
if err != nil { if err != nil {
return nil return nil
} }
@ -40,21 +46,31 @@ func (t *Textures) AddTexture(name string, texture Texture) {
t.textures[name] = texture t.textures[name] = texture
} }
func (t *Textures) AddTextureFn(name string, create CreateImageFn) error { func (t *Textures) createTexture(name string, create func() (Texture, error)) (Texture, error) {
im, err := create() texture, err := create()
if err != nil { if err != nil {
return err return nil, err
}
return t.AddTextureNative(name, im)
}
func (t *Textures) AddTextureNative(name string, im image.Image) error {
texture, err := t.render.CreateTexture(im)
if err != nil {
return err
} }
t.AddTexture(name, texture) t.AddTexture(name, texture)
return nil return texture, nil
}
func (t *Textures) CreateTexture(name string, source ImageSource) (Texture, error) {
return t.createTexture(name, func() (Texture, error) {
return t.render.CreateTexture(source)
})
}
func (t *Textures) CreateTextureGo(name string, im image.Image, source bool) (Texture, error) {
return t.createTexture(name, func() (Texture, error) {
return t.render.CreateTextureGo(im, source)
})
}
func (t *Textures) CreateTexturePath(name string, path string, source bool) (Texture, error) {
return t.createTexture(name, func() (Texture, error) {
return t.render.CreateTexturePath(path, source)
})
} }
func (t *Textures) Destroy() { func (t *Textures) Destroy() {