Compare commits

..

103 Commits

Author SHA1 Message Date
5c29e37e68 Added support for go.mod. 2023-06-14 13:22:30 +02:00
f56c5dd389 Added support for getting & setting location of Renderer. 2022-05-01 12:05:17 +00:00
ddf9476920 Added support for borderless windows. 2022-05-01 12:40:09 +02:00
eb46741165 Added support for scrolling a control into view (Overflow, only vertically currently). 2021-08-19 18:26:15 +02:00
a6415a1d60 Added UserCache{File,Dir} and renamed User{File,Dir} to UserConfig{File,Dir}. 2021-08-09 19:15:08 +02:00
5e4afbe038 Removed drop dependency from allg5ui renderer. 2021-08-09 11:13:20 +02:00
98a75206bc Add alignment to FPS control. 2021-08-09 09:33:26 +02:00
12a9f5ee39 Last visibile item in StackPanel is now clipped to maximum available area. 2021-07-19 17:55:43 +02:00
de8ce3e7bc Added text overflow to label.
Generalised text fitting (to width) and implemented binary search.
2021-07-19 17:55:10 +02:00
5dcecb8cc1 Added clipping for Overflow/ScrollControl. 2021-07-19 07:59:02 +02:00
c0586c1d8f Added overridable text padding. 2021-07-19 07:58:46 +02:00
764f2a0dd2 Added rendering of bounds in debug mode. 2021-07-18 22:48:48 +02:00
5a4dcd52b0 Added embedres.
- Exposes embed.FS (native) as a ui.Resources.
2021-07-18 22:47:34 +02:00
bcd32f8372 Fixed panic in Overflow. 2021-06-17 20:18:23 +02:00
e5bfd1394c Added text changed event to TextBox. 2021-06-17 19:34:37 +02:00
7fa5601307 Added Pos methods to DisplayDrag{Enter,Move}Event. 2021-06-07 07:56:52 +02:00
b63fc999e1 Fixed example 02_drop. 2021-06-04 20:14:56 +02:00
6839870055 Fixed bug with Line (sdlui.Renderer). 2021-06-04 17:17:41 +02:00
3c89748eac Added drag & drop addon.
- Drop addon is based on WM_DROPFILES, dragdrop addon is based on the OLE IDragDrop interface and thus can registerer more interactions.
- The allg5ui implementation will try to fall back on the drop addon (because the dragdrop addon wouldn't work properly).
- Drop addon is refactored to use the same interface as the dragdrop addon.
2021-06-04 17:17:22 +02:00
302ae1c338 Added simple drop (files) addon. 2021-06-01 21:14:51 +02:00
4cff23cd37 Mouse wheel now increments/decrements the slider value by 1.
- With shift pressed it moves the value 10.
- With control pressed it moves the value with 0.1 (does nothing when the Integer flag is enabled).
2021-05-11 08:25:15 +02:00
72138bb8e3 Added Line to Renderer. 2021-03-23 12:31:31 +01:00
7aa88e2dc6 Fixed bug in slider when maximum equal to or less than the minimum was supplied. 2021-01-14 11:44:17 +01:00
dbeacc3794 Split rendering of buffer & blitting it to the display. 2021-01-12 20:35:07 +01:00
480e864b53 Show event wasn't invoked on overlay when visibility was set to true on add. 2021-01-09 17:08:35 +01:00
9dc301eed8 Fixed two issues with slider.
Value was always set back to original value.
Events were always handled by slider (handle).
2021-01-09 17:07:10 +01:00
102c187566 Added generic margin. 2020-12-13 07:43:00 +01:00
11e37af9c2 Added shorthand method for retrieving default font for the control. 2020-12-13 07:40:58 +01:00
5babda0ca9 Added (optional) dropshadow for label. 2020-12-13 07:40:58 +01:00
b1cdbea90f Changed colors of button text a bit. 2020-12-13 07:40:58 +01:00
7d5168614e Added override for scrollbar color. 2020-12-13 07:40:58 +01:00
b0a13d1a3c ContainerBase now provides a DesiredSize (maximum of all children, if any). 2020-12-13 07:40:58 +01:00
cc32cf5bc3 Added scaled ImageSource. 2020-12-13 07:40:58 +01:00
0f03760e66 Added Resize & SetIcon to Renderer.
Refactored Size (on Renderer) to return geom.Point instead of geom.PointF32.
Refactored Width and Height (on Texture) to return int instead of float32.

Refactored texture dimensions to be represented by ints instead of float32s.
2020-12-13 07:40:19 +01:00
de87c5d3aa Try casting to PhysicalResources first when setting resource provider.
SetResourceProvider accepts Resources instead of factory method to it.
2020-07-07 18:19:34 +02:00
67e73a8671 Resources only exposes OpenResource (and Destroy).
PhysicalResources derives from Resources and exposes FetchResource.
Made dependency specific resource addons.
Extended the available resource options (fallback, path, refactored copy).
NewRenderer provides DefaultResources to the created renderer.
2020-05-25 22:24:06 +02:00
869f87dd4f Added a debug overlay. 2020-05-24 20:15:40 +02:00
43d49a0dbb Fixed bug where spacing was not respecting a fixed width/height when asking its proxied control for its desired size. 2020-05-24 19:15:45 +02:00
352984d6d9 Overlays must be handled & rendered in their order. 2020-05-24 18:42:22 +02:00
cdc999ad42 Added option to Pan using tile coordinates.
Fixed incorrect panning.
2020-05-23 11:02:12 +02:00
7793fe823f Added IsometricProjection. 2020-05-23 10:19:10 +02:00
2238f8749a Changed allg5ui.font.Measure() implementation to be more consistent with the sdlui implementation. 2020-05-23 08:28:21 +02:00
b434a71f00 Added Animation.IsActive. 2020-05-23 08:20:20 +02:00
3bab08a0a6 Added support for rendering a partial texture. 2020-05-23 08:20:04 +02:00
23115b8a0f Added support for rendering text to a texture. 2020-05-22 17:42:02 +02:00
9371a8738e Fixed bug where incorrect path was stored as source for CreateTexturePath in the Allegro renderer. 2020-05-22 09:08:50 +02:00
a2cb2d03ca Allegro CreateTexture{Go,Path} didn't respect source flag. 2020-05-20 19:45:45 +02:00
39766e9f01 SDL surface is created from non-alpha-premultiplied colors. 2020-05-20 19:45:16 +02:00
0fe9a2ce63 Added FPS counter. 2020-05-18 21:01:17 +02:00
7dde894bf0 Fixed two bugs with overlay visibility.
- Toggle didn't toggle but hide.
- Hide hid the overlay but didn't trigger any callbacks/events.
2020-05-18 20:59:05 +02:00
16d4e26cd0 Exposed last known key modifiers in context. 2020-05-18 20:35:34 +02:00
e7ada7fea0 Using normal cursor when control is disabled. 2020-05-18 20:28:13 +02:00
ea5e1a4989 Added DisabledColor to button. 2020-05-18 12:37:42 +02:00
7f2e155edd Renderers now respect Location of NewRendererOptions. 2020-05-17 21:02:38 +02:00
32c53eb947 Fixed tooltip flickering when not blocking on events. 2020-05-17 21:02:07 +02:00
0f54224cc7 Removed OverlayProxy and incorporated logic into Proxy. 2020-05-17 20:07:52 +02:00
bcf3093c87 Overlays all handle the same events but if a overlay handles the event then the content won't get the event. 2020-05-17 16:11:47 +02:00
0c399a8d93 Only animate when textbox has focus. 2020-05-17 15:31:06 +02:00
8560204c39 Added display move event.
- The Allegro (alui) implementation only provides emulation of the event (comparing the position every time other events are handled).
2020-05-17 15:30:52 +02:00
22cc3ce444 Added support for a fixed icon height on buttons. 2020-05-17 14:59:28 +02:00
c78c4052d0 Refactored DrawTexture on Renderer to favor rendering using destination rectangle instead of a point. 2020-05-17 11:12:45 +02:00
f20397c684 Moved default NewRendererOptions to generic part instead of in specific renderers. 2020-05-17 08:29:02 +02:00
e2472cffef Added default style if style is not set on Run{,Wait}.
Exposed Resources in Context as well.
2020-05-17 07:54:54 +02:00
b78f215c8c Fixed bug where nil was stored as a scaled texture. 2020-05-17 07:54:19 +02:00
b9534ee255 Made tooltip overlay switch visibility only when changed.
Added Tooltip.SetVisibility.
Fixed bug where no events where triggered for change in visibility.
2020-05-17 07:21:58 +02:00
75fce53716 Added Paragraph control.
Extended DesiredSize method with extra arguments that tells the control how much space is available for the parent control (note: this might not be the actual given size when Arrange is called on the control).
2020-05-16 15:37:53 +02:00
add33c6e7e Fixed bug where selection wasn't overwritten by typed character. 2020-05-16 13:47:25 +02:00
ae46d2a1f2 Added Disabled to controls. 2020-05-16 13:46:07 +02:00
d673653d3f Renamed EventFn to EventEmptyFn and EventStateFn to EventFn (both in ui and zntg packages). 2020-05-16 12:07:13 +02:00
8c48c949e9 Renamed Overlay callbacks & extended interface of Overlay (must be a Control as well). 2020-05-16 10:57:14 +02:00
9af85d79a6 Added callbacks & events when visibility of an overlay changes.
Added proxy that proxies overlay callbacks as well (on top of control callbacks).
2020-05-16 10:12:54 +02:00
7f3d836254 Added a generic Events struct in zntg (without ui.Context).
Refactored ui.Events to re-use zntg.Events.
2020-05-16 09:36:04 +02:00
ff4b04262c Moved fs addon as res addon. 2020-05-16 09:22:32 +02:00
661a11fecd Added tests for HexColor. 2020-05-16 08:54:45 +02:00
d742fba7e9 Added Events and changed event handlers of controls.
Removed EventHandlers from Control interface.
2020-05-15 19:00:43 +02:00
4e37f4b23e Added secondary colors to palette. 2020-05-15 17:02:18 +02:00
3a18d3adf9 Added support for tooltips. 2020-05-15 16:53:57 +02:00
6db13c8f46 Added overlays. 2020-05-15 16:02:54 +02:00
3591e22c97 Added Fonts() to context similarly as Textures().
- Fonts are now managed by context instead of the implementation specific renderers.
2020-05-15 15:42:24 +02:00
02ee819a99 Favoring Context.Textures() over direct texture assignment to controls. 2020-05-15 14:44:55 +02:00
b28b3e1838 Added Resources abstraction. 2020-05-15 14:20:07 +02:00
a0660a9650 Moved clipboard to addons. 2020-05-15 13:14:08 +02:00
5d297c98b8 Keeping state of a drag operation separately. 2020-05-15 12:15:44 +02:00
c0c5235d5a Added methods to retrieve the path to the user configuration dir. 2020-05-15 11:38:21 +02:00
5a1e5f6f7f Added image & JSON encoding/decoding facilities. 2020-05-15 11:33:33 +02:00
cf12afe2bb Added hexadecimal color string conversions. 2020-05-15 11:32:47 +02:00
744c639abd Added animation. 2020-05-15 10:58:02 +02:00
9b04eeb7a3 Added documentation for Action{,Err,s}. 2020-05-15 10:57:38 +02:00
893bf513ad Few mouse interaction extensions for sdlui.
- Added Mouse{Enter,Leave}Event support.
- Simulating MouseWheel for MouseMotionEvent.
2020-05-15 09:59:53 +02:00
cdfb863ab0 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.
2020-05-15 09:20:44 +02:00
f618c55b25 Embedded Allegro font struct directly inside font wrapped. 2020-05-13 16:49:45 +02:00
8c11aec276 Renamed image.go to texture.go. 2020-05-13 16:36:46 +02:00
1aad3bf11a Added keys.
Fixed bug where copy/cut/paste/select all weren't executed.
Fixed bug where start/end of selection wasn't properly set when typing.
Removed SetClipboard.
2020-05-13 15:57:04 +02:00
48aaf30182 Renamed Image{,s} to Texture{,s}. 2020-05-12 23:03:43 +02:00
2c9007ce9b Added renderer factory.
- Removes dependency on the specific backend from an application point-of-view.
2020-05-12 22:46:58 +02:00
5ecfd10754 Made cache more generic. 2020-05-12 20:58:42 +02:00
280b4842e8 Moved allg5ui package one layer up. 2020-05-12 17:38:37 +02:00
4ca400d985 Moved allg5 package to separate repository. 2019-12-19 07:07:43 +01:00
4ae1db7969 Added support for audio recording.
Added unhandled event handler.
2019-10-12 09:01:46 +02:00
06a38d8e4a Updated example. 2019-07-22 20:02:57 +02:00
52d22e7a18 Added value changed event for slider.
Added flag for snapping to integer values.
2019-07-09 19:08:40 +02:00
757758d8e9 Fixed right/center alignment of labels. 2019-07-09 19:07:40 +02:00
432281f08d Added Slider.
Refactored Icon related methods & added alpha support for icon/image generation.
2019-07-08 19:17:11 +02:00
112 changed files with 7040 additions and 2414 deletions

45
action.go Normal file
View File

@ -0,0 +1,45 @@
package zntg
// Action is a method without arguments or return values.
type Action func()
// Err converts the Action to an ActionErr.
func (a Action) Err() ActionErr {
return func() error {
a()
return nil
}
}
// ActionErr is a method that only returns an error.
type ActionErr func() error
// Actions is a slice of ActionErr's.
type Actions []ActionErr
// Add adds an Action and returns the new list of Actions.
func (a Actions) Add(fn Action) Actions {
return a.AddErr(fn.Err())
}
// AddErr adds an ActionErr and return the new list of Actions.
func (a Actions) AddErr(fn ActionErr) Actions {
return append(a, fn)
}
// Do executes all actions.
func (a Actions) Do() {
for _, a := range a {
a()
}
}
// DoErr executes all actions but stops on the first action that returns an error.
func (a Actions) DoErr() error {
for _, a := range a {
if err := a(); err != nil {
return err
}
}
return nil
}

23
addons/aferores/afero.go Normal file
View File

@ -0,0 +1,23 @@
package aferores
import (
"io"
"github.com/spf13/afero"
"opslag.de/schobers/zntg/ui"
)
type aferoResources struct {
afero.Fs
}
var _ ui.Resources = &aferoResources{}
// New provides resources from a afero file system.
func New(fs afero.Fs) ui.Resources {
return &aferoResources{fs}
}
func (r *aferoResources) Destroy() error { return nil }
func (r *aferoResources) OpenResource(name string) (io.ReadCloser, error) { return r.Fs.Open(name) }

View File

@ -0,0 +1,176 @@
#include <Windows.h>
#include <oleidl.h>
#include <stdio.h>
#include "vector.h"
#include "_cgo_export.h"
#define STDULONGMETHODIMP STDMETHODIMP_(ULONG)
#define DRAGDROP_MAXFILEPATHSIZE 32767
typedef struct
{
IDropTargetVtbl *_vtbl;
long _refCount;
uint32_t _handle;
HWND _windowHandle;
} DragDropHandler;
static void screenToClient(HWND windowHandle, POINTL *point)
{
ScreenToClient(windowHandle, (LPPOINT)point);
}
static STDMETHODIMP QueryInterface(IDropTarget *this, REFIID riid, LPVOID *ppvObj)
{
// Always set out parameter to NULL, validating it first.
if (!ppvObj)
return E_INVALIDARG;
*ppvObj = NULL;
if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IDropTarget))
{
// Increment the reference count and return the pointer.
*ppvObj = (LPVOID)this;
((DragDropHandler *)this)->_vtbl->AddRef(this);
return NOERROR;
}
return E_NOINTERFACE;
}
static STDULONGMETHODIMP AddRef(IDropTarget *this)
{
InterlockedIncrement(&((DragDropHandler *)this)->_refCount);
return ((DragDropHandler *)this)->_refCount;
}
static STDULONGMETHODIMP Release(IDropTarget *this)
{
// Decrement the object's internal counter.
ULONG ulRefCount = InterlockedDecrement(&((DragDropHandler *)this)->_refCount);
if (0 == ((DragDropHandler *)this)->_refCount)
{
GlobalFree(this);
}
return ulRefCount;
}
static STDMETHODIMP DragEnter(IDropTarget *this, __RPC__in_opt IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect)
{
*pdwEffect = DROPEFFECT_COPY;
wchar_t filePath[DRAGDROP_MAXFILEPATHSIZE];
FORMATETC format;
format.cfFormat = CF_HDROP;
format.ptd = NULL;
format.dwAspect = DVASPECT_CONTENT;
format.lindex = -1;
format.tymed = TYMED_HGLOBAL;
STGMEDIUM medium;
HRESULT result = pDataObj->lpVtbl->GetData(pDataObj, &format, &medium);
if (result != S_OK)
return E_UNEXPECTED;
HDROP drop = (HDROP)GlobalLock(medium.hGlobal);
clearFiles(((DragDropHandler *)this)->_handle);
UINT numberOfFiles = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0);
for (UINT fileIndex = 0; fileIndex < numberOfFiles; fileIndex++)
{
UINT length = DragQueryFileW(drop, fileIndex, filePath, DRAGDROP_MAXFILEPATHSIZE);
addFile(((DragDropHandler *)this)->_handle, &filePath[0], length);
}
GlobalUnlock(medium.hGlobal);
if (medium.pUnkForRelease != NULL)
medium.pUnkForRelease->lpVtbl->Release(medium.pUnkForRelease);
screenToClient(((DragDropHandler *)this)->_windowHandle, &pt);
onDragEnter(((DragDropHandler *)this)->_handle, pt.x, pt.y);
return S_OK;
}
static STDMETHODIMP DragOver(IDropTarget *this, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect)
{
*pdwEffect = DROPEFFECT_COPY;
screenToClient(((DragDropHandler *)this)->_windowHandle, &pt);
onDragOver(((DragDropHandler *)this)->_handle, pt.x, pt.y);
return S_OK;
}
static STDMETHODIMP DragLeave(IDropTarget *this)
{
onDragLeave(((DragDropHandler *)this)->_handle);
return S_OK;
}
static STDMETHODIMP Drop(IDropTarget *this, __RPC__in_opt IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, __RPC__inout DWORD *pdwEffect)
{
*pdwEffect = DROPEFFECT_COPY;
screenToClient(((DragDropHandler *)this)->_windowHandle, &pt);
onDrop(((DragDropHandler *)this)->_handle, pt.x, pt.y);
return S_OK;
}
static const IDropTargetVtbl DragDropHandlerVtbl = {
QueryInterface,
AddRef,
Release,
DragEnter,
DragOver,
DragLeave,
Drop,
};
static DragDropHandler *newDragDropHandler(HWND windowHandle, uint32_t handle)
{
DragDropHandler *handler = malloc(sizeof(DragDropHandler));
handler->_vtbl = (IDropTargetVtbl *)&DragDropHandlerVtbl;
handler->_refCount = 0;
handler->_windowHandle = windowHandle;
handler->_handle = handle;
return handler;
}
BOOL dragDropInitialized = FALSE;
typedef vector(DragDropHandler *) DragDropHandlers;
DragDropHandlers handlers;
static void VerifyResult(CHAR *component, HRESULT result)
{
if (result == S_OK)
return;
printf("%s failed (error code: %d)\n", component, result);
}
static void initDragDrop(void)
{
if (dragDropInitialized == TRUE)
return;
HRESULT result = OleInitialize(NULL);
VerifyResult("OleInitialize", result);
vector_init(handlers);
dragDropInitialized = TRUE;
}
uint32_t RegisterHandler(void *window)
{
initDragDrop();
size_t handle = handlers.count + 1;
HWND windowHandle = (HWND)window;
DragDropHandler *handler = newDragDropHandler(windowHandle, handle);
vector_append(DragDropHandler *, handlers, handler);
DragAcceptFiles(windowHandle, TRUE);
HRESULT result = RegisterDragDrop(windowHandle, (IDropTarget *)handler);
VerifyResult("RegisterDragDrop", result);
return (uint32_t)handle;
}

View File

@ -0,0 +1,96 @@
// +build windows
package dragdrop
import (
"unsafe"
"golang.org/x/text/encoding/unicode"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
/*
#cgo LDFLAGS: -lole32 -luuid
#include <Windows.h>
#include <oleidl.h>
#include <stdint.h>
#include <stdlib.h>
extern uint32_t RegisterHandler(void* windowHandle);
*/
import "C"
//export clearFiles
func clearFiles(handle uint32) {
handler := handlers[handle]
handler.Files = nil
handlers[handle] = handler
}
//export addFile
func addFile(handle uint32, pathNative *C.wchar_t, pathNativeLength C.UINT) {
pathBytes := C.GoBytes(unsafe.Pointer(pathNative), C.int(pathNativeLength)*C.sizeof_wchar_t)
decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()
path, err := decoder.Bytes(pathBytes)
if err != nil {
return
}
handler := handlers[handle]
handler.Files = append(handler.Files, string(path))
handlers[handle] = handler
}
//export onDragEnter
func onDragEnter(handle uint32, x, y C.LONG) {
invokeHandler(handle, func(target ui.DragDropEventTarget, files []string) {
target.DragEnter(pos(x, y), files)
})
}
//export onDragOver
func onDragOver(handle uint32, x, y C.LONG) {
invokeHandler(handle, func(target ui.DragDropEventTarget, _ []string) {
target.DragMove(pos(x, y))
})
}
//export onDragLeave
func onDragLeave(handle uint32) {
invokeHandler(handle, func(target ui.DragDropEventTarget, _ []string) {
target.DragLeave()
})
}
//export onDrop
func onDrop(handle uint32, x, y C.LONG) {
invokeHandler(handle, func(target ui.DragDropEventTarget, files []string) {
target.Drop(pos(x, y), files)
})
}
type handler struct {
Target ui.DragDropEventTarget
Files []string
}
var handlers map[uint32]handler = map[uint32]handler{}
func invokeHandler(handle uint32, invoke func(ui.DragDropEventTarget, []string)) {
handler := handlers[handle]
invoke(handler.Target, handler.Files)
}
func pos(x, y C.LONG) geom.PointF32 { return geom.Pt(int(x), int(y)).ToF32() }
type provider struct{}
func (p provider) Register(windowHandle uintptr, target ui.DragDropEventTarget) {
handle := C.RegisterHandler(unsafe.Pointer(windowHandle))
handlers[uint32(handle)] = handler{target, nil}
}
func init() {
ui.DefaultDragDropProvider = provider{}
}

26
addons/dragdrop/vector.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef __VECTOR_H__
#define __VECTOR_H__
#define vector(type) \
struct \
{ \
type *items; \
size_t count; \
}
#define vector_init(v) \
do \
{ \
(v).items = 0; \
(v).count = 0; \
} while (0);
#define vector_append(type, v, i) \
do \
{ \
(v).count++; \
(v).items = (type *)realloc((v).items, sizeof(type) * (v).count); \
(v).items[(v).count - 1] = i; \
} while (0);
#endif // __VECTOR_H__

7
addons/drop/drop.go Normal file
View File

@ -0,0 +1,7 @@
// +build !windows
package drop
func Register(dropper Dropper) error {
return nil
}

View File

@ -0,0 +1,63 @@
#include <Windows.h>
#include <shellapi.h>
#include <stdint.h>
#include "_cgo_export.h"
#define droppedFilePathSize 32767
BOOL dropHookInitialized = FALSE;
HHOOK nextHook;
LRESULT CALLBACK DragAndDropHook(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam)
{
if (nCode < 0 || wParam == 0)
return CallNextHookEx(nextHook, nCode, wParam, lParam);
LPMSG message = (LPMSG)lParam;
switch (message->message)
{
case WM_DROPFILES:
{
wchar_t droppedFilePath[droppedFilePathSize];
clearDrop();
HDROP drop = (HDROP)message->wParam;
UINT numberOfFiles = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0);
for (UINT fileIndex = 0; fileIndex < numberOfFiles; fileIndex++)
{
UINT length = DragQueryFileW(drop, fileIndex, droppedFilePath, droppedFilePathSize);
addDroppedFile(&droppedFilePath[0], length);
}
POINT position;
DragQueryPoint(drop, &position);
dropFinished(position.x, position.y);
}
break;
}
return CallNextHookEx(nextHook, nCode, wParam, lParam);
}
void initDropHook(HWND windowHandle)
{
if (dropHookInitialized == TRUE)
return;
DWORD threadId = GetWindowThreadProcessId(windowHandle, NULL);
nextHook = SetWindowsHookEx(WH_GETMESSAGE, DragAndDropHook, NULL, threadId);
dropHookInitialized = TRUE;
}
void SetDragAndDropHook(void *window)
{
HWND windowHandle = (HWND)window;
initDropHook(windowHandle);
DragAcceptFiles(windowHandle, TRUE);
}

View File

