Compare commits

...

157 Commits
ui ... master

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
4d05127c6d Removed Context and Control from event handlers. 2019-07-06 07:42:40 +02:00
36d620108c Optimized rendering a lot of labels.
- Added caching of the desired size of a label.
2019-07-04 22:15:32 +02:00
9e577ab1aa Optimizations for rendering large number of items (in a stack panel) and improved visualization of scrollbar. 2019-07-04 22:06:48 +02:00
3adf44a516 Added some extra display related functions. 2019-07-04 22:05:08 +02:00
3bd4001cc3 Fixed mouse cursor bug.
If the Run loop was non-blocking (wait set to false) the cursor was reset back to the default.
2019-07-04 22:03:29 +02:00
a6bb05794f Added mouse enter/leave event. 2019-06-24 21:56:52 +02:00
e867925de8 Added (horizontal) text alignment. 2019-06-24 20:58:14 +02:00
ff064bbf1f Added TextPositive & TextNegative colors. 2019-06-24 20:57:58 +02:00
c709d906d0 Improved synchronization with a quit signal. 2019-06-21 10:49:06 +02:00
4e6d089efe Added support for creating image from a sub image.
- With a non-zero Min coordinate.
2019-06-21 10:47:10 +02:00
eee707c4ec Added overloads to add an image to the Images container. 2019-05-01 09:42:09 +02:00
fe4a2b7b73 Added webp dependency on Windows. 2019-05-01 09:41:41 +02:00
6a71720b71 Init function can return an error. 2019-05-01 09:40:59 +02:00
7f9f10075f Replaced implementation of WidthOf by TextWidth.
- TextDimensions (the underlying al_get_text_dimensions) returns the dimensions of the printable characters (non-whitespace) which gives weird results when measuring spaces.
2019-04-13 10:18:00 +02:00
dbc017507c Added Parent to Controls.
Fixed issue where events of partially and invisible controls where incorrectly handled.
2019-04-11 23:38:32 +02:00
3e7e2ab682 Added OnSelectedChanged to Checkbox.
Updated graphics for Checkbox.
2019-04-11 23:30:26 +02:00
99c76545ef Added RenderOutline to ControlBase.
Added HoverColor to Button.
2019-04-11 23:27:25 +02:00
8d78083583 Moved Shadow and added Background control. 2019-04-11 22:33:31 +02:00
6bcab622ae Added support for Home and End buttons in TextBox.
Fixed scaling of icon in Button.
2019-04-11 22:22:23 +02:00
d072e202ab Added checkbox.
Added utility methods for generating icons.
2019-04-11 21:23:51 +02:00
15f02d4d0e Infinite margins should not be taken into account for DesiredSize. 2019-04-11 20:05:30 +02:00
70d9e23b0c Added shadow control. 2019-04-11 20:03:36 +02:00
8f17c02634 Fixed incorrect bounds being returned by Overflow control. 2019-04-11 20:03:26 +02:00
28af8aba06 Changed palette (added PrimaryDark color and changed PrimaryHighlight and PrimaryLight colors). 2019-04-11 20:02:15 +02:00
3390d46f34 Moved scaling by height to Images. 2019-04-11 20:00:26 +02:00
3acb3f09af Added scaled images to image cache. 2019-04-10 22:41:28 +02:00
f550220120 Added (OS specific) clipboard implementation.
- Based on github.com/atotto/clipboard.
2019-04-11 08:44:38 +02:00
fcf31b381b Added TextBox.
Updated basic example.
2019-04-11 08:44:38 +02:00
eb0e8322ef Changed Length.Zero to return value instead of Length.
Spacing shorthand methods now returns Spacing object instead of Control interface.
2019-04-11 08:44:38 +02:00
04779d6588 Changed Buffer.Render signature.
Added BufferControl on top of Buffer.
2019-04-11 08:44:01 +02:00
67658d3bee Added key event.
Some small additions/changes.
2019-04-10 21:23:56 +02:00
71cdc234ee Added clipboard.
- Default implementation only copies to application memory. Is open for using external implementation.
2019-04-10 21:21:55 +02:00
fa7796a4ae Added support for animations. 2019-04-10 21:20:39 +02:00
552de0c748 Fixed padding of button. 2019-04-09 20:32:59 +02:00
2ffb579340 Factored out (render) buffer. 2019-04-09 20:32:42 +02:00
c024416702 Added ui.RefreshEvent.
- Refresh signal is now propagated to controls.
2019-03-13 21:20:31 +01:00
a6718e335d Fixed bug in StackPanel
- Children now get actual bounds, might be outside of the parent's bounds.
2019-03-13 20:18:15 +01:00
4d518849e5 Added IconScale for Button. 2019-03-13 20:16:54 +01:00
ff51378aff Added button types.
Go image can be reconstructed from ui.Image/allg5.Bitmap.
Changed allg5.Scale from interface to struct.
2019-03-13 19:49:00 +01:00
a3aa398909 Restructered spacing controls & added margin helpers. 2019-03-13 19:03:06 +01:00
3ea0d76efb Created unified spacing control.
Build control methods now accept nil as visitor method.
2019-03-12 21:11:43 +01:00
d14a9bd0e7 Fixed bugs in Overflow control.
- Tries to take as much space as possible.
- Draw buffer should be exactly size needed.
2019-03-12 20:08:47 +01:00
f31cd28771 Added color to palette.
Added comments to palette.
2019-03-12 19:50:34 +01:00
c83a6d5aad Added FontStyle on ControlBase.
Forced putting text on integer coordinates.
2019-03-12 19:43:52 +01:00
c973305b6d Cleanup of (font) resources on destroy. 2019-03-12 19:23:47 +01:00
1d6587adbd Added changing the mouse cursor. 2019-03-11 17:30:41 +01:00
0387e313de Added user events & possibility to 'refresh'.
- Refresh unblocks event loop for the next redraw.
2019-03-06 18:23:53 +01:00
c08474a01c Added display resize event. 2019-03-06 18:23:53 +01:00
ff2ece7d06 Added mouse wheel implementation (scrollbar & overflow).
Fixed bug in length of scrollbar handle.
2019-03-06 18:22:30 +01:00
19daffd110 Added UI elements.
Reversed order of operands when comparing with nil/0 .
2019-03-05 21:52:18 +01:00
89a3d49992 Renamed allegro5 package to allg5. 2018-11-21 14:35:14 +01:00
fd9be87470 Added -static flag to LDFLAGS for static build. 2018-11-21 14:30:33 +01:00
5a3818976a Added SetIcon to Display. 2018-11-21 14:30:32 +01:00
8bdf7d0b76 Various additions:
- Added sub-bitmaps.
- Color implements color.Color interface.
- Added methods to retrieve font dimensions and its rendered text.
- Added DrawPolyline method.
2018-11-21 14:30:31 +01:00
115 changed files with 9046 additions and 1043 deletions

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
# Visual Studio Code
.vscode
# Go
debug
debug.test
# Project
ui/examples/99_playground

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,21 @@
package clipboard
import (
clip "github.com/atotto/clipboard" // actual implementation
"opslag.de/schobers/zntg/ui"
)
type clipboard struct {
}
func (c clipboard) WriteText(t string) error {
return clip.WriteAll(t)
}
func (c clipboard) ReadText() (string, error) {
return clip.ReadAll()
}
func init() {
ui.DefaultClipboard = &clipboard{}
}

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,224 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
// #include <stdlib.h>
import "C"
import (
"errors"
"image"
"image/color"
"unsafe"
)
// Bitmap represents an in memory bitmap
type Bitmap struct {
bitmap *C.ALLEGRO_BITMAP
width int
height int
}
type DrawOptions struct {
Center bool
Scale Scale
Tint *Color
Rotation *Rotation
}
type Scale interface {
Horizontal() float32
Vertical() float32
}
type scale struct {
horizontal float32
vertical float32
}
func (s *scale) Horizontal() float32 { return s.horizontal }
func (s *scale) Vertical() float32 { return s.vertical }
func NewScale(horizontal, vertical float32) Scale {
return &scale{horizontal, vertical}
}
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 nil != mut {
mut(m)
}
for _, f := range flags {
m.Set(f)
}
})
b := C.al_create_bitmap(C.int(width), C.int(height))
if nil == b {
return nil, errors.New("error creating bitmap")
}
return &Bitmap{b, width, height}, 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(im 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 = im.Bounds()
width, height := bnd.Dx(), bnd.Dy()
var b = C.al_create_bitmap(C.int(width), C.int(height))
if nil == b {
return nil, errors.New("error creating memory bitmap")
}
row := make([]uint8, width*4)
rgn := C.al_lock_bitmap(b, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_WRITEONLY)
if nil == rgn {
C.al_destroy_bitmap(b)
return nil, errors.New("unable to lock bitmap")
}
data := (*[1 << 30]uint8)(rgn.data)
offset := 0
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
pix := color.RGBAModel.Convert(im.At(x, y)).(color.RGBA)
row[x*4] = pix.R
row[x*4+1] = pix.G
row[x*4+2] = pix.B
row[x*4+3] = pix.A
}
copy(data[offset:], row)
offset += int(rgn.pitch)
}
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
}
// 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 nil == b {
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
}
// 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 := nil != options.Scale
if scale {
width *= options.Scale.Horizontal()
height *= options.Scale.Vertical()
}
if options.Center {
left -= width * 0.5
top -= height * 0.5
}
rotated := nil != options.Rotation
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 nil == options.Tint { // 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 nil == options.Tint {
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)
}
}
}
}
func (b *Bitmap) Width() int {
return b.width
}
func (b *Bitmap) Height() int {
return b.height
}
func (b *Bitmap) SetAsTarget() {
C.al_set_target_bitmap(b.bitmap)
}
// Destroy destroys the bitmap
func (b *Bitmap) Destroy() {
C.al_destroy_bitmap(b.bitmap)
}

