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
case allg5.KeyEnd:
return ui.KeyEnd
case allg5.KeyEnter:
return ui.KeyEnter
case allg5.KeyEquals:
return ui.KeyEquals
case allg5.KeyEscape:

View File

@ -4,6 +4,9 @@ import (
"image"
"image/color"
"math"
"unicode"
"opslag.de/schobers/zntg"
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
@ -13,20 +16,25 @@ import (
var _ ui.Renderer = &Renderer{}
func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
var clean zntg.Actions
defer func() { clean.Do() }()
var err = allg5.Init(allg5.InitAll)
if err != nil {
return nil, err
}
disp, err := allg5.NewDisplay(w, h, opts)
if err != nil {
return nil, err
}
clean = clean.Add(disp.Destroy)
eq, err := allg5.NewEventQueue()
if err != nil {
disp.Destroy()
return nil, err
}
clean = clean.Add(eq.Destroy)
user := allg5.NewUserEventSource()
eq.RegisterKeyboard()
@ -34,18 +42,25 @@ func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
eq.RegisterDisplay(disp)
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.
type Renderer struct {
disp *allg5.Display
eq *allg5.EventQueue
unh func(allg5.Event)
ft map[string]*font
user *allg5.UserEventSource
disp *allg5.Display
eq *allg5.EventQueue
unh func(allg5.Event)
ft map[string]*font
user *allg5.UserEventSource
keys ui.KeyState
modifiers ui.KeyModifier
cursor ui.MouseCursor
newCursor ui.MouseCursor
}
// Renderer implementation (events)
@ -53,11 +68,14 @@ type Renderer struct {
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
r.disp.Flip()
r.newCursor = ui.MouseCursorDefault
var ev = eventWait(r.eq, wait)
if ev == nil {
return
}
cursor := r.cursor
r.cursor = ui.MouseCursorDefault
var unhandled bool
for ev != nil {
switch e := ev.(type) {
case *allg5.DisplayCloseEvent:
@ -65,7 +83,21 @@ func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
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))})
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:
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseButtonUpEvent:
@ -82,12 +114,12 @@ func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) {
if r.unh != nil {
r.unh(e)
}
unhandled = true
}
ev = r.eq.Get()
}
if r.newCursor != r.cursor {
r.cursor = r.newCursor
if !unhandled && cursor != r.cursor {
switch r.cursor {
case ui.MouseCursorNone:
r.disp.SetMouseCursor(allg5.MouseCursorNone)
@ -130,32 +162,47 @@ func (r *Renderer) Clear(c color.Color) {
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)
if err != nil {
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)
if err != nil {
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))
if err != nil {
return nil, err
}
return &texture{bmp}, nil
return &texture{bmp, nil}, nil
}
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 }
@ -191,7 +238,7 @@ func (r *Renderer) Font(name string) ui.Font {
func (r *Renderer) mustGetBitmap(t ui.Texture) *allg5.Bitmap {
texture, ok := t.(*texture)
if !ok {
panic("image must be created on same renderer")
panic("texture must be created on same renderer")
}
return texture.bmp
}
@ -244,7 +291,7 @@ func (r *Renderer) SetIcon(texture ui.Texture) {
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.newCursor = c
r.cursor = c
}
func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) {
@ -256,7 +303,7 @@ func (r *Renderer) SetWindowTitle(t string) {
}
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) {

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) {
renderer, err := NewRenderer(width, height, allg5.NewDisplayOptions{
Resizable: opts.Resizable,
Vsync: opts.VSync,
})
if err != nil {
return nil, err

View File

@ -8,21 +8,27 @@ import (
)
var _ ui.Texture = &texture{}
var _ ui.ImageSource = &texture{}
type texture struct {
bmp *allg5.Bitmap
bmp *allg5.Bitmap
source ui.ImageSource
}
func (t *texture) Destroy() {
func (t *texture) Destroy() error {
t.bmp.Destroy()
return nil
}
func (t *texture) Height() float32 {
return float32(t.bmp.Height())
}
func (t *texture) Texture() image.Image {
return t.bmp.Image()
func (t *texture) CreateImage() (image.Image, error) {
if t.source == nil {
return t.bmp.Image(), nil
}
return t.source.CreateImage()
}
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.size = geom.ZeroPtF32
}
texture, err := ctx.Renderer().CreateTextureSize(size.X, size.Y)
texture, err := ctx.Renderer().CreateTextureTarget(size.X, size.Y)
if err != nil {
return err
}

View File

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

View File

@ -30,8 +30,9 @@ func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
if len(c.Text) != 0 {
w += pad + font.WidthOf(c.Text)
}
icon, _ := ctx.Textures().ScaledHeight(c.getOrCreateNormalIcon(ctx), h)
w += pad + icon.Width()
icon := c.getOrCreateNormalIcon(ctx)
_, iconWidth := ScaleToHeight(SizeOfTexture(icon), h)
w += pad + iconWidth
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)
}
func (c *Checkbox) iconBorder() geom.PolygonF32 {
return geom.PolF32(
geom.PtF32(48, 80),
geom.PtF32(400, 80),
geom.PtF32(400, 432),
geom.PtF32(48, 432),
)
var checkBoxIconBorder = geom.PolF32(
geom.PtF32(48, 80),
geom.PtF32(400, 80),
geom.PtF32(400, 432),
geom.PtF32(48, 432),
)
var checkBoxCheckMark = geom.PointsF32{
geom.PtF32(96, 256),
geom.PtF32(180, 340),
geom.PtF32(340, 150),
}
func (c *Checkbox) checkMark() geom.PointsF32 {
return geom.PointsF32{
geom.PtF32(96, 256),
geom.PtF32(180, 340),
geom.PtF32(340, 150),
}
func (c *Checkbox) hoverIcon(pt geom.PointF32) bool {
return (pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)) || pt.DistanceToLines(checkBoxCheckMark) < 24
}
func (c *Checkbox) hoverIcon() ImagePixelTestFn {
border := c.iconBorder()
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(pt geom.PointF32) bool {
return pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)
}
func (c *Checkbox) normalIcon() ImagePixelTestFn {
border := c.iconBorder()
return func(pt geom.PointF32) bool {
return pt.DistanceToPolygon(border) < 48 && !pt.InPolygon(border)
}
}
func (c *Checkbox) selectedIcon() ImagePixelTestFn {
border := c.iconBorder()
check := c.checkMark()
return func(pt geom.PointF32) bool {
if pt.DistanceToPolygon(border) < 48 || pt.InPolygon(border) {
return pt.DistanceToLines(check) > 24
}
return false
func (c *Checkbox) selectedIcon(pt geom.PointF32) bool {
if pt.DistanceToPolygon(checkBoxIconBorder) < 48 || pt.InPolygon(checkBoxIconBorder) {
return pt.DistanceToLines(checkBoxCheckMark) > 24
}
return false
}
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
bounds = bounds.Inset(pad)
boundsH := bounds.Dy()
pos := bounds.Min
icon, _ := ctx.Textures().ScaledHeight(c.icon(ctx), bounds.Dy())
icon := c.icon(ctx)
if icon != nil {
iconColor := fore
if c.Selected && c.Font.Color == nil {
iconColor = palette.Primary
}
ctx.Renderer().DrawTextureOptions(icon, geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-icon.Height())), DrawOptions{Tint: iconColor})
pos.X += icon.Width() + pad
scaledIcon, _ := ctx.Textures().ScaledHeight(icon, boundsH) // try to pre-scale icon
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 {
var fontName = c.FontName(ctx)
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 }
// Handle implement EventTarget
func (c *context) Handle(e Event) {
switch e.(type) {
case *DisplayCloseEvent:

View File

@ -10,3 +10,11 @@ type DrawOptions struct {
Tint color.Color
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
KeyModifierControl
KeyModifierAlt
KeyModifierOSCommand
)
type KeyPressEvent struct {
type KeyDownEvent struct {
EventBase
Key Key
Modifiers KeyModifier
}
type KeyUpEvent struct {
EventBase
Key Key
Modifiers KeyModifier
Character rune
}
type MouseButton int
@ -82,3 +88,8 @@ type MouseMoveEvent struct {
type RefreshEvent struct {
EventBase
}
type TextInputEvent struct {
EventBase
Character rune
}

View File

@ -4,7 +4,8 @@ import (
"image/color"
"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/zntg/ui"
@ -73,7 +74,7 @@ func run() error {
if err != nil {
return err
}
plus, err := render.CreateTexturePath("../resources/images/plus.png")
plus, err := render.CreateTexturePath("../resources/images/plus.png", true)
if err != nil {
return err
}

View File

@ -7,11 +7,30 @@ import (
"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
func createTexture(ctx Context, image image.Image) Texture {
texture, err := ctx.Renderer().CreateTexture(image)
func (f ImageAlphaPixelTestFn) CreateImageSource(size geom.Point) ImageSource {
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 {
return nil
}
@ -19,18 +38,15 @@ func createTexture(ctx Context, image image.Image) Texture {
}
func CreateIcon(ctx Context, test ImagePixelTestFn) Texture {
icon := DrawIcon(test)
return createTexture(ctx, icon)
return createTexture(ctx, test.CreateImageSource(IconSize()))
}
func CreateTexture(ctx Context, size geom.Point, test ImagePixelTestFn) Texture {
image := DrawImage(size, test)
return createTexture(ctx, image)
return createTexture(ctx, test.CreateImageSource(size))
}
func CreateTextureAlpha(ctx Context, size geom.Point, test ImageAlphaPixelTestFn) Texture {
image := DrawImageAlpha(size, test)
return createTexture(ctx, image)
return createTexture(ctx, test.CreateImageSource(size))
}
func DrawIcon(test ImagePixelTestFn) image.Image {
@ -66,20 +82,28 @@ func DrawImageAlpha(size geom.Point, test ImageAlphaPixelTestFn) image.Image {
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)
if texture != nil {
return texture
}
test := testFactory()
texture = CreateIcon(ctx, test)
if texture == nil {
texture, err := ctx.Textures().CreateTexture(name, test.CreateImageSource(IconSize()))
if err != nil {
return nil
}
ctx.Textures().AddTexture(name, texture)
return texture
}
func IconSize() geom.Point {
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
KeyE
KeyEnd
KeyEnter
KeyEquals
KeyEscape
KeyF
@ -135,3 +136,22 @@ const (
KeyY
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
Clear(c color.Color)
CreateTexture(m image.Image) (Texture, error)
CreateTexturePath(path string) (Texture, error)
CreateTextureSize(w, h float32) (Texture, error)
CreateTexture(m ImageSource) (Texture, error)
CreateTextureGo(m image.Image, source bool) (Texture, error)
CreateTexturePath(path string, source bool) (Texture, error)
CreateTextureTarget(w, h float32) (Texture, error)
DefaultTarget() Texture
DrawTexture(t Texture, p geom.PointF32)
DrawTextureOptions(t Texture, p geom.PointF32, opts DrawOptions)

View File

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

View File

@ -2,7 +2,6 @@ package ui
import (
"fmt"
"image/color"
"strings"
"time"
"unicode"
@ -144,7 +143,7 @@ func (b *TextBox) Handle(ctx Context, e Event) {
b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
b.Selection.End = b.Selection.Caret
}
case *KeyPressEvent:
case *KeyDownEvent:
if !b.Focus {
break
}
@ -217,14 +216,12 @@ func (b *TextBox) Handle(ctx Context, e Event) {
case KeyX:
DefaultClipboard.WriteText(b.cut())
}
default:
if e.Modifiers == KeyModifierNone || e.Modifiers&KeyModifierShift == KeyModifierShift {
caret := b.Selection.Caret
b.Text = fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:])
b.Selection.Caret = caret + 1
b.Selection.SetSelectionToCaret()
}
}
case *TextInputEvent:
caret := b.Selection.Caret
b.Text = fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:])
b.Selection.Caret = caret + 1
b.Selection.SetSelectionToCaret()
}
if b.over {
ctx.Renderer().SetMouseCursor(MouseCursorText)
@ -242,7 +239,11 @@ func (b *TextBox) Render(ctx Context) {
var caretWidth float32 = 1
b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) {
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 {
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)

View File

@ -1,10 +1,11 @@
package ui
import "image"
import "opslag.de/schobers/geom"
type Texture interface {
Destroy()
Destroy() error
Height() float32
Texture() image.Image
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"
)
type CreateImageFn func() (image.Image, error)
func ScaleTexture(render Renderer, texture Texture, scale float32) Texture {
w := uint(texture.Width() * scale)
if w == 0 {
return nil
}
scaled := resize.Resize(w, 0, texture.Texture(), resize.Bilinear)
res, err := render.CreateTexture(scaled)
source, ok := texture.(ImageSource)
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 {
return nil
}
@ -40,21 +46,31 @@ func (t *Textures) AddTexture(name string, texture Texture) {
t.textures[name] = texture
}
func (t *Textures) AddTextureFn(name string, create CreateImageFn) error {
im, err := create()
func (t *Textures) createTexture(name string, create func() (Texture, error)) (Texture, error) {
texture, err := create()
if err != nil {
return 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
return nil, err
}
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() {