@ -0,0 +1,65 @@
// +build windows
package drop
import (
"unsafe"
"golang.org/x/text/encoding/unicode"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
/*
#include <Windows.h>
#include <stdint.h>
void SetDragAndDropHook(void* window);
*/
import "C"
type handler struct {
Target ui.DragDropEventTarget
Files []string
}
var targets map[ui.DragDropEventTarget]struct{} = map[ui.DragDropEventTarget]struct{}{}
var droppedFiles []string
//export clearDrop
func clearDrop() {
droppedFiles = nil
}
//export dropFinished
func dropFinished(x, y C.INT) {
for target := range targets {
target.Drop(geom.PtF32(float32(x), float32(y)), droppedFiles)
}
}
//export addDroppedFile
func addDroppedFile(filePath *C.wchar_t, filePathLength C.UINT) {
pathBytes := C.GoBytes(unsafe.Pointer(filePath), C.int(filePathLength)*C.sizeof_wchar_t)
decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()
path, err := decoder.Bytes(pathBytes)
if err != nil {
return
}
droppedFiles = append(droppedFiles, string(path))
}
func RegisterAsDefaultProvider() {
ui.DefaultDragDropProvider = &provider{}
}
type provider struct{}
func (p provider) Register(windowHandle uintptr, target ui.DragDropEventTarget) {
C.SetDragAndDropHook(unsafe.Pointer(windowHandle))
targets[target] = struct{}{}
}
func init() {
ui.DefaultDragDropProvider = provider{}
}

View File

@ -0,0 +1,24 @@
package embedres
import (
"embed"
"io"
"opslag.de/schobers/zntg/ui"
)
var _ ui.Resources = &resources{}
type resources struct {
fs embed.FS
}
func New(fs embed.FS) ui.Resources {
return &resources{fs}
}
func (r resources) Destroy() error { return nil }
func (r resources) OpenResource(name string) (io.ReadCloser, error) {
return r.fs.Open(name)
}

23
addons/riceres/rice.go Normal file
View File

@ -0,0 +1,23 @@
package riceres
import (
"io"
rice "github.com/GeertJohan/go.rice"
"opslag.de/schobers/zntg/ui"
)
type riceResources struct {
*rice.Box
}
var _ ui.Resources = &riceResources{}
// New provides resources from a rice Box.
func New(box *rice.Box) ui.Resources {
return &riceResources{box}
}
func (r *riceResources) Destroy() error { return nil }
func (r *riceResources) OpenResource(name string) (io.ReadCloser, error) { return r.Box.Open(name) }

View File

@ -1,260 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
// #include <stdlib.h>
import "C"
import (
"errors"
"image"
"unsafe"
)
// Bitmap represents an in memory bitmap
type Bitmap struct {
bitmap *C.ALLEGRO_BITMAP
width int
height int
subs []*Bitmap
}
type DrawOptions struct {
Center bool
Scale *Scale
Tint *Color
Rotation *Rotation
}
type Scale struct {
Horizontal float32
Vertical float32
}
func NewScale(hor, ver float32) *Scale {
return &Scale{hor, ver}
}
func NewUniformScale(s float32) *Scale {
return &Scale{s, s}
}
type Rotation struct {
Angle float32
Center bool
}
func newBitmap(width, height int, mut func(m FlagMutation), flags []NewBitmapFlag) (*Bitmap, error) {
var newBmpFlags = CaptureNewBitmapFlags()
defer newBmpFlags.Revert()
newBmpFlags.Mutate(func(m FlagMutation) {
if mut != nil {
mut(m)
}
for _, f := range flags {
m.Set(f)
}
})
b := C.al_create_bitmap(C.int(width), C.int(height))
if b == nil {
return nil, errors.New("error creating bitmap")
}
return &Bitmap{b, width, height, nil}, nil
}
// NewBitmap creates a new bitmap of given width and height and optional flags
func NewBitmap(width, height int, flags ...NewBitmapFlag) (*Bitmap, error) {
return newBitmap(width, height, nil, flags)
}
// NewVideoBitmap creates a new video bitmap of given width and height and optional flags
func NewVideoBitmap(width, height int, flags ...NewBitmapFlag) (*Bitmap, error) {
return newBitmap(width, height, func(m FlagMutation) {
m.Unset(NewBitmapFlagMemoryBitmap)
m.Set(NewBitmapFlagVideoBitmap)
}, flags)
}
// NewMemoryBitmap creates a new video bitmap of given width and height and optional flags
func NewMemoryBitmap(width, height int, flags ...NewBitmapFlag) (*Bitmap, error) {
return newBitmap(width, height, func(m FlagMutation) {
m.Unset(NewBitmapFlagVideoBitmap)
m.Set(NewBitmapFlagMemoryBitmap)
}, flags)
}
// NewBitmapFromImage creates a new bitmap starting from a Go native image (image.Image)
func NewBitmapFromImage(src image.Image, video bool) (*Bitmap, error) {
var newBmpFlags = CaptureNewBitmapFlags()
defer newBmpFlags.Revert()
newBmpFlags.Mutate(func(m FlagMutation) {
m.Unset(NewBitmapFlagVideoBitmap)
m.Set(NewBitmapFlagMemoryBitmap)
m.Set(NewBitmapFlagMinLinear)
})
var bnd = src.Bounds()
width, height := bnd.Dx(), bnd.Dy()
var b = C.al_create_bitmap(C.int(width), C.int(height))
if b == nil {
return nil, errors.New("error creating memory bitmap")
}
region := C.al_lock_bitmap(b, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_WRITEONLY)
if region == nil {
C.al_destroy_bitmap(b)
return nil, errors.New("unable to lock bitmap")
}
dst := (*[1 << 30]uint8)(region.data)
left, top := bnd.Min.X, bnd.Min.Y
for y := 0; y < height; y++ {
row := dst[y*int(region.pitch):]
for x := 0; x < width; x++ {
r, g, b, a := src.At(left+x, top+y).RGBA()
row[x*4] = uint8(r >> 8)
row[x*4+1] = uint8(g >> 8)
row[x*4+2] = uint8(b >> 8)
row[x*4+3] = uint8(a >> 8)
}
}
C.al_unlock_bitmap(b)
if video {
newBmpFlags.Mutate(func(m FlagMutation) {
m.Unset(NewBitmapFlagMemoryBitmap)
m.Set(NewBitmapFlagVideoBitmap)
m.Set(NewBitmapFlagMinLinear)
})
C.al_convert_bitmap(b)
}
return &Bitmap{b, width, height, nil}, nil
}
// LoadBitmap tries to load the image at the specified path as a bitmap
func LoadBitmap(path string) (*Bitmap, error) {
p := C.CString(path)
defer C.free(unsafe.Pointer(p))
b := C.al_load_bitmap(p)
if b == nil {
return nil, errors.New("error loading bitmap")
}
width := int(C.al_get_bitmap_width(b))
height := int(C.al_get_bitmap_height(b))
return &Bitmap{b, width, height, nil}, nil
}
// Draw draws the bitmap at the given location
func (b *Bitmap) Draw(left, top float32) {
C.al_draw_bitmap(b.bitmap, C.float(left), C.float(top), 0)
}
func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) {
width := float32(b.width)
height := float32(b.height)
scale := options.Scale != nil
if scale {
width *= options.Scale.Horizontal
height *= options.Scale.Vertical
}
if options.Center {
left -= width * 0.5
top -= height * 0.5
}
rotated := options.Rotation != nil
var centerX C.float
var centerY C.float
if rotated && options.Rotation.Center {
centerX = C.float(b.width) * 0.5
centerY = C.float(b.height) * 0.5
}
if scale {
if options.Tint == nil { // scaled
if rotated { // scaled & rotated
C.al_draw_scaled_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal), C.float(options.Scale.Vertical), C.float(options.Rotation.Angle), 0)
} else { // scaled
C.al_draw_scaled_bitmap(b.bitmap, 0, 0, C.float(b.width), C.float(b.height), C.float(left), C.float(top), C.float(width), C.float(height), 0)
}
} else { // tinted & scaled
if rotated { // scaled, tinted & rotated
C.al_draw_tinted_scaled_rotated_bitmap(b.bitmap, options.Tint.color, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal), C.float(options.Scale.Vertical), C.float(options.Rotation.Angle), 0)
} else { // tinted, scaled
C.al_draw_tinted_scaled_bitmap(b.bitmap, options.Tint.color, 0, 0, C.float(b.width), C.float(b.height), C.float(left), C.float(top), C.float(width), C.float(height), 0)
}
}
} else {
if options.Tint == nil {
if rotated { // rotated
C.al_draw_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Rotation.Angle), 0)
} else {
C.al_draw_bitmap(b.bitmap, C.float(left), C.float(top), 0)
}
} else { // tinted
if rotated { // tinted & rotated
C.al_draw_tinted_rotated_bitmap(b.bitmap, options.Tint.color, centerX, centerY, C.float(left), C.float(top), C.float(options.Rotation.Angle), 0)
} else {
C.al_draw_tinted_bitmap(b.bitmap, options.Tint.color, C.float(left), C.float(top), 0)
}
}
}
}
// Sub creates a sub-bitmap of the original bitmap
func (b *Bitmap) Sub(x, y, w, h int) *Bitmap {
var sub = C.al_create_sub_bitmap(b.bitmap, C.int(x), C.int(y), C.int(w), C.int(h))
if sub == nil {
return nil
}
var bmp = &Bitmap{sub, w, h, nil}
b.subs = append(b.subs, bmp)
return bmp
}
// Subs returns the slice of sub-bitmaps
func (b *Bitmap) Subs() []*Bitmap {
return b.subs
}
func (b *Bitmap) Width() int {
return b.width
}
func (b *Bitmap) Height() int {
return b.height
}
func (b *Bitmap) IsVideo() bool {
return C.al_get_bitmap_flags(b.bitmap)&C.ALLEGRO_VIDEO_BITMAP == C.ALLEGRO_VIDEO_BITMAP
}
func (b *Bitmap) Image() image.Image {
im := image.NewRGBA(image.Rect(0, 0, b.width, b.height))
region := C.al_lock_bitmap(b.bitmap, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_READONLY)
if region == nil {
return nil
}
defer C.al_unlock_bitmap(b.bitmap)
src := (*[1 << 30]uint8)(region.data)
dst := im.Pix
var srcOff, dstOff int
for y := 0; y < b.height; y++ {
copy(dst[dstOff:], src[srcOff:srcOff+b.width*4])
srcOff += int(region.pitch)
dstOff += im.Stride
}
return im
}
func (b *Bitmap) SetAsTarget() {
C.al_set_target_bitmap(b.bitmap)
}
// Destroy destroys the bitmap
func (b *Bitmap) Destroy() {
var bmp = b.bitmap
if bmp == nil {
return
}
b.bitmap = nil
for _, sub := range b.subs {
sub.Destroy()
}
C.al_destroy_bitmap(bmp)
}

View File

@ -1,6 +0,0 @@
// +build !windows
package allg5
// #cgo pkg-config: allegro-5 allegro_font-5 allegro_image-5 allegro_primitives-5 allegro_ttf-5
import "C"

View File

@ -1,6 +0,0 @@
// +build windows,!static
package allg5
// #cgo LDFLAGS: -lallegro -lallegro_font -lallegro_image -lallegro_primitives -lallegro_ttf
import "C"

View File

@ -1,6 +0,0 @@
// +build windows,static
package allg5
// #cgo LDFLAGS: -lallegro_monolith-static -static -ljpeg -ldumb -lFLAC -lfreetype -lvorbisfile -lvorbis -logg -lphysfs -lpng16 -lzlib -luuid -lkernel32 -lwinmm -lpsapi -lopengl32 -lglu32 -luser32 -lcomdlg32 -lgdi32 -lshell32 -lole32 -ladvapi32 -lws2_32 -lshlwapi -lstdc++ -lwebp
import "C"

View File

@ -1,36 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
import "image/color"
var _ color.Color = &Color{}
type Color struct {
color C.ALLEGRO_COLOR
}
func NewColor(r, g, b byte) Color {
return Color{C.al_map_rgb(C.uchar(r), C.uchar(g), C.uchar(b))}
}
func NewColorAlpha(r, g, b, a byte) Color {
return Color{C.al_map_rgba(C.uchar(r), C.uchar(g), C.uchar(b), C.uchar(a))}
}
func NewColorGo(c color.Color) Color {
r, g, b, a := c.RGBA()
return Color{C.al_premul_rgba(C.uchar(r>>8), C.uchar(g>>8), C.uchar(b>>8), C.uchar(a>>8))}
}
// RGBA implements the color.Color interface.
func (c Color) RGBA() (r, g, b, a uint32) {
var cr, cg, cb, ca C.uchar
C.al_unmap_rgba(c.color, &cr, &cg, &cb, &ca)
a = uint32(ca)
r = uint32(cr) * a
g = uint32(cg) * a
b = uint32(cb) * a
a *= a
return
}

View File

@ -1,125 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
import (
"errors"
"unsafe"
)
// Display represents a display
type Display struct {
display *C.ALLEGRO_DISPLAY
}
type NewDisplayOptions struct {
Fullscreen bool
Resizable bool
Windowed bool
Maximized bool
Frameless bool
Shaders bool
OpenGL bool
}
// NewDisplay creates a display
func NewDisplay(width, height int, options NewDisplayOptions) (*Display, error) {
var flags C.int = C.ALLEGRO_WINDOWED
if options.Fullscreen {
if options.Windowed {
flags |= C.ALLEGRO_FULLSCREEN_WINDOW
} else {
flags = C.ALLEGRO_FULLSCREEN
}
} else if options.Frameless {
flags |= C.ALLEGRO_FRAMELESS
}
if options.Resizable {
flags |= C.ALLEGRO_RESIZABLE
if options.Maximized {
flags |= C.ALLEGRO_MAXIMIZED
}
}
if options.OpenGL {
flags |= C.ALLEGRO_OPENGL
}
if options.Shaders {
flags |= C.ALLEGRO_PROGRAMMABLE_PIPELINE
}
C.al_set_new_display_flags(flags)
d := C.al_create_display(C.int(width), C.int(height))
if d == nil {
return nil, errors.New("error creating display")
}
return &Display{d}, nil
}
// Flip flips the buffer to the display
func (d *Display) Flip() {
C.al_flip_display()
}
func (d *Display) Width() int {
return int(C.al_get_display_width(d.display))
}
func (d *Display) Height() int {
return int(C.al_get_display_height(d.display))
}
func (d *Display) Position() (int, int) {
var x, y C.int
C.al_get_window_position(d.display, &x, &y)
return int(x), int(y)
}
func (d *Display) Resize(w, h int) {
C.al_resize_display(d.display, C.int(w), C.int(h))
}
func (d *Display) SetAsTarget() {
C.al_set_target_backbuffer(d.display)
}
func (d *Display) SetIcon(i *Bitmap) {
C.al_set_display_icon(d.display, i.bitmap)
}
func (d *Display) SetMouseCursor(c MouseCursor) {
C.al_set_system_mouse_cursor(d.display, C.ALLEGRO_SYSTEM_MOUSE_CURSOR(c))
}
func (d *Display) SetMousePosition(x, y int) {
C.al_set_mouse_xy(d.display, C.int(x), C.int(y))
}
func (d *Display) SetPosition(x, y int) {
C.al_set_window_position(d.display, C.int(x), C.int(y))
}
func (d *Display) SetWindowTitle(title string) {
t := C.CString(title)
defer C.free(unsafe.Pointer(t))
C.al_set_window_title(d.display, t)
}
func (d *Display) Target() *Bitmap {
return &Bitmap{C.al_get_backbuffer(d.display), d.Width(), d.Height(), nil}
}
// Destroy destroys the display
func (d *Display) Destroy() {
C.al_destroy_display(d.display)
}
func CurrentTarget() *Bitmap {
var bmp = C.al_get_target_bitmap()
return &Bitmap{bmp, int(C.al_get_bitmap_width(bmp)), int(C.al_get_bitmap_height(bmp)), nil}
}
func SetNewWindowTitle(title string) {
t := C.CString(title)
defer C.free(unsafe.Pointer(t))
C.al_set_new_window_title(t)
}

View File

@ -1,249 +0,0 @@
package allg5
/*
#include <allegro5/allegro.h>
#define USER_EVENT_TYPE ALLEGRO_GET_EVENT_TYPE('u', 's', 'e', 'r')
void init_user_event(ALLEGRO_EVENT* e)
{
e->user.type = USER_EVENT_TYPE;
}
*/
import "C"
import (
"errors"
"unsafe"
)
type EventQueue struct {
queue *C.ALLEGRO_EVENT_QUEUE
}
type Event interface {
Stamp() float64
}
type EventBase struct {
stamp float64
}
func (eb EventBase) Stamp() float64 {
return eb.stamp
}
type DisplayCloseEvent struct {
EventBase
}
type DisplayResizeEvent struct {
EventBase
X, Y int
Width int
Height int
}
type DisplayOrientation int
const (
DisplayOrientation0Degrees DisplayOrientation = iota
DisplayOrientation90Degrees
DisplayOrientation180Degrees
DisplayOrientation270Degrees
DisplayOrientationFaceUp
DisplayOrientationFaceDown
)
func toDisplayOrientation(o C.int) DisplayOrientation {
switch o {
case C.ALLEGRO_DISPLAY_ORIENTATION_0_DEGREES:
return DisplayOrientation0Degrees
case C.ALLEGRO_DISPLAY_ORIENTATION_90_DEGREES:
return DisplayOrientation90Degrees
case C.ALLEGRO_DISPLAY_ORIENTATION_180_DEGREES:
return DisplayOrientation180Degrees
case C.ALLEGRO_DISPLAY_ORIENTATION_270_DEGREES:
return DisplayOrientation270Degrees
case C.ALLEGRO_DISPLAY_ORIENTATION_FACE_UP:
return DisplayOrientationFaceUp
case C.ALLEGRO_DISPLAY_ORIENTATION_FACE_DOWN:
return DisplayOrientationFaceDown
default:
panic("not supported")
}
}
type DisplayOrientationEvent struct {
EventBase
Orientation DisplayOrientation
}
type KeyEvent struct {
EventBase
KeyCode Key
Display *Display
}
type KeyCharEvent struct {
KeyEvent
UnicodeCharacter rune
Modifiers KeyMod
Repeat bool
}
type KeyDownEvent struct {
KeyEvent
}
type KeyUpEvent struct {
KeyEvent
}
type MouseButtonDownEvent struct {
MouseEvent
Button MouseButton
Pressure float32
}
type MouseButtonUpEvent struct {
MouseEvent
Button MouseButton
Pressure float32
}
type MouseEnterEvent struct {
MouseEvent
}
type MouseEvent struct {
EventBase
X, Y int
Z, W int
Display *Display
}
type MouseLeaveEvent struct {
MouseEvent
}
type MouseMoveEvent struct {
MouseEvent
DeltaX, DeltaY int
DeltaZ, DeltaW int
Pressure float32
}
type UserEvent struct {
EventBase
}
type UserEventSource struct {
source *C.ALLEGRO_EVENT_SOURCE
}
func NewEventQueue() (*EventQueue, error) {
q := C.al_create_event_queue()
if q == nil {
return nil, errors.New("unable to create event queue")
}
return &EventQueue{q}, nil
}
func (eq *EventQueue) register(source *C.ALLEGRO_EVENT_SOURCE) {
C.al_register_event_source(eq.queue, source)
}
func (eq *EventQueue) RegisterDisplay(d *Display) {
eq.register(C.al_get_display_event_source(d.display))
}
func (eq *EventQueue) RegisterMouse() {
eq.register(C.al_get_mouse_event_source())
}
func (eq *EventQueue) RegisterKeyboard() {
eq.register(C.al_get_keyboard_event_source())
}
func (eq *EventQueue) RegisterUserEvents(source *UserEventSource) {
eq.register(source.source)
}
func (eq *EventQueue) mapEvent(e *C.ALLEGRO_EVENT) Event {
any := (*C.ALLEGRO_ANY_EVENT)(unsafe.Pointer(e))
eb := EventBase{float64(any.timestamp)}
switch any._type {
case C.ALLEGRO_EVENT_DISPLAY_CLOSE:
return &DisplayCloseEvent{eb}
case C.ALLEGRO_EVENT_DISPLAY_ORIENTATION:
display := (*C.ALLEGRO_DISPLAY_EVENT)(unsafe.Pointer(e))
return &DisplayOrientationEvent{eb, toDisplayOrientation(display.orientation)}
case C.ALLEGRO_EVENT_DISPLAY_RESIZE:
display := (*C.ALLEGRO_DISPLAY_EVENT)(unsafe.Pointer(e))
C.al_acknowledge_resize(display.source)
return &DisplayResizeEvent{eb, int(display.x), int(display.y), int(display.width), int(display.height)}
case C.ALLEGRO_EVENT_MOUSE_AXES:
mouse := (*C.ALLEGRO_MOUSE_EVENT)(unsafe.Pointer(e))
return &MouseMoveEvent{MouseEvent{eb, int(mouse.x), int(mouse.y), int(mouse.z), int(mouse.w), nil}, int(mouse.dx), int(mouse.dy), int(mouse.dz), int(mouse.dw), float32(mouse.pressure)}
case C.ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
mouse := (*C.ALLEGRO_MOUSE_EVENT)(unsafe.Pointer(e))
return &MouseButtonDownEvent{MouseEvent{eb, int(mouse.x), int(mouse.y), int(mouse.z), int(mouse.w), nil}, MouseButton(mouse.button), float32(mouse.pressure)}
case C.ALLEGRO_EVENT_MOUSE_ENTER_DISPLAY:
mouse := (*C.ALLEGRO_MOUSE_EVENT)(unsafe.Pointer(e))
return &MouseEnterEvent{MouseEvent{eb, int(mouse.x), int(mouse.y), int(mouse.z), int(mouse.w), nil}}
case C.ALLEGRO_EVENT_MOUSE_BUTTON_UP:
mouse := (*C.ALLEGRO_MOUSE_EVENT)(unsafe.Pointer(e))
return &MouseButtonUpEvent{MouseEvent{eb, int(mouse.x), int(mouse.y), int(mouse.z), int(mouse.w), nil}, MouseButton(mouse.button), float32(mouse.pressure)}
case C.ALLEGRO_EVENT_MOUSE_LEAVE_DISPLAY:
mouse := (*C.ALLEGRO_MOUSE_EVENT)(unsafe.Pointer(e))
return &MouseLeaveEvent{MouseEvent{eb, int(mouse.x), int(mouse.y), int(mouse.z), int(mouse.w), nil}}
case C.ALLEGRO_EVENT_KEY_DOWN:
key := (*C.ALLEGRO_KEYBOARD_EVENT)(unsafe.Pointer(e))
return &KeyDownEvent{KeyEvent{eb, Key(key.keycode), nil}}
case C.ALLEGRO_EVENT_KEY_UP:
key := (*C.ALLEGRO_KEYBOARD_EVENT)(unsafe.Pointer(e))
return &KeyUpEvent{KeyEvent{eb, Key(key.keycode), nil}}
case C.ALLEGRO_EVENT_KEY_CHAR:
key := (*C.ALLEGRO_KEYBOARD_EVENT)(unsafe.Pointer(e))
return &KeyCharEvent{KeyEvent{eb, Key(key.keycode), nil}, rune(key.unichar), KeyMod(key.modifiers), bool(key.repeat)}
case C.USER_EVENT_TYPE:
return &UserEvent{eb}
}
return nil
}
func (eq *EventQueue) Get() Event {
var event C.ALLEGRO_EVENT
if !bool(C.al_get_next_event(eq.queue, &event)) {
return nil
}
return eq.mapEvent(&event)
}
func (eq *EventQueue) GetWait() Event {
var event C.ALLEGRO_EVENT
C.al_wait_for_event(eq.queue, &event)
return eq.mapEvent(&event)
}
func (eq *EventQueue) Destroy() {
C.al_destroy_event_queue(eq.queue)
}
func NewUserEventSource() *UserEventSource {
s := (*C.ALLEGRO_EVENT_SOURCE)(C.malloc(C.sizeof_ALLEGRO_EVENT_SOURCE))
source := &UserEventSource{s}
C.al_init_user_event_source(s)
return source
}
func (s *UserEventSource) Destroy() {
C.al_destroy_user_event_source(s.source)
C.free(unsafe.Pointer(s.source))
}
func (s *UserEventSource) EmitEvent() bool {
e := (*C.ALLEGRO_EVENT)(C.malloc(C.sizeof_ALLEGRO_EVENT))
C.init_user_event(e)
return bool(C.al_emit_user_event(s.source, e, nil))
}

View File

@ -1,23 +0,0 @@
package allg5
// #include <stdlib.h>
import "C"
type FlagMutation interface {
Set(f NewBitmapFlag)
Unset(f NewBitmapFlag)
}
type flagMut struct {
flg C.int
}
func (m *flagMut) Set(f NewBitmapFlag) {
m.flg |= C.int(f)
}
func (m *flagMut) Unset(f NewBitmapFlag) {
if m.flg&C.int(f) == C.int(f) {
m.flg ^= C.int(f)
}
}

View File

@ -1,107 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_font.h>
// #include <allegro5/allegro_ttf.h>
import "C"
import (
"fmt"
"unsafe"
)
type Font struct {
font *C.ALLEGRO_FONT
hght float32
asc float32
desc float32
}
type HorizontalAlignment int
const (
AlignLeft HorizontalAlignment = iota
AlignCenter
AlignRight
)
func LoadTTFFont(path string, size int) (*Font, error) {
p := C.CString(path)
defer C.free(unsafe.Pointer(p))
f := C.al_load_ttf_font(p, C.int(size), 0)
if f == nil {
return nil, fmt.Errorf("unable to load ttf font '%s'", path)
}
return &Font{f, 0, 0, 0}, nil
}
func (f *Font) drawFlags(a HorizontalAlignment) C.int {
switch a {
case AlignLeft:
return C.ALLEGRO_ALIGN_LEFT
case AlignCenter:
return C.ALLEGRO_ALIGN_CENTRE
case AlignRight:
return C.ALLEGRO_ALIGN_RIGHT
}
return C.ALLEGRO_ALIGN_LEFT
}
func (f *Font) Draw(left, top float32, color Color, align HorizontalAlignment, text string) {
t := C.CString(text)
defer C.free(unsafe.Pointer(t))
flags := f.drawFlags(align)
C.al_draw_text(f.font, color.color, C.float(left), C.float(top), flags, t)
}
// Ascent returns the ascent of the font
func (f *Font) Ascent() float32 {
if f.asc == 0 {
f.asc = float32(C.al_get_font_ascent(f.font))
}
return f.asc
}
// Descent returns the descent of the font.
func (f *Font) Descent() float32 {
if f.desc == 0 {
f.desc = float32(C.al_get_font_descent(f.font))
}
return f.desc
}
// Height returns the height of the font
func (f *Font) Height() float32 {
if f.hght == 0 {
f.hght = f.Ascent() + f.Descent()
}
return f.hght
}
// TextDimensions returns the bounding box of the rendered text.
func (f *Font) TextDimensions(text string) (x, y, w, h float32) {
t := C.CString(text)
defer C.free(unsafe.Pointer(t))
var bbx, bby, bbw, bbh C.int
C.al_get_text_dimensions(f.font, t, &bbx, &bby, &bbw, &bbh)
x = float32(bbx)
y = float32(bby)
w = float32(bbw)
h = float32(bbh)
return
}
// TextWidth returns the width of the rendered text.
func (f *Font) TextWidth(text string) float32 {
t := C.CString(text)
defer C.free(unsafe.Pointer(t))
return float32(C.al_get_text_width(f.font, t))
}
func (f *Font) Destroy() {
C.al_destroy_font(f.font)
}

View File

@ -1,26 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
// BitmapFlag is extra information provided for creating a bitmap
type BitmapFlag int
const (
// BitmapFlagLinearScaleDown enables linear scaling when scaling down. Gives better results when combined with BitmapFlagMipMap
BitmapFlagLinearScaleDown BitmapFlag = C.ALLEGRO_MIN_LINEAR
// BitmapFlagLinearScaleUp enables linear scaling when scaling up.
BitmapFlagLinearScaleUp = C.ALLEGRO_MAG_LINEAR
// BitmapFlagMipMap enables mipmaps for drawing a scaled down version. Bitmap must square and its sides must be a power of two.
BitmapFlagMipMap = C.ALLEGRO_MIPMAP
)
// ClearToColor clears the target bitmap to the color
func ClearToColor(c Color) {
C.al_clear_to_color(c.color)
}
// SetNewBitmapFlags sets the default bitmap flags for a newly created bitmap
func SetNewBitmapFlags(flags BitmapFlag) {
C.al_set_new_bitmap_flags(C.int(flags))
}

View File

@ -1,188 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
type Key int
const (
KeyA Key = 1
KeyB = 2
KeyC = 3
KeyD = 4
KeyE = 5
KeyF = 6
KeyG = 7
KeyH = 8
KeyI = 9
KeyJ = 10
KeyK = 11
KeyL = 12
KeyM = 13
KeyN = 14
KeyO = 15
KeyP = 16
KeyQ = 17
KeyR = 18
KeyS = 19
KeyT = 20
KeyU = 21
KeyV = 22
KeyW = 23
KeyX = 24
KeyY = 25
KeyZ = 26
Key0 = 27
Key1 = 28
Key2 = 29
Key3 = 30
Key4 = 31
Key5 = 32
Key6 = 33
Key7 = 34
Key8 = 35
Key9 = 36
KeyPad0 = 37
KeyPad1 = 38
KeyPad2 = 39
KeyPad3 = 40
KeyPad4 = 41
KeyPad5 = 42
KeyPad6 = 43
KeyPad7 = 44
KeyPad8 = 45
KeyPad9 = 46
KeyF1 = 47
KeyF2 = 48
KeyF3 = 49
KeyF4 = 50
KeyF5 = 51
KeyF6 = 52
KeyF7 = 53
KeyF8 = 54
KeyF9 = 55
KeyF10 = 56
KeyF11 = 57
KeyF12 = 58
KeyEscape = 59
KeyTilde = 60
KeyMinus = 61
KeyEquals = 62
KeyBackspace = 63
KeyTab = 64
KeyOpenBrace = 65
KeyCloseBrace = 66
KeyEnter = 67
KeySemicolon = 68
KeyQuote = 69
KeyBackslash = 70
KeyBackslash2 = 71 /* DirectInput calls this DIK_OEM_102: "< > | on UK/Germany keyboards" */
KeyComma = 72
KeyFullstop = 73
KeySlash = 74
KeySpace = 75
KeyInsert = 76
KeyDelete = 77
KeyHome = 78
KeyEnd = 79
KeyPageUp = 80
KeyPageDown = 81
KeyLeft = 82
KeyRight = 83
KeyUp = 84
KeyDown = 85
KeyPadSlash = 86
KeyPadAsterisk = 87
KeyPadMinus = 88
KeyPadPlus = 89
KeyPadDelete = 90
KeyPadEnter = 91
KeyPrintScreen = 92
KeyPause = 93
KeyAbntC1 = 94
KeyYen = 95
KeyKana = 96
KeyConvert = 97
KeyNoConvert = 98
KeyAt = 99
KeyCircumflex = 100
KeyColon2 = 101
KeyKanji = 102
KeyPadEquals = 103 /* MacOS X */
KeyBackQuote = 104 /* MacOS X */
KeySemicolon2 = 105 /* MacOS X -- TODO: ask lillo what this should be */
KeyCommand = 106 /* MacOS X */
KeyBack = 107 /* Android back key */
KeyVolumeUp = 108
KeyVolumeDown = 109
KeySearch = 110
KeyDPadCenter = 111
KeyButtonX = 112
KeyButtonY = 113
KeyDPadUp = 114
KeyDPadDown = 115
KeyDPadLeft = 116
KeyDPadRight = 117
KeySelect = 118
KeyStart = 119
KeyButtonL1 = 120
KeyButtonR1 = 121
KeyButtonL2 = 122
KeyButtonR2 = 123
KeyButtonA = 124
KeyButtonB = 125
KeyThumbL = 126
KeyThumbR = 127
KeyUnknown = 128
KeyModifiers = 215
KeyLShift = 215
KeyRShift = 216
KeyLCtrl = 217
KeyRCtrl = 218
KeyAlt = 219
KeyAltGr = 220
KeyLWin = 221
KeyRWin = 222
KeyMenu = 223
KeyScrollLock = 224
KeyNumLock = 225
KeyCapsLock = 226
)
type KeyMod uint
const (
KeyModShift KeyMod = 0x00001
KeyModCtrl = 0x00002
KeyModAlt = 0x00004
KeyModLWin = 0x00008
KeyModRWin = 0x00010
KeyModMenu = 0x00020
KeyModAltGr = 0x00040
KeyModCommand = 0x00080
KeyModScrollLock = 0x00100
KeyModNumlock = 0x00200
KeyModCapsLock = 0x00400
KeyModInaltseq = 0x00800
KeyModAccent1 = 0x01000
KeyModAccent2 = 0x02000
KeyModAccent3 = 0x04000
KeyModAccent4 = 0x08000
)
func IsKeyDown(k Key) bool {
var state C.ALLEGRO_KEYBOARD_STATE
C.al_get_keyboard_state(&state)
return bool(C.al_key_down(&state, C.int(k)))
}
func IsAnyKeyDown(keys ...Key) bool {
var state C.ALLEGRO_KEYBOARD_STATE
C.al_get_keyboard_state(&state)
for _, k := range keys {
if bool(C.al_key_down(&state, C.int(k))) {
return true
}
}
return false
}