View File

@ -1,6 +0,0 @@
// +build !windows
package allegro5
// #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 allegro5
// #cgo LDFLAGS: -lallegro -lallegro_font -lallegro_image -lallegro_primitives -lallegro_ttf
import "C"

View File

@ -1,6 +0,0 @@
// +build windows,static
package allegro5
// #cgo LDFLAGS: -lallegro_monolith-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++
import "C"

View File

@ -1,16 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
import "C"
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))}
}

View File

@ -1,100 +0,0 @@
package allegro5
// #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
}
// 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
}
}
C.al_set_new_display_flags(flags)
d := C.al_create_display(C.int(width), C.int(height))
if nil == d {
return nil, errors.New("error creating display")
}
return &Display{d}, nil
}
// Flips 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) 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)
}
// Destroy destroys the display
func (d *Display) Destroy() {
C.al_destroy_display(d.display)
}
func SetNewWindowTitle(title string) {
t := C.CString(title)
defer C.free(unsafe.Pointer(t))
C.al_set_new_window_title(t)
}

View File

@ -1,195 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
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 MouseEvent struct {
EventBase
X, Y int
Z, W int
Display *Display
}
type MouseMoveEvent struct {
MouseEvent
DeltaX, DeltaY int
DeltaZ, DeltaW int
Pressure float32
}
func NewEventQueue() (*EventQueue, error) {
q := C.al_create_event_queue()
if nil == q {
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) 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_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_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)}
}
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)
}

