From cdfb863ab0bda005713ef88d2c3d3d4364a3f98a Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Fri, 15 May 2020 09:20:44 +0200 Subject: [PATCH] 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. --- action.go | 37 +++ allg5ui/key.go | 2 + allg5ui/renderer.go | 91 ++++-- allg5ui/rendererfactory.go | 1 + allg5ui/texture.go | 14 +- sdlui/color.go | 11 + sdlui/events.go | 526 ++++++++++++++++++++++++++++++++++ sdlui/events_test.go | 13 + sdlui/font.go | 24 ++ sdlui/image.go | 16 ++ sdlui/rectangle.go | 38 +++ sdlui/renderer.go | 421 +++++++++++++++++++++++++++ sdlui/rendererfactory.go | 24 ++ sdlui/texture.go | 65 +++++ ui/buffer.go | 2 +- ui/button.go | 23 +- ui/checkbox.go | 72 +++-- ui/context.go | 1 + ui/drawoptions.go | 8 + ui/event.go | 15 +- ui/examples/01_basic/basic.go | 5 +- ui/icon.go | 52 +++- ui/imagesource.go | 37 +++ ui/key.go | 20 ++ ui/renderer.go | 7 +- ui/rendererfactory.go | 1 + ui/textbox.go | 21 +- ui/texture.go | 7 +- ui/textures.go | 48 ++-- 29 files changed, 1471 insertions(+), 131 deletions(-) create mode 100644 action.go create mode 100644 sdlui/color.go create mode 100644 sdlui/events.go create mode 100644 sdlui/events_test.go create mode 100644 sdlui/font.go create mode 100644 sdlui/image.go create mode 100644 sdlui/rectangle.go create mode 100644 sdlui/renderer.go create mode 100644 sdlui/rendererfactory.go create mode 100644 sdlui/texture.go create mode 100644 ui/imagesource.go diff --git a/action.go b/action.go new file mode 100644 index 0000000..7c75ab1 --- /dev/null +++ b/action.go @@ -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 +} diff --git a/allg5ui/key.go b/allg5ui/key.go index 690c5cc..c485be5 100644 --- a/allg5ui/key.go +++ b/allg5ui/key.go @@ -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: diff --git a/allg5ui/renderer.go b/allg5ui/renderer.go index 4c8ffd4..bf08db6 100644 --- a/allg5ui/renderer.go +++ b/allg5ui/renderer.go @@ -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) { diff --git a/allg5ui/rendererfactory.go b/allg5ui/rendererfactory.go index 2be3eca..f1358f5 100644 --- a/allg5ui/rendererfactory.go +++ b/allg5ui/rendererfactory.go @@ -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 diff --git a/allg5ui/texture.go b/allg5ui/texture.go index aa82de3..5def2fe 100644 --- a/allg5ui/texture.go +++ b/allg5ui/texture.go @@ -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 { diff --git a/sdlui/color.go b/sdlui/color.go new file mode 100644 index 0000000..7f7f405 --- /dev/null +++ b/sdlui/color.go @@ -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)) +} diff --git a/sdlui/events.go b/sdlui/events.go new file mode 100644 index 0000000..046fa60 --- /dev/null +++ b/sdlui/events.go @@ -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), + } +} diff --git a/sdlui/events_test.go b/sdlui/events_test.go new file mode 100644 index 0000000..dd7043a --- /dev/null +++ b/sdlui/events_test.go @@ -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)) +} diff --git a/sdlui/font.go b/sdlui/font.go new file mode 100644 index 0000000..e81b374 --- /dev/null +++ b/sdlui/font.go @@ -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) +} diff --git a/sdlui/image.go b/sdlui/image.go new file mode 100644 index 0000000..3e29bc0 --- /dev/null +++ b/sdlui/image.go @@ -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 +} diff --git a/sdlui/rectangle.go b/sdlui/rectangle.go new file mode 100644 index 0000000..930cf15 --- /dev/null +++ b/sdlui/rectangle.go @@ -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 +} diff --git a/sdlui/renderer.go b/sdlui/renderer.go new file mode 100644 index 0000000..e0f96e7 --- /dev/null +++ b/sdlui/renderer.go @@ -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 } diff --git a/sdlui/rendererfactory.go b/sdlui/rendererfactory.go new file mode 100644 index 0000000..6d2d6ca --- /dev/null +++ b/sdlui/rendererfactory.go @@ -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, + }) +} diff --git a/sdlui/texture.go b/sdlui/texture.go new file mode 100644 index 0000000..84e80f0 --- /dev/null +++ b/sdlui/texture.go @@ -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() +} diff --git a/ui/buffer.go b/ui/buffer.go index a949a3e..aebda26 100644 --- a/ui/buffer.go +++ b/ui/buffer.go @@ -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 } diff --git a/ui/button.go b/ui/button.go index de20edd..d340ee3 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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 { diff --git a/ui/checkbox.go b/ui/checkbox.go index 12d6934..105fcc8 100644 --- a/ui/checkbox.go +++ b/ui/checkbox.go @@ -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) } } diff --git a/ui/context.go b/ui/context.go index c5273ca..f8303ef 100644 --- a/ui/context.go +++ b/ui/context.go @@ -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: diff --git a/ui/drawoptions.go b/ui/drawoptions.go index 9266607..37834f9 100644 --- a/ui/drawoptions.go +++ b/ui/drawoptions.go @@ -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 +} diff --git a/ui/event.go b/ui/event.go index 786a88e..40af5e6 100644 --- a/ui/event.go +++ b/ui/event.go @@ -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 +} diff --git a/ui/examples/01_basic/basic.go b/ui/examples/01_basic/basic.go index b9d3931..d9e1fde 100644 --- a/ui/examples/01_basic/basic.go +++ b/ui/examples/01_basic/basic.go @@ -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 } diff --git a/ui/icon.go b/ui/icon.go index 3e7351d..8cfbd45 100644 --- a/ui/icon.go +++ b/ui/icon.go @@ -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 +} diff --git a/ui/imagesource.go b/ui/imagesource.go new file mode 100644 index 0000000..e19f08d --- /dev/null +++ b/ui/imagesource.go @@ -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 +} diff --git a/ui/key.go b/ui/key.go index cbbfefa..f6e935d 100644 --- a/ui/key.go +++ b/ui/key.go @@ -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 +} diff --git a/ui/renderer.go b/ui/renderer.go index 379cbe2..3e43463 100644 --- a/ui/renderer.go +++ b/ui/renderer.go @@ -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) diff --git a/ui/rendererfactory.go b/ui/rendererfactory.go index d8bccb1..a80a0a8 100644 --- a/ui/rendererfactory.go +++ b/ui/rendererfactory.go @@ -28,4 +28,5 @@ func SetRendererFactory(factory RendererFactory) { type NewRendererOptions struct { Location *geom.PointF32 Resizable bool + VSync bool } diff --git a/ui/textbox.go b/ui/textbox.go index aabd6c9..c20a019 100644 --- a/ui/textbox.go +++ b/ui/textbox.go @@ -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) diff --git a/ui/texture.go b/ui/texture.go index 7e50933..8234a69 100644 --- a/ui/texture.go +++ b/ui/texture.go @@ -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()) } diff --git a/ui/textures.go b/ui/textures.go index 3f3fbe1..8897fce 100644 --- a/ui/textures.go +++ b/ui/textures.go @@ -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() {