View File

@ -1,34 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
type Monitor struct {
X1, Y1 int
X2, Y2 int
}
func monitor(m *C.ALLEGRO_MONITOR_INFO) Monitor {
return Monitor{int(m.x1), int(m.y1), int(m.x2), int(m.y2)}
}
func DefaultMonitor() Monitor {
var m C.ALLEGRO_MONITOR_INFO
C.al_get_monitor_info(C.ALLEGRO_DEFAULT_DISPLAY_ADAPTER, &m)
return monitor(&m)
}
func Monitors() []Monitor {
var n = NumberOfVideoAdapters()
var mons []Monitor
var m C.ALLEGRO_MONITOR_INFO
for i := 0; i < n; i++ {
C.al_get_monitor_info(C.int(i), &m)
mons = append(mons, monitor(&m))
}
return mons
}
func NumberOfVideoAdapters() int {
return int(C.al_get_num_video_adapters())
}

View File

@ -1,54 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
type MouseButton uint
const (
MouseButtonLeft MouseButton = 1
MouseButtonRight = 2
MouseButtonMiddle = 3
)
type MouseCursor uint
const (
MouseCursorNone MouseCursor = 0
MouseCursorDefault = 1
MouseCursorArrow = 2
MouseCursorBusy = 3
MouseCursorQuestion = 4
MouseCursorEdit = 5
MouseCursorMove = 6
MouseCursorResizeN = 7
MouseCursorResizeW = 8
MouseCursorResizeS = 9
MouseCursorResizeE = 10
MouseCursorResizeNW = 11
MouseCursorResizeSW = 12
MouseCursorResizeSE = 13
MouseCursorResizeNE = 14
MouseCursorProgress = 15
MouseCursorPrecision = 16
MouseCursorLink = 17
MouseCursorAltSelect = 18
MouseCursorUnavailable = 19
)
func IsMouseButtonDown(b MouseButton) bool {
var state C.ALLEGRO_MOUSE_STATE
C.al_get_mouse_state(&state)
return bool(C.al_mouse_button_down(&state, C.int(b)))
}
func IsAnyMouseButtonDown(buttons ...MouseButton) bool {
var state C.ALLEGRO_MOUSE_STATE
C.al_get_mouse_state(&state)
for _, b := range buttons {
if bool(C.al_mouse_button_down(&state, C.int(b))) {
return true
}
}
return false
}

View File

@ -1,31 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
import "C"
type NewBitmapFlag int
const (
NewBitmapFlagMemoryBitmap = NewBitmapFlag(C.ALLEGRO_MEMORY_BITMAP)
NewBitmapFlagVideoBitmap = NewBitmapFlag(C.ALLEGRO_VIDEO_BITMAP)
NewBitmapFlagMinLinear = NewBitmapFlag(C.ALLEGRO_MIN_LINEAR)
)
type NewBitmapFlagsCapture struct {
cap C.int
}
func CaptureNewBitmapFlags() *NewBitmapFlagsCapture {
var cap = C.al_get_new_bitmap_flags()
return &NewBitmapFlagsCapture{cap}
}
func (c *NewBitmapFlagsCapture) Mutate(mut func(FlagMutation)) {
var m = &flagMut{c.cap}
mut(m)
C.al_set_new_bitmap_flags(m.flg)
}
func (c *NewBitmapFlagsCapture) Revert() {
C.al_set_new_bitmap_flags(c.cap)
}

View File

@ -1,12 +0,0 @@
// +build windows
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_windows.h>
import "C"
import "unsafe"
func (d *Display) WindowHandle() unsafe.Pointer {
return unsafe.Pointer(C.al_get_win_window_handle(d.display))
}

View File

@ -1,176 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_primitives.h>
/*
void SetTransform(ALLEGRO_TRANSFORM* t, float ix, float iy, float iz, float jx, float jy, float jz, float kx, float ky, float kz)
{
t->m[0][0] = ix;
t->m[0][1] = iy;
t->m[0][2] = iz;
t->m[1][0] = jx;
t->m[1][1] = jy;
t->m[1][2] = jz;
t->m[2][0] = kx;
t->m[2][1] = ky;
t->m[2][2] = kz;
}
bool SetTextureMatrix(ALLEGRO_TRANSFORM* t)
{
bool result = true;
result &= al_set_shader_bool(ALLEGRO_SHADER_VAR_USE_TEX_MATRIX, 1);
result &= al_set_shader_matrix(ALLEGRO_SHADER_VAR_TEX_MATRIX, t);
return result;
}
*/
import "C"
import (
"unsafe"
"opslag.de/schobers/geom"
"opslag.de/schobers/geom/lin"
)
func DrawFilledRectangle(x1, y1, x2, y2 float32, c Color) {
C.al_draw_filled_rectangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color)
}
func DrawFilledTriangle(x1, y1, x2, y2, x3, y3 float32, c Color) {
C.al_draw_filled_triangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), C.float(x3), C.float(y3), c.color)
}
func DrawLine(x1, y1, x2, y2 float32, c Color, thickness float32) {
C.al_draw_line(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color, C.float(thickness))
}
func DrawPolyline(vertices []float32, c Color, thickness float32) {
C.al_draw_polyline((*C.float)(unsafe.Pointer(&vertices[0])), 8, C.int(len(vertices)>>1), C.ALLEGRO_LINE_JOIN_ROUND, C.ALLEGRO_LINE_CAP_ROUND, c.color, C.float(thickness), 1)
}
func DrawRectangle(x1, y1, x2, y2 float32, c Color, thickness float32) {
C.al_draw_rectangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), c.color, C.float(thickness))
}
func DrawTexture(x1, y1, x2, y2, x3, y3, x4, y4 float32, b *Bitmap) {
var vtxs [4]C.ALLEGRO_VERTEX
var white = NewColor(255, 255, 255)
vtxs[0].x = C.float(x1)
vtxs[0].y = C.float(y1)
vtxs[0].u = 0
vtxs[0].v = 0
vtxs[0].color = white.color
vtxs[1].x = C.float(x2)
vtxs[1].y = C.float(y2)
vtxs[1].u = C.float(b.width)
vtxs[1].v = 0
vtxs[1].color = white.color
vtxs[2].x = C.float(x4)
vtxs[2].y = C.float(y4)
vtxs[2].u = 0
vtxs[2].v = C.float(b.height)
vtxs[2].color = white.color
vtxs[3].x = C.float(x3)
vtxs[3].y = C.float(y3)
vtxs[3].u = C.float(b.width)
vtxs[3].v = C.float(b.height)
vtxs[3].color = white.color
C.al_draw_prim(unsafe.Pointer(&vtxs), nil, b.bitmap, 0, 4, C.ALLEGRO_PRIM_TRIANGLE_STRIP)
}
func DrawTextureProj(x1, y1, x2, y2, x3, y3, x4, y4 float32, b *Bitmap) {
var w, h = float32(b.width), float32(b.height)
var proj = lin.Projection32(
geom.PtF32(0, 0), geom.PtF32(x1, y1),
geom.PtF32(w, 0), geom.PtF32(x2, y2),
geom.PtF32(w, h), geom.PtF32(x3, y3),
geom.PtF32(0, h), geom.PtF32(x4, y4))
var w05, h05 = .5 * w, .5 * h
var pt0 = lin.Project32(proj, geom.PtF32(w05, h05))
var pt2 = lin.Project32(proj, geom.PtF32(w05, 0))
var pt4 = lin.Project32(proj, geom.PtF32(w, h05))
var pt6 = lin.Project32(proj, geom.PtF32(w05, h))
var pt8 = lin.Project32(proj, geom.PtF32(0, h05))
var cw, ch, cw05, ch05 = C.float(w), C.float(h), C.float(w05), C.float(h05)
var vtxs [10]C.ALLEGRO_VERTEX
var white = NewColor(255, 255, 255)
vtxs[0].x = C.float(pt0.X)
vtxs[0].y = C.float(pt0.Y)
vtxs[0].u = cw05
vtxs[0].v = ch05
vtxs[0].color = white.color
vtxs[1].x = C.float(x1)
vtxs[1].y = C.float(y1)
vtxs[1].u = 0
vtxs[1].v = 0
vtxs[1].color = white.color
vtxs[2].x = C.float(pt2.X)
vtxs[2].y = C.float(pt2.Y)
vtxs[2].u = cw05
vtxs[2].v = 0
vtxs[2].color = white.color
vtxs[3].x = C.float(x2)
vtxs[3].y = C.float(y2)
vtxs[3].u = cw
vtxs[3].v = 0
vtxs[3].color = white.color
vtxs[4].x = C.float(pt4.X)
vtxs[4].y = C.float(pt4.Y)
vtxs[4].u = cw
vtxs[4].v = ch05
vtxs[4].color = white.color
vtxs[5].x = C.float(x3)
vtxs[5].y = C.float(y3)
vtxs[5].u = cw
vtxs[5].v = ch
vtxs[5].color = white.color
vtxs[6].x = C.float(pt6.X)
vtxs[6].y = C.float(pt6.Y)
vtxs[6].u = cw05
vtxs[6].v = ch
vtxs[6].color = white.color
vtxs[7].x = C.float(x4)
vtxs[7].y = C.float(y4)
vtxs[7].u = 0
vtxs[7].v = ch
vtxs[7].color = white.color
vtxs[8].x = C.float(pt8.X)
vtxs[8].y = C.float(pt8.Y)
vtxs[8].u = 0
vtxs[8].v = ch05
vtxs[8].color = white.color
vtxs[9].x = C.float(x1)
vtxs[9].y = C.float(y1)
vtxs[9].u = 0
vtxs[9].v = 0
vtxs[9].color = white.color
C.al_draw_prim(unsafe.Pointer(&vtxs), nil, b.bitmap, 0, 10, C.ALLEGRO_PRIM_TRIANGLE_FAN)
// var lineC = NewColor(0, 255, 0)
// drawLine := func(p1, p2 geom.PointF32) {
// DrawLine(p1.X, p1.Y, p2.X, p2.Y, lineC, 2)
// }
// bounds := []geom.PointF32{geom.PtF32(x1, y1), pt2, geom.PtF32(x2, y2), pt4, geom.PtF32(x3, y3), pt6, geom.PtF32(x4, y4), pt8}
// for i, pt := range bounds {
// drawLine(pt, bounds[(i+1)%8])
// drawLine(pt0, pt)
// }
// var pointC = NewColor(255, 0, 0)
// drawPt := func(p geom.PointF32) {
// DrawFilledRectangle(p.X-2, p.Y-2, p.X+2, p.Y+2, pointC)
// }
// drawPt(pt0)
// drawPt(pt2)
// drawPt(pt4)
// drawPt(pt6)
// drawPt(pt8)
}
func DrawTriangle(x1, y1, x2, y2, x3, y3 float32, c Color, thickness float32) {
C.al_draw_triangle(C.float(x1), C.float(y1), C.float(x2), C.float(y2), C.float(x3), C.float(y3), c.color, C.float(thickness))
}

View File

@ -1,56 +0,0 @@
package allg5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_font.h>
// #include <allegro5/allegro_image.h>
// #include <allegro5/allegro_primitives.h>
// #include <allegro5/allegro_ttf.h>
// bool init() {
// return al_init();
// }
import "C"
import (
"errors"
"runtime"
)
func init() {
runtime.LockOSThread()
}
type InitConfig struct {
Font bool
Image bool
Primitives bool
Keyboard bool
Mouse bool
}
var InitAll = InitConfig{true, true, true, true, true}
// Init initializes the Allegro system
func Init(config InitConfig) error {
if !bool(C.init()) {
return errors.New("failed to initialize Allegro")
}
if config.Font && !bool(C.al_init_font_addon()) {
return errors.New("failed to initialize font addon")
}
if config.Font && !bool(C.al_init_ttf_addon()) {
return errors.New("failed to initialize ttf addon")
}
if config.Image && !bool(C.al_init_image_addon()) {
return errors.New("failed to initialize image addon")
}
if config.Primitives && !bool(C.al_init_primitives_addon()) {
return errors.New("failed to initialize primitives addon")
}
if config.Keyboard && !bool(C.al_install_keyboard()) {
return errors.New("failed to install keyboard")
}
if config.Mouse && !bool(C.al_install_mouse()) {
return errors.New("failed to install mouse")
}
return nil
}

View File

@ -1,8 +1,8 @@
package allg5ui
import (
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
)
type FontDefinition struct {
@ -15,32 +15,35 @@ func NewFontDefinition(name string, size int) FontDefinition {
}
type font struct {
f *allg5.Font
*allg5.Font
}
func newFont(f *allg5.Font) *font {
return &font{f}
}
func (f *font) Destroy() {
f.f.Destroy()
func (f *font) Destroy() error {
f.Font.Destroy()
return nil
}
func (f *font) Height() float32 {
if f == nil {
return 0
}
return f.f.Height()
return f.Font.Height()
}
func (f *font) WidthOf(t string) float32 {
return f.f.TextWidth(t)
return f.TextWidth(t)
}
func (f *font) Measure(t string) geom.RectangleF32 {
if f == nil {
return geom.RectangleF32{}
}
var x, y, w, h = f.f.TextDimensions(t)
return geom.RectF32(x, y, x+w, y+h)
// allg5.Font.TextDimentions (previous implementation) seems to return the closest fit rectangle to the drawn text (so depending on the glyphs). allg5.Font.TextWidth is giving the full width (not trimmed) which gives a better result together with allg5.Font.Height.
w := f.TextWidth(t)
h := f.Height()
return geom.RectRelF32(0, 0, w, h)
}

276
allg5ui/key.go Normal file
View File

@ -0,0 +1,276 @@
package allg5ui
import (
"opslag.de/schobers/allg5"
"opslag.de/schobers/zntg/ui"
)
func key(key allg5.Key) ui.Key {
switch key {
case allg5.Key0:
return ui.Key0
case allg5.Key1:
return ui.Key1
case allg5.Key2:
return ui.Key2
case allg5.Key3:
return ui.Key3
case allg5.Key4:
return ui.Key4
case allg5.Key5:
return ui.Key5
case allg5.Key6:
return ui.Key6
case allg5.Key7:
return ui.Key7
case allg5.Key8:
return ui.Key8
case allg5.Key9:
return ui.Key9
case allg5.KeyA:
return ui.KeyA
case allg5.KeyAlt:
return ui.KeyAlt
case allg5.KeyAltGr:
return ui.KeyAltGr
case allg5.KeyAt:
return ui.KeyAt
case allg5.KeyB:
return ui.KeyB
case allg5.KeyBack:
return ui.KeyBack
case allg5.KeyBackslash:
return ui.KeyBackslash
case allg5.KeyBackslash2:
return ui.KeyBackslash
case allg5.KeyBackspace:
return ui.KeyBackspace
case allg5.KeyBackQuote:
return ui.KeyBacktick
case allg5.KeyButtonA:
return ui.KeyButtonA
case allg5.KeyButtonB:
return ui.KeyButtonB
case allg5.KeyButtonL1:
return ui.KeyButtonL1
case allg5.KeyButtonL2:
return ui.KeyButtonL2
case allg5.KeyButtonR1:
return ui.KeyButtonR1
case allg5.KeyButtonR2:
return ui.KeyButtonR2
case allg5.KeyButtonX:
return ui.KeyButtonX
case allg5.KeyButtonY:
return ui.KeyButtonY
case allg5.KeyC:
return ui.KeyC
case allg5.KeyCapsLock:
return ui.KeyCapsLock
case allg5.KeyCircumflex:
return ui.KeyCircumflex
case allg5.KeyCloseBrace:
return ui.KeyCloseBrace
case allg5.KeyColon2:
return ui.KeyColon2
case allg5.KeyComma:
return ui.KeyComma
case allg5.KeyCommand:
return ui.KeyCommand
case allg5.KeyD:
return ui.KeyD
case allg5.KeyDelete:
return ui.KeyDelete
case allg5.KeyDown:
return ui.KeyDown
case allg5.KeyDPadCenter:
return ui.KeyDPadCenter
case allg5.KeyDPadDown:
return ui.KeyDPadDown
case allg5.KeyDPadLeft:
return ui.KeyDPadLeft
case allg5.KeyDPadRight:
return ui.KeyDPadRight
case allg5.KeyDPadUp:
return ui.KeyDPadUp
case allg5.KeyE:
return ui.KeyE
case allg5.KeyEnd:
return ui.KeyEnd
case allg5.KeyEnter:
return ui.KeyEnter
case allg5.KeyEquals:
return ui.KeyEquals
case allg5.KeyEscape:
return ui.KeyEscape
case allg5.KeyF:
return ui.KeyF
case allg5.KeyF1:
return ui.KeyF1
case allg5.KeyF2:
return ui.KeyF2
case allg5.KeyF3:
return ui.KeyF3
case allg5.KeyF4:
return ui.KeyF4
case allg5.KeyF5:
return ui.KeyF5
case allg5.KeyF6:
return ui.KeyF6
case allg5.KeyF7:
return ui.KeyF7
case allg5.KeyF8:
return ui.KeyF8
case allg5.KeyF9:
return ui.KeyF9
case allg5.KeyF10:
return ui.KeyF10
case allg5.KeyF11:
return ui.KeyF11
case allg5.KeyF12:
return ui.KeyF12
case allg5.KeyFullstop:
return ui.KeyFullstop
case allg5.KeyG:
return ui.KeyG
case allg5.KeyH:
return ui.KeyH
case allg5.KeyHome:
return ui.KeyHome
case allg5.KeyI:
return ui.KeyI
case allg5.KeyInsert:
return ui.KeyInsert
case allg5.KeyJ:
return ui.KeyJ
case allg5.KeyK:
return ui.KeyK
case allg5.KeyL:
return ui.KeyL
case allg5.KeyLeft:
return ui.KeyLeft
case allg5.KeyLCtrl:
return ui.KeyLeftControl
case allg5.KeyLShift:
return ui.KeyLeftShift
case allg5.KeyLWin:
return ui.KeyLeftWin
case allg5.KeyM:
return ui.KeyM
case allg5.KeyMenu:
return ui.KeyMenu
case allg5.KeyMinus:
return ui.KeyMinus
case allg5.KeyN:
return ui.KeyN
case allg5.KeyNumLock:
return ui.KeyNumLock
case allg5.KeyO:
return ui.KeyO
case allg5.KeyOpenBrace:
return ui.KeyOpenBrace
case allg5.KeyP:
return ui.KeyP
case allg5.KeyPad0:
return ui.KeyPad0
case allg5.KeyPad1:
return ui.KeyPad1
case allg5.KeyPad2:
return ui.KeyPad2
case allg5.KeyPad3:
return ui.KeyPad3
case allg5.KeyPad4:
return ui.KeyPad4
case allg5.KeyPad5:
return ui.KeyPad5
case allg5.KeyPad6:
return ui.KeyPad6
case allg5.KeyPad7:
return ui.KeyPad7
case allg5.KeyPad8:
return ui.KeyPad8
case allg5.KeyPad9:
return ui.KeyPad9
case allg5.KeyPadAsterisk:
return ui.KeyPadAsterisk
case allg5.KeyPadDelete:
return ui.KeyPadDelete
case allg5.KeyPadEnter:
return ui.KeyPadEnter
case allg5.KeyPadEquals:
return ui.KeyPadEquals
case allg5.KeyPadMinus:
return ui.KeyPadMinus
case allg5.KeyPadPlus:
return ui.KeyPadPlus
case allg5.KeyPadSlash:
return ui.KeyPadSlash
case allg5.KeyPageDown:
return ui.KeyPageDown
case allg5.KeyPageUp:
return ui.KeyPageUp
case allg5.KeyPause:
return ui.KeyPause
case allg5.KeyPrintScreen:
return ui.KeyPrintScreen
case allg5.KeyQ:
return ui.KeyQ
case allg5.KeyQuote:
return ui.KeyQuote
case allg5.KeyR:
return ui.KeyR
case allg5.KeyRight:
return ui.KeyRight
case allg5.KeyRCtrl:
return ui.KeyRightControl
case allg5.KeyRShift:
return ui.KeyRightShift
case allg5.KeyRWin:
return ui.KeyRightWin
case allg5.KeyS:
return ui.KeyS
case allg5.KeyScrollLock:
return ui.KeyScrollLock
case allg5.KeySearch:
return ui.KeySearch
case allg5.KeySelect:
return ui.KeySelect
case allg5.KeySemicolon:
return ui.KeySemicolon
case allg5.KeySlash:
return ui.KeySlash
case allg5.KeySpace:
return ui.KeySpace
case allg5.KeyStart:
return ui.KeyStart
case allg5.KeyT:
return ui.KeyT
case allg5.KeyTab:
return ui.KeyTab
case allg5.KeyThumbL:
return ui.KeyThumbL
case allg5.KeyThumbR:
return ui.KeyThumbR
case allg5.KeyTilde:
return ui.KeyTilde
case allg5.KeyU:
return ui.KeyU
case allg5.KeyUp:
return ui.KeyUp
case allg5.KeyV:
return ui.KeyV
case allg5.KeyVolumeDown:
return ui.KeyVolumeDown
case allg5.KeyVolumeUp:
return ui.KeyVolumeUp
case allg5.KeyW:
return ui.KeyW
case allg5.KeyX:
return ui.KeyX
case allg5.KeyY:
return ui.KeyY
case allg5.KeyZ:
return ui.KeyZ
}
return ui.KeyNone
}

458
allg5ui/renderer.go Normal file
View File

@ -0,0 +1,458 @@
package allg5ui
import (
"image"
"image/color"
"math"
"unicode"
"opslag.de/schobers/zntg"
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
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 {
return nil, err
}
clean = clean.Add(eq.Destroy)
user := allg5.NewUserEventSource()
eq.RegisterKeyboard()
eq.RegisterMouse()
eq.RegisterDisplay(disp)
eq.RegisterUserEvents(user)
allg5.CaptureNewBitmapFlags().Mutate(func(m allg5.FlagMutation) {
m.Set(allg5.NewBitmapFlagMinLinear)
})
clean = nil
return &Renderer{disp, eq, nil, user, &ui.OSResources{}, dispPos(disp), 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)
user *allg5.UserEventSource
res ui.PhysicalResources
dispPos geom.Point
keys ui.KeyState
modifiers ui.KeyModifier
cursor ui.MouseCursor
}
// Renderer implementation (events)
func (r *Renderer) PushEvents(t ui.EventTarget, wait bool) bool {
r.disp.Flip()
var ev = eventWait(r.eq, wait)
if ev == nil {
return false
}
cursor := r.cursor
r.cursor = ui.MouseCursorDefault
var unhandled bool
for ev != nil {
switch e := ev.(type) {
case *allg5.DisplayCloseEvent:
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
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:
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:
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseEnterEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseLeaveEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseMoveEvent:
t.Handle(&ui.MouseMoveEvent{MouseEvent: mouseEvent(e.MouseEvent), MouseWheel: float32(e.DeltaZ)})
case *allg5.UserEvent:
t.Handle(&ui.RefreshEvent{EventBase: eventBase(e)})
default:
if r.unh != nil {
r.unh(e)
}
unhandled = true
}
ev = r.eq.Get()
}
dispPos := dispPos(r.disp)
if dispPos != r.dispPos {
r.dispPos = dispPos
w := r.disp.Width()
h := r.disp.Height()
t.Handle(&ui.DisplayMoveEvent{Bounds: r.dispPos.RectRel2D(w, h).ToF32()})
}
if !unhandled && cursor != r.cursor {
switch r.cursor {
case ui.MouseCursorNone:
r.disp.SetMouseCursor(allg5.MouseCursorNone)
case ui.MouseCursorDefault:
r.disp.SetMouseCursor(allg5.MouseCursorDefault)
case ui.MouseCursorNotAllowed:
r.disp.SetMouseCursor(allg5.MouseCursorUnavailable)
case ui.MouseCursorPointer:
r.disp.SetMouseCursor(allg5.MouseCursorLink)
case ui.MouseCursorText:
r.disp.SetMouseCursor(allg5.MouseCursorEdit)
}
}
return true
}
func (r *Renderer) RegisterRecorder(rec *allg5.Recorder) {
r.eq.RegisterRecorder(rec)
}
func (r *Renderer) Refresh() {
r.user.EmitEvent()
}
func (r *Renderer) Stamp() float64 {
return allg5.GetTime()
}
// Renderer implementation (lifetime)
func (r *Renderer) Destroy() error {
r.user.Destroy()
r.eq.Destroy()
r.disp.Destroy()
r.res.Destroy()
return nil
}
// Renderer implementation (drawing)
func (r *Renderer) Clear(c color.Color) {
allg5.ClearToColor(newColor(c))
}
func (r *Renderer) CreateFontPath(path string, size int) (ui.Font, error) {
path, err := r.res.FetchResource(path)
if err != nil {
return nil, err
}
f, err := allg5.LoadTTFFont(path, size)
if err != nil {
return nil, err
}
return &font{f}, nil
}
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (*texture, error) {
im, err := source.CreateImage()
if err != nil {
return nil, err
}
bmp, err := allg5.NewBitmapFromImage(im, true)
if err != nil {
return nil, err
}
if keepSource {
return &texture{bmp, source}, nil
}
return &texture{bmp, nil}, nil
}
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.ImageSourceGo{Image: im}, source)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
resourcePath, err := r.res.FetchResource(path)
if err != nil {
return nil, err
}
bmp, err := allg5.LoadBitmap(resourcePath)
if err != nil {
return nil, err
}
if source {
return &texture{bmp, ui.ImageSourceResource{Resources: r.res, Name: path}}, nil
}
return &texture{bmp, nil}, nil
}
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}, nil
}
func (r *Renderer) DefaultTarget() ui.Texture {
return &texture{r.disp.Target(), nil}
}
func (r *Renderer) Display() *allg5.Display { return r.disp }
func (r *Renderer) DrawTexture(texture ui.Texture, p geom.RectangleF32) {
r.DrawTextureOptions(texture, p, ui.DrawOptions{})
}
func (r *Renderer) DrawTextureOptions(texture ui.Texture, p geom.RectangleF32, opts ui.DrawOptions) {
bmp, ok := r.mustGetSubBitmap(texture, opts.Source)
if ok {
defer bmp.Destroy()
}
x, y := snap(p.Min)
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
w, h := p.Dx(), p.Dy()
bmpW, bmpH := float32(bmp.Width()), float32(bmp.Height())
if w != bmpW || h != bmpH {
o.Scale = &allg5.Scale{Horizontal: w / bmpW, Vertical: h / bmpH}
}
bmp.DrawOptions(x, y, o)
}
func (r *Renderer) DrawTexturePoint(texture ui.Texture, p geom.PointF32) {
bmp := r.mustGetBitmap(texture)
x, y := snap(p)
bmp.Draw(x, y)
}
func (r *Renderer) DrawTexturePointOptions(texture ui.Texture, p geom.PointF32, opts ui.DrawOptions) {
bmp, ok := r.mustGetSubBitmap(texture, opts.Source)
if ok {
defer bmp.Destroy()
}
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
x, y := snap(p)
bmp.DrawOptions(x, y, o)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
allg5.DrawFilledRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c))
}
func (r *Renderer) Line(p, q geom.PointF32, color color.Color, thickness float32) {
allg5.DrawLine(p.X, p.Y, q.X, q.Y, newColor(color), thickness)
}
func (r *Renderer) Location() geom.Point {
x, y := r.disp.Position()
return geom.Pt(x, y)
}
func (r *Renderer) Move(to geom.Point) {
r.disp.SetPosition(to.X, to.Y)
}
func (r *Renderer) mustGetBitmap(t ui.Texture) *allg5.Bitmap {
texture, ok := t.(*texture)
if !ok {
panic("texture must be created on same renderer")
}
return texture.bmp
}
func (r *Renderer) mustGetSubBitmap(t ui.Texture, source *geom.RectangleF32) (*allg5.Bitmap, bool) {
texture, ok := t.(*texture)
if !ok {
panic("texture must be created on same renderer")
}
if source == nil {
return texture.bmp, false
}
src := source.ToInt()
return texture.bmp.Sub(src.Min.X, src.Min.Y, src.Dx(), src.Dy()), true
}
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
minX, minY := snap(rect.Min)
maxX, maxY := snap(rect.Max)
allg5.DrawRectangle(minX, minY, maxX, maxY, newColor(c), thickness)
}
func (r *Renderer) RenderTo(texture ui.Texture) {
bmp := r.mustGetBitmap(texture)
bmp.SetAsTarget()
}
func (r *Renderer) RenderToDisplay() {
r.disp.SetAsTarget()
}
func (r *Renderer) Resize(width, height int) {
r.disp.Resize(width, height)
}
func (r *Renderer) Resources() ui.Resources { return r.res }
func (r *Renderer) Size() geom.Point {
return geom.Pt(r.disp.Width(), r.disp.Height())
}
func (r *Renderer) SetIcon(source ui.ImageSource) {
texture, err := r.createTexture(source, false)
if err != nil {
return
}
defer texture.Destroy()
r.disp.SetIcon(texture.bmp)
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.cursor = c
}
func (r *Renderer) SetPosition(p geom.PointF32) { r.disp.SetPosition(int(p.X), int(p.Y)) }
func (r *Renderer) SetResourceProvider(res ui.Resources) {
if r.res != nil {
r.res.Destroy()
}
if phys, ok := res.(ui.PhysicalResources); ok {
r.res = phys
} else {
copy, err := ui.NewCopyResources("allg5ui", res, false)
if err != nil {
return
}
r.res = copy
}
}
func (r *Renderer) SetUnhandledEventHandler(handler func(allg5.Event)) {
r.unh = handler
}
func (r *Renderer) SetWindowTitle(t string) {
r.disp.SetWindowTitle(t)
}
func (r *Renderer) Target() ui.Texture {
return &texture{allg5.CurrentTarget(), nil}
}
func (r *Renderer) text(f ui.Font, p geom.PointF32, c color.Color, t string, align allg5.HorizontalAlignment) {
font, ok := f.(*font)
if !ok {
return
}
x, y := snap(p)
font.Draw(x, y, newColor(c), align, t)
}
func (r *Renderer) Text(font ui.Font, p geom.PointF32, c color.Color, t string) {
r.text(font, p, c, t, allg5.AlignLeft)
}
func (r *Renderer) TextAlign(font ui.Font, p geom.PointF32, c color.Color, t string, align ui.HorizontalAlignment) {
var alignment = allg5.AlignLeft
switch align {
case ui.AlignCenter:
alignment = allg5.AlignCenter
case ui.AlignRight:
alignment = allg5.AlignRight
}
r.text(font, p, c, t, alignment)
}
func (r *Renderer) TextTexture(font ui.Font, color color.Color, text string) (ui.Texture, error) {
return ui.TextTexture(r, font, color, text)
}
// Utility functions
func eventWait(eq *allg5.EventQueue, wait bool) allg5.Event {
if wait {
return eq.GetWait()
}
return eq.Get()
}
func eventBase(e allg5.Event) ui.EventBase {
return ui.EventBase{StampInSeconds: e.Stamp()}
}
func keyModifiers(mods allg5.KeyMod) ui.KeyModifier {
var m ui.KeyModifier
if mods&allg5.KeyModShift == allg5.KeyModShift {
m |= ui.KeyModifierShift
} else if mods&allg5.KeyModCtrl == allg5.KeyModCtrl {
m |= ui.KeyModifierControl
} else if mods&allg5.KeyModAlt == allg5.KeyModAlt {
m |= ui.KeyModifierAlt
}
return m
}
func mouseEvent(e allg5.MouseEvent) ui.MouseEvent {
return ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}
}
func newColor(c color.Color) allg5.Color {
if c == nil {
return newColor(color.Black)
}
return allg5.NewColorGo(c)
}
func snap(p geom.PointF32) (float32, float32) {
return float32(math.Round(float64(p.X))), float32(math.Round(float64(p.Y)))
}
func dispPos(disp *allg5.Display) geom.Point {
x, y := disp.Position()
return geom.Pt(x, y)
}