View File

@ -1,23 +0,0 @@
package allegro5
// #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,65 +0,0 @@
package allegro5
// #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
}
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 nil == f {
return nil, fmt.Errorf("unable to load ttf font '%s'", path)
}
return &Font{f}, 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)
}
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 allegro5
// #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 allegro5
// #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 allegro5
// #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,29 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
import "C"
type MouseButton uint
const (
MouseButtonLeft MouseButton = 1
MouseButtonRight = 2
MouseButtonMiddle = 3
)
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 allegro5
// #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 allegro5
// #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,25 +0,0 @@
package allegro5
// #include <allegro5/allegro.h>
// #include <allegro5/allegro_primitives.h>
import "C"
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 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 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 allegro5
// #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
}

49
allg5ui/font.go Normal file
View File

@ -0,0 +1,49 @@
package allg5ui
import (
"opslag.de/schobers/allg5"
"opslag.de/schobers/geom"
)
type FontDefinition struct {
Name string
Size int
}
func NewFontDefinition(name string, size int) FontDefinition {
return FontDefinition{Name: name, Size: size}
}
type font struct {
*allg5.Font
}
func newFont(f *allg5.Font) *font {
return &font{f}
}
func (f *font) Destroy() error {
f.Font.Destroy()
return nil
}
func (f *font) Height() float32 {
if f == nil {
return 0
}
return f.Font.Height()
}
func (f *font) WidthOf(t string) float32 {
return f.TextWidth(t)
}
func (f *font) Measure(t string) geom.RectangleF32 {
if f == nil {
return geom.RectangleF32{}
}
// 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()
}

9
ui/alignment.go Normal file
View File

@ -0,0 +1,9 @@
package ui
type HorizontalAlignment int
const (
AlignLeft HorizontalAlignment = iota
AlignCenter
AlignRight
)

78
ui/buffer.go Normal file
View File

@ -0,0 +1,78 @@
package ui
import (
"errors"
"opslag.de/schobers/geom"
)
type RenderBufferFn func(ctx Context, size geom.PointF32)
type Buffer struct {
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.texture != nil {
if size == b.size {
return nil
}
b.texture.Destroy()
b.texture = nil
b.size = geom.ZeroPtF32
}
texture, err := ctx.Renderer().CreateTextureTarget(size.X, size.Y)
if err != nil {
return err
}
b.texture = texture
b.size = size
return ErrNewBufferSize
}
func (b *Buffer) Render(ctx Context, pos geom.PointF32, fn RenderBufferFn) {
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.texture)
fn(ctx, b.size)
renderer.RenderTo(currTarget)
}
func (b *Buffer) RenderToDisplay(ctx Context, pos geom.PointF32) {
if b.texture == nil {
return
}
ctx.Renderer().DrawTexturePoint(b.texture, pos)
}
type BufferControl struct {
ControlBase
buffer Buffer
}
func (c *BufferControl) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
c.ControlBase.Arrange(ctx, bounds, offset, parent)
c.buffer.Update(ctx, bounds.Size())
}
func (c *BufferControl) Render(ctx Context) {
c.RenderFn(ctx, c.RenderBuffer)
}
func (c *BufferControl) RenderFn(ctx Context, render RenderBufferFn) {
c.buffer.Render(ctx, c.bounds.Min, render)
}
func (c *BufferControl) RenderBuffer(ctx Context, size geom.PointF32) {}

235
ui/button.go Normal file
View File

@ -0,0 +1,235 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type Button struct {
ControlBase
DisabledColor color.Color
HoverColor color.Color
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
const (
ButtonTypeContained ButtonType = iota
ButtonTypeIcon
ButtonTypeOutlined
ButtonTypeText
)
func BuildButton(text string, fn func(b *Button)) *Button { return BuildIconButton("", text, fn) }
func BuildIconButton(icon, text string, fn func(b *Button)) *Button {
var b = &Button{Text: text, Icon: icon}
if fn != nil {
fn(b)
}
return b
}
func (b *Button) desiredSize(ctx Context) geom.PointF32 {
var pad = b.ActualTextPadding(ctx)
var font = b.ActualFont(ctx)
var w, h float32 = 0, font.Height()
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 w == 0 {
return geom.ZeroPtF32
}
return geom.PtF32(w, pad.Top+h+pad.Bottom)
}
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) 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
}
return b.Background
}
if b.over {
if b.HoverColor != nil {
return b.HoverColor
}
switch b.Type {
case ButtonTypeContained:
return p.PrimaryLight
default:
return p.PrimaryHighlight
}
}
switch b.Type {
case ButtonTypeContained:
return p.Primary
}
return nil
}
func (b *Button) fontColor(c color.Color) color.Color {
if b.Font.Color == nil {
return c
}
return b.Font.Color
}
func (b *Button) textColor(p *Palette) color.Color {
if b.Disabled {
if b.Background != nil {
return p.TextOnDisabled
}
switch b.Type {
case ButtonTypeContained:
return p.TextOnDisabled
}
return b.disabledColor(p)
}
switch b.Type {
case ButtonTypeContained:
return b.fontColor(p.TextOnPrimary)
case ButtonTypeIcon:
if b.over {
if b.HoverColor != nil {
return b.HoverColor
}
return b.fontColor(p.Primary)
}
return b.fontColor(p.Text)
default:
return b.fontColor(p.Primary)
}
}
func (b *Button) Render(ctx Context) {
var style = ctx.Style()
var palette = style.Palette
textColor := b.textColor(palette)
fillColor := b.fillColor(palette)
if fillColor != nil {
ctx.Renderer().FillRectangle(b.bounds, fillColor)
}
size := b.desiredSize(ctx)
bounds := b.bounds
deltaX, deltaY := bounds.Dx()-size.X, bounds.Dy()-size.Y
bounds.Min.X += .5 * deltaX
bounds.Min.Y += .5 * deltaY
pad := b.ActualTextPadding(ctx)
bounds = pad.InsetRect(bounds)
boundsH := bounds.Dy()
pos := bounds.Min
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 {
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.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
}

134
ui/checkbox.go Normal file
View File

@ -0,0 +1,134 @@
package ui
import (
"opslag.de/schobers/geom"
)
type Checkbox struct {
ControlBase
selectedChanged Events
Selected bool
Text string
}
func BuildCheckbox(text string, fn func(c *Checkbox)) *Checkbox {
var c = &Checkbox{Text: text}
if fn != nil {
fn(c)
}
return c
}
func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
pad := c.ActualTextPadding(ctx)
font := c.ActualFont(ctx)
var w, h float32 = 0, font.Height()
if len(c.Text) != 0 {
w += pad.Left + font.WidthOf(c.Text)
}
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) Texture {
if c.Selected {
return GetOrCreateIcon(ctx, "ui-default-checkbox-selected", c.selectedIcon)
} 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) Texture {
return GetOrCreateIcon(ctx, "ui-default-checkbox", c.normalIcon)
}
var checkBoxIconBorder = geom.PolF32(
geom.PtF32(48, 80),
geom.PtF32(400, 80),
geom.PtF32(400, 432),
geom.PtF32(48, 432),
)
var checkBoxCheckMark = geom.PointsF32{
geom.PtF32(96, 256),
geom.PtF32(180, 340),
geom.PtF32(340, 150),
}
func (c *Checkbox) hoverIcon(pt geom.PointF32) bool {
return (pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)) || pt.DistanceToLines(checkBoxCheckMark) < 24
}
func (c *Checkbox) normalIcon(pt geom.PointF32) bool {
return pt.DistanceToPolygon(checkBoxIconBorder) < 48 && !pt.InPolygon(checkBoxIconBorder)
}
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
}
switch e := e.(type) {
case *MouseButtonDownEvent:
if e.Button == MouseButtonLeft && c.over {
c.Selected = !c.Selected
return c.selectedChanged.Notify(ctx, c.Selected)
}
}
return false
}
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.TextColor(ctx)
bounds := c.bounds
pad := c.ActualTextPadding(ctx)
bounds = pad.InsetRect(bounds)
boundsH := bounds.Dy()
pos := bounds.Min
icon := c.icon(ctx)
if icon != nil {
iconColor := fore
if c.Selected {
iconColor = c.FontColor(ctx, palette.Primary)
}
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 {
font := c.ActualFont(ctx)
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fore, c.Text)
}
}

21
ui/clipboard.go Normal file
View File

@ -0,0 +1,21 @@
package ui
type Clipboard interface {
WriteText(t string) error
ReadText() (string, error)
}
var DefaultClipboard Clipboard = &clipboard{}
type clipboard struct {
value string
}
func (c *clipboard) WriteText(t string) error {
c.value = t
return nil
}
func (c *clipboard) ReadText() (string, error) {
return c.value, nil
}

71
ui/containerbase.go Normal file
View File

@ -0,0 +1,71 @@
package ui
import (
"opslag.de/schobers/geom"
)
type ContainerBase struct {
ControlBase
Children []Control
}
func BuildContainerBase(controls ...Control) ContainerBase {
return ContainerBase{
Children: controls,
}
}
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) {
for _, child := range c.Children {
child.Arrange(ctx, bounds, offset, c)
}
c.ControlBase.Arrange(ctx, bounds, offset, parent)
}
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) {
c.RenderBackground(ctx)
for _, child := range c.Children {
child.Render(ctx)
}
}

111
ui/context.go Normal file
View File

@ -0,0 +1,111 @@
package ui
import (
"opslag.de/schobers/geom"
)
type Context interface {
Animate()
Fonts() *Fonts
HasQuit() bool
KeyModifiers() KeyModifier
MousePosition() geom.PointF32
Overlays() *Overlays
Quit()
Renderer() Renderer
Resources() Resources
ShowTooltip(t string)
Style() *Style
Textures() *Textures
}
var _ Context = &context{}
var _ EventTarget = &context{}
type context struct {
animate bool
quit chan struct{}
renderer Renderer
view Control
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:
return true
default:
return false
}
}
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() {
close(c.quit)
}
}
func (c *context) Textures() *Textures { return c.textures }
// Handle implement EventTarget
func (c *context) Handle(e Event) {
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.overlays.Handle(c, e)
}

51
ui/control.go Normal file
View File

@ -0,0 +1,51 @@
package ui
import (
"opslag.de/schobers/geom"
)
type Control interface {
Arrange(Context, geom.RectangleF32, geom.PointF32, Control)
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
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
Init(Context) error
}

306
ui/controlbase.go Normal file
View File

@ -0,0 +1,306 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
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
drag Dragable
over bool
pressed bool
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) {
c.bounds = bounds
c.offset = offset
c.parent = parent
}
func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds }
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)
}
}()
var over = func(e MouseEvent) bool {
pos := e.Pos()
if !c.IsInBounds(pos) {
return false
}
parent := c.Parent()
for parent != nil {
if !parent.IsInBounds(pos) {
return false
}
parent = parent.Parent()
}
return true
}
switch e := e.(type) {
case *MouseMoveEvent:
c.over = over(e.MouseEvent)
if c.pressed {
if start, ok := c.drag.IsDragging(); ok {
var move = c.ToControlPosition(e.Pos())
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
case *MouseButtonDownEvent:
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 {
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) 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 = color
}
return text
}
func (c *ControlBase) FontName(ctx Context) string {
var name = c.Font.Name
if len(name) == 0 {
name = ctx.Style().Fonts.Default
}
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 {
bounds.Min.X = 0
}
if bounds.Min.Y < 0 {
bounds.Min.Y = 0
}
return c.ToControlPosition(p).In(c.bounds)
}
func (c *ControlBase) IsOver() bool { return c.over }
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) OutlineColor(ctx Context) color.Color {
return c.FontColor(ctx, ctx.Style().Palette.Primary)
}
func (c *ControlBase) Render(Context) {}
func (c *ControlBase) RenderBackground(ctx Context) {
if c.Background != nil {
ctx.Renderer().FillRectangle(c.bounds, c.Background)
}
}
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
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)
}

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})
}

20
ui/drawoptions.go Normal file
View File

@ -0,0 +1,20 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type DrawOptions struct {
Source *geom.RectangleF32
Tint color.Color
}
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
}

133
ui/event.go Normal file
View File

@ -0,0 +1,133 @@
package ui
import "opslag.de/schobers/geom"
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
}
type Event interface {
Stamp() float64
}
type EventBase struct {
StampInSeconds float64
}
func (e *EventBase) Stamp() float64 {
return e.StampInSeconds
}
type KeyModifier int
const (
KeyModifierNone KeyModifier = 0
KeyModifierShift = 1 << iota
KeyModifierControl
KeyModifierAlt
KeyModifierOSCommand
)
type KeyDownEvent struct {
EventBase
Key Key
Modifiers KeyModifier
}
type KeyUpEvent struct {
EventBase
Key Key
Modifiers KeyModifier
}
type MouseButton int
const (
MouseButtonLeft MouseButton = 1
MouseButtonRight MouseButton = 2
MouseButtonMiddle MouseButton = 3
)
type MouseButtonDownEvent struct {
MouseEvent
Button MouseButton
}
type MouseButtonUpEvent struct {
MouseEvent
Button MouseButton
}
type MouseEnterEvent struct {
MouseEvent
}
type MouseEvent struct {
EventBase
X, Y float32
}
func (e *MouseEvent) Pos() geom.PointF32 {
return geom.PtF32(e.X, e.Y)
}
type MouseLeaveEvent struct {
MouseEvent
}
type MouseMoveEvent struct {
MouseEvent
MouseWheel float32
}
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) })
}

5
ui/eventtarget.go Normal file
View File

@ -0,0 +1,5 @@
package ui
type EventTarget interface {
Handle(Event)
}

View File

@ -0,0 +1,133 @@
package main
import (
"image/color"
"log"
_ "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/ui"
)
type basic struct {
ui.StackPanel
}
func (b *basic) Init(ctx ui.Context) error {
_, err := ctx.Fonts().CreateFontPath("default", "../resources/font/OpenSans-Regular.ttf", 14)
if err != nil {
return err
}
_, 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()
s.Margin.Left = ui.Fixed(margin)
s.Margin.Top = ui.Fixed(margin)
s.Margin.Right = ui.Fixed(margin)
s.Margin.Bottom = ui.Fixed(margin)
})
}
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),
}
}),
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) {
p.Children = []ui.Control{
&ui.Checkbox{},
ui.BuildCheckbox("Check me!", nil),
}
}),
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.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
}
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() {
var err = run()
if err != nil {
log.Fatal(err)
}
}

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)
}
}

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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
}

19
ui/font.go Normal file
View File

@ -0,0 +1,19 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type Font interface {
Destroy() error
Height() float32
Measure(t string) geom.RectangleF32
WidthOf(t string) float32
}
type FontStyle struct {
Name string
Color color.Color
}

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)
}

109
ui/icon.go Normal file
View File

@ -0,0 +1,109 @@
package ui
import (
"image"
"image/color"
"opslag.de/schobers/geom"
)
type AlphaPixelImageSource struct {
ImageAlphaPixelTestFn
Size geom.Point
}
func (s *AlphaPixelImageSource) CreateImage() (image.Image, error) {
return DrawImageAlpha(s.Size, s.ImageAlphaPixelTestFn), nil
}
type ImageAlphaPixelTestFn func(geom.PointF32) uint8
func (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 texture
}
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++ {
for x := 0; x < size.X; x++ {
pt := geom.PtF32(float32(x), float32(y))
if test(pt) {
icon.Set(x, y, color.White)
}
}
}
return icon
}
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})
}
}
}
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
}
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
}

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
}

77
ui/label.go Normal file
View File

@ -0,0 +1,77 @@
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
desired CachedValue
}
func BuildLabel(text string, fn func(*Label)) *Label {
var l = &Label{Text: text}
if fn != nil {
fn(l)
}
return l
}
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)
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)
}

77
ui/length.go Normal file
View File

@ -0,0 +1,77 @@
package ui
import "opslag.de/schobers/geom"
type Length struct {
float32
}
func (l *Length) Value() float32 {
if l == nil {
return 0
}
return l.float32
}
func (l *Length) Zero(value float32) float32 {
if l == nil {
return value
}
return l.Value()
}
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
}

11
ui/mouse.go Normal file
View File

@ -0,0 +1,11 @@
package ui
type MouseCursor int
const (
MouseCursorNone MouseCursor = iota
MouseCursorDefault
MouseCursorNotAllowed
MouseCursorPointer
MouseCursorText
)

5
ui/notifier.go Normal file
View File

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

76
ui/orientation.go Normal file
View File

@ -0,0 +1,76 @@
package ui
import "opslag.de/schobers/geom"
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationHorizontal
)
func (o Orientation) FlipPt(p geom.PointF32) geom.PointF32 {
if o == OrientationHorizontal {
return geom.PtF32(p.Y, p.X)
}
return p
}
func (o Orientation) FlipRect(r geom.RectangleF32) geom.RectangleF32 {
if o == OrientationHorizontal {
return geom.RectF32(r.Min.Y, r.Min.X, r.Max.Y, r.Max.X)
}
return r
}
func (o Orientation) LengthParallel(pt geom.PointF32) float32 {
if o == OrientationVertical {
return pt.Y
}
return pt.X
}
func (o Orientation) LengthPerpendicular(pt geom.PointF32) float32 {
if o == OrientationVertical {
return pt.X
}
return pt.Y
}
func (o Orientation) Pt(parallel, perpendicular float32) geom.PointF32 {
if o == OrientationVertical {
return geom.PtF32(perpendicular, parallel)
}
return geom.PtF32(parallel, perpendicular)
}
func (o Orientation) Rect(min geom.PointF32, parallel, perpendicular float32) geom.RectangleF32 {
if o == OrientationVertical {
return geom.RectF32(min.X, min.Y, min.X+perpendicular, min.Y+parallel)
}
return geom.RectF32(min.X, min.Y, min.X+parallel, min.Y+perpendicular)
}
func (o Orientation) SizeParallel(r geom.RectangleF32) float32 {
if o == OrientationVertical {
return r.Dy()
}
return r.Dx()
}
func (o Orientation) SizePerpendicular(r geom.RectangleF32) float32 {
if o == OrientationVertical {
return r.Dx()
}
return r.Dy()
}
func (o Orientation) String() string {
switch o {
case OrientationVertical:
return "vertical"
case OrientationHorizontal:
return "horizontal"
}
return "undefined"
}

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 }

221
ui/overflow.go Normal file
View File

@ -0,0 +1,221 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type overflow struct {
Proxy
Background color.Color
ClipHorizontal bool
ClipVertical bool
barWidth float32
desired geom.PointF32
bounds geom.RectangleF32
offset geom.PointF32
parent Control
content Buffer
proxied Control
hor *Scrollbar
ver *Scrollbar
}
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) ScrollControl {
return BuildOverflow(content, func(c ScrollControl) {
c.SetBackgroundColor(back)
})
}
func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
var scroll = func(need, actual float32) bool {
return !geom.IsNaN32(need) && need > actual
}
var size = o.desired
hor = scroll(size.X, bounds.Dx())
ver = scroll(size.Y, bounds.Dy())
if ver && !hor {
hor = scroll(size.X+o.barWidth, bounds.Dx())
}
if hor && !ver {
ver = scroll(size.Y+o.barWidth, bounds.Dy())
}
hor = hor && !o.ClipHorizontal
ver = ver && !o.ClipVertical
return
}
func (o *overflow) doOnVisibleBars(fn func(bar *Scrollbar)) {
hor, ver := o.shouldScroll(o.bounds)
if hor {
fn(o.hor)
}
if ver {
fn(o.ver)
}
}
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, bounds.Size())
o.bounds = bounds
o.offset = offset
o.parent = parent
var hor, ver = o.shouldScroll(bounds)
var contentX, contentY float32 = 0, 0
var contentW, contentH = bounds.Dx(), bounds.Dy()
if hor {
contentX -= o.hor.ContentOffset
contentH = geom.Max32(0, contentH-o.barWidth)
}
if ver {
contentY -= o.ver.ContentOffset
contentW = geom.Max32(0, contentW-o.barWidth)
}
o.Content.Arrange(ctx, geom.RectF32(contentX, contentY, contentW, contentH), offset.Add(bounds.Min), o)
if hor {
o.hor.ContentLength = o.desired.X
o.hor.Arrange(ctx, geom.RectF32(bounds.Min.X, bounds.Min.Y+contentH, bounds.Min.X+contentW, bounds.Max.Y), offset, o)
}
if ver {
o.ver.ContentLength = o.desired.Y
o.ver.Arrange(ctx, geom.RectF32(bounds.Min.X+contentW, bounds.Min.Y, bounds.Max.X, bounds.Min.Y+contentH), offset, o)
}
}
func (o *overflow) Bounds() geom.RectangleF32 { return o.bounds }
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) bool {
if o.Content != o.proxied {
o.hor.ContentOffset = 0
o.ver.ContentOffset = 0
o.proxied = o.Content
}
hor, ver := o.shouldScroll(o.bounds)
if hor {
o.hor.Handle(ctx, e)
}
if ver {
o.ver.Handle(ctx, e)
}
switch e := e.(type) {
case *MouseMoveEvent:
if ver {
var contentO = o.Content.Offset()
var content = o.Content.Bounds()
content.Min = contentO
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
}
}
}
return o.Content.Handle(ctx, e)
}
func (o *overflow) IsInBounds(p geom.PointF32) bool { return p.Sub(o.offset).In(o.bounds) }
func (o *overflow) Offset() geom.PointF32 { return o.offset }
func (o *overflow) Parent() Control { return o.parent }
func (o *overflow) Render(ctx Context) {
var renderer = ctx.Renderer()
if o.Background != nil {
renderer.FillRectangle(o.bounds, o.Background)
}
var content = o.Content.Bounds()
content.Min = geom.ZeroPtF32
err := o.content.Update(ctx, content.Size())
if err != nil && err != ErrNewBufferSize {
panic(err)
}
o.content.Render(ctx, o.bounds.Min, func(Context, geom.PointF32) {
renderer.Clear(color.Transparent)
o.Content.Render(ctx)
})
o.doOnVisibleBars(func(bar *Scrollbar) {
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)
}

72
ui/proxy.go Normal file
View File

@ -0,0 +1,72 @@
package ui
import "opslag.de/schobers/geom"
var _ Control = &Proxy{}
var _ Overlay = &Proxy{}
type Proxy struct {
Content Control
}
func (p *Proxy) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
p.Content.Arrange(ctx, bounds, offset, parent)
}
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) bool {
return p.Content.Handle(ctx, e)
}
func (p *Proxy) Render(ctx Context) {
p.Content.Render(ctx)
}
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) 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) }

67
ui/renderer.go Normal file
View File

@ -0,0 +1,67 @@
package ui
import (
"image"
"image/color"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
type Renderer interface {
// Events
PushEvents(t EventTarget, wait bool) bool
Refresh()
Stamp() float64 // in seconds
// Lifetime
Destroy() error
// Drawing
Clear(c color.Color)
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)
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(Texture)
RenderToDisplay()
Resize(width, height int)
SetIcon(source ImageSource)
SetMouseCursor(c MouseCursor)
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
}

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