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() {