View File

@ -0,0 +1,5 @@
// +build !windows
package allg5ui
func (r *Renderer) WindowHandle() uintptr { return 0 }

View File

@ -0,0 +1,7 @@
// +build windows
package allg5ui
func (r *Renderer) WindowHandle() uintptr {
return uintptr(r.disp.WindowHandle())
}

View File

@ -0,0 +1,28 @@
package allg5ui
import (
"opslag.de/schobers/allg5"
"opslag.de/schobers/zntg/ui"
)
func init() {
ui.SetRendererFactory(&rendererFactory{})
}
type rendererFactory struct{}
func (f rendererFactory) New(title string, width, height int, opts ui.NewRendererOptions) (ui.Renderer, error) {
renderer, err := NewRenderer(width, height, allg5.NewDisplayOptions{
Frameless: opts.Borderless,
Resizable: opts.Resizable,
Vsync: opts.VSync,
})
if err != nil {
return nil, err
}
renderer.SetWindowTitle(title)
if opts.Location != nil {
renderer.SetPosition(*opts.Location)
}
return renderer, nil
}

36
allg5ui/texture.go Normal file
View File

@ -0,0 +1,36 @@
package allg5ui
import (
"image"
"opslag.de/schobers/allg5"
"opslag.de/schobers/zntg/ui"
)
var _ ui.Texture = &texture{}
var _ ui.ImageSource = &texture{}
type texture struct {
bmp *allg5.Bitmap
source ui.ImageSource
}
func (t *texture) Destroy() error {
t.bmp.Destroy()
return nil
}
func (t *texture) Height() int {
return t.bmp.Height()
}
func (t *texture) CreateImage() (image.Image, error) {
if t.source == nil {
return t.bmp.Image(), nil
}
return t.source.CreateImage()
}
func (t *texture) Width() int {
return t.bmp.Width()
}

81
animation.go Normal file
View File

@ -0,0 +1,81 @@
package zntg
import "time"
// Animation is a struct that keeps track of time for animation.
type Animation struct {
// The interval of the animation
Interval time.Duration
active bool
start time.Time
lastUpdate time.Duration
}
// NewAnimation creates an Animation given the specified interval. The animation is immediately started.
func NewAnimation(interval time.Duration) Animation {
return Animation{
Interval: interval,
active: true,
start: time.Now(),
}
}
// NewAnimationPtr creates an Animation given the specified interval and returns a pointer to it. The animation is immediately started.
func NewAnimationPtr(interval time.Duration) *Animation {
ani := NewAnimation(interval)
return &ani
}
// Animate checks if enough time as elapsed to animate a single interval and advances the single interval.
func (a *Animation) Animate() bool {
since := time.Since(a.start)
if !a.active || since < a.lastUpdate+a.Interval {
return false
}
a.lastUpdate += a.Interval
return true
}
// AnimateDelta checks how many natural intervals have elapsed and advances that many intervals. Returns the total of time that has been advanced and the number of intervals.
func (a *Animation) AnimateDelta() (time.Duration, int) {
if !a.active {
return 0, 0
}
since := time.Since(a.start)
n := (since - a.lastUpdate) / a.Interval
delta := n * a.Interval
a.lastUpdate += delta
return delta, int(n)
}
// AnimateFn calls fn for every interval and advances that interval for every interval that has elapsed until it caught up again.
func (a *Animation) AnimateFn(fn func()) {
if !a.active {
return
}
since := time.Since(a.start)
for a.active && since > a.lastUpdate+a.Interval {
fn()
a.lastUpdate += a.Interval
}
}
// Pause pauses the animation causing the Animate{,Delta,Fn} methods to do nothing.
func (a *Animation) Pause() {
a.active = false
}
// IsActive returns true when the animation is started (and false when it either was never started or paused)
func (a *Animation) IsActive() bool { return a.active }
// Start starts the animation (when paused or not started yet).
func (a *Animation) Start() {
if a.active {
return
}
a.active = true
a.start = time.Now()
a.lastUpdate = 0
}

74
color.go Normal file
View File

@ -0,0 +1,74 @@
package zntg
import (
"errors"
"image/color"
"regexp"
)
var hexColorRE = regexp.MustCompile(`^#?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})?$`)
// HexColor parses hexadecimal string s and returns the RGBA color. Accepted are 3 or 4 components (R, G, B, A) following an optional hash character '#'.
func HexColor(s string) (color.RGBA, error) {
match := hexColorRE.FindStringSubmatch(s)
if match == nil {
return color.RGBA{}, errors.New("invalid color format")
}
values, err := HexToInts(match[1:]...)
if err != nil {
return color.RGBA{}, err
}
a := 255
if len(match[4]) > 0 {
a = values[3]
}
return RGBA(uint8(values[0]), uint8(values[1]), uint8(values[2]), uint8(a)), nil
}
// HexToInt tries to convert a string with hexadecimal characters to an integer.
func HexToInt(s string) (int, error) {
var i int
for _, c := range s {
i *= 16
if c >= '0' && c <= '9' {
i += int(c - '0')
} else if c >= 'A' && c <= 'F' {
i += int(c - 'A' + 10)
} else if c >= 'a' && c <= 'f' {
i += int(c - 'a' + 10)
} else {
return 0, errors.New("hex digit not supported")
}
}
return i, nil
}
// HexToInts tries to convert multiple strings with hexadecimal characters to a slice of integers.
func HexToInts(s ...string) ([]int, error) {
ints := make([]int, len(s))
for i, s := range s {
value, err := HexToInt(s)
if err != nil {
return nil, err
}
ints[i] = value
}
return ints, nil
}
// MustHexColor parses hexadecimal string s and returns the RGBA color. If the conversion fails it panics.
func MustHexColor(s string) color.RGBA {
color, err := HexColor(s)
if err != nil {
panic(err)
}
return color
}
// RGB creates an opaque color with the specified red, green and red values.
func RGB(r, g, b byte) color.RGBA { return RGBA(r, g, b, 0xff) }
// RGB creates a color with the specified red, green, red and alpha values.
func RGBA(r, g, b, a byte) color.RGBA {
return color.RGBA{R: r, G: g, B: b, A: a}
}

33
color_test.go Normal file
View File

@ -0,0 +1,33 @@
package zntg
import (
"image/color"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHexColor(t *testing.T) {
type test struct {
Hex string
Success bool
Expected color.RGBA
}
tests := []test{
test{Hex: "#AA3939", Success: true, Expected: color.RGBA{R: 170, G: 57, B: 57, A: 255}},
test{Hex: "AA3939", Success: true, Expected: color.RGBA{R: 170, G: 57, B: 57, A: 255}},
test{Hex: "#AA3939BB", Success: true, Expected: color.RGBA{R: 170, G: 57, B: 57, A: 187}},
test{Hex: "AG3939", Success: false, Expected: color.RGBA{}},
test{Hex: "AA3939A", Success: false, Expected: color.RGBA{}},
test{Hex: "AA39A", Success: false, Expected: color.RGBA{}},
}
for _, test := range tests {
color, err := HexColor(test.Hex)
if test.Success {
assert.Nil(t, err)
assert.Equal(t, color, test.Expected)
} else {
assert.NotNil(t, err)
}
}
}

46
events.go Normal file
View File

@ -0,0 +1,46 @@
package zntg
type EventEmptyFn func()
type EventFn func(interface{})
func NewEvents() *Events {
return &Events{events: map[uint]EventFn{}}
}
type Events struct {
nextID uint
events map[uint]EventFn
}
type EventHandler interface {
AddHandler(EventFn) uint
AddHandlerEmpty(EventEmptyFn) uint
RemoveHandler(uint)
}
func (e *Events) Notify(state interface{}) bool {
if e.events == nil {
return false
}
for _, handler := range e.events {
handler(state)
}
return len(e.events) > 0
}
func (e *Events) AddHandler(handler EventFn) uint {
if e.events == nil {
e.events = map[uint]EventFn{}
}
id := e.nextID
e.nextID++
e.events[id] = handler
return id
}
func (e *Events) AddHandlerEmpty(handler EventEmptyFn) uint {
return e.AddHandler(func(interface{}) { handler() })
}
func (e *Events) RemoveHandler(id uint) { delete(e.events, id) }

22
go.mod Normal file
View File

@ -0,0 +1,22 @@
module opslag.de/schobers/zntg
go 1.20
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/atotto/clipboard v0.1.4
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/spf13/afero v1.9.5
github.com/stretchr/testify v1.8.4
github.com/veandco/go-sdl2 v0.4.35
golang.org/x/text v0.10.0
opslag.de/schobers/allg5 v0.0.0-20220501103818-24f2f9691c81
opslag.de/schobers/geom v0.0.0-20210808233716-e01aa3242dc8
)
require (
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

468
go.sum Normal file
View File

@ -0,0 +1,468 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/veandco/go-sdl2 v0.4.35 h1:NohzsfageDWGtCd9nf7Pc3sokMK/MOK+UA2QMJARWzQ=
github.com/veandco/go-sdl2 v0.4.35/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
opslag.de/schobers/allg5 v0.0.0-20220501103818-24f2f9691c81 h1:Zfa0GMHLmu4ZvzBp/fUtLchKQbKCMfiqid+DoQPWvOI=
opslag.de/schobers/allg5 v0.0.0-20220501103818-24f2f9691c81/go.mod h1:QAMuvQKxgb3NMghDp8w9mxROxXg9c/skwUQII+2AvRs=
opslag.de/schobers/geom v0.0.0-20210808233716-e01aa3242dc8 h1:Mfni39F8c/ix6fJdEwaZQSprBRxz3E9L61E5KnE6Kx4=
opslag.de/schobers/geom v0.0.0-20210808233716-e01aa3242dc8/go.mod h1:zkcRVIMwRHtxqb2qP2XZ0neyVFdX1tc24PObzuBL9IE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

187
io.go Normal file
View File

@ -0,0 +1,187 @@
package zntg
import (
"encoding/json"
"image"
_ "image/gif" // decoding of GIF
_ "image/jpeg" // decoding of JPEG
"image/png"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// CreateJSONDecoder create a new a generic JSON decoder for the specified value.
func CreateJSONDecoder(value interface{}) DecoderFn {
return func(r io.Reader) (interface{}, error) {
decoder := json.NewDecoder(r)
return value, decoder.Decode(value)
}
}
// DecoderFn describes a generic decoder.
type DecoderFn func(io.Reader) (interface{}, error)
// DecodeFile is a generic decode method to decode content from disk.
func DecodeFile(path string, decoder func(io.Reader) (interface{}, error)) (interface{}, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return decoder(f)
}
// DecodeImage tries to decode a file to an image.
func DecodeImage(path string) (image.Image, error) {
value, err := DecodeFile(path, ImageDecoder)
if err != nil {
return nil, err
}
return value.(image.Image), nil
}
// DecodeJSON tries to decode a file to specifed value.
func DecodeJSON(path string, value interface{}) error {
_, err := DecodeFile(path, CreateJSONDecoder(value))
return err
}
// Dir is a convenience struct for representing a path to a directory.
type Dir struct {
Path string
}
// FilePath returns the path of a file with the specified name in the directory.
func (d *Dir) FilePath(name string) string {
return filepath.Join(d.Path, name)
}
// Write writes the content of the reader into a file with the specified name.
func (d *Dir) Write(name string, r io.Reader) error {
path := d.FilePath(name)
dir := filepath.Dir(path)
os.MkdirAll(dir, 0777)
return EncodeFile(path, r, CopyReader)
}
// Destroy removes the directory.
func (d *Dir) Destroy() error {
return os.RemoveAll(d.Path)
}
// EncoderFn describes a generic encoder.
type EncoderFn func(io.Writer, interface{}) error
// EncodeFile is a generic encode method to encode content to disk.
func EncodeFile(path string, value interface{}, encoder EncoderFn) error {
dst, err := os.Create(path)
if err != nil {
return err
}
defer dst.Close()
return encoder(dst, value)
}
// EncodeJSON encodes a JSON struct to a file.
func EncodeJSON(path string, value interface{}) error {
return EncodeFile(path, value, JSONEncoder)
}
// EncodePNG encodes an image to a file.
func EncodePNG(path string, im image.Image) error {
return EncodeFile(path, im, PNGEncoder)
}
// FileExists returns if file exists on specified path.
func FileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
var _ DecoderFn = ImageDecoder
// ImageDecoder is a generic image decoder.
func ImageDecoder(r io.Reader) (interface{}, error) {
im, _, err := image.Decode(r)
return im, err
}
var _ EncoderFn = JSONEncoder
// JSONEncoder is a generic JSON encoder.
func JSONEncoder(w io.Writer, value interface{}) error {
encoder := json.NewEncoder(w)
return encoder.Encode(value)
}
var _ EncoderFn = PNGEncoder
// PNGEncoder is a generic PNG encoder.
func PNGEncoder(w io.Writer, value interface{}) error { return png.Encode(w, value.(image.Image)) }
// NewTempDir creates a temporary directory.
func NewTempDir(prefix string) (*Dir, error) {
path, err := ioutil.TempDir("", prefix)
if nil != err {
return nil, err
}
return &Dir{path}, nil
}
// UserCacheDir gives back the user configuration directory with given name.
func UserCacheDir(name string) (string, error) {
config, err := os.UserCacheDir()
if err != nil {
return "", err
}
dir := filepath.Join(config, name)
err = os.MkdirAll(dir, 0777)
if err != nil {
return "", err
}
return dir, nil
}
// UserCacheFile gives back the path to the file with the specified name and the directory named app.
func UserCacheFile(app, name string) (string, error) {
dir, err := UserCacheDir(app)
if err != nil {
return "", err
}
return filepath.Join(dir, name), nil
}
// UserConfigDir gives back the user configuration directory with given name.
func UserConfigDir(name string) (string, error) {
config, err := os.UserConfigDir()
if err != nil {
return "", err
}
dir := filepath.Join(config, name)
err = os.MkdirAll(dir, 0777)
if err != nil {
return "", err
}
return dir, nil
}
// UserConfigFile gives back the path to the file with the specified name and the directory named app.
func UserConfigFile(app, name string) (string, error) {
dir, err := UserConfigDir(app)
if err != nil {
return "", err
}
return filepath.Join(dir, name), nil
}
var _ EncoderFn = CopyReader
// CopyReader copies the provided value to the output.
func CopyReader(w io.Writer, value interface{}) error {
_, err := io.Copy(w, value.(io.Reader))
return err
}

71
play/fps.go Normal file
View File

@ -0,0 +1,71 @@
package play
import (
"fmt"
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
type FPS struct {
ui.ControlBase
Align ui.HorizontalAlignment
update zntg.Animation
i int
frames []int
total int
}
func (f *FPS) Shown() {
f.update.Interval = 20 * time.Millisecond
f.update.Start()
f.i = 0
f.frames = make([]int, 51)
f.total = 0
}
func (f *FPS) Hidden() {
f.update.Pause()
}
func (f *FPS) font(ctx ui.Context) ui.Font {
font := ctx.Fonts().Font("debug")
if font != nil {
return font
}
return ctx.Fonts().Font("default")
}
func (f *FPS) Render(ctx ui.Context) {
_, n := f.update.AnimateDelta()
for i := 0; i < n; i++ {
f.total += f.frames[f.i]
f.i = (f.i + 1) % len(f.frames)
f.total -= f.frames[f.i]
f.frames[f.i] = 0
}
f.frames[f.i]++
font := f.font(ctx)
fps := fmt.Sprintf("FPS: %d", f.total)
renderer := ctx.Renderer()
switch f.Align {
case ui.AlignLeft:
ctx.Renderer().Text(font, geom.PtF32(5, 5), ctx.Style().Palette.Background, fps)
ctx.Renderer().Text(font, geom.PtF32(4, 4), ctx.Style().Palette.Text, fps)
case ui.AlignCenter:
center := .5 * renderer.Size().ToF32().X
ctx.Renderer().TextAlign(font, geom.PtF32(center, 5), ctx.Style().Palette.Background, fps, ui.AlignCenter)
ctx.Renderer().TextAlign(font, geom.PtF32(center+1, 4), ctx.Style().Palette.Text, fps, ui.AlignCenter)
case ui.AlignRight:
right := renderer.Size().ToF32().X - 5
ctx.Renderer().TextAlign(font, geom.PtF32(right, 5), ctx.Style().Palette.Background, fps, ui.AlignRight)
ctx.Renderer().TextAlign(font, geom.PtF32(right+1, 4), ctx.Style().Palette.Text, fps, ui.AlignRight)
}
}

163
play/isometricprojection.go Normal file
View File

@ -0,0 +1,163 @@
package play
import (
"opslag.de/schobers/geom"
)
// IsometricProjection represents an 2D area (view) that contains isometric tiles.
type IsometricProjection struct {
center geom.PointF32 // tile coordinate
zoom float32 // factor a tile is blown up (negative is smaller, possitive is larger)
zoomInverse float32 // 1/zoom; calculated
tileSize geom.PointF32 // size of a single tile (maximum width & height difference of its corners)
viewBounds geom.RectangleF32 // bounds of the view (screen coordinates)
viewCenter geom.PointF32 // center of view; calculated
tileSizeTransformed geom.PointF32 // calculated
tileToViewTransformation geom.PointF32 // calculated
viewToTileTransformation geom.PointF32 // calculated
}
// NewIsometricProjection creates a new isometric projection. By default the tile with the coordinate (0, 0) will be centered in the viewBounds. The tile size is represented with maximum width & height difference of its corners.
func NewIsometricProjection(tileSize geom.PointF32, viewBounds geom.RectangleF32) *IsometricProjection {
p := &IsometricProjection{zoom: 1, tileSize: tileSize, viewBounds: viewBounds}
p.update()
return p
}
func (p *IsometricProjection) update() {
if p.zoom == 0 {
p.zoom = 1
}
p.zoomInverse = 1 / p.zoom
p.viewCenter = p.viewBounds.Center()
p.tileSizeTransformed = p.tileSize.Mul(p.zoom)
p.tileToViewTransformation = p.tileSize.Mul(.5 * p.zoom)
p.viewToTileTransformation = geom.PtF32(1/p.tileSizeTransformed.X, 1/p.tileSizeTransformed.Y)
}
// Center gives back the coordinate of the center tile
func (p *IsometricProjection) Center() geom.PointF32 { return p.center }
// Enumerate enumerates all tiles in the set view bounds and calls action for every tile.
func (p *IsometricProjection) Enumerate(action func(tile geom.PointF32, view geom.PointF32)) {
p.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
action(tile.ToF32(), view)
})
}
// EnumerateInt enumerates all tiles in the set view bounds and calls action for every tile.
func (p *IsometricProjection) EnumerateInt(action func(tile geom.Point, view geom.PointF32)) {
visible := p.viewBounds
visible.Max.Y += p.tileSize.Y * p.zoom
topLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Min.Y))
topRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Min.Y))
bottomLeft := p.ViewToTile(geom.PtF32(visible.Min.X, visible.Max.Y))
bottomRight := p.ViewToTile(geom.PtF32(visible.Max.X, visible.Max.Y))
minY, maxY := int(geom.Floor32(topRight.Y)), int(geom.Ceil32(bottomLeft.Y))
minX, maxX := int(geom.Floor32(topLeft.X)), int(geom.Ceil32(bottomRight.X))
tileOffset := p.tileSizeTransformed.Mul(.5)
for y := minY; y <= maxY; y++ {
for x := minX; x <= maxX; x++ {
tile := geom.Pt(x, y)
view := p.TileToView(tile.ToF32())
if view.X+tileOffset.X < visible.Min.X || view.Y+tileOffset.Y < visible.Min.Y {
continue
}
if view.X-tileOffset.X > visible.Max.X || view.Y-tileOffset.Y > visible.Max.Y {
break
}
action(tile, view)
}
}
}
// MoveCenterTo moves the center of the projection to the given tile.
func (p *IsometricProjection) MoveCenterTo(tile geom.PointF32) {
p.center = tile
p.update()
}
// Pan translates the center of the projection with the given delta in view coordinates.
func (p *IsometricProjection) Pan(delta geom.PointF32) {
p.PanTile(p.ViewToTileRelative(delta))
}
// PanTile translates the center of the projection with the given delta in tile coordinates.
func (p *IsometricProjection) PanTile(delta geom.PointF32) {
p.MoveCenterTo(p.center.Add(delta))
}
// SetTileSize sets the size of a single tile (maximum width & height difference of its corners).
func (p *IsometricProjection) SetTileSize(size geom.PointF32) {
p.tileSize = size
p.update()
}
// SetViewBounds sets the bounds of the view coordinates. Used to calculate the center with & for calculating the visible tiles.
func (p *IsometricProjection) SetViewBounds(bounds geom.RectangleF32) {
p.viewBounds = bounds
p.update()
}
// SetZoom changes the zoom to and keeps the around (tile) coordinate on the same position.
func (p *IsometricProjection) SetZoom(around geom.PointF32, zoom float32) {
if p.zoom == zoom {
return
}
p.center = around.Sub(around.Sub(p.center).Mul(p.zoom / zoom))
p.zoom = zoom
p.update()
}
// TileInt gives the integer tile coordinate.
func (p *IsometricProjection) TileInt(tile geom.PointF32) geom.Point {
return geom.Pt(int(geom.Round32(tile.X)), int(geom.Round32(tile.Y)))
}
// TileToView transforms the tile coordinate to the corresponding view coordinate.
func (p *IsometricProjection) TileToView(tile geom.PointF32) geom.PointF32 {
translated := tile.Sub(p.center)
return p.viewCenter.Add2D((translated.X-translated.Y)*p.tileToViewTransformation.X, (translated.X+translated.Y)*p.tileToViewTransformation.Y)
}
// ViewCenter returns the center of the view (calculated from the set view bounds).
func (p *IsometricProjection) ViewCenter() geom.PointF32 { return p.viewCenter }
// ViewToTile transforms the view coordinate to the corresponding tile coordinate.
func (p *IsometricProjection) ViewToTile(view geom.PointF32) geom.PointF32 {
return p.ViewToTileRelative(view.Sub(p.viewCenter)).Add(p.center)
}
// ViewToTileInt transforms the view coordinate to the corresponding integer tile coordinate.
func (p *IsometricProjection) ViewToTileInt(view geom.PointF32) geom.Point {
tile := p.ViewToTile(view)
return p.TileInt(tile)
}
// ViewToTileRelative transforms the relative (to 0,0) view coordinate to the corresponding tile coordinate
func (p *IsometricProjection) ViewToTileRelative(view geom.PointF32) geom.PointF32 {
return geom.PtF32(view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y, -view.X*p.viewToTileTransformation.X+view.Y*p.viewToTileTransformation.Y)
}
// Zoom returns the current zoom.
func (p *IsometricProjection) Zoom() float32 { return p.zoom }
// ZoomIn zooms in around the given tile coordinate.
func (p *IsometricProjection) ZoomIn(around geom.PointF32) {
if p.zoom >= 2 {
return
}
p.SetZoom(around, 2*p.zoom)
}
// ZoomOut zooms in around the given tile coordinate.
func (p *IsometricProjection) ZoomOut(around geom.PointF32) {
if p.zoom <= .25 {
return
}
p.SetZoom(around, .5*p.zoom)
}

View File

@ -0,0 +1,36 @@
package play
import (
"testing"
"github.com/stretchr/testify/assert"
"opslag.de/schobers/geom"
)
func createIsometricProjection() *IsometricProjection {
return NewIsometricProjection(geom.PtF32(23, 11), geom.RectRelF32(0, 0, 160, 160))
}
func TestViewToTile(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.PtF32(0, 0), p.ViewToTile(geom.PtF32(80, 80)))
assert.Equal(t, geom.PtF32(-1, 1), p.ViewToTile(geom.PtF32(57, 80)))
assert.Equal(t, geom.PtF32(2, 2), p.ViewToTile(geom.PtF32(80, 102)))
assert.Equal(t, geom.PtF32(-1, -3), p.ViewToTile(geom.PtF32(103, 58)))
}
func TestViewToTileInt(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.Pt(0, 0), p.ViewToTileInt(geom.PtF32(80, 80)))
assert.Equal(t, geom.Pt(0, 0), p.ViewToTileInt(geom.PtF32(69, 80)))
assert.Equal(t, geom.Pt(-1, 1), p.ViewToTileInt(geom.PtF32(68, 80)))
}
func TestTileToView(t *testing.T) {
p := createIsometricProjection()
assert.Equal(t, geom.PtF32(80, 80), p.TileToView(geom.PtF32(0, 0)))
assert.Equal(t, geom.PtF32(57, 80), p.TileToView(geom.PtF32(-1, 1)))
assert.Equal(t, geom.PtF32(80, 102), p.TileToView(geom.PtF32(2, 2)))
assert.Equal(t, geom.PtF32(103, 58), p.TileToView(geom.PtF32(-1, -3)))
}

11
sdlui/color.go Normal file
View File

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

526
sdlui/events.go Normal file
View File

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

13
sdlui/events_test.go Normal file
View File

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

29
sdlui/font.go Normal file
View File

@ -0,0 +1,29 @@
package sdlui
import (
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/geom"
)
type Font struct {
*ttf.Font
}
func (f *Font) Destroy() error {
f.Font.Close()
return nil
}
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)
}

26
sdlui/image.go Normal file
View File

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

38
sdlui/rectangle.go Normal file
View File

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

565
sdlui/renderer.go Normal file
View File

@ -0,0 +1,565 @@
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
resources ui.PhysicalResources
mouse geom.PointF32
cursor ui.MouseCursor
cursors map[sdl.SystemCursor]*sdl.Cursor
}
var _ ui.Renderer = &Renderer{}
var _ ui.Texture = &Renderer{}
type NewRendererOptions struct {
Borderless bool
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.Borderless {
windowFlags |= sdl.WINDOW_BORDERLESS
}
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,
resources: &ui.OSResources{},
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) bool {
r.renderer.Present()
waitOrPoll := func() sdl.Event {
if wait {
return sdl.WaitEvent()
}
return sdl.PollEvent()
}
cursor := r.cursor
var pushed bool
for event := waitOrPoll(); event != nil; event = sdl.PollEvent() {
pushed = true
r.cursor = ui.MouseCursorDefault
var unhandled bool
switch e := event.(type) {
case *sdl.WindowEvent:
switch e.Event {
case sdl.WINDOWEVENT_CLOSE:
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
case sdl.WINDOWEVENT_MOVED:
t.Handle(&ui.DisplayMoveEvent{EventBase: eventBase(e), Bounds: r.WindowBounds()})
case sdl.WINDOWEVENT_RESIZED:
t.Handle(&ui.DisplayResizeEvent{EventBase: eventBase(e), Bounds: r.WindowBounds()})
case sdl.WINDOWEVENT_ENTER:
t.Handle(&ui.MouseEnterEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}})
case sdl.WINDOWEVENT_LEAVE:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}})
default:
unhandled = true
}
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:
r.mouse = geom.PtF32(float32(e.X), float32(e.Y))
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}, MouseWheel: 0})
case *sdl.MouseWheelEvent:
t.Handle(&ui.MouseMoveEvent{MouseEvent: ui.MouseEvent{EventBase: eventBase(e), X: r.mouse.X, Y: r.mouse.Y}, MouseWheel: float32(e.Y)})
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))
}
}
return pushed
}
func (r *Renderer) Refresh() {
windowID, _ := r.window.GetID()
e := &sdl.UserEvent{
Type: r.refresh,
WindowID: windowID,
}
sdl.PushEvent(e)
}
func (r *Renderer) Stamp() float64 {
return .001 * float64(sdl.GetTicks())
}
// Lifetime
func (r *Renderer) Destroy() error {
r.renderer.Destroy()
r.window.Destroy()
ttf.Quit()
sdl.Quit()
r.resources.Destroy()
return nil
}
// Drawing
func (r *Renderer) Clear(c color.Color) {
if c == color.Transparent {
return
}
r.SetDrawColorGo(c)
r.renderer.Clear()
}
func (r *Renderer) CreateFontPath(path string, size int) (ui.Font, error) {
path, err := r.resources.FetchResource(path)
if err != nil {
return nil, err
}
font, err := ttf.OpenFont(path, size)
if err != nil {
return nil, err
}
return &Font{font}, nil
}
func (r *Renderer) createSurface(source ui.ImageSource) (*sdl.Surface, error) {
m, err := source.CreateImage()
if err != nil {
return nil, err
}
rgba := NRGBAImage(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
}
return surface, nil
}
func (r *Renderer) createTexture(source ui.ImageSource, keepSource bool) (ui.Texture, error) {
surface, err := r.createSurface(source)
if err != nil {
return nil, err
}
defer surface.Free()
texture, err := r.renderer.CreateTextureFromSurface(surface)
if err != nil {
return nil, err
}
texture.SetBlendMode(sdl.BLENDMODE_BLEND)
if keepSource {
return &TextureImageSource{&Texture{texture}, source}, nil
}
return &Texture{texture}, nil
}
func (r *Renderer) createTextureTarget(w, h float32) (*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) 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.ImageSourceGo{Image: m}, source)
}
func (r *Renderer) CreateTexturePath(path string, source bool) (ui.Texture, error) {
return r.createTexture(ui.ImageSourceResource{Resources: r.resources, Name: path}, source)
}
func (r *Renderer) CreateTextureTarget(w, h float32) (ui.Texture, error) {
return r.createTextureTarget(w, h)
}
func (r *Renderer) DefaultTarget() ui.Texture { return r }
func (r *Renderer) drawTexture(t sdlTexture, src, dst sdl.Rect, opts ui.DrawOptions) {
if opts.Tint != nil {
t.SetColor(opts.Tint)
}
r.renderer.Copy(t.Native(), &src, &dst)
}
func (r *Renderer) DrawTexture(t ui.Texture, dst geom.RectangleF32) {
r.DrawTextureOptions(t, dst, ui.DrawOptions{})
}
func (r *Renderer) DrawTextureOptions(t ui.Texture, dst geom.RectangleF32, opts ui.DrawOptions) {
texture, ok := t.(sdlTexture)
if !ok {
return
}
var source sdl.Rect
if opts.Source != nil {
source = RectAbs(int32(opts.Source.Min.X), int32(opts.Source.Min.Y), int32(opts.Source.Max.X), int32(opts.Source.Max.Y))
} else {
width, height, err := texture.Size()
if err != nil {
return
}
source = Rect(0, 0, width, height)
}
r.drawTexture(texture, source, RectAbs(int32(dst.Min.X), int32(dst.Min.Y), int32(dst.Max.X), int32(dst.Max.Y)), opts)
}
func (r *Renderer) DrawTexturePoint(t ui.Texture, dst geom.PointF32) {
r.DrawTexturePointOptions(t, dst, ui.DrawOptions{})
}
func (r *Renderer) DrawTexturePointOptions(t ui.Texture, dst geom.PointF32, opts ui.DrawOptions) {
texture, ok := t.(sdlTexture)
if !ok {
return
}
var source, destination sdl.Rect
if opts.Source != nil {
source = RectAbs(int32(opts.Source.Min.X), int32(opts.Source.Min.Y), int32(opts.Source.Max.X), int32(opts.Source.Max.Y))
destination = Rect(int32(dst.X), int32(dst.Y), source.W, source.H)
} else {
width, height, err := texture.Size()
if err != nil {
return
}
source = Rect(0, 0, width, height)
destination = Rect(int32(dst.X), int32(dst.Y), width, height)
}
r.drawTexture(texture, source, destination, opts)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
r.SetDrawColorGo(c)
r.renderer.FillRect(SDLRectanglePtr(rect))
}
func (r *Renderer) Line(p, q geom.PointF32, color color.Color, thickness float32) {
r.SetDrawColorGo(color)
r.renderer.DrawLineF(p.X, p.Y, q.X, q.Y)
}
func (r *Renderer) Location() geom.Point {
x, y := r.window.GetPosition()
return geom.Pt(int(x), int(y))
}
func (r *Renderer) Move(to geom.Point) {
r.window.SetPosition(int32(to.X), int32(to.Y))
}
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) 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) Resize(width, height int) {
r.window.SetSize(int32(width), int32(height))
}
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) SetIcon(source ui.ImageSource) {
window := r.window
if window == nil {
return
}
surface, err := r.createSurface(source)
if err != nil {
return
}
defer surface.Free()
window.SetIcon(surface)
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) { r.cursor = c }
func (r *Renderer) Size() geom.Point {
w, h, err := r.renderer.GetOutputSize()
if err != nil {
return geom.ZeroPt
}
return geom.Pt(int(w), int(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(font ui.Font, color color.Color, text string) (*Texture, error) {
f, ok := font.(*Font)
if !ok {
return nil, errors.New("font not created with renderer")
}
surface, err := f.RenderUTF8Blended(text, ColorSDL(color))
if err != nil {
return nil, err
}
defer surface.Free()
texture, err := r.renderer.CreateTextureFromSurface(surface)
if err != nil {
return nil, err
}
return &Texture{texture}, nil
}
func (r *Renderer) Text(font ui.Font, p geom.PointF32, color color.Color, text string) {
texture, err := r.text(font, color, text)
if err != nil {
return
}
defer texture.Destroy()
r.DrawTexturePoint(texture, p)
}
func (r *Renderer) TextAlign(font ui.Font, p geom.PointF32, color color.Color, text string, align ui.HorizontalAlignment) {
switch align {
case ui.AlignLeft:
r.Text(font, p, color, text)
case ui.AlignCenter:
width := font.WidthOf(text)
r.Text(font, p.Add2D(-.5*width, 0), color, text)
case ui.AlignRight:
width := font.WidthOf(text)
r.Text(font, p.Add2D(-width, 0), color, text)
}
}
func (r *Renderer) TextTexture(font ui.Font, color color.Color, text string) (ui.Texture, error) {
return r.text(font, color, text)
}
func (r *Renderer) WindowHandle() uintptr {
info, err := r.window.GetWMInfo()
if err != nil {
return 0
}
switch info.Subsystem {
case sdl.SYSWM_COCOA:
return uintptr(info.GetCocoaInfo().Window)
case sdl.SYSWM_DIRECTFB:
return uintptr(info.GetDFBInfo().Window)
case sdl.SYSWM_UIKIT:
return uintptr(info.GetUIKitInfo().Window)
case sdl.SYSWM_WINDOWS:
return uintptr(info.GetWindowsInfo().Window)
case sdl.SYSWM_X11:
return uintptr(info.GetX11Info().Window)
}
return 0
}
// Resources
func (r *Renderer) Resources() ui.Resources { return r.resources }
func (r *Renderer) SetResourceProvider(resources ui.Resources) {
if r.resources != nil {
r.resources.Destroy()
}
if physical, ok := resources.(ui.PhysicalResources); ok {
r.resources = physical
} else {
copy, err := ui.NewCopyResources("sdlui", resources, false)
if err != nil {
return
}
r.resources = copy
}
}
// Texture
func (r *Renderer) Image() image.Image { return nil }
func (r *Renderer) Height() int { return r.Size().Y }
func (r *Renderer) Width() int { return r.Size().X }

26
sdlui/rendererfactory.go Normal file
View File

@ -0,0 +1,26 @@
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, opts ui.NewRendererOptions) (ui.Renderer, error) {
location := sdl.Point{X: sdl.WINDOWPOS_UNDEFINED, Y: sdl.WINDOWPOS_UNDEFINED}
if opts.Location != nil {
location.X = int32(opts.Location.X)
location.Y = int32(opts.Location.Y)
}
return NewRenderer(title, int32(width), int32(height), NewRendererOptions{
Borderless: opts.Borderless,
Location: location,
Resizable: opts.Resizable,
VSync: opts.VSync,
})
}

64
sdlui/texture.go Normal file
View File

@ -0,0 +1,64 @@
package sdlui
import (
"image"
"image/color"
"github.com/veandco/go-sdl2/sdl"
"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() int {
_, _, _, height, err := t.Texture.Query()
if err != nil {
return -1
}
return int(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() int {
_, _, width, _, err := t.Texture.Query()
if err != nil {
return -1
}
return int(width)
}
var _ ui.ImageSource = &TextureImageSource{}
type TextureImageSource struct {
*Texture
source ui.ImageSource
}
func (s TextureImageSource) CreateImage() (image.Image, error) {
return s.source.CreateImage()
}

View File

@ -1,30 +0,0 @@
package allg5ui
import (
"image"
"opslag.de/schobers/zntg/allg5"
"opslag.de/schobers/zntg/ui"
)
var _ ui.Image = &uiImage{}
type uiImage struct {
bmp *allg5.Bitmap
}
func (i *uiImage) Destroy() {
i.bmp.Destroy()
}
func (i *uiImage) Height() float32 {
return float32(i.bmp.Height())
}
func (i *uiImage) Image() image.Image {
return i.bmp.Image()
}
func (i *uiImage) Width() float32 {
return float32(i.bmp.Width())
}

View File

@ -1,335 +0,0 @@
package allg5ui
import (
"image"
"image/color"
"math"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/allg5"
"opslag.de/schobers/zntg/ui"
)
var _ ui.Renderer = &Renderer{}
func NewRenderer(w, h int, opts allg5.NewDisplayOptions) (*Renderer, error) {
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
}
eq, err := allg5.NewEventQueue()
if err != nil {
disp.Destroy()
return nil, err
}
user := allg5.NewUserEventSource()
eq.RegisterKeyboard()
eq.RegisterMouse()
eq.RegisterDisplay(disp)
eq.RegisterUserEvents(user)
return &Renderer{disp, eq, map[string]*font{}, user, ui.MouseCursorDefault, ui.MouseCursorDefault}, nil
}
// Renderer implements ui.Renderer using Allegro 5.
type Renderer struct {
disp *allg5.Display
eq *allg5.EventQueue
ft map[string]*font
user *allg5.UserEventSource
cursor ui.MouseCursor
newCursor ui.MouseCursor
}
// Renderer implementation (events)
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
}
for ev != nil {
switch e := ev.(type) {
case *allg5.DisplayCloseEvent:
t.Handle(&ui.DisplayCloseEvent{EventBase: eventBase(e)})
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})
case *allg5.MouseButtonDownEvent:
t.Handle(&ui.MouseButtonDownEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseButtonUpEvent:
t.Handle(&ui.MouseButtonUpEvent{MouseEvent: mouseEvent(e.MouseEvent), Button: ui.MouseButton(e.Button)})
case *allg5.MouseEnterEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseLeaveEvent:
t.Handle(&ui.MouseLeaveEvent{MouseEvent: mouseEvent(e.MouseEvent)})
case *allg5.MouseMoveEvent:
t.Handle(&ui.MouseMoveEvent{MouseEvent: mouseEvent(e.MouseEvent), MouseWheel: float32(e.DeltaZ)})
case *allg5.UserEvent:
t.Handle(&ui.RefreshEvent{EventBase: eventBase(e)})
}
ev = r.eq.Get()
}
if r.newCursor != r.cursor {
r.cursor = r.newCursor
switch r.cursor {
case ui.MouseCursorNone:
r.disp.SetMouseCursor(allg5.MouseCursorNone)
case ui.MouseCursorDefault:
r.disp.SetMouseCursor(allg5.MouseCursorDefault)
case ui.MouseCursorNotAllowed:
r.disp.SetMouseCursor(allg5.MouseCursorUnavailable)
case ui.MouseCursorPointer:
r.disp.SetMouseCursor(allg5.MouseCursorLink)
case ui.MouseCursorText:
r.disp.SetMouseCursor(allg5.MouseCursorEdit)
}
}
}
func (r *Renderer) Refresh() {
r.user.EmitEvent()
}
// Renderer implementation (lifetime)
func (r *Renderer) Destroy() error {
r.user.Destroy()
r.eq.Destroy()
for _, f := range r.ft {
f.Destroy()
}
r.ft = nil
r.disp.Destroy()
return nil
}
// Renderer implementation (drawing)
func (r *Renderer) Clear(c color.Color) {
allg5.ClearToColor(newColor(c))
}
func (r *Renderer) CreateImage(im image.Image) (ui.Image, error) {
bmp, err := allg5.NewBitmapFromImage(im, true)
if err != nil {
return nil, err
}
return &uiImage{bmp}, nil
}
func (r *Renderer) CreateImagePath(path string) (ui.Image, error) {
bmp, err := allg5.LoadBitmap(path)
if err != nil {
return nil, err
}
return &uiImage{bmp}, nil
}
func (r *Renderer) CreateImageSize(w, h float32) (ui.Image, error) {
bmp, err := allg5.NewVideoBitmap(int(w), int(h))
if err != nil {
return nil, err
}
return &uiImage{bmp}, nil
}
func (r *Renderer) DefaultTarget() ui.Image {
return &uiImage{r.disp.Target()}
}
func (r *Renderer) Display() *allg5.Display { return r.disp }
func (r *Renderer) DrawImage(im ui.Image, p geom.PointF32) {
bmp := r.mustGetBitmap(im)
x, y := snap(p)
bmp.Draw(x, y)
}
func (r *Renderer) DrawImageOptions(im ui.Image, p geom.PointF32, opts ui.DrawOptions) {
bmp := r.mustGetBitmap(im)
var o allg5.DrawOptions
if opts.Tint != nil {
tint := newColor(opts.Tint)
o.Tint = &tint
}
if opts.Scale != nil {
o.Scale = &allg5.Scale{Horizontal: opts.Scale.X, Vertical: opts.Scale.Y}
}
x, y := snap(p)
bmp.DrawOptions(x, y, o)
}
func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) {
allg5.DrawFilledRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c))
}
func (r *Renderer) Font(name string) ui.Font {
return r.ft[name]
}
func (r *Renderer) mustGetBitmap(im ui.Image) *allg5.Bitmap {
m, ok := im.(*uiImage)
if !ok {
panic("image must be created on same renderer")
}
return m.bmp
}
func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) {
minX, minY := snap(rect.Min)
maxX, maxY := snap(rect.Max)
allg5.DrawRectangle(minX, minY, maxX, maxY, newColor(c), thickness)
}
func (r *Renderer) RegisterFont(path, name string, size int) error {
var f, err = allg5.LoadTTFFont(path, int(size))
if err != nil {
return err
}
var prev = r.ft[name]
if prev != nil {
prev.Destroy()
}
r.ft[name] = newFont(f)
return nil
}
func (r *Renderer) RegisterFonts(path string, fonts ...FontDefinition) error {
for _, f := range fonts {
err := r.RegisterFont(path, f.Name, f.Size)
if err != nil {
return err
}
}
return nil
}
func (r *Renderer) RenderTo(im ui.Image) {
bmp := r.mustGetBitmap(im)
bmp.SetAsTarget()
}
func (r *Renderer) RenderToDisplay() {
r.disp.SetAsTarget()
}
func (r *Renderer) Size() geom.PointF32 {
return geom.PtF32(float32(r.disp.Width()), float32(r.disp.Height()))
}
func (r *Renderer) SetIcon(im ui.Image) {
bmp := r.mustGetBitmap(im)
r.disp.SetIcon(bmp)
}
func (r *Renderer) SetMouseCursor(c ui.MouseCursor) {
r.newCursor = c
}
func (r *Renderer) SetWindowTitle(t string) {
r.disp.SetWindowTitle(t)
}
func (r *Renderer) Target() ui.Image {
return &uiImage{allg5.CurrentTarget()}
}
func (r *Renderer) text(p geom.PointF32, font string, c color.Color, t string, align allg5.HorizontalAlignment) {
var f = r.ft[font]
if f == nil {
return
}
x, y := snap(p)
f.f.Draw(x, y, newColor(c), align, t)
}
func (r *Renderer) Text(p geom.PointF32, font string, c color.Color, t string) {
r.text(p, font, c, t, allg5.AlignLeft)
}
func (r *Renderer) TextAlign(p geom.PointF32, font string, c color.Color, t string, align ui.HorizontalAlignment) {
var alignment = allg5.AlignLeft
switch align {
case ui.AlignCenter:
alignment = allg5.AlignCenter
case ui.AlignRight:
alignment = allg5.AlignRight
}
r.text(p, font, c, t, alignment)
}
// Utility functions
func eventWait(eq *allg5.EventQueue, wait bool) allg5.Event {
if wait {
return eq.GetWait()
}
return eq.Get()
}
func eventBase(e allg5.Event) ui.EventBase {
return ui.EventBase{StampInSeconds: e.Stamp()}
}
func key(key allg5.Key) ui.Key {
switch key {
case allg5.KeyBackspace:
return ui.KeyBackspace
case allg5.KeyDelete:
return ui.KeyDelete
case allg5.KeyDown:
return ui.KeyDown
case allg5.KeyEnd:
return ui.KeyEnd
case allg5.KeyEscape:
return ui.KeyEscape
case allg5.KeyHome:
return ui.KeyHome
case allg5.KeyLeft:
return ui.KeyLeft
case allg5.KeyRight:
return ui.KeyRight
case allg5.KeyUp:
return ui.KeyUp
}
return ui.KeyNone
}
func keyModifiers(mods allg5.KeyMod) ui.KeyModifier {
var m ui.KeyModifier
if mods&allg5.KeyModShift == allg5.KeyModShift {
m |= ui.KeyModifierShift
} else if mods&allg5.KeyModCtrl == allg5.KeyModCtrl {
m |= ui.KeyModifierControl
} else if mods&allg5.KeyModAlt == allg5.KeyModAlt {
m |= ui.KeyModifierAlt
}
return m
}
func mouseEvent(e allg5.MouseEvent) ui.MouseEvent {
return ui.MouseEvent{EventBase: eventBase(e), X: float32(e.X), Y: float32(e.Y)}
}
func newColor(c color.Color) allg5.Color {
if c == nil {
return newColor(color.Black)
}
return allg5.NewColorGo(c)
}
func snap(p geom.PointF32) (float32, float32) {
return float32(math.Round(float64(p.X))), float32(math.Round(float64(p.Y)))
}

View File

@ -1,42 +1,59 @@
package ui
import "opslag.de/schobers/geom"
import (
"errors"
"opslag.de/schobers/geom"
)
type RenderBufferFn func(ctx Context, size geom.PointF32)
type Buffer struct {
im Image
texture Texture
size geom.PointF32
}
var ErrNewBufferSize = errors.New("buffer has been resized")
func (b *Buffer) Update(ctx Context, size geom.PointF32) error {
if b.im != nil {
if b.texture != nil {
if size == b.size {
return nil
}
b.im.Destroy()
b.im = nil
b.texture.Destroy()
b.texture = nil
b.size = geom.ZeroPtF32
}
im, err := ctx.Renderer().CreateImageSize(size.X, size.Y)
texture, err := ctx.Renderer().CreateTextureTarget(size.X, size.Y)
if err != nil {
return err
}
b.im = im
b.texture = texture
b.size = size
return nil
return ErrNewBufferSize
}
func (b *Buffer) Render(ctx Context, pos geom.PointF32, fn RenderBufferFn) {
if b.im == nil {
if b.texture == nil {
return
}
b.RenderContent(ctx, fn)
b.RenderToDisplay(ctx, pos)
}
func (b *Buffer) RenderContent(ctx Context, fn RenderBufferFn) {
renderer := ctx.Renderer()
currTarget := renderer.Target()
renderer.RenderTo(b.im)
renderer.RenderTo(b.texture)
fn(ctx, b.size)
renderer.RenderTo(currTarget)
renderer.DrawImage(b.im, pos)
}
func (b *Buffer) RenderToDisplay(ctx Context, pos geom.PointF32) {
if b.texture == nil {
return
}
ctx.Renderer().DrawTexturePoint(b.texture, pos)
}
type BufferControl struct {

View File

@ -9,11 +9,15 @@ import (
type Button struct {
ControlBase
DisabledColor color.Color
HoverColor color.Color
Icon Image
IconScale float32
Icon string // optional: icon to display in front of the text.
IconHeight float32 // overrides the height of the icon (overrides auto-scaling when text is provided).
Text string
Type ButtonType
clicked ControlClickedEvents
}
type ButtonType int
@ -25,10 +29,10 @@ const (
ButtonTypeText
)
func BuildButton(text string, fn func(b *Button)) *Button { return BuildIconButton(nil, text, fn) }
func BuildButton(text string, fn func(b *Button)) *Button { return BuildIconButton("", text, fn) }
func BuildIconButton(i Image, text string, fn func(b *Button)) *Button {
var b = &Button{Text: text, Icon: i}
func BuildIconButton(icon, text string, fn func(b *Button)) *Button {
var b = &Button{Text: text, Icon: icon}
if fn != nil {
fn(b)
}
@ -36,34 +40,97 @@ func BuildIconButton(i Image, text string, fn func(b *Button)) *Button {
}
func (b *Button) desiredSize(ctx Context) geom.PointF32 {
var pad = ctx.Style().Dimensions.TextPadding
var font = ctx.Renderer().Font(b.FontName(ctx))
var pad = b.ActualTextPadding(ctx)
var font = b.ActualFont(ctx)
var w, h float32 = 0, font.Height()
if len(b.Text) != 0 {
w += pad + font.WidthOf(b.Text)
icon, iconW, iconH := b.icon(ctx)
if len(b.Text) == 0 {
if icon != nil && iconH > 0 {
w = pad.Left + iconW + pad.Right
h = iconH
}
} else {
w += pad.Left + font.WidthOf(b.Text) + pad.Right
if icon != nil && iconH > 0 {
if b.IconHeight == 0 {
iconW = iconW * h / iconH
// iconH = h
}
w += iconW + pad.Right
}
if b.Icon != nil && b.Icon.Height() > 0 {
iconW := b.scale(b.Icon.Width() * h / b.Icon.Height())
w += pad + iconW
}
if w == 0 {
return geom.ZeroPtF32
}
return geom.PtF32(w+pad, pad+h+pad)
return geom.PtF32(w, pad.Top+h+pad.Bottom)
}
func (b *Button) DesiredSize(ctx Context) geom.PointF32 {
func (b *Button) icon(ctx Context) (Texture, float32, float32) {
if b.Icon == "" {
return nil, 0, 0
}
icon := ctx.Textures().Texture(b.Icon)
iconW, iconH := float32(icon.Width()), float32(icon.Height())
if b.IconHeight != 0 {
iconW = b.IconHeight * iconW / iconH
iconH = b.IconHeight
}
return icon, iconW, iconH
}
func (b *Button) ButtonClicked() ControlClickedEventHandler { return &b.clicked }
func (b *Button) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
return b.desiredSize(ctx)
}
func (b *Button) Handle(ctx Context, e Event) {
b.ControlBase.Handle(ctx, e)
func (b *Button) Handle(ctx Context, e Event) bool {
result := b.ControlBase.HandleNotify(ctx, e, b)
if b.over {
if b.Disabled {
return true
}
ctx.Renderer().SetMouseCursor(MouseCursorPointer)
}
return result
}
func (b *Button) Notify(ctx Context, state interface{}) bool {
switch state.(type) {
case ControlClickedArgs:
if !b.Disabled {
if b.clicked.Notify(ctx, state) {
return true
}
}
}
return b.ControlBase.Notify(ctx, state)
}
func (b *Button) disabledColor(p *Palette) color.Color {
if b.DisabledColor != nil {
return b.DisabledColor
}
return p.Disabled
}
func (b *Button) fillColor(p *Palette) color.Color {
if b.Type == ButtonTypeIcon {
return nil
}
if b.Disabled {
if b.Background != nil {
return b.disabledColor(p)
}
switch b.Type {
case ButtonTypeContained:
return b.disabledColor(p)
default:
return nil
}
}
if b.Background != nil {
if b.over && b.HoverColor != nil {
return b.HoverColor
@ -77,7 +144,6 @@ func (b *Button) fillColor(p *Palette) color.Color {
switch b.Type {
case ButtonTypeContained:
return p.PrimaryLight
case ButtonTypeIcon:
default:
return p.PrimaryHighlight
}
@ -85,33 +151,41 @@ func (b *Button) fillColor(p *Palette) color.Color {
switch b.Type {
case ButtonTypeContained:
return p.Primary
case ButtonTypeIcon:
default:
}
return nil
}
func (b *Button) scale(f float32) float32 {
if b.IconScale == 0 {
return f
func (b *Button) fontColor(c color.Color) color.Color {
if b.Font.Color == nil {
return c
}
return b.IconScale * f
return b.Font.Color
}
func (b *Button) textColor(p *Palette) color.Color {
if b.Font.Color != nil {
return b.Font.Color
if b.Disabled {
if b.Background != nil {
return p.TextOnDisabled
}
switch b.Type {
case ButtonTypeContained:
return p.TextOnPrimary
return p.TextOnDisabled
}
return b.disabledColor(p)
}
switch b.Type {
case ButtonTypeContained:
return b.fontColor(p.TextOnPrimary)
case ButtonTypeIcon:
if b.over {
return p.Primary
if b.HoverColor != nil {
return b.HoverColor
}
return p.Text
return b.fontColor(p.Primary)
}
return b.fontColor(p.Text)
default:
return p.Primary
return b.fontColor(p.Primary)
}
}
@ -129,23 +203,33 @@ func (b *Button) Render(ctx Context) {
bounds.Min.X += .5 * deltaX
bounds.Min.Y += .5 * deltaY
var pad = style.Dimensions.TextPadding
bounds = bounds.Inset(pad)
pad := b.ActualTextPadding(ctx)
bounds = pad.InsetRect(bounds)
boundsH := bounds.Dy()
pos := bounds.Min
if b.Icon != nil && b.Icon.Height() > 0 {
icon, _ := ctx.Images().ScaledHeight(b.Icon, b.scale(bounds.Dy()))
if icon != nil {
ctx.Renderer().DrawImageOptions(icon, geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-icon.Height())), DrawOptions{Tint: textColor})
pos.X += icon.Width() + pad
icon, iconW, iconH := b.icon(ctx)
var iconOffsetY float32
if icon != nil && iconH > 0 {
if b.Text != "" {
if b.IconHeight == 0 {
iconH = boundsH
scaled, _ := ctx.Textures().ScaledHeight(icon, iconH) // try to pre-scale scaled
if scaled != nil { // let the renderer scale
icon = scaled
}
_, iconW = ScaleToHeight(SizeOfTexture(icon).ToF32(), iconH)
}
iconOffsetY = .5 * (boundsH - iconH)
}
ctx.Renderer().DrawTextureOptions(icon, geom.RectRelF32(pos.X, pos.Y+iconOffsetY, iconW, iconH), DrawOptions{Tint: textColor})
pos.X += iconW + pad.Right
}
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)
font := b.ActualFont(ctx)
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), textColor, b.Text)
}
if b.Type == ButtonTypeOutlined {
b.RenderOutline(ctx)
b.RenderOutlineDefault(ctx, textColor)
}
}

64
ui/cache.go Normal file
View File

@ -0,0 +1,64 @@
package ui
type CacheHashFn func(interface{}) string
type CacheHashContextFn func(Context) string
func (c CacheHashContextFn) Fn() CacheHashFn {
return func(state interface{}) string { return c(state.(Context)) }
}
type CacheUpdateFn func(interface{}) interface{}
type CacheUpdateContextFn func(Context) interface{}
func (c CacheUpdateContextFn) Fn() CacheUpdateFn {
return func(state interface{}) interface{} { return c(state.(Context)) }
}
type Cache struct {
value CachedValue
update CacheUpdateFn
hash CacheHashFn
}
func NewCache(update CacheUpdateFn, hash CacheHashFn) *Cache {
return &Cache{update: update, hash: hash}
}
func (c *Cache) Get(state interface{}) interface{} {
return c.value.Get(state, c.update, c.hash)
}
type CacheContext struct {
value CachedValue
update CacheUpdateContextFn
hash CacheHashContextFn
}
func NewCacheContext(update CacheUpdateContextFn, hash CacheHashContextFn) *CacheContext {
return &CacheContext{update: update, hash: hash}
}
func (c *CacheContext) Get(ctx Context) interface{} {
return c.value.GetContext(ctx, c.update, c.hash)
}
type CachedValue struct {
value interface{}
hash string
}
func (c *CachedValue) Get(state interface{}, update CacheUpdateFn, hash CacheHashFn) interface{} {
if hash(state) != c.hash {
c.value = update(state)
}
return c.value
}
func (c *CachedValue) GetContext(ctx Context, update CacheUpdateContextFn, hash CacheHashContextFn) interface{} {
if hash(ctx) != c.hash {
c.value = update(ctx)
}
return c.value
}

View File

@ -4,12 +4,10 @@ import (
"opslag.de/schobers/geom"
)
type SelectedChangedFn func(selected bool)
type Checkbox struct {
ControlBase
onSelectedChanged SelectedChangedFn
selectedChanged Events
Selected bool
Text string
@ -24,119 +22,113 @@ func BuildCheckbox(text string, fn func(c *Checkbox)) *Checkbox {
}
func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
var pad = ctx.Style().Dimensions.TextPadding
var font = ctx.Renderer().Font(c.FontName(ctx))
pad := c.ActualTextPadding(ctx)
font := c.ActualFont(ctx)
var w, h float32 = 0, font.Height()
if len(c.Text) != 0 {
w += pad + font.WidthOf(c.Text)
w += pad.Left + font.WidthOf(c.Text)
}
icon, _ := ctx.Images().ScaledHeight(c.getOrCreateNormalIcon(ctx), h)
w += pad + icon.Width()
return geom.PtF32(w+pad, pad+h+pad)
icon := c.getOrCreateNormalIcon(ctx)
_, iconWidth := ScaleToHeight(SizeOfTexture(icon).ToF32(), h)
w += pad.Left + iconWidth
return geom.PtF32(w+pad.Right, pad.Top+h+pad.Bottom)
}
func (c *Checkbox) icon(ctx Context) Image {
func (c *Checkbox) icon(ctx Context) Texture {
if c.Selected {
return GetOrCreateIcon(ctx, "ui-default-checkbox-selected", c.selectedIcon)
} else if c.over {
} else if c.over && !c.Disabled {
return GetOrCreateIcon(ctx, "ui-default-checkbox-hover", c.hoverIcon)
}
return c.getOrCreateNormalIcon(ctx)
}
func (c *Checkbox) getOrCreateNormalIcon(ctx Context) Image {
func (c *Checkbox) getOrCreateNormalIcon(ctx Context) Texture {
return GetOrCreateIcon(ctx, "ui-default-checkbox", c.normalIcon)
}
func (c *Checkbox) iconBorder() geom.PolygonF32 {
return geom.PolF32(
var checkBoxIconBorder = geom.PolF32(
geom.PtF32(48, 80),
geom.PtF32(400, 80),
geom.PtF32(400, 432),
geom.PtF32(48, 432),
)
}
func (c *Checkbox) checkMark() geom.PointsF32 {
return geom.PointsF32{
var checkBoxCheckMark = 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() IconPixelTestFn {
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() IconPixelTestFn {
border := c.iconBorder()
return func(pt geom.PointF32) bool {
return pt.DistanceToPolygon(border) < 48 && !pt.InPolygon(border)
}
}
func (c *Checkbox) selectedIcon() IconPixelTestFn {
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
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) geom.PointF32 { return c.desiredSize(ctx) }
func (c *Checkbox) Handle(ctx Context, e Event) bool {
result := c.ControlBase.Handle(ctx, e)
if c.over {
if c.Disabled {
return true
}
ctx.Renderer().SetMouseCursor(MouseCursorPointer)
}
if result {
return true
}
func (c *Checkbox) DesiredSize(ctx Context) geom.PointF32 { return c.desiredSize(ctx) }
func (c *Checkbox) Handle(ctx Context, e Event) {
switch e := e.(type) {
case *MouseButtonDownEvent:
if e.Button == MouseButtonLeft && c.over {
c.Selected = !c.Selected
onSelectedChanged := c.onSelectedChanged
if onSelectedChanged != nil {
onSelectedChanged(c.Selected)
return c.selectedChanged.Notify(ctx, c.Selected)
}
}
}
c.ControlBase.Handle(ctx, e)
if c.over {
ctx.Renderer().SetMouseCursor(MouseCursorPointer)
}
return false
}
func (c *Checkbox) OnSelectedChanged(fn SelectedChangedFn) {
c.onSelectedChanged = fn
}
func (c *Checkbox) SelectedChanged() EventHandler { return &c.selectedChanged }
func (c *Checkbox) Render(ctx Context) {
c.RenderBackground(ctx)
var style = ctx.Style()
var palette = style.Palette
fore := c.FontColor(ctx)
fore := c.TextColor(ctx)
bounds := c.bounds
var pad = style.Dimensions.TextPadding
bounds = bounds.Inset(pad)
pad := c.ActualTextPadding(ctx)
bounds = pad.InsetRect(bounds)
boundsH := bounds.Dy()
pos := bounds.Min
icon, _ := ctx.Images().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
if c.Selected {
iconColor = c.FontColor(ctx, palette.Primary)
}
ctx.Renderer().DrawImageOptions(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
}
_, iconWidth := ScaleToHeight(SizeOfTexture(scaledIcon).ToF32(), boundsH)
rect := geom.RectRelF32(pos.X, pos.Y, iconWidth, boundsH)
ctx.Renderer().DrawTextureOptions(scaledIcon, rect, DrawOptions{Tint: iconColor})
pos.X += iconWidth + pad.Right
}
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)
font := c.ActualFont(ctx)
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fore, c.Text)
}
}

View File

@ -7,8 +7,6 @@ type Clipboard interface {
var DefaultClipboard Clipboard = &clipboard{}
func SetClipboard(c Clipboard) { DefaultClipboard = c }
type clipboard struct {
value string
}

View File

@ -17,6 +17,9 @@ func BuildContainerBase(controls ...Control) ContainerBase {
func (c *ContainerBase) AddChild(child ...Control) {
c.Children = append(c.Children, child...)
for _, child := range child {
child.SetSelf(child)
}
}
func (c *ContainerBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
@ -26,11 +29,38 @@ func (c *ContainerBase) Arrange(ctx Context, bounds geom.RectangleF32, offset ge
c.ControlBase.Arrange(ctx, bounds, offset, parent)
}
func (c *ContainerBase) Handle(ctx Context, e Event) {
c.ControlBase.Handle(ctx, e)
for _, child := range c.Children {
child.Handle(ctx, e)
func (c *ContainerBase) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 {
if len(path) == 0 {
return c.bounds
}
next := path[0]
for _, child := range c.Children {
if child == next {
return child.BoundsUnclipped(ctx, path[1:])
}
}
panic("child not found in path")
}
func (c *ContainerBase) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
var max geom.PointF32
for _, child := range c.Children {
s := child.DesiredSize(ctx, size)
max = geom.MaxPtF32(max, s)
}
return max
}
func (c *ContainerBase) Handle(ctx Context, e Event) bool {
if c.ControlBase.Handle(ctx, e) {
return true
}
for _, child := range c.Children {
if child.Handle(ctx, e) {
return true
}
}
return false
}
func (c *ContainerBase) Render(ctx Context) {

View File

@ -1,12 +1,22 @@
package ui
import (
"opslag.de/schobers/geom"
)
type Context interface {
Animate()
Fonts() *Fonts
HasQuit() bool
Images() *Images
KeyModifiers() KeyModifier
MousePosition() geom.PointF32
Overlays() *Overlays
Quit()
Renderer() Renderer
Resources() Resources
ShowTooltip(t string)
Style() *Style
Textures() *Textures
}
var _ Context = &context{}
@ -15,14 +25,41 @@ var _ EventTarget = &context{}
type context struct {
animate bool
quit chan struct{}
r Renderer
renderer Renderer
view Control
ims *Images
modifiers KeyModifier
mouse geom.PointF32
tooltip *Tooltip
overlays *Overlays
fonts *Fonts
textures *Textures
style *Style
}
func newContext(r Renderer, s *Style, view Control) *context {
ctx := &context{
quit: make(chan struct{}),
renderer: r,
style: s,
view: view,
tooltip: &Tooltip{},
overlays: NewOverlays(view),
fonts: NewFonts(r),
textures: NewTextures(r)}
ctx.overlays.AddOnTop(uiDefaultTooltipOverlay, ctx.tooltip, false)
ctx.overlays.AddOnTop(DefaultDebugOverlay, NewDebugOverlay(view), false)
return ctx
}
func (c *context) Animate() { c.animate = true }
func (c *context) Destroy() {
c.fonts.Destroy()
c.textures.Destroy()
}
func (c *context) Fonts() *Fonts { return c.fonts }
func (c *context) HasQuit() bool {
select {
case <-c.quit:
@ -32,7 +69,21 @@ func (c *context) HasQuit() bool {
}
}
func (c *context) Images() *Images { return c.ims }
func (c *context) KeyModifiers() KeyModifier { return c.modifiers }
func (c *context) MousePosition() geom.PointF32 { return c.mouse }
func (c *context) Overlays() *Overlays { return c.overlays }
func (c *context) Renderer() Renderer { return c.renderer }
func (c *context) Resources() Resources { return c.renderer.Resources() }
func (c *context) ShowTooltip(t string) {
c.tooltip.Text = t
}
func (c *context) Style() *Style { return c.style }
func (c *context) Quit() {
if !c.HasQuit() {
@ -40,16 +91,21 @@ func (c *context) Quit() {
}
}
func (c *context) Renderer() Renderer { return c.r }
func (c *context) Style() *Style { return c.style }
func (c *context) Textures() *Textures { return c.textures }
// Handle implement EventTarget
func (c *context) Handle(e Event) {
switch e.(type) {
switch e := e.(type) {
case *DisplayCloseEvent:
c.Quit()
return
case *MouseMoveEvent:
c.mouse = e.Pos()
case *KeyDownEvent:
c.modifiers = e.Modifiers
case *KeyUpEvent:
c.modifiers = e.Modifiers
}
c.view.Handle(c, e)
c.overlays.Handle(c, e)
}

View File

@ -6,21 +6,44 @@ import (
type Control interface {
Arrange(Context, geom.RectangleF32, geom.PointF32, Control)
DesiredSize(Context) geom.PointF32
Handle(Context, Event)
DesiredSize(Context, geom.PointF32) geom.PointF32
Handle(Context, Event) bool
Render(Context)
Bounds() geom.RectangleF32
BoundsUnclipped(Context, ControlPath) geom.RectangleF32
Disable()
Enable()
IsDisabled() bool
IsInBounds(p geom.PointF32) bool
IsOver() bool
Offset() geom.PointF32
OnClick(ClickFn)
OnDragStart(DragStartFn)
OnDragMove(DragMoveFn)
OnDragEnd(DragEndFn)
ScrollIntoView(Context, ControlPath)
Self() Control
SetSelf(Control)
Parent() Control
}
type ControlPath []Control
func (p ControlPath) BoundsUnclipped(ctx Context, c Control) geom.RectangleF32 {
if len(p) == 0 {
return c.Bounds()
}
// switch next := p[0].(type) {
// case *ContainerBase:
// return next.BoundsUnclipped(ctx, p[1:])
// case *StackPanel:
// return next.BoundsUnclipped(ctx, p[1:])
// }
return p[0].BoundsUnclipped(ctx, p[1:])
}
func (p ControlPath) Prepend(control Control) ControlPath {
return append(ControlPath{control}, p...)
}
type RootControl interface {
Control

View File

@ -6,31 +6,109 @@ import (
"opslag.de/schobers/geom"
)
type ClickFn func(pos geom.PointF32, btn MouseButton)
type DragEndFn func(start, end geom.PointF32)
type DragMoveFn func(start, move geom.PointF32)
type DragStartFn func(start geom.PointF32)
type MouseEnterFn func()
type MouseLeaveFn func()
type ControlClickedArgs struct {
Position geom.PointF32
Button MouseButton
}
type ControlClickedEventHandler interface {
AddHandler(func(Context, ControlClickedArgs)) uint
}
type ControlClickedEvents struct {
Events
}
func (e *ControlClickedEvents) AddHandler(handler func(Context, ControlClickedArgs)) uint {
return e.Events.AddHandler(func(ctx Context, state interface{}) {
args := state.(ControlClickedArgs)
handler(ctx, args)
})
}
type DragEndedArgs struct {
Start geom.PointF32
End geom.PointF32
}
type DragEndedEventHandler interface {
AddHandler(func(Context, DragEndedArgs)) uint
}
type DragEndedEvents struct {
Events
}
func (e *DragEndedEvents) AddHandler(handler func(Context, DragEndedArgs)) uint {
return e.Events.AddHandler(func(ctx Context, state interface{}) {
args := state.(DragEndedArgs)
handler(ctx, args)
})
}
type DragMovedArgs struct {
Start geom.PointF32
Current geom.PointF32
}
type DragMovedEventHandler interface {
AddHandler(func(Context, DragMovedArgs)) uint
}
type DragMovedEvents struct {
Events
}
func (e *DragMovedEvents) AddHandler(handler func(Context, DragMovedArgs)) uint {
return e.Events.AddHandler(func(ctx Context, state interface{}) {
args := state.(DragMovedArgs)
handler(ctx, args)
})
}
type DragStartedArgs struct {
Start geom.PointF32
}
type DragStartedEventHandler interface {
AddHandler(func(Context, DragStartedArgs)) uint
}
type DragStartedEvents struct {
Events
}
func (e *DragStartedEvents) AddHandler(handler func(Context, DragStartedArgs)) uint {
return e.Events.AddHandler(func(ctx Context, state interface{}) {
args := state.(DragStartedArgs)
handler(ctx, args)
})
}
var _ Control = &ControlBase{}
type ControlBase struct {
bounds geom.RectangleF32
offset geom.PointF32
self Control
parent Control
dragStart *geom.PointF32
drag Dragable
over bool
pressed bool
onClick ClickFn
onDragEnd DragEndFn
onDragMove DragMoveFn
onDragStart DragStartFn
clicked ControlClickedEvents
dragEnded DragEndedEvents
dragMoved DragMovedEvents
dragStarted DragStartedEvents
Background color.Color
Font FontStyle
TextAlignment HorizontalAlignment
TextPadding SideLengths
Disabled bool
Tooltip string
}
func (c *ControlBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
@ -41,9 +119,33 @@ func (c *ControlBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom
func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds }
func (c *ControlBase) DesiredSize(Context) geom.PointF32 { return geom.ZeroPtF32 }
func (c *ControlBase) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 {
return path.BoundsUnclipped(ctx, c)
}
func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked }
func (c *ControlBase) DesiredSize(Context, geom.PointF32) geom.PointF32 { return geom.ZeroPtF32 }
func (c *ControlBase) Disable() { c.Disabled = true }
func (c *ControlBase) DragEnded() DragEndedEventHandler { return &c.dragEnded }
func (c *ControlBase) DragMoved() DragMovedEventHandler { return &c.dragMoved }
func (c *ControlBase) DragStarted() DragStartedEventHandler { return &c.dragStarted }
func (c *ControlBase) Enable() { c.Disabled = false }
func (c *ControlBase) Handle(ctx Context, e Event) bool { return c.HandleNotify(ctx, e, c) }
func (c *ControlBase) HandleNotify(ctx Context, e Event, notifier Notifier) bool {
defer func() {
if c.Tooltip != "" && c.over {
ctx.ShowTooltip(c.Tooltip)
}
}()
func (c *ControlBase) Handle(ctx Context, e Event) {
var over = func(e MouseEvent) bool {
pos := e.Pos()
if !c.IsInBounds(pos) {
@ -62,19 +164,14 @@ func (c *ControlBase) Handle(ctx Context, e Event) {
case *MouseMoveEvent:
c.over = over(e.MouseEvent)
if c.pressed {
if c.dragStart == nil {
var start = c.ToControlPosition(e.Pos())
c.dragStart = &start
if c.onDragStart != nil {
c.onDragStart(start)
}
} else {
var start = *c.dragStart
if start, ok := c.drag.IsDragging(); ok {
var move = c.ToControlPosition(e.Pos())
if c.onDragMove != nil {
c.onDragMove(start, move)
}
c.drag.Move(move)
return notifier.Notify(ctx, DragMovedArgs{Start: start, Current: move})
}
var start = c.ToControlPosition(e.Pos())
c.drag.Start(start)
return notifier.Notify(ctx, DragStartedArgs{Start: start})
}
case *MouseLeaveEvent:
c.over = false
@ -82,31 +179,37 @@ func (c *ControlBase) Handle(ctx Context, e Event) {
c.over = over(e.MouseEvent)
if c.over && e.Button == MouseButtonLeft {
c.pressed = true
return notifier.Notify(ctx, ControlClickedArgs{Position: e.Pos(), Button: e.Button})
}
case *MouseButtonUpEvent:
if e.Button == MouseButtonLeft {
if c.dragStart != nil {
var start = *c.dragStart
var end = c.ToControlPosition(e.Pos())
c.dragStart = nil
if c.onDragEnd != nil {
c.onDragEnd(start, end)
}
}
if c.pressed {
if c.onClick != nil {
c.onClick(c.ToControlPosition(e.Pos()), e.Button)
}
}
c.pressed = false
if start, ok := c.drag.IsDragging(); ok {
var end = c.ToControlPosition(e.Pos())
c.drag.Cancel()
return notifier.Notify(ctx, DragEndedArgs{Start: start, End: end})
}
}
}
return false
}
func (c *ControlBase) FontColor(ctx Context) color.Color {
func (c *ControlBase) ActualFont(ctx Context) Font {
name := c.FontName(ctx)
return ctx.Fonts().Font(name)
}
func (c *ControlBase) ActualTextPadding(ctx Context) Sides {
return c.TextPadding.Zero(ctx.Style().Dimensions.TextPadding)
}
func (c *ControlBase) FontColor(ctx Context, color color.Color) color.Color {
if c.Disabled {
return ctx.Style().Palette.TextOnDisabled
}
var text = c.Font.Color
if text == nil {
text = ctx.Style().Palette.Text
text = color
}
return text
}
@ -119,6 +222,8 @@ func (c *ControlBase) FontName(ctx Context) string {
return name
}
func (c *ControlBase) IsDisabled() bool { return c.Disabled }
func (c *ControlBase) IsInBounds(p geom.PointF32) bool {
bounds := c.bounds
if bounds.Min.X < 0 {
@ -132,24 +237,31 @@ func (c *ControlBase) IsInBounds(p geom.PointF32) bool {
func (c *ControlBase) IsOver() bool { return c.over }
func (c *ControlBase) Parent() Control { return c.parent }
func (c *ControlBase) IsPressed() bool { return c.pressed }
func (c *ControlBase) Notify(ctx Context, state interface{}) bool {
switch state.(type) {
case ControlClickedArgs:
return c.clicked.Notify(ctx, state)
case DragEndedArgs:
return c.dragEnded.Notify(ctx, state)
case DragMovedArgs:
return c.dragMoved.Notify(ctx, state)
case DragStartedArgs:
return c.dragStarted.Notify(ctx, state)
default:
return false
}
}
func (c *ControlBase) Parent() Control { return c.parent }
func (c *ControlBase) Offset() geom.PointF32 { return c.offset }
func (c *ControlBase) OnClick(fn ClickFn) { c.onClick = fn }
func (c *ControlBase) OnDragStart(fn DragStartFn) {
c.onDragStart = fn
func (c *ControlBase) OutlineColor(ctx Context) color.Color {
return c.FontColor(ctx, ctx.Style().Palette.Primary)
}
func (c *ControlBase) OnDragMove(fn DragMoveFn) {
c.onDragMove = fn
}
func (c *ControlBase) OnDragEnd(fn DragEndFn) { c.onDragEnd = fn }
func (c *ControlBase) Render(Context) {}
func (c *ControlBase) RenderBackground(ctx Context) {
@ -159,13 +271,36 @@ func (c *ControlBase) RenderBackground(ctx Context) {
}
func (c *ControlBase) RenderOutline(ctx Context) {
c.RenderOutlineDefault(ctx, nil)
}
func (c *ControlBase) RenderOutlineDefault(ctx Context, color color.Color) {
style := ctx.Style()
width := style.Dimensions.OutlineWidth
color := style.Palette.Primary
if c.Font.Color != nil {
color = c.Font.Color
if color == nil {
color = c.OutlineColor(ctx)
}
ctx.Renderer().Rectangle(c.bounds.Inset(.5*width), color, width)
}
func (c *ControlBase) TextColor(ctx Context) color.Color {
return c.FontColor(ctx, ctx.Style().Palette.Text)
}
func (c *ControlBase) ScrollIntoView(ctx Context, path ControlPath) {
if c.parent == nil {
return
}
c.parent.ScrollIntoView(ctx, path.Prepend(c))
}
func (c *ControlBase) Self() Control {
if c.self == nil {
return c
}
return c.self
}
func (c *ControlBase) SetSelf(self Control) { c.self = self }
func (c *ControlBase) ToControlPosition(p geom.PointF32) geom.PointF32 { return p.Sub(c.offset) }

67
ui/copyresources.go Normal file
View File

@ -0,0 +1,67 @@
package ui
import (
"io"
"os"
"opslag.de/schobers/zntg"
)
var _ PhysicalResources = &CopyResources{}
// CopyResources copies the resource to a temporary directory when fetched. Optionally the resource is fetched as well before opening.
type CopyResources struct {
Source Resources
copy *zntg.Dir
mustCopyBeforeOpen bool
}
func newCopyResources(prefix string, source Resources, mustCopyBeforeOpen bool) (*CopyResources, error) {
copy, err := zntg.NewTempDir(prefix)
if nil != err {
return nil, err
}
return &CopyResources{source, copy, mustCopyBeforeOpen}, nil
}
// NewCopyResources creates a proxy that copies resources first to disk. Copy on OpenResource only happens when mustCopyBeforeOpen is set to true.
func NewCopyResources(prefix string, source Resources, mustCopyBeforeOpen bool) (*CopyResources, error) {
return newCopyResources(prefix, source, false)
}
// FetchResource copies the file from the source to disk and returns the path to it.
func (r *CopyResources) FetchResource(name string) (string, error) {
path := r.copy.FilePath(name)
if !zntg.FileExists(path) {
src, err := r.Source.OpenResource(name)
if err != nil {
return "", err
}
defer src.Close()
err = r.copy.Write(name, src)
if nil != err {
return "", err
}
}
return path, nil
}
// OpenResource opens the (optionally copied) resource.
func (r *CopyResources) OpenResource(name string) (io.ReadCloser, error) {
if r.mustCopyBeforeOpen {
path, err := r.FetchResource(name)
if err != nil {
return nil, err
}
return os.Open(path)
}
return r.Source.OpenResource(name)
}
// Destroy destroy the copy of the resources.
func (r *CopyResources) Destroy() error {
return r.copy.Destroy()
}

161
ui/debug.go Normal file
View File

@ -0,0 +1,161 @@
package ui
import (
"image/color"
"reflect"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
const DefaultDebugOverlay = "ui-default-debug"
func controlChildren(control Control) []Control {
container, ok := control.(*ContainerBase)
if ok {
return container.Children
}
proxy, ok := control.(*Proxy)
if ok {
return []Control{proxy.Content}
}
return controlChildrenReflect(reflect.ValueOf(control))
}
func controlChildrenReflect(control reflect.Value) []Control {
switch control.Kind() {
case reflect.Interface:
return controlChildrenReflect(control.Elem())
case reflect.Ptr:
return controlChildrenReflect(control.Elem())
}
if reflect.TypeOf(ContainerBase{}) == control.Type() {
container := control.Interface().(ContainerBase)
return container.Children
}
if reflect.TypeOf(Proxy{}) == control.Type() {
proxy := control.Interface().(Proxy)
return []Control{proxy.Content}
}
if control.NumField() == 0 {
return nil
}
field := control.Type().Field(0)
if !field.Anonymous {
return nil
}
return controlChildrenReflect(control.Field(0))
}
func controlName(control Control) string {
typ := reflect.TypeOf(control)
return typ.Elem().Name()
}
type debugOverlay struct {
ControlBase
root Control
hoverNodes *controlNode
boundsColor color.Color
textColor color.Color
textShadowColor color.Color
}
func NewDebugOverlay(root Control) *debugOverlay {
return &debugOverlay{
root: root,
boundsColor: zntg.MustHexColor(`#00FF003F`),
textColor: zntg.MustHexColor(`#FFFFFF3F`),
textShadowColor: zntg.MustHexColor(`#0000003F`),
}
}
func (o *debugOverlay) renderControl(ctx Context, control Control) {
renderer := ctx.Renderer()
currentColor := zntg.MustHexColor("#FF0000")
parentColor := zntg.MustHexColor("#0000FF")
var maxY float32
var renderHoverNode func(pos geom.PointF32, node *controlNode)
renderHoverNode = func(pos geom.PointF32, node *controlNode) {
if node == nil {
return
}
nameTexture, err := ctx.Fonts().TextTexture("debug", color.White, node.Name)
if err != nil {
return
}
defer nameTexture.Destroy()
nameTextureWidth := float32(nameTexture.Width())
nameTextureHeight := float32(nameTexture.Height())
renderer.FillRectangle(pos.RectRel2D(nameTextureWidth, nameTextureHeight), color.Black)
renderer.DrawTexturePoint(nameTexture, pos)
childPos := pos.Add2D(nameTextureWidth+ctx.Style().Dimensions.Margin, 0)
if len(node.Children) == 0 {
renderer.Rectangle(node.Parent.Bounds, parentColor, 1)
renderer.Rectangle(node.Bounds, currentColor, 1)
}
for _, child := range node.Children {
if childPos.Y == maxY {
childPos.Y = maxY + nameTextureHeight
}
renderHoverNode(childPos, child)
maxY = childPos.Y
childPos.Y += nameTextureHeight + ctx.Style().Dimensions.Margin
}
}
renderHoverNode(geom.PtF32(4, 4), o.hoverNodes)
children := controlChildren(control)
for _, child := range children {
o.renderControl(ctx, child)
}
}
func createHoverNodes(hover geom.PointF32, control Control) *controlNode {
bounds := control.Bounds()
if !hover.In(bounds) {
return nil
}
node := &controlNode{Name: controlName(control), Bounds: bounds}
for _, child := range controlChildren(control) {
childNode := createHoverNodes(hover, child)
if childNode == nil {
continue
}
childNode.Parent = node
node.Children = append(node.Children, childNode)
}
return node
}
func (o *debugOverlay) Handle(ctx Context, e Event) bool {
switch e := e.(type) {
case *MouseMoveEvent:
o.hoverNodes = createHoverNodes(e.Pos(), o.root)
case *MouseLeaveEvent:
o.hoverNodes = nil
}
return false
}
func (o *debugOverlay) Hidden() {}
func (o *debugOverlay) Render(ctx Context) {
o.renderControl(ctx, o.root)
}
func (o *debugOverlay) Shown() {}
type controlNode struct {
Name string
Bounds geom.RectangleF32
Parent *controlNode
Children []*controlNode
}

21
ui/debug_test.go Normal file
View File

@ -0,0 +1,21 @@
package ui
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestControlName(t *testing.T) {
assert.Equal(t, "ControlBase", controlName(&ControlBase{}))
assert.Equal(t, "Label", controlName(&Label{}))
}
func TestControlChildren(t *testing.T) {
assert.Len(t, controlChildren(&ContainerBase{}), 0)
assert.Len(t, controlChildren(&ControlBase{}), 0)
assert.Len(t, controlChildren(&ContainerBase{Children: []Control{nil, nil}}), 2)
assert.Len(t, controlChildren(&StackPanel{ContainerBase: ContainerBase{Children: []Control{nil, nil}}}), 2)
assert.Len(t, controlChildren(&Proxy{Content: &ControlBase{}}), 1)
assert.Len(t, controlChildren(&overflow{Proxy: Proxy{Content: &ControlBase{}}}), 1)
}

View File

@ -1,21 +0,0 @@
package ui
import (
"github.com/minio/highwayhash"
"opslag.de/schobers/geom"
)
type desiredSizeCache struct {
sum [32]byte
size geom.PointF32
}
func (c *desiredSizeCache) Update(ctx Context, data string, calcFn func(Context) geom.PointF32) geom.PointF32 {
var key = [32]byte{}
sum := highwayhash.Sum([]byte(data), key[:])
if c.sum != sum {
c.size = calcFn(ctx)
c.sum = sum
}
return c.size
}

44
ui/dragable.go Normal file
View File

@ -0,0 +1,44 @@
package ui
import "opslag.de/schobers/geom"
// Dragable keeps track of the mouse position during a drag operation.
type Dragable struct {
start *geom.PointF32
current *geom.PointF32
}
// Cancel cancels the drag operation and returns the start and end position.
func (d *Dragable) Cancel() (geom.PointF32, geom.PointF32) {
if d.start == nil {
return geom.ZeroPtF32, geom.ZeroPtF32
}
start, end := *d.start, *d.current
d.start = nil
d.current = nil
return start, end
}
// IsDragging returns if the drag operation is in progress and the start location if so.
func (d *Dragable) IsDragging() (geom.PointF32, bool) {
if d.start != nil {
return *d.start, true
}
return geom.ZeroPtF32, false
}
// Move calculates the delta between the start point and the current position.
func (d *Dragable) Move(p geom.PointF32) (geom.PointF32, bool) {
if d.start == nil {
return geom.ZeroPtF32, false
}
delta := p.Sub(*d.current)
d.current = &p
return delta, true
}
// Start set the start point of the drag operation.
func (d *Dragable) Start(p geom.PointF32) {
d.start = &p
d.current = &p
}

46
ui/dragdrop.go Normal file
View File

@ -0,0 +1,46 @@
package ui
import "opslag.de/schobers/geom"
type DragDropEventTarget interface {
DragEnter(geom.PointF32, []string)
DragMove(geom.PointF32)
DragLeave()
Drop(geom.PointF32, []string)
}
type DragDropProvider interface {
Register(windowHandle uintptr, target DragDropEventTarget)
}
var DefaultDragDropProvider DragDropProvider = nil
type dragDropEventTarget struct {
renderer Renderer
events []Event
}
func (t *dragDropEventTarget) eventBase() EventBase {
return EventBase{StampInSeconds: t.renderer.Stamp()}
}
func (t *dragDropEventTarget) pushEvent(e Event) {
t.events = append(t.events, e)
t.renderer.Refresh()
}
func (t *dragDropEventTarget) DragEnter(pos geom.PointF32, files []string) {
t.pushEvent(&DisplayDragEnterEvent{EventBase: t.eventBase(), X: pos.X, Y: pos.Y, Files: files})
}
func (t *dragDropEventTarget) DragMove(pos geom.PointF32) {
t.pushEvent(&DisplayDragMoveEvent{EventBase: t.eventBase(), X: pos.X, Y: pos.Y})
}
func (t *dragDropEventTarget) DragLeave() {
t.pushEvent(&DisplayDragLeaveEvent{EventBase: t.eventBase()})
}
func (t *dragDropEventTarget) Drop(pos geom.PointF32, files []string) {
t.pushEvent(&DisplayDropEvent{EventBase: t.eventBase(), X: pos.X, Y: pos.Y, Files: files})
}

View File

@ -7,6 +7,14 @@ import (
)
type DrawOptions struct {
Source *geom.RectangleF32
Tint color.Color
Scale *geom.PointF32
}
func ScaleToHeight(size geom.PointF32, height float32) (*geom.PointF32, float32) {
if size.Y == height {
return nil, size.X
}
factor := height / size.Y
return &geom.PointF32{X: factor, Y: factor}, factor * size.X
}

View File

@ -6,6 +6,44 @@ type DisplayCloseEvent struct {
EventBase
}
type DisplayDragEnterEvent struct {
EventBase
X, Y float32
Files []string
}
func (e DisplayDragEnterEvent) Pos() geom.PointF32 {
return geom.PtF32(e.X, e.Y)
}
type DisplayDragLeaveEvent struct {
EventBase
}
type DisplayDragMoveEvent struct {
EventBase
X, Y float32
}
func (e DisplayDragMoveEvent) Pos() geom.PointF32 {
return geom.PtF32(e.X, e.Y)
}
type DisplayDropEvent struct {
EventBase
X, Y float32
Files []string
}
func (e DisplayDropEvent) Pos() geom.PointF32 {
return geom.PtF32(e.X, e.Y)
}
type DisplayMoveEvent struct {
EventBase
Bounds geom.RectangleF32
}
type DisplayResizeEvent struct {
EventBase
Bounds geom.RectangleF32
@ -23,21 +61,6 @@ func (e *EventBase) Stamp() float64 {
return e.StampInSeconds
}
type Key int
const (
KeyNone Key = iota
KeyBackspace
KeyDelete
KeyDown
KeyEnd
KeyEscape
KeyHome
KeyLeft
KeyRight
KeyUp
)
type KeyModifier int
const (
@ -45,13 +68,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
@ -97,3 +126,8 @@ type MouseMoveEvent struct {
type RefreshEvent struct {
EventBase
}
type TextInputEvent struct {
EventBase
Character rune
}

37
ui/events.go Normal file
View File

@ -0,0 +1,37 @@
package ui
import "opslag.de/schobers/zntg"
type EventEmptyFn func(Context)
type EventFn func(Context, interface{})
type EventArgs struct {
Context Context
State interface{}
}
type Events struct {
zntg.Events
}
type EventHandler interface {
AddHandler(EventFn) uint
AddHandlerEmpty(EventEmptyFn) uint
RemoveHandler(uint)
}
func (e *Events) Notify(ctx Context, state interface{}) bool {
return e.Events.Notify(EventArgs{Context: ctx, State: state})
}
func (e *Events) AddHandler(handler EventFn) uint {
return e.Events.AddHandler(func(state interface{}) {
args := state.(EventArgs)
handler(args.Context, args.State)
})
}
func (e *Events) AddHandlerEmpty(handler EventEmptyFn) uint {
return e.AddHandler(func(ctx Context, _ interface{}) { handler(ctx) })
}

View File

@ -4,30 +4,28 @@ import (
"image/color"
"log"
"opslag.de/schobers/geom"
_ "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/zntg/allg5"
"opslag.de/schobers/zntg/ui"
"opslag.de/schobers/zntg/ui/allg5ui"
)
func run() error {
var render, err = allg5ui.NewRenderer(800, 600, allg5.NewDisplayOptions{})
if err != nil {
return err
type basic struct {
ui.StackPanel
}
defer render.Destroy()
err = render.RegisterFont("../resources/font/OpenSans-Regular.ttf", "default", 14)
func (b *basic) Init(ctx ui.Context) error {
_, err := ctx.Fonts().CreateFontPath("default", "../resources/font/OpenSans-Regular.ttf", 14)
if err != nil {
return err
}
plus, err := render.CreateImagePath("../resources/images/plus.png")
if err != nil {
return err
}
defer plus.Destroy()
_, err = ctx.Textures().CreateTexturePath("plus", "../resources/images/plus.png", true)
if err != nil {
return err
}
var style = ctx.Style()
var stretch = func(content ui.Control, margin float32) ui.Control {
return ui.BuildSpacing(content, func(s *ui.Spacing) {
s.Width = ui.Infinite()
@ -38,17 +36,35 @@ func run() error {
})
}
var style = ui.DefaultStyle()
var view = ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.Background = color.White
p.Children = []ui.Control{
b.Background = color.White
b.Children = []ui.Control{
&ui.Label{Text: "Hello, world!"},
ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
p.Children = []ui.Control{
stretch(ui.BuildIconButton(plus, "Contained", func(b *ui.Button) { b.Type = ui.ButtonTypeContained }), 8),
stretch(ui.BuildIconButton(plus, "Icon", func(b *ui.Button) { b.Type = ui.ButtonTypeIcon }), 8),
stretch(ui.BuildIconButton(plus, "Outlined", func(b *ui.Button) { b.Type = ui.ButtonTypeOutlined }), 8),
stretch(ui.BuildIconButton(plus, "Text", func(b *ui.Button) { b.Type = ui.ButtonTypeText }), 8),
stretch(ui.BuildIconButton("plus", "Contained", func(b *ui.Button) { b.Type = ui.ButtonTypeContained }), 8),
stretch(ui.BuildIconButton("plus", "Icon", func(b *ui.Button) { b.Type = ui.ButtonTypeIcon }), 8),
stretch(ui.BuildIconButton("plus", "Outlined", func(b *ui.Button) { b.Type = ui.ButtonTypeOutlined }), 8),
stretch(ui.BuildIconButton("plus", "Text", func(b *ui.Button) { b.Type = ui.ButtonTypeText }), 8),
}
}),
ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
p.Children = []ui.Control{
stretch(ui.BuildIconButton("plus", "Contained", func(b *ui.Button) {
b.Type = ui.ButtonTypeContained
b.Disabled = true
}), 8),
stretch(ui.BuildIconButton("plus", "Icon", func(b *ui.Button) {
b.Type = ui.ButtonTypeIcon
b.Disabled = true
}), 8),
stretch(ui.BuildIconButton("plus", "Outlined", func(b *ui.Button) {
b.Type = ui.ButtonTypeOutlined
b.Disabled = true
}), 8),
stretch(ui.BuildIconButton("plus", "Text", func(b *ui.Button) {
b.Type = ui.ButtonTypeText
b.Disabled = true
}), 8),
}
}),
ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
@ -57,23 +73,56 @@ func run() error {
ui.BuildCheckbox("Check me!", nil),
}
}),
ui.Stretch(&ui.Label{Text: "Content"}),
ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
p.Children = []ui.Control{
ui.BuildCheckbox("", func(b *ui.Checkbox) { b.Disabled = true }),
ui.BuildCheckbox("You can't check me!", func(b *ui.Checkbox) {
b.Selected = true
b.Disabled = true
}),
}
}),
ui.Stretch(ui.BuildParagraph(
"Content"+
"\n\n"+
"Could be on multiple lines...\n"+
"And if the line is long enough (and without line breaks in the string) it will wrap around to the next line on the screen. You can test this behaviour by resizing the window. Go ahead!", nil)),
ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) {
b.Text = "Type here..."
})), 8),
ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) {
b.Text = "You can't type here..."
b.Disabled = true
})), 8),
ui.Margin(ui.BuildButton("Quit", func(b *ui.Button) {
b.OnClick(func(ctx ui.Context, _ ui.Control, _ geom.PointF32, _ ui.MouseButton) {
b.ButtonClicked().AddHandler(func(ui.Context, ui.ControlClickedArgs) {
ctx.Quit()
})
b.Tooltip = "Will quit the application"
}), 8),
ui.Margin(ui.BuildButton("Can't quit", func(b *ui.Button) {
b.ButtonClicked().AddHandler(func(ui.Context, ui.ControlClickedArgs) {
ctx.Quit()
})
b.Tooltip = "Will not quit the application"
b.Disabled = true
}), 8),
ui.BuildLabel("Status...", func(l *ui.Label) {
l.Background = style.Palette.PrimaryDark
l.Font.Color = style.Palette.TextOnPrimary
}),
}
})
return nil
}
return ui.RunWait(render, style, view, true)
func run() error {
var render, err = ui.NewRendererDefault("Basic Example", 800, 600)
if err != nil {
return err
}
defer render.Destroy()
return ui.RunWait(render, ui.DefaultStyle(), &basic{}, true)
}
func main() {

110
ui/examples/02_drop/drop.go Normal file
View File

@ -0,0 +1,110 @@
package main
import (
"image/color"
"log"
"strings"
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/addons/drop"
_ "opslag.de/schobers/zntg/allg5ui" // import the renderer for the UI
"opslag.de/schobers/zntg/ui"
)
type dropFiles struct {
ui.StackPanel
ctx ui.Context
files *ui.Paragraph
ping *ping
}
type ping struct {
ui.OverlayBase
circle ui.Texture
position geom.PointF32
tick time.Time
}
func (p *ping) Render(ctx ui.Context) {
elapsed := time.Since(p.tick)
const animationDuration = 500 * time.Millisecond
if elapsed > animationDuration {
return
}
tint := color.Gray{Y: uint8(elapsed * 255 / animationDuration)}
center := geom.PtF32(float32(p.circle.Width()), float32(p.circle.Height())).Mul(.5)
ctx.Renderer().DrawTexturePointOptions(p.circle, p.position.Sub(center), ui.DrawOptions{Tint: tint})
ctx.Animate()
}
func (d *dropFiles) WindowHandle() uintptr { return d.ctx.Renderer().WindowHandle() }
func (d *dropFiles) FilesDropped(position geom.PointF32, files []string) {
d.files.Text = "Files dropped:\n" + strings.Join(files, "\n")
d.ping.tick = time.Now()
d.ping.position = position
d.ctx.Animate()
}
func newCircle() ui.ImageSource {
const side = 16
center := geom.PtF32(.5*side, .5*side)
return &ui.AlphaPixelImageSource{
ImageAlphaPixelTestFn: func(p geom.PointF32) uint8 {
dist := p.Distance(center)
if dist < 7 {
return 255
} else if dist > 8 {
return 0
}
return uint8((8 - dist) * 255 * 0.5)
},
Size: geom.Pt(side, side),
}
}
func (d *dropFiles) Init(ctx ui.Context) error {
d.ctx = ctx
_, err := ctx.Fonts().CreateFontPath("default", "../resources/font/OpenSans-Regular.ttf", 14)
if err != nil {
return err
}
d.files = ui.BuildParagraph("", nil)
pingCircle, err := ctx.Renderer().CreateTexture(newCircle())
if err != nil {
return err
}
d.ping = &ping{circle: pingCircle}
ctx.Overlays().AddOnTop("ping", d.ping, true)
d.Background = color.White
d.Children = []ui.Control{
&ui.Label{Text: "Drop files on this window!"},
d.files,
}
return nil
}
func run() error {
var render, err = ui.NewRendererDefault("Files Drop Example", 800, 600)
if err != nil {
return err
}
defer render.Destroy()
return ui.RunWait(render, ui.DefaultStyle(), &dropFiles{}, true)
}
func main() {
var err = run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,122 @@
package main
import (
"image/color"
"log"
"strings"
"time"
"opslag.de/schobers/geom"
_ "opslag.de/schobers/zntg/addons/dragdrop" // import drag & drop functionality
_ "opslag.de/schobers/zntg/sdlui" // import the renderer for the UI
"opslag.de/schobers/zntg/ui"
)
type dropFiles struct {
ui.StackPanel
ctx ui.Context
files *ui.Paragraph
ping *ping
}
type ping struct {
ui.OverlayBase
circle ui.Texture
position geom.PointF32
over bool
tick time.Time
}
func (p *ping) Render(ctx ui.Context) {
elapsed := time.Since(p.tick)
const animationDuration = 500 * time.Millisecond
if elapsed > animationDuration {
return
}
tint := color.Gray{Y: uint8(elapsed * 255 / animationDuration)}
center := geom.PtF32(float32(p.circle.Width()), float32(p.circle.Height())).Mul(.5)
ctx.Renderer().DrawTexturePointOptions(p.circle, p.position.Sub(center), ui.DrawOptions{Tint: tint})
ctx.Animate()
}
func (d *dropFiles) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) {
case *ui.DisplayDragMoveEvent:
d.ping.tick = time.Now()
d.ping.position = geom.PtF32(e.X, e.Y)
d.ping.over = true
d.ctx.Animate()
return true
case *ui.DisplayDropEvent:
d.files.Text = "Files dropped:\n" + strings.Join(e.Files, "\n")
d.ping.tick = time.Now()
d.ping.position = geom.PtF32(e.X, e.Y)
d.ping.over = false
d.ctx.Animate()
return true
}
return d.StackPanel.Handle(ctx, e)
}
func newCircle() ui.ImageSource {
const side = 16
center := geom.PtF32(.5*side, .5*side)
return &ui.AlphaPixelImageSource{
ImageAlphaPixelTestFn: func(p geom.PointF32) uint8 {
dist := p.Distance(center)
if dist < 7 {
return 255
} else if dist > 8 {
return 0
}
return uint8((8 - dist) * 255 * 0.5)
},
Size: geom.Pt(side, side),
}
}
func (d *dropFiles) Init(ctx ui.Context) error {
d.ctx = ctx
_, err := ctx.Fonts().CreateFontPath("default", "../resources/font/OpenSans-Regular.ttf", 14)
if err != nil {
return err
}
d.files = ui.BuildParagraph("", nil)
pingCircle, err := ctx.Renderer().CreateTexture(newCircle())
if err != nil {
return err
}
d.ping = &ping{circle: pingCircle}
ctx.Overlays().AddOnTop("ping", d.ping, true)
d.Background = color.White
d.Children = []ui.Control{
&ui.Label{Text: "Drop files on this window!"},
d.files,
}
return nil
}
func run() error {
var render, err = ui.NewRendererDefault("Files Drop Example", 800, 600)
if err != nil {
return err
}
defer render.Destroy()
return ui.RunWait(render, ui.DefaultStyle(), &dropFiles{}, true)
}
func main() {
var err = run()
if err != nil {
log.Fatal(err)
}
}

34
ui/fallbackresources.go Normal file
View File

@ -0,0 +1,34 @@
package ui
import "io"
var _ Resources = &fallbackResources{}
type fallbackResources struct {
resources Resources
fallback Resources
}
// NewFallbackResources creates a Resources that first will try to access resources and on failure will try to access the fallback resources. Will take ownership of both resources (Destroy).
func NewFallbackResources(resources, fallback Resources) Resources {
return &fallbackResources{resources, fallback}
}
func (r *fallbackResources) OpenResource(name string) (io.ReadCloser, error) {
if reader, err := r.resources.OpenResource(name); err == nil {
return reader, nil
}
return r.fallback.OpenResource(name)
}
func (r *fallbackResources) Destroy() error {
errResources := r.resources.Destroy()
errFallback := r.fallback.Destroy()
if errResources != nil {
return errResources
}
if errFallback != nil {
return errFallback
}
return nil
}

View File

@ -7,6 +7,7 @@ import (
)
type Font interface {
Destroy() error
Height() float32
Measure(t string) geom.RectangleF32
WidthOf(t string) float32

79
ui/fonts.go Normal file
View File

@ -0,0 +1,79 @@
package ui
import (
"fmt"
"image/color"
"opslag.de/schobers/geom"
)
type Fonts struct {
render Renderer
fonts map[string]Font
}
func NewFonts(render Renderer) *Fonts {
return &Fonts{render, map[string]Font{}}
}
func (f *Fonts) AddFont(name string, font Font) {
curr := f.fonts[name]
if curr != nil {
curr.Destroy()
}
f.fonts[name] = font
}
func (f *Fonts) createFont(name string, create func() (Font, error)) (Font, error) {
font, err := create()
if err != nil {
return nil, err
}
f.AddFont(name, font)
return font, nil
}
func (f *Fonts) CreateFontPath(name, path string, size int) (Font, error) {
return f.createFont(name, func() (Font, error) {
return f.render.CreateFontPath(path, size)
})
}
func (f *Fonts) Destroy() {
for _, font := range f.fonts {
font.Destroy()
}
f.fonts = nil
}
func (f *Fonts) Font(name string) Font {
font, ok := f.fonts[name]
if ok {
return font
}
return nil
}
func (f *Fonts) Text(fontName string, p geom.PointF32, color color.Color, text string) {
font := f.Font(fontName)
if font == nil {
return
}
f.render.Text(font, p, color, text)
}
func (f *Fonts) TextAlign(fontName string, p geom.PointF32, color color.Color, text string, align HorizontalAlignment) {
font := f.Font(fontName)
if font == nil {
return
}
f.render.TextAlign(font, p, color, text, align)
}
func (f *Fonts) TextTexture(fontName string, color color.Color, text string) (Texture, error) {
font := f.Font(fontName)
if font == nil {
return nil, fmt.Errorf("no font with name '%s'", fontName)
}
return f.render.TextTexture(font, color, text)
}

View File

@ -7,23 +7,54 @@ import (
"opslag.de/schobers/geom"
)
type IconPixelTestFn func(geom.PointF32) bool
type AlphaPixelImageSource struct {
ImageAlphaPixelTestFn
func IconSize() geom.Point {
return geom.Pt(448, 512)
Size geom.Point
}
func CreateIcon(ctx Context, test IconPixelTestFn) Image {
icon := DrawIcon(test)
im, err := ctx.Renderer().CreateImage(icon)
func (s *AlphaPixelImageSource) CreateImage() (image.Image, error) {
return DrawImageAlpha(s.Size, s.ImageAlphaPixelTestFn), nil
}
type ImageAlphaPixelTestFn func(geom.PointF32) uint8
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
}
return im
return texture
}
func DrawIcon(test IconPixelTestFn) image.Image {
func CreateIcon(ctx Context, test ImagePixelTestFn) Texture {
return createTexture(ctx, test.CreateImageSource(IconSize()))
}
func CreateTexture(ctx Context, size geom.Point, test ImagePixelTestFn) Texture {
return createTexture(ctx, test.CreateImageSource(size))
}
func CreateTextureAlpha(ctx Context, size geom.Point, test ImageAlphaPixelTestFn) Texture {
return createTexture(ctx, test.CreateImageSource(size))
}
func DrawIcon(test ImagePixelTestFn) image.Image {
size := IconSize()
return DrawImage(size, test)
}
func DrawImage(size geom.Point, test ImagePixelTestFn) image.Image {
r := image.Rect(0, 0, size.X, size.Y)
icon := image.NewRGBA(r)
for y := 0; y < size.Y; y++ {
@ -37,16 +68,42 @@ func DrawIcon(test IconPixelTestFn) image.Image {
return icon
}
func GetOrCreateIcon(ctx Context, name string, testFactory func() IconPixelTestFn) Image {
im := ctx.Images().Image(name)
if im != nil {
return im
func DrawImageAlpha(size geom.Point, test ImageAlphaPixelTestFn) image.Image {
r := image.Rect(0, 0, size.X, size.Y)
icon := image.NewAlpha(r)
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
pt := geom.PtF32(float32(x), float32(y))
if a := test(pt); a > 0 {
icon.Set(x, y, color.Alpha{A: a})
}
test := testFactory()
im = CreateIcon(ctx, test)
if im == nil {
}
}
return icon
}
func GetOrCreateIcon(ctx Context, name string, test ImagePixelTestFn) Texture {
texture := ctx.Textures().Texture(name)
if texture != nil {
return texture
}
texture, err := ctx.Textures().CreateTexture(name, test.CreateImageSource(IconSize()))
if err != nil {
return nil
}
ctx.Images().AddImage(name, im)
return im
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
}

View File

@ -1,10 +0,0 @@
package ui
import "image"
type Image interface {
Destroy()
Height() float32
Image() image.Image
Width() float32
}

View File

@ -1,123 +0,0 @@
package ui
import (
"image"
"github.com/nfnt/resize"
"opslag.de/schobers/geom"
)
type CreateImageFn func() (image.Image, error)
func ScaleImage(render Renderer, im Image, scale float32) Image {
w := uint(im.Width() * scale)
if w == 0 {
return nil
}
scaled := resize.Resize(w, 0, im.Image(), resize.Bilinear)
res, err := render.CreateImage(scaled)
if err != nil {
return nil
}
return res
}
type Images struct {
render Renderer
ims map[string]Image
scaled map[Image]ScaledImages
}
func NewImages(render Renderer) *Images {
return &Images{render, map[string]Image{}, map[Image]ScaledImages{}}
}
func (i *Images) AddImage(name string, im Image) {
curr := i.ims[name]
if curr != nil {
curr.Destroy()
}
i.ims[name] = im
}
func (i *Images) AddImageFn(name string, create CreateImageFn) error {
im, err := create()
if err != nil {
return err
}
return i.AddImageNative(name, im)
}
func (i *Images) AddImageNative(name string, im image.Image) error {
m, err := i.render.CreateImage(im)
if err != nil {
return err
}
i.AddImage(name, m)
return nil
}
func (i *Images) Destroy() {
for _, im := range i.ims {
im.Destroy()
}
i.ims = nil
for _, ims := range i.scaled {
ims.Destroy()
}
i.scaled = nil
}
func (i *Images) Image(name string) Image {
im, ok := i.ims[name]
if ok {
return im
}
return nil
}
func (i *Images) Scaled(im Image, scale float32) Image {
if scale <= 0 {
return nil
}
if scale == 1 {
return im
}
ims := i.scaled[im]
if ims == nil {
ims = make(ScaledImages)
} else {
scaled := ims[scale]
if scaled != nil {
return scaled
}
}
scaled := ScaleImage(i.render, im, scale)
ims[scale] = scaled
i.scaled[im] = ims
return scaled
}
func (i *Images) ScaledHeight(im Image, height float32) (Image, float32) {
scale := height / im.Height()
if geom.IsNaN32(scale) {
return nil, 0
}
return i.Scaled(im, scale), scale
}
func (i *Images) ScaledByName(name string, scale float32) Image {
im := i.Image(name)
if im == nil {
return nil
}
return i.Scaled(im, scale)
}
type ScaledImages map[float32]Image
func (i ScaledImages) Destroy() {
for _, im := range i {
im.Destroy()
}
}

70
ui/imagesource.go Normal file
View File

@ -0,0 +1,70 @@
package ui
import (
"image"
"github.com/nfnt/resize"
"opslag.de/schobers/zntg"
)
type ImageSource interface {
CreateImage() (image.Image, error)
}
type ImageSourceFile string
var _ ImageSource = ImageSourceFile("")
func (s ImageSourceFile) CreateImage() (image.Image, error) {
return zntg.DecodeImage(string(s))
}
type ImageSourceGo struct {
image.Image
}
var _ ImageSource = ImageSourceGo{}
func (s ImageSourceGo) CreateImage() (image.Image, error) {
return s.Image, nil
}
type ImageSourceResource struct {
Resources Resources
Name string
}
func (s ImageSourceResource) CreateImage() (image.Image, error) {
src, err := s.Resources.OpenResource(s.Name)
if err != nil {
return nil, err
}
defer src.Close()
value, err := zntg.ImageDecoder(src)
if err != nil {
return nil, err
}
return value.(image.Image), nil
}
func ImageSourceFromResources(res Resources, name string) ImageSourceResource {
return ImageSourceResource{Resources: res, Name: name}
}
func Scaled(source ImageSource, width, height int) ImageSource {
return scaledImageSource{ImageSource: source, width: width, height: height}
}
type scaledImageSource struct {
ImageSource
width, height int
}
func (s scaledImageSource) CreateImage() (image.Image, error) {
im, err := s.ImageSource.CreateImage()
if err != nil {
return nil, err
}
return resize.Resize(uint(s.width), uint(s.height), im, resize.Bicubic), nil
}

157
ui/key.go Normal file
View File

@ -0,0 +1,157 @@
package ui
type Key int
const (
KeyNone Key = iota
Key0
Key1
Key2
Key3
Key4
Key5
Key6
Key7
Key8
Key9
KeyA
KeyAlt
KeyAltGr
KeyAt
KeyB
KeyBack
KeyBackslash //x2
KeyBackspace
KeyBacktick
KeyButtonA
KeyButtonB
KeyButtonL1
KeyButtonL2
KeyButtonR1
KeyButtonR2
KeyButtonX
KeyButtonY
KeyC
KeyCapsLock
KeyCircumflex
KeyCloseBrace
KeyColon2
KeyComma
KeyCommand
KeyD
KeyDelete
KeyDown
KeyDPadCenter
KeyDPadDown
KeyDPadLeft
KeyDPadRight
KeyDPadUp
KeyE
KeyEnd
KeyEnter
KeyEquals
KeyEscape
KeyF
KeyF1
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyFullstop
KeyG
KeyH
KeyHome
KeyI
KeyInsert
KeyJ
KeyK
KeyL
KeyLeft
KeyLeftControl
KeyLeftShift
KeyLeftWin
KeyM
KeyMenu
KeyMinus
KeyN
KeyNumLock
KeyO
KeyOpenBrace
KeyP
KeyPad0
KeyPad1
KeyPad2
KeyPad3
KeyPad4
KeyPad5
KeyPad6
KeyPad7
KeyPad8
KeyPad9
KeyPadAsterisk
KeyPadDelete
KeyPadEnter
KeyPadEquals
KeyPadMinus
KeyPadPlus
KeyPadSlash
KeyPageDown
KeyPageUp
KeyPause
KeyPrintScreen
KeyQ
KeyQuote
KeyR
KeyRight
KeyRightControl
KeyRightShift
KeyRightWin
KeyS
KeyScrollLock
KeySearch
KeySelect
KeySemicolon
KeySlash
KeySpace
KeyStart
KeyT
KeyTab
KeyThumbL
KeyThumbR
KeyTilde
KeyU
KeyUp
KeyV
KeyVolumeDown
KeyVolumeUp
KeyW
KeyX
KeyY
KeyZ
)
type KeyState map[Key]bool
func (s KeyState) Modifiers() KeyModifier {
var mods KeyModifier
if s[KeyAlt] || s[KeyAltGr] {
mods |= KeyModifierAlt
}
if s[KeyLeftControl] || s[KeyRightControl] {
mods |= KeyModifierControl
}
if s[KeyLeftShift] || s[KeyRightShift] {
mods |= KeyModifierShift
}
if s[KeyLeftWin] || s[KeyRightWin] || s[KeyCommand] {
mods |= KeyModifierOSCommand
}
return mods
}

View File

@ -1,15 +1,26 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type TextOverflow int
const (
TextOverflowClip TextOverflow = iota
TextOverflowEllipsis
)
type Label struct {
ControlBase
Text string
TextOverflow TextOverflow
DropShadow color.Color
size desiredSizeCache
desired CachedValue
}
func BuildLabel(text string, fn func(*Label)) *Label {
@ -20,21 +31,47 @@ func BuildLabel(text string, fn func(*Label)) *Label {
return l
}
func (l *Label) DesiredSize(ctx Context) geom.PointF32 {
var fontName = l.FontName(ctx)
return l.size.Update(ctx, fontName+l.Text, func(ctx Context) geom.PointF32 {
var font = ctx.Renderer().Font(fontName)
var width = font.WidthOf(l.Text)
var height = font.Height()
var pad = ctx.Style().Dimensions.TextPadding
return geom.PtF32(width+pad*2, height+pad*2)
})
func (l *Label) hashDesiredSize(ctx Context) string {
return l.FontName(ctx) + l.Text
}
func (l *Label) desiredSize(ctx Context) interface{} {
font := l.ActualFont(ctx)
width := font.WidthOf(l.Text)
height := font.Height()
pad := l.ActualTextPadding(ctx)
return geom.PtF32(pad.Left+width+pad.Right, pad.Top+height+pad.Bottom)
}
func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
return l.desired.GetContext(ctx, l.desiredSize, l.hashDesiredSize).(geom.PointF32)
}
func (l *Label) getLabelTopLeft(ctx Context) geom.PointF32 {
pad := l.ActualTextPadding(ctx)
bounds := pad.InsetRect(l.bounds)
switch l.TextAlignment {
case AlignRight:
return geom.PtF32(bounds.Max.X, bounds.Min.Y)
case AlignCenter:
return geom.PtF32(.5*(bounds.Min.X+bounds.Max.X), bounds.Min.Y)
default:
return bounds.Min
}
}
func (l *Label) Render(ctx Context) {
l.RenderBackground(ctx)
var c = l.FontColor(ctx)
var f = l.FontName(ctx)
var pad = ctx.Style().Dimensions.TextPadding
ctx.Renderer().TextAlign(l.bounds.Min.Add(geom.PtF32(pad, pad)), f, c, l.Text, l.TextAlignment)
fontColor := l.TextColor(ctx)
font := l.ActualFont(ctx)
topLeft := l.getLabelTopLeft(ctx)
text := l.Text
availableWidth := l.bounds.Dx()
if l.TextOverflow == TextOverflowEllipsis {
text = fitTextEllipsis(font, text, availableWidth)
}
if l.DropShadow != nil {
ctx.Renderer().TextAlign(font, topLeft.Add2D(1, 1), l.DropShadow, text, l.TextAlignment)
}
ctx.Renderer().TextAlign(font, topLeft, fontColor, text, l.TextAlignment)
}

View File

@ -24,9 +24,54 @@ func Fixed(l float32) *Length { return &Length{l} }
func Infinite() *Length { return &Length{geom.NaN32()} }
func ZL() *Length { return &Length{0} }
type SideLengths struct {
Left *Length
Top *Length
Right *Length
Bottom *Length
}
func (l SideLengths) InsetRect(r geom.RectangleF32) geom.RectangleF32 {
return Sides{
Left: l.Left.Value(),
Top: l.Top.Value(),
Right: l.Right.Value(),
Bottom: l.Bottom.Value(),
}.InsetRect(r)
}
func (l SideLengths) Zero(value float32) Sides {
return Sides{
Left: l.Left.Zero(value),
Top: l.Top.Zero(value),
Right: l.Right.Zero(value),
Bottom: l.Bottom.Zero(value),
}
}
type Sides struct {
Left float32
Top float32
Right float32
Bottom float32
}
func (s Sides) InsetRect(r geom.RectangleF32) geom.RectangleF32 {
if r.Dx() < (s.Left + s.Right) {
r.Min.X += r.Dx() * s.Left / (s.Left + s.Right)
r.Max.X = r.Min.X
} else {
r.Min.X += s.Left
r.Max.X -= s.Right
}
if r.Dy() < (s.Top + s.Bottom) {
r.Min.Y += r.Dy() * s.Top / (s.Top + s.Bottom)
r.Max.Y = r.Min.Y
} else {
r.Min.Y += s.Top
r.Max.Y -= s.Bottom
}
return r
}

5
ui/notifier.go Normal file
View File

@ -0,0 +1,5 @@
package ui
type Notifier interface {
Notify(Context, interface{}) bool
}

34
ui/osresources.go Normal file
View File

@ -0,0 +1,34 @@
package ui
import (
"io"
"os"
)
var _ PhysicalResources = &OSResources{}
// DefaultResources returns the default Resources implementation (OSResources).
func DefaultResources() PhysicalResources {
return &OSResources{}
}
// OSResources is Resources implementation that uses the default file system directly.
type OSResources struct {
}
// FetchResource checks if file is available and returns the specified path.
func (r *OSResources) FetchResource(name string) (string, error) {
_, err := os.Stat(name)
if err != nil {
return "", err
}
return name, nil
}
// OpenResource opens the specified file on disk.
func (r *OSResources) OpenResource(name string) (io.ReadCloser, error) {
return os.Open(name)
}
// Destroy does nothing.
func (r *OSResources) Destroy() error { return nil }

View File

@ -11,6 +11,9 @@ type overflow struct {
Background color.Color
ClipHorizontal bool
ClipVertical bool
barWidth float32
desired geom.PointF32
bounds geom.RectangleF32
@ -23,15 +26,33 @@ type overflow struct {
ver *Scrollbar
}
func Overflow(content Control) Control {
type ScrollControl interface {
Control
SetBackgroundColor(color.Color)
SetClipHorizontal(bool)
SetClipVertical(bool)
SetScrollbarColor(bar color.Color, hover color.Color)
}
func BuildOverflow(content Control, build func(c ScrollControl)) ScrollControl {
o := &overflow{Proxy: Proxy{Content: content}}
o.hor = BuildScrollbar(OrientationHorizontal, func(*Scrollbar) {})
o.ver = BuildScrollbar(OrientationVertical, func(*Scrollbar) {})
if build != nil {
build(o)
}
return o
}
func Overflow(content Control) ScrollControl {
return OverflowBackground(content, nil)
}
func OverflowBackground(content Control, back color.Color) Control {
var o = &overflow{Proxy: Proxy{Content: content}, Background: back}
o.hor = BuildScrollbar(OrientationHorizontal, func(*Scrollbar) {})
o.ver = BuildScrollbar(OrientationVertical, func(*Scrollbar) {})
return o
func OverflowBackground(content Control, back color.Color) ScrollControl {
return BuildOverflow(content, func(c ScrollControl) {
c.SetBackgroundColor(back)
})
}
func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
@ -47,6 +68,8 @@ func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
if hor && !ver {
ver = scroll(size.Y+o.barWidth, bounds.Dy())
}
hor = hor && !o.ClipHorizontal
ver = ver && !o.ClipVertical
return
}
@ -62,7 +85,7 @@ func (o *overflow) doOnVisibleBars(fn func(bar *Scrollbar)) {
func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
o.barWidth = ctx.Style().Dimensions.ScrollbarWidth
o.desired = o.Content.DesiredSize(ctx)
o.desired = o.Content.DesiredSize(ctx, bounds.Size())
o.bounds = bounds
o.offset = offset
o.parent = parent
@ -91,11 +114,15 @@ func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Po
func (o *overflow) Bounds() geom.RectangleF32 { return o.bounds }
func (o *overflow) DesiredSize(ctx Context) geom.PointF32 {
func (o *overflow) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 {
return path.BoundsUnclipped(ctx, o)
}
func (o *overflow) DesiredSize(Context, geom.PointF32) geom.PointF32 {
return geom.PtF32(geom.NaN32(), geom.NaN32())
}
func (o *overflow) Handle(ctx Context, e Event) {
func (o *overflow) Handle(ctx Context, e Event) bool {
if o.Content != o.proxied {
o.hor.ContentOffset = 0
o.ver.ContentOffset = 0
@ -117,10 +144,11 @@ func (o *overflow) Handle(ctx Context, e Event) {
content.Max = content.Max.Add(contentO)
if e.MouseWheel != 0 && e.Pos().In(content) {
o.ver.ContentOffset = geom.Max32(0, geom.Min32(o.ver.ContentLength-content.Dy(), o.ver.ContentOffset-36*e.MouseWheel))
return true
}
}
}
o.Content.Handle(ctx, e)
return o.Content.Handle(ctx, e)
}
func (o *overflow) IsInBounds(p geom.PointF32) bool { return p.Sub(o.offset).In(o.bounds) }
@ -139,7 +167,7 @@ func (o *overflow) Render(ctx Context) {
var content = o.Content.Bounds()
content.Min = geom.ZeroPtF32
err := o.content.Update(ctx, content.Size())
if err != nil {
if err != nil && err != ErrNewBufferSize {
panic(err)
}
o.content.Render(ctx, o.bounds.Min, func(Context, geom.PointF32) {
@ -151,3 +179,43 @@ func (o *overflow) Render(ctx Context) {
bar.Render(ctx)
})
}
func (o *overflow) SetBackgroundColor(c color.Color) {
o.Background = c
}
func (o *overflow) SetClipHorizontal(clip bool) {
o.ClipHorizontal = clip
}
func (o *overflow) SetClipVertical(clip bool) {
o.ClipVertical = clip
}
func (o *overflow) SetScrollbarColor(bar color.Color, hover color.Color) {
o.hor.BarColor = bar
o.hor.BarHoverColor = hover
o.ver.BarColor = bar
o.ver.BarHoverColor = hover
}
func (o *overflow) ScrollIntoView(ctx Context, path ControlPath) {
view := path.BoundsUnclipped(ctx, o)
size := geom.PtF32(o.hor.Bounds().Dx(), o.ver.Bounds().Dy())
view.Min.Y += o.ver.ContentOffset
view.Max.Y += o.ver.ContentOffset
if view.Max.Y > o.ver.ContentLength {
view.Max.Y = o.ver.ContentLength
if view.Min.Y > view.Max.Y {
view.Min.Y = view.Max.Y
}
}
if view.Min.Y < 0 {
view.Min.Y = 0
}
if view.Max.Y > o.ver.ContentOffset+size.Y {
o.ver.ContentOffset = view.Max.Y - size.Y
}
if view.Min.Y < o.ver.ContentOffset {
o.ver.ContentOffset = view.Min.Y
}
}

8
ui/overlay.go Normal file
View File

@ -0,0 +1,8 @@
package ui
type Overlay interface {
Control
Shown()
Hidden()
}

9
ui/overlaybase.go Normal file
View File

@ -0,0 +1,9 @@
package ui
type OverlayBase struct {
ControlBase
}
func (o *OverlayBase) Hidden() {}
func (o *OverlayBase) Shown() {}

109
ui/overlays.go Normal file
View File

@ -0,0 +1,109 @@
package ui
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
var _ Control = &Overlays{}
type OverlayVisibilityChangedArgs struct {
Name string
Visible bool
}
type Overlays struct {
Proxy
overlays map[string]Overlay
order []string
visible map[string]bool
overlayHidden zntg.Events
overlayShown zntg.Events
visibilityChanged zntg.Events
}
func NewOverlays(over Control) *Overlays {
return &Overlays{
Proxy: Proxy{Content: over},
overlays: map[string]Overlay{},
visible: map[string]bool{},
}
}
func (o *Overlays) setVisibility(name string, visible bool) {
if o.visible[name] == visible {
return
}
o.visible[name] = visible
overlay, ok := o.overlays[name].(Overlay)
if ok {
if visible {
overlay.Shown()
} else {
overlay.Hidden()
}
}
o.visibilityChanged.Notify(OverlayVisibilityChangedArgs{Name: name, Visible: visible})
if visible {
o.overlayShown.Notify(name)
} else {
o.overlayHidden.Notify(name)
}
}
func (o *Overlays) AddOnTop(name string, overlay Overlay, visible bool) {
o.order = append(o.order, name)
o.overlays[name] = overlay
o.visible[name] = false
if visible {
o.Show(name)
}
}
func (o *Overlays) AddOnBottom(name string, overlay Overlay, visible bool) {
o.order = append([]string{name}, o.order...)
o.overlays[name] = overlay
o.visible[name] = visible
}
func (o *Overlays) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
o.Proxy.Arrange(ctx, bounds, offset, parent)
for _, overlay := range o.overlays {
overlay.Arrange(ctx, bounds, offset, parent)
}
}
func (o *Overlays) Handle(ctx Context, e Event) bool {
var handled bool
for _, overlay := range o.order {
if o.visible[overlay] {
if o.overlays[overlay].Handle(ctx, e) { // handle all overlays regardless of return value
handled = true
}
}
}
if handled {
return true
}
return o.Proxy.Handle(ctx, e)
}
func (o *Overlays) Hide(name string) { o.SetVisibility(name, false) }
func (o *Overlays) Render(ctx Context) {
o.Proxy.Render(ctx)
for _, overlay := range o.order {
if o.visible[overlay] {
o.overlays[overlay].Render(ctx)
}
}
}
func (o *Overlays) SetVisibility(name string, visible bool) { o.setVisibility(name, visible) }
func (o *Overlays) Show(name string) { o.SetVisibility(name, true) }
func (o *Overlays) Toggle(name string) { o.SetVisibility(name, !o.visible[name]) }

97
ui/paragraph.go Normal file
View File

@ -0,0 +1,97 @@
package ui
import (
"strconv"
"strings"
"opslag.de/schobers/geom"
)
type Paragraph struct {
Label
width float32
lines CachedValue
}
func BuildParagraph(text string, fn func(*Paragraph)) *Paragraph {
var p = &Paragraph{}
p.Text = text
if fn != nil {
fn(p)
}
return p
}
func fastFormatFloat32(f float32) string { return strconv.FormatFloat(float64(f), 'b', 32, 32) }
func (p *Paragraph) desiredSize(ctx Context) interface{} {
font := p.ActualFont(ctx)
pad := p.ActualTextPadding(ctx)
lines := p.splitInLines(ctx, p.width-pad.Left-pad.Right)
return geom.PtF32(p.width, float32(len(lines))*font.Height()+pad.Top+pad.Bottom)
}
func (p *Paragraph) hashTextArranged(ctx Context) string {
return p.FontName(ctx) + p.Text + fastFormatFloat32(p.Bounds().Dx())
}
func (p *Paragraph) hashTextDesired(ctx Context) string {
return p.FontName(ctx) + p.Text + fastFormatFloat32(p.width)
}
func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
font := p.ActualFont(ctx)
var lines []string
for _, line := range strings.Split(p.Text, "\n") {
if len(line) == 0 {
lines = append(lines, line)
continue
}
for len(line) > 0 {
clipped := fitTextWord(font, line, width)
lines = append(lines, clipped)
line = strings.TrimLeft(line[len(clipped):], " ")
}
}
return lines
}
func (p *Paragraph) updateLines(ctx Context) interface{} {
pad := p.ActualTextPadding(ctx)
return p.splitInLines(ctx, p.Bounds().Dx()-pad.Left-pad.Right)
}
func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
// stores the given width, is used when calculating the new desired size (and thus used in the hash method as well)
p.width = size.X
return p.desired.GetContext(ctx, p.desiredSize, p.hashTextDesired).(geom.PointF32)
}
func (p *Paragraph) Render(ctx Context) {
p.RenderBackground(ctx)
pad := p.ActualTextPadding(ctx)
width := p.Bounds().Dx() - pad.Left - pad.Right
lines := p.lines.GetContext(ctx, p.updateLines, p.hashTextArranged).([]string)
fontColor := p.TextColor(ctx)
fontName := p.FontName(ctx)
fontHeight := ctx.Fonts().Font(fontName).Height()
bounds := pad.InsetRect(p.bounds)
left := bounds.Min.X
switch p.TextAlignment {
case AlignRight:
left = bounds.Max.X
case AlignCenter:
left += .5 * width
}
offset := bounds.Min.Y
for _, line := range lines {
ctx.Fonts().TextAlign(fontName, geom.PtF32(left, offset), fontColor, line, p.TextAlignment)
offset += fontHeight
}
}

62
ui/pathresources.go Normal file
View File

@ -0,0 +1,62 @@
package ui
import (
"io"
"path/filepath"
)
var _ Resources = &PathResources{}
// PathResources implements Resources by adding a prefix to the requested source name before proxying it to its source.
type PathResources struct {
Source Resources
Prefix string
}
// NewPathResources creates a new Resources by adding a prefix to the requested source name before proxying it to its source. If source is nil it will use the OS file system for its resources.
func NewPathResources(source Resources, prefix string) *PathResources {
if source == nil {
source = &OSResources{}
}
return &PathResources{source, prefix}
}
// Destroy destroys the source.
func (r *PathResources) Destroy() error { return r.Source.Destroy() }
// OpenResource opens the resource with the prefixed name.
func (r *PathResources) OpenResource(name string) (io.ReadCloser, error) {
path := filepath.Join(r.Prefix, name)
return r.Source.OpenResource(path)
}
var _ PhysicalResources = &PathPhysicalResources{}
// PathPhysicalResources implements PhysicalResources by adding a prefix to the requested source name before proxying it to its source.
type PathPhysicalResources struct {
Source PhysicalResources
Prefix string
}
// NewPathPhysicalResources creates a new PhysicalResources by adding a prefix to the requested source name before proxying it to its source. If source is nil it will use the OS file system for its resources.
func NewPathPhysicalResources(source PhysicalResources, prefix string) *PathPhysicalResources {
if source == nil {
source = &OSResources{}
}
return &PathPhysicalResources{source, prefix}
}
// Destroy destroys the source.
func (r *PathPhysicalResources) Destroy() error { return r.Source.Destroy() }
// FetchResource fetches the resource with the prefixed name.
func (r *PathPhysicalResources) FetchResource(name string) (string, error) {
path := filepath.Join(r.Prefix, name)
return r.Source.FetchResource(path)
}
// OpenResource opens the resource with the prefixed name.
func (r *PathPhysicalResources) OpenResource(name string) (io.ReadCloser, error) {
path := filepath.Join(r.Prefix, name)
return r.Source.OpenResource(path)
}

View File

@ -3,6 +3,7 @@ package ui
import "opslag.de/schobers/geom"
var _ Control = &Proxy{}
var _ Overlay = &Proxy{}
type Proxy struct {
Content Control
@ -12,12 +13,12 @@ func (p *Proxy) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Point
p.Content.Arrange(ctx, bounds, offset, parent)
}
func (p *Proxy) DesiredSize(ctx Context) geom.PointF32 {
return p.Content.DesiredSize(ctx)
func (p *Proxy) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
return p.Content.DesiredSize(ctx, size)
}
func (p *Proxy) Handle(ctx Context, e Event) {
p.Content.Handle(ctx, e)
func (p *Proxy) Handle(ctx Context, e Event) bool {
return p.Content.Handle(ctx, e)
}
func (p *Proxy) Render(ctx Context) {
@ -28,26 +29,44 @@ func (p *Proxy) Bounds() geom.RectangleF32 {
return p.Content.Bounds()
}
func (p *Proxy) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 {
return path.BoundsUnclipped(ctx, p)
}
func (p *Proxy) Disable() { p.Content.Disable() }
func (p *Proxy) Enable() { p.Content.Enable() }
func (p *Proxy) Hidden() {
overlay, ok := p.Content.(Overlay)
if ok {
overlay.Hidden()
}
}
func (p *Proxy) IsDisabled() bool { return p.Content.IsDisabled() }
func (p *Proxy) IsInBounds(pt geom.PointF32) bool { return p.Content.IsInBounds(pt) }
func (p *Proxy) IsOver() bool { return p.Content.IsOver() }
func (p *Proxy) Offset() geom.PointF32 { return p.Content.Offset() }
func (p *Proxy) OnClick(fn ClickFn) {
p.Content.OnClick(fn)
}
func (p *Proxy) OnDragStart(fn DragStartFn) {
p.Content.OnDragStart(fn)
}
func (p *Proxy) OnDragMove(fn DragMoveFn) {
p.Content.OnDragMove(fn)
}
func (p *Proxy) OnDragEnd(fn DragEndFn) {
p.Content.OnDragEnd(fn)
}
func (p *Proxy) Parent() Control { return p.Content.Parent() }
func (p *Proxy) Shown() {
overlay, ok := p.Content.(Overlay)
if ok {
overlay.Shown()
}
}
func (p *Proxy) ScrollIntoView(ctx Context, path ControlPath) {
p.Content.Parent().ScrollIntoView(ctx, path.Prepend(p))
}
func (p *Proxy) Self() Control {
return p.Content.Self()
}
func (p *Proxy) SetSelf(self Control) { p.Content.SetSelf(self) }

View File

@ -5,32 +5,63 @@ import (
"image/color"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
type Renderer interface {
// Events
PushEvents(t EventTarget, wait bool)
PushEvents(t EventTarget, wait bool) bool
Refresh()
Stamp() float64 // in seconds
// Lifetime
Destroy() error
// Drawing
Clear(c color.Color)
CreateImage(m image.Image) (Image, error)
CreateImagePath(path string) (Image, error)
CreateImageSize(w, h float32) (Image, error)
DefaultTarget() Image
DrawImage(im Image, p geom.PointF32)
DrawImageOptions(im Image, p geom.PointF32, opts DrawOptions)
CreateFontPath(path string, size int) (Font, 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.RectangleF32)
DrawTextureOptions(t Texture, p geom.RectangleF32, opts DrawOptions)
DrawTexturePoint(t Texture, p geom.PointF32)
DrawTexturePointOptions(t Texture, p geom.PointF32, opts DrawOptions)
FillRectangle(r geom.RectangleF32, c color.Color)
Font(font string) Font
Line(p, q geom.PointF32, color color.Color, thickness float32)
Location() geom.Point
Move(geom.Point)
Rectangle(r geom.RectangleF32, c color.Color, thickness float32)
RenderTo(Image)
RenderTo(Texture)
RenderToDisplay()
Resize(width, height int)
SetIcon(source ImageSource)
SetMouseCursor(c MouseCursor)
Size() geom.PointF32
Target() Image
Text(p geom.PointF32, font string, color color.Color, text string)
TextAlign(p geom.PointF32, font string, color color.Color, text string, align HorizontalAlignment)
Size() geom.Point
Target() Texture
Text(font Font, p geom.PointF32, color color.Color, text string)
TextAlign(font Font, p geom.PointF32, color color.Color, text string, align HorizontalAlignment)
TextTexture(font Font, color color.Color, text string) (Texture, error)
WindowHandle() uintptr
// Resources
Resources() Resources
SetResourceProvider(resources Resources)
}
// TextTexture renders specified text to a new texture.
func TextTexture(render Renderer, font Font, color color.Color, text string) (Texture, error) {
target := render.Target()
defer render.RenderTo(target)
bounds := font.Measure(text)
texture, err := render.CreateTextureTarget(bounds.Dx(), bounds.Dy())
if err != nil {
return nil, err
}
render.RenderTo(texture)
render.Clear(zntg.MustHexColor(`#00000000`))
render.Text(font, bounds.Min.Invert(), color, text)
return texture, nil
}

47
ui/rendererfactory.go Normal file
View File

@ -0,0 +1,47 @@
package ui
import (
"errors"
"opslag.de/schobers/geom"
)
// RendererFactory can be used to inject a new factory for creating renderers.
type RendererFactory interface {
New(title string, width, height int, opts NewRendererOptions) (Renderer, error)
}
var rendererFactory RendererFactory
// NewRenderer creates a new renderer based on the registered renderer factory.
func NewRenderer(title string, width, height int, opts NewRendererOptions) (Renderer, error) {
if rendererFactory == nil {
return nil, errors.New("no renderer factory registered")
}
renderer, err := rendererFactory.New(title, width, height, opts)
if err != nil {
return nil, err
}
renderer.SetResourceProvider(DefaultResources())
return renderer, nil
}
// NewRendererDefault creates a new renderer with default options set based on the registered renderer factory.
func NewRendererDefault(title string, width, height int) (Renderer, error) {
return NewRenderer(title, width, height, NewRendererOptions{
Resizable: true,
})
}
// SetRendererFactory sets the new factory that is used to create a new renderer.
func SetRendererFactory(factory RendererFactory) {
rendererFactory = factory
}
// NewRendererOptions provides options when creating a new renderer.
type NewRendererOptions struct {
Location *geom.PointF32
Borderless bool
Resizable bool
VSync bool
}

19
ui/resources.go Normal file
View File

@ -0,0 +1,19 @@
package ui
import "io"
// Resources is an abstraction for opening resources.
type Resources interface {
// OpenResource should open the resource with the specified name. The user is responsible for closing the resource.
OpenResource(name string) (io.ReadCloser, error)
// Destroy can be used for cleaning up at the end of the applications lifetime.
Destroy() error
}
// PhysicalResources is an abstraction for opening and fetching (to disk) resources.
type PhysicalResources interface {
Resources
// FetchResource should fetch the resource with the specified name and return a path (on disk) where the resource can be accessed.
FetchResource(name string) (string, error)
}

View File

@ -1,7 +1,10 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
type Scrollbar struct {
@ -9,6 +12,8 @@ type Scrollbar struct {
Orientation Orientation
BarColor color.Color
BarHoverColor color.Color
ContentLength float32
ContentOffset float32
@ -18,14 +23,14 @@ type Scrollbar struct {
func BuildScrollbar(o Orientation, fn func(s *Scrollbar)) *Scrollbar {
var s = &Scrollbar{Orientation: o, ContentLength: 0, ContentOffset: 0}
s.handle.OnDragStart(func(_ geom.PointF32) {
s.handle.DragStarted().AddHandler(func(Context, DragStartedArgs) {
s.startDragOffset = s.ContentOffset
})
s.handle.OnDragMove(func(start, move geom.PointF32) {
s.handle.DragMoved().AddHandler(func(_ Context, args DragMovedArgs) {
var length = s.Orientation.SizeParallel(s.bounds)
var handleMaxOffset = length - s.Orientation.SizeParallel(s.handle.bounds)
var hidden = s.ContentLength - length
var offset = (s.Orientation.LengthParallel(move) - s.Orientation.LengthParallel(start)) / handleMaxOffset
var offset = (s.Orientation.LengthParallel(args.Current) - s.Orientation.LengthParallel(args.Start)) / handleMaxOffset
s.ContentOffset = geom.Max32(0, geom.Min32(s.startDragOffset+offset*hidden, hidden))
})
if fn != nil {
@ -39,24 +44,25 @@ func (s *Scrollbar) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.P
s.updateBar(ctx)
}
func (s *Scrollbar) DesiredSize(ctx Context) geom.PointF32 {
func (s *Scrollbar) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
return s.Orientation.Pt(geom.NaN32(), ctx.Style().Dimensions.ScrollbarWidth)
}
func (s *Scrollbar) Handle(ctx Context, e Event) {
func (s *Scrollbar) Handle(ctx Context, e Event) bool {
s.handle.Handle(ctx, e)
switch e := e.(type) {
case *MouseMoveEvent:
if e.MouseWheel != 0 && e.Pos().Sub(s.offset).In(s.bounds) {
s.ContentOffset = geom.Max32(0, geom.Min32(s.ContentLength-s.Orientation.SizeParallel(s.bounds), s.ContentOffset-36*e.MouseWheel))
return true
}
}
s.ControlBase.Handle(ctx, e)
return s.ControlBase.Handle(ctx, e)
}
func (s *Scrollbar) Render(ctx Context) {
ctx.Renderer().FillRectangle(s.bounds, RGBA(0, 0, 0, 1))
s.handle.Render(ctx)
ctx.Renderer().FillRectangle(s.bounds, zntg.RGBA(0, 0, 0, 1))
s.handle.Render(ctx, s)
}
func (s *Scrollbar) updateBar(ctx Context) {
@ -81,12 +87,22 @@ type ScrollbarHandle struct {
ControlBase
}
func (h *ScrollbarHandle) Render(ctx Context) {
func (h *ScrollbarHandle) Render(ctx Context, s *Scrollbar) {
h.RenderBackground(ctx)
p := ctx.Style().Palette
fill := p.Primary
var fill color.Color
if h.over {
if s.BarHoverColor == nil {
fill = p.PrimaryLight
} else {
fill = s.BarHoverColor
}
} else {
if s.BarColor == nil {
fill = p.Primary
} else {
fill = s.BarColor
}
}
ctx.Renderer().FillRectangle(h.bounds.Inset(1), fill)
}

Some files were not shown because too many files have changed in this diff Show More