├── cmd ├── calibrate │ ├── calibrate │ ├── main.go │ ├── tinydisplay.go │ └── rp2040.go └── demo │ ├── tinydisplay.go │ ├── main.go │ └── rp2040.go ├── examples ├── calc │ ├── CalcExample.png │ └── calcGui.go ├── temp │ ├── TempExample.png │ └── temperatureGui.go ├── test │ ├── TestExample.png │ └── testGui.go ├── calibrate │ ├── CalibrateExample.png │ └── calibrate.go ├── toggleled │ ├── ToggleLedExample.png │ └── toggleled.go └── nav │ └── navGui.go ├── stack ├── golang.go ├── stack.go └── rp2040.go ├── event ├── event.go └── eventtype.go ├── component ├── commonprops.go ├── event.go ├── textfield.go ├── iconbutton.go ├── floatingactionbutton.go ├── switch.go ├── checkbox.go ├── slider.go ├── radiobutton.go ├── dashcard.go ├── bottomnavigation.go ├── dashlinegraph.go └── button.go ├── icon ├── icon.go ├── add.go ├── checkmark.go ├── edit.go ├── timeline.go ├── announcement.go ├── backspace.go └── calc.go ├── displayer.go ├── adapter ├── fillrectadapter.go ├── tinydisplaydapter.go ├── rotated.go └── upsidedownadapter.go ├── layout └── grid.go ├── LICENSE.md ├── clip.go ├── primitive ├── color.go ├── hsv.go ├── shape.go └── primitives.go ├── theme └── theme.go ├── guicontext.go ├── go.mod ├── README.md ├── tinygui.go └── go.sum /cmd/calibrate/calibrate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/cmd/calibrate/calibrate -------------------------------------------------------------------------------- /examples/calc/CalcExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/examples/calc/CalcExample.png -------------------------------------------------------------------------------- /examples/temp/TempExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/examples/temp/TempExample.png -------------------------------------------------------------------------------- /examples/test/TestExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/examples/test/TestExample.png -------------------------------------------------------------------------------- /examples/calibrate/CalibrateExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/examples/calibrate/CalibrateExample.png -------------------------------------------------------------------------------- /examples/toggleled/ToggleLedExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spearson78/tinygui/HEAD/examples/toggleled/ToggleLedExample.png -------------------------------------------------------------------------------- /stack/golang.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package stack 5 | 6 | func UpdateStackPointer(loc string) { 7 | } 8 | -------------------------------------------------------------------------------- /stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import "math" 4 | 5 | var MinStackLocation string = "." 6 | var MinStackPointer = int32(math.MaxInt32) 7 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type Event struct { 4 | Type Type 5 | 6 | X uint16 7 | Y uint16 8 | 9 | DragX uint16 10 | DragY uint16 11 | } 12 | -------------------------------------------------------------------------------- /component/commonprops.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | type ComponentPos struct { 4 | X int16 5 | Y int16 6 | } 7 | 8 | type ComponentSize struct { 9 | W int16 10 | H int16 11 | } 12 | -------------------------------------------------------------------------------- /icon/icon.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | ) 8 | 9 | type Icon func(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) 10 | -------------------------------------------------------------------------------- /displayer.go: -------------------------------------------------------------------------------- 1 | package tinygui 2 | 3 | import ( 4 | "image/color" 5 | 6 | "tinygo.org/x/drivers" 7 | ) 8 | 9 | type Displayer interface { 10 | drivers.Displayer 11 | FillRect(x, y, w, h int16, c color.RGBA) 12 | } 13 | -------------------------------------------------------------------------------- /event/eventtype.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type Type byte 4 | 5 | const ( 6 | None Type = 0 7 | Invalidate Type = 1 8 | Update Type = 2 9 | Click Type = 4 10 | Drag Type = 8 11 | DragEnd Type = 16 12 | ) 13 | -------------------------------------------------------------------------------- /icon/add.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "tinygo.org/x/tinydraw" 8 | ) 9 | 10 | func Add(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 11 | center := w / 2 12 | 13 | tinydraw.FilledRectangleEx(g.Display, x, y+center-1, w, 2, c) 14 | tinydraw.FilledRectangleEx(g.Display, x+center-1, y, 2, w, c) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /stack/rp2040.go: -------------------------------------------------------------------------------- 1 | //go:build rp2040 2 | // +build rp2040 3 | 4 | package stack 5 | 6 | import ( 7 | //"device/arm" 8 | ) 9 | 10 | /* 11 | func UpdateStackPointer(loc string) { 12 | var sp int32 13 | 14 | arm.AsmFull(` 15 | push {r0} 16 | mov r0,sp 17 | str r0,{result} 18 | pop {r0} 19 | `, map[string]interface{}{ 20 | "result": &sp, 21 | }) 22 | 23 | if sp <= MinStackPointer { 24 | MinStackPointer = sp 25 | MinStackLocation = loc 26 | } 27 | } 28 | */ 29 | -------------------------------------------------------------------------------- /icon/checkmark.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "tinygo.org/x/tinydraw" 8 | ) 9 | 10 | func Checkmark(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 11 | 12 | third := int16(float32(0.4) * float32(w)) 13 | 14 | tinydraw.LineEx(g.Display, x, y+w-third, x+third-1, y+w, c) 15 | tinydraw.LineEx(g.Display, x+third, y+w, x+w-1, y, c) 16 | 17 | tinydraw.LineEx(g.Display, x+1, y+w-third, x+third, y+w, c) 18 | tinydraw.LineEx(g.Display, x+third+1, y+w, x+w, y, c) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cmd/calibrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/examples/calibrate" 8 | "github.com/spearson78/tinygui/theme" 9 | ) 10 | 11 | func main() { 12 | 13 | display, touch := initHardware() 14 | 15 | gui := tinygui.New(display, touch, theme.DefaultTheme(), calibrate.TouchCalibration) 16 | 17 | gui.Init() 18 | 19 | for { 20 | time.Sleep(50 * time.Millisecond) 21 | g := gui.DoTouchNonBlocking() 22 | if g != nil { 23 | calibrate.Gui(g) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /cmd/calibrate/tinydisplay.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/sago35/tinydisplay" 10 | "github.com/spearson78/tinygui" 11 | "github.com/spearson78/tinygui/adapter" 12 | "tinygo.org/x/drivers/touch" 13 | ) 14 | 15 | func initHardware() (tinygui.Displayer, touch.Pointer) { 16 | display, err := tinydisplay.NewClient("", 9812, 240, 320) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | fillRectAdapter := adapter.NewTinyDisplayAdapter(display) 22 | 23 | return &fillRectAdapter, display 24 | } 25 | -------------------------------------------------------------------------------- /adapter/fillrectadapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "image/color" 5 | 6 | "tinygo.org/x/drivers" 7 | ) 8 | 9 | type fillRectAdapter struct { 10 | d drivers.Displayer 11 | } 12 | 13 | func AddFillRect(d drivers.Displayer) fillRectAdapter { 14 | return fillRectAdapter{ 15 | d: d, 16 | } 17 | } 18 | 19 | func (d *fillRectAdapter) Size() (x, y int16) { 20 | return d.d.Size() 21 | } 22 | 23 | func (d *fillRectAdapter) SetPixel(x, y int16, c color.RGBA) { 24 | d.d.SetPixel(x, y, c) 25 | } 26 | 27 | func (d *fillRectAdapter) Display() error { 28 | return d.d.Display() 29 | } 30 | 31 | func (d *fillRectAdapter) FillRect(x, y, w, h int16, c color.RGBA) { 32 | for py := y; py < y+h; py++ { 33 | for px := x; px < x+w; px++ { 34 | d.d.SetPixel(px, py, c) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /adapter/tinydisplaydapter.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package adapter 5 | 6 | import ( 7 | "image/color" 8 | 9 | "github.com/sago35/tinydisplay" 10 | ) 11 | 12 | type tinyDisplayAdapter struct { 13 | c *tinydisplay.Client 14 | } 15 | 16 | func NewTinyDisplayAdapter(c *tinydisplay.Client) tinyDisplayAdapter { 17 | return tinyDisplayAdapter{ 18 | c: c, 19 | } 20 | } 21 | 22 | func (d *tinyDisplayAdapter) Size() (x, y int16) { 23 | return d.c.Size() 24 | } 25 | 26 | func (d *tinyDisplayAdapter) SetPixel(x, y int16, c color.RGBA) { 27 | d.c.SetPixel(x, y, c) 28 | } 29 | 30 | func (d *tinyDisplayAdapter) Display() error { 31 | return d.c.Display() 32 | } 33 | 34 | func (d *tinyDisplayAdapter) FillRect(x, y, w, h int16, c color.RGBA) { 35 | d.c.FillRectangle(x, y, w, h, c) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/demo/tinydisplay.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/sago35/tinydisplay" 10 | "github.com/spearson78/tinygui" 11 | "github.com/spearson78/tinygui/adapter" 12 | "tinygo.org/x/drivers/touch" 13 | ) 14 | 15 | var tempDelta = float32(0.1) 16 | var lastTemp = float32(24) 17 | 18 | func ReadTemp() float32 { 19 | lastTemp = lastTemp + tempDelta 20 | if lastTemp > 25 || lastTemp < 18.5 { 21 | tempDelta = -tempDelta 22 | } 23 | 24 | return lastTemp 25 | } 26 | 27 | func initHardware() (tinygui.Displayer, touch.Pointer) { 28 | display, err := tinydisplay.NewClient("", 9812, 240, 320) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | fillRectAdapter := adapter.NewTinyDisplayAdapter(display) 34 | 35 | return &fillRectAdapter, display 36 | } 37 | -------------------------------------------------------------------------------- /examples/nav/navGui.go: -------------------------------------------------------------------------------- 1 | package nav 2 | 3 | import ( 4 | "github.com/spearson78/tinygui" 5 | "github.com/spearson78/tinygui/component" 6 | "github.com/spearson78/tinygui/examples/calc" 7 | "github.com/spearson78/tinygui/examples/temp" 8 | "github.com/spearson78/tinygui/examples/test" 9 | ) 10 | 11 | var NavigationState = component.BottomNavigationState{ 12 | Selected: 0, 13 | } 14 | 15 | func NavGui(g *tinygui.GuiContext) { 16 | 17 | switch NavigationState.Selected { 18 | case 0: 19 | calc.CalcGui(g) 20 | case 1: 21 | temp.TemperatureSensorGui(g) 22 | default: 23 | test.TestGui(g) 24 | } 25 | 26 | navProps := component.NewBottomNavigationProps(g) 27 | navProps.X = 0 28 | navProps.Y = 280 29 | navProps.PermaLabel = false 30 | if component.BottomNavigation(g, &NavigationState, &navProps) { 31 | g.Invalidate(true) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /icon/edit.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/primitive" 8 | ) 9 | 10 | var editShape = primitive.Shape{ 11 | Lines: []primitive.LineSegment{ 12 | 13 | {primitive.Point8{1, 202}, primitive.Point8{1, 254}}, 14 | {primitive.Point8{1, 202}, primitive.Point8{157, 47}}, 15 | {primitive.Point8{157, 47}, primitive.Point8{209, 99}}, 16 | {primitive.Point8{209, 99}, primitive.Point8{54, 254}}, 17 | 18 | {primitive.Point8{173, 31}, primitive.Point8{202, 3}}, 19 | {primitive.Point8{225, 82}, primitive.Point8{173, 31}}, 20 | 21 | {primitive.Point8{214, 3}, primitive.Point8{253, 42}}, 22 | {primitive.Point8{253, 42}, primitive.Point8{253, 53}}, 23 | {primitive.Point8{253, 53}, primitive.Point8{225, 82}}, 24 | }, 25 | } 26 | 27 | func Edit(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 28 | primitive.FillShape(g.Display, x, y, w, w, &editShape, c) 29 | } 30 | -------------------------------------------------------------------------------- /layout/grid.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import "github.com/spearson78/tinygui/component" 4 | 5 | type GridLayout struct { 6 | x int16 7 | y int16 8 | currCell int16 9 | currRow int16 10 | cellWidth int16 11 | cellHeight int16 12 | margin int16 13 | } 14 | 15 | func Grid(x, y, cellWidth, cellHeight, margin int16) GridLayout { 16 | return GridLayout{ 17 | x: x, 18 | y: y, 19 | cellWidth: cellWidth + margin, 20 | cellHeight: cellHeight + margin, 21 | } 22 | } 23 | 24 | func (g *GridLayout) NextCell() component.ComponentPos { 25 | pos := component.ComponentPos{ 26 | X: g.x + (g.currCell * g.cellWidth), 27 | Y: g.y + (g.currRow * g.cellHeight), 28 | } 29 | g.currCell++ 30 | return pos 31 | } 32 | 33 | func (g *GridLayout) EndRow() component.ComponentPos { 34 | pos := component.ComponentPos{ 35 | X: g.x + (g.currCell * g.cellWidth), 36 | Y: g.y + (g.currRow * g.cellHeight), 37 | } 38 | g.currCell = 0 39 | g.currRow++ 40 | return pos 41 | } 42 | -------------------------------------------------------------------------------- /adapter/rotated.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "tinygo.org/x/drivers/touch" 8 | ) 9 | 10 | type rotateAdapter struct { 11 | d tinygui.Displayer 12 | t touch.Pointer 13 | xs int16 14 | ys int16 15 | } 16 | 17 | func Rotate(d tinygui.Displayer, t touch.Pointer) rotateAdapter { 18 | xs, ys := d.Size() 19 | return rotateAdapter{ 20 | d: d, 21 | t: t, 22 | xs: xs, 23 | ys: ys, 24 | } 25 | } 26 | 27 | func (r *rotateAdapter) Size() (int16, int16) { 28 | return r.ys, r.xs 29 | } 30 | 31 | func (r *rotateAdapter) SetPixel(x, y int16, c color.RGBA) { 32 | r.d.SetPixel(r.xs-y, x, c) 33 | } 34 | 35 | func (r *rotateAdapter) Display() error { 36 | return r.d.Display() 37 | } 38 | 39 | func (r *rotateAdapter) FillRect(x, y, w, h int16, c color.RGBA) { 40 | r.d.FillRect(r.xs-y-h, x, h, w, c) 41 | } 42 | 43 | func (r *rotateAdapter) Read() touch.Point { 44 | p := r.t.ReadTouchPoint() 45 | return touch.Point{ 46 | X: p.Y, 47 | Y: int(r.xs) - p.X, 48 | Z: p.Z, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /icon/timeline.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | "math" 6 | 7 | "github.com/spearson78/tinygui" 8 | "tinygo.org/x/tinydraw" 9 | ) 10 | 11 | //go:noinline 12 | func Timeline(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 13 | 14 | ax := int16(float32(0.12) * float32(w)) 15 | bx := int16(float32(0.42) * float32(w)) 16 | cx := int16(float32(0.62) * float32(w)) 17 | dx := int16(float32(0.88) * float32(w)) 18 | 19 | ay := int16(float32(0.67) * float32(w)) 20 | by := int16(float32(0.38) * float32(w)) 21 | cy := int16(float32(0.59) * float32(w)) 22 | dy := int16(float32(0.33) * float32(w)) 23 | 24 | r := int16(math.Max(1.0, float64(0.0085)*float64(w))) 25 | 26 | tinydraw.LineEx(g.Display, x+ax, y+ay, x+bx, y+by, c) 27 | tinydraw.LineEx(g.Display, x+bx, y+by, x+cx, y+cy, c) 28 | tinydraw.LineEx(g.Display, x+cx, y+cy, x+dx, y+dy, c) 29 | 30 | tinydraw.FilledCircleEx(g.Display, x+ax, y+ay, r, c) 31 | tinydraw.FilledCircleEx(g.Display, x+bx, y+by, r, c) 32 | tinydraw.FilledCircleEx(g.Display, x+cx, y+cy, r, c) 33 | tinydraw.FilledCircleEx(g.Display, x+dx, y+dy, r, c) 34 | } 35 | -------------------------------------------------------------------------------- /adapter/upsidedownadapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "tinygo.org/x/drivers/touch" 8 | ) 9 | 10 | type upsideDownAdapter struct { 11 | d tinygui.Displayer 12 | t touch.Pointer 13 | xs int16 14 | ys int16 15 | } 16 | 17 | func UpsideDown(d tinygui.Displayer, t touch.Pointer) upsideDownAdapter { 18 | xs, ys := d.Size() 19 | return upsideDownAdapter{ 20 | d: d, 21 | t: t, 22 | xs: xs, 23 | ys: ys, 24 | } 25 | } 26 | 27 | func (r *upsideDownAdapter) Size() (int16, int16) { 28 | return r.xs, r.ys 29 | } 30 | 31 | func (r *upsideDownAdapter) SetPixel(x, y int16, c color.RGBA) { 32 | r.d.SetPixel(r.xs-x, r.ys-y, c) 33 | } 34 | 35 | func (r *upsideDownAdapter) Display() error { 36 | return r.d.Display() 37 | } 38 | 39 | func (r *upsideDownAdapter) FillRect(x, y, w, h int16, c color.RGBA) { 40 | r.d.FillRect(r.xs-x-w, r.ys-y-h, w, h, c) 41 | } 42 | 43 | func (r *upsideDownAdapter) ReadTouchPoint() touch.Point { 44 | tp := r.t.ReadTouchPoint() 45 | tp.X = ((1 << 16) - tp.X) 46 | tp.Y = ((1 << 16) - tp.Y) 47 | return tp 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steven Pearson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /clip.go: -------------------------------------------------------------------------------- 1 | package tinygui 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | type ClippedDisplay struct { 9 | D Displayer 10 | ClipRect image.Rectangle 11 | } 12 | 13 | func (r *ClippedDisplay) Size() (int16, int16) { 14 | return r.D.Size() 15 | } 16 | 17 | func (r *ClippedDisplay) SetPixel(x, y int16, c color.RGBA) { 18 | 19 | if r.ClipRect.Empty() { 20 | r.D.SetPixel(x, y, c) 21 | } else { 22 | if x < int16(r.ClipRect.Min.X) || x > int16(r.ClipRect.Max.X) || y < int16(r.ClipRect.Min.Y) || y > int16(r.ClipRect.Max.Y) { 23 | return 24 | } 25 | 26 | r.D.SetPixel(x, y, c) 27 | } 28 | } 29 | 30 | func (r *ClippedDisplay) Display() error { 31 | return r.D.Display() 32 | } 33 | 34 | func (r *ClippedDisplay) FillRect(x, y, w, h int16, c color.RGBA) { 35 | 36 | if r.ClipRect.Empty() { 37 | r.D.FillRect(x, y, w, h, c) 38 | } else { 39 | intersection := r.ClipRect.Intersect(image.Rect(int(x), int(y), int(x+w), int(y+h))) 40 | 41 | if !intersection.Empty() { 42 | r.D.FillRect(int16(intersection.Min.X), int16(intersection.Min.Y), int16(intersection.Dx()), int16(intersection.Dy()), c) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/toggleled/toggleled.go: -------------------------------------------------------------------------------- 1 | package toggleled 2 | 3 | import ( 4 | "github.com/spearson78/tinygui" 5 | "github.com/spearson78/tinygui/component" 6 | 7 | "machine" 8 | ) 9 | 10 | //State variable to track whether LED is on or off 11 | var LedState bool 12 | 13 | //State for Toggle Button 14 | var btnToggleLed component.ButtonState 15 | 16 | //Gui Function GuiContext is provided by Tiny Gui 17 | func ToggleLedGui(g *tinygui.GuiContext) { 18 | 19 | //Create button props including default values based on current theme 20 | btnToggleProps := component.NewButtonProps(g) 21 | 22 | //Set position and Size of the button 23 | btnToggleProps.X = 10 24 | btnToggleProps.Y = 10 25 | btnToggleProps.W = 220 26 | btnToggleProps.H = 50 27 | 28 | //Set the label of the button 29 | btnToggleProps.Label = "TOGGLE LED" 30 | 31 | //Set the style of the button 32 | btnToggleProps.Style = component.Contained 33 | 34 | //Render the Button and handle the click 35 | if component.Button(g, &btnToggleLed, &btnToggleProps) { 36 | //component.Button returns true if the button was clicked 37 | LedState = !LedState 38 | machine.LED.Set(LedState) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /primitive/color.go: -------------------------------------------------------------------------------- 1 | package primitive 2 | 3 | import ( 4 | "image/color" 5 | ) 6 | 7 | //go:noinline 8 | func TransitionColor(fromColor color.RGBA, toColor color.RGBA, amount uint8) color.RGBA { 9 | 10 | fh, fs, fv := ToHSV(fromColor.R, fromColor.G, fromColor.B) 11 | th, ts, tv := ToHSV(toColor.R, toColor.G, toColor.B) 12 | 13 | if ts <= 25 { 14 | th = fh 15 | } 16 | 17 | dh := int(fh) - int(th) 18 | ds := int(fs) - int(ts) 19 | dv := int(fv) - int(tv) 20 | 21 | ch := uint8(int(fh) - (dh*(255-int(amount)))/255) 22 | cs := uint8(int(fs) - (ds*(255-int(amount)))/255) 23 | cv := uint8(int(fv) - (dv*(255-int(amount)))/255) 24 | 25 | sr, sg, sb := ToRGB(ch, cs, cv) 26 | 27 | return color.RGBA{sr, sg, sb, 255} 28 | } 29 | 30 | //go:noinline 31 | func Desaturate(c color.RGBA, amount uint8) color.RGBA { 32 | ch, cs, cv := ToHSV(c.R, c.G, c.B) 33 | 34 | cs = uint8((int(cs) * (255 - int(amount))) / 255) 35 | icv := int(cv) 36 | if cs < 25 { 37 | icv = 127 38 | } 39 | icv = icv + int(amount) 40 | 41 | if icv > 250 { 42 | cv = 250 43 | } else { 44 | cv = uint8(icv) 45 | } 46 | 47 | sr, sg, sb := ToRGB(ch, cs, cv) 48 | 49 | return color.RGBA{sr, sg, sb, 255} 50 | } 51 | -------------------------------------------------------------------------------- /icon/announcement.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/primitive" 8 | ) 9 | 10 | var announcementShape = primitive.Shape{ 11 | Lines: []primitive.LineSegment{ 12 | 13 | //Top Left 14 | {primitive.Point8{22, 39}, primitive.Point8{38, 22}}, 15 | //Middle Left 16 | {primitive.Point8{22, 39}, primitive.Point8{23, 232}}, 17 | 18 | //Exclaim Top 19 | {primitive.Point8{116, 53}, primitive.Point8{116, 118}}, 20 | {primitive.Point8{139, 53}, primitive.Point8{139, 118}}, 21 | 22 | //Exclaim Bottom 23 | {primitive.Point8{116, 137}, primitive.Point8{116, 161}}, 24 | {primitive.Point8{139, 137}, primitive.Point8{139, 161}}, 25 | 26 | //TOP right 27 | {primitive.Point8{232, 39}, primitive.Point8{217, 22}}, 28 | //MIDDLE RIGHT 29 | {primitive.Point8{232, 39}, primitive.Point8{232, 174}}, 30 | 31 | //Bottom Right bubble 32 | {primitive.Point8{23, 232}, primitive.Point8{63, 190}}, 33 | 34 | //Bottom Right triangle 35 | {primitive.Point8{232, 174}, primitive.Point8{216, 190}}, 36 | }, 37 | } 38 | 39 | func Announcement(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 40 | primitive.FillShape(g.Display, x, y, w, w, &announcementShape, c) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/calibrate/rp2040.go: -------------------------------------------------------------------------------- 1 | //go:build rp2040 2 | // +build rp2040 3 | 4 | package main 5 | 6 | import ( 7 | "image/color" 8 | "machine" 9 | 10 | "github.com/spearson78/tinygui" 11 | "github.com/spearson78/tinygui/adapter" 12 | "tinygo.org/x/drivers/ssd1289" 13 | "tinygo.org/x/drivers/touch" 14 | "tinygo.org/x/drivers/xpt2046" 15 | ) 16 | 17 | func initHardware() (tinygui.Displayer, touch.Pointer) { 18 | 19 | /* 20 | bus := ssd1289.NewPinBus([16]machine.Pin{ 21 | machine.GP4, 22 | machine.GP5, 23 | machine.GP6, 24 | machine.GP7, 25 | machine.GP8, 26 | machine.GP9, 27 | machine.GP10, 28 | machine.GP11, 29 | machine.GP12, 30 | machine.GP13, 31 | machine.GP14, 32 | machine.GP15, 33 | machine.GP16, 34 | machine.GP17, 35 | machine.GP18, 36 | machine.GP19, 37 | }) 38 | */ 39 | bus := ssd1289.NewRP2040Bus(machine.GP4) 40 | 41 | utft := ssd1289.New(machine.GP0, machine.GP1, machine.GP2, machine.GP3, bus) 42 | utft.Configure() 43 | utft.FillDisplay(color.RGBA{250, 0, 0, 255}) 44 | 45 | touch := xpt2046.New(machine.GP20, machine.GP21, machine.GP22, machine.GP26, machine.GP27) 46 | touch.Configure(&xpt2046.Config{ 47 | Precision: 10, 48 | }) 49 | rotated := adapter.UpsideDown(&utft, &touch) 50 | 51 | return &rotated, &rotated 52 | } 53 | -------------------------------------------------------------------------------- /theme/theme.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinyamifont" 7 | "github.com/spearson78/tinyamifont/fonts/newwebfont/voyager" 8 | ) 9 | 10 | type Theme struct { 11 | Text color.RGBA 12 | Background color.RGBA 13 | Border color.RGBA 14 | DefaultColor color.RGBA 15 | SecondaryColor color.RGBA 16 | SuccessColor color.RGBA 17 | ErrorColor color.RGBA 18 | DisabedColor color.RGBA 19 | Shadow1 color.RGBA 20 | Shadow2 color.RGBA 21 | Font *tinyamifont.Font 22 | SecondaryFont *tinyamifont.Font 23 | } 24 | 25 | func DefaultTheme() Theme { 26 | 27 | font := tinyamifont.MustLoadFont(voyager.Regular18pt) 28 | secondaryFont := tinyamifont.MustLoadFont(voyager.Regular9pt) 29 | 30 | return Theme{ 31 | Text: color.RGBA{0, 0, 0, 255}, 32 | Background: color.RGBA{250, 250, 250, 255}, 33 | Border: color.RGBA{117, 117, 117, 255}, 34 | Shadow1: color.RGBA{177, 177, 177, 255}, 35 | Shadow2: color.RGBA{205, 205, 205, 255}, 36 | DefaultColor: color.RGBA{25, 118, 210, 255}, 37 | SecondaryColor: color.RGBA{156, 39, 176, 255}, 38 | SuccessColor: color.RGBA{46, 125, 50, 255}, 39 | ErrorColor: color.RGBA{212, 55, 80, 255}, 40 | DisabedColor: color.RGBA{224, 224, 224, 255}, 41 | Font: &font, 42 | SecondaryFont: &secondaryFont, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cmd/demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/examples/calibrate" 8 | "github.com/spearson78/tinygui/examples/nav" 9 | "github.com/spearson78/tinygui/examples/temp" 10 | "github.com/spearson78/tinygui/theme" 11 | ) 12 | 13 | func guiFunc(g *tinygui.GuiContext) { 14 | if !calibrate.IsCalibrated() { 15 | calibrate.Gui(g) 16 | if calibrate.IsCalibrated() { 17 | g.TriggerInvalidate = true 18 | g.ClearDisplay = true 19 | } 20 | } else { 21 | nav.NavGui(g) 22 | //toggleled.ToggleLedGui(g) 23 | } 24 | } 25 | 26 | func main() { 27 | 28 | //stack.UpdateStackPointer("main") 29 | //baseStackPointer := stack.MinStackPointer 30 | 31 | display, touch := initHardware() 32 | 33 | theme := theme.DefaultTheme() 34 | gui := tinygui.New(display, touch, theme, calibrate.TouchCalibration) 35 | 36 | gui.Init() 37 | 38 | for { 39 | time.Sleep(50 * time.Millisecond) 40 | g := gui.DoTouchNonBlocking() 41 | if g != nil { 42 | guiFunc(g) 43 | } 44 | 45 | if nav.NavigationState.Selected == 1 { 46 | temp.DoSensor(gui, &theme, ReadTemp) 47 | } 48 | 49 | /* 50 | txt := strconv.FormatInt(int64(baseStackPointer)-int64(stack.MinStackPointer), 10) 51 | primitive.WriteLine(display, theme.Font, 10, 40, txt, color.RGBA{255, 0, 0, 0255}) 52 | primitive.WriteLine(display, theme.Font, 10, 70, stack.MinStackLocation, color.RGBA{255, 0, 0, 0255}) 53 | */ 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /guicontext.go: -------------------------------------------------------------------------------- 1 | package tinygui 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/spearson78/tinygui/event" 7 | "github.com/spearson78/tinygui/theme" 8 | ) 9 | 10 | type GuiContext struct { 11 | Display Displayer 12 | Theme theme.Theme 13 | Event event.Event 14 | 15 | TriggerUpdateState bool 16 | TriggerInvalidate bool 17 | ClearDisplay bool 18 | 19 | InvalidX0 int16 20 | InvalidY0 int16 21 | InvalidX1 int16 22 | InvalidY1 int16 23 | } 24 | 25 | func (t *GuiContext) UpdateState() { 26 | t.TriggerUpdateState = true 27 | } 28 | 29 | func (t *GuiContext) Invalidate(clearDisplay bool) { 30 | t.TriggerInvalidate = true 31 | t.InvalidX0 = 0 32 | t.InvalidY0 = 0 33 | t.InvalidX1 = math.MaxInt16 - 1 34 | t.InvalidY1 = math.MaxInt16 - 1 35 | t.ClearDisplay = t.ClearDisplay || clearDisplay 36 | } 37 | 38 | func minInvalidCoord(cur int16, new int16) int16 { 39 | if cur == -1 || new < cur { 40 | return new 41 | } else { 42 | return cur 43 | } 44 | } 45 | 46 | func maxInvalidCoord(cur int16, new int16) int16 { 47 | if cur == -1 || new > cur { 48 | return new 49 | } else { 50 | return cur 51 | } 52 | } 53 | 54 | func (t *GuiContext) InvalidateRect(x0, y0, x1, y1 int16, clearDisplay bool) { 55 | t.TriggerInvalidate = true 56 | t.ClearDisplay = t.ClearDisplay || clearDisplay 57 | t.InvalidX0 = minInvalidCoord(t.InvalidX0, x0) 58 | t.InvalidY0 = minInvalidCoord(t.InvalidY0, y0) 59 | t.InvalidX1 = maxInvalidCoord(t.InvalidX1, x1) 60 | t.InvalidY1 = maxInvalidCoord(t.InvalidY1, y1) 61 | } 62 | -------------------------------------------------------------------------------- /icon/backspace.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/primitive" 8 | ) 9 | 10 | var backspaceShape = primitive.Shape{ 11 | Lines: []primitive.LineSegment{ 12 | 13 | //Left 14 | {primitive.Point8{1, 128}, primitive.Point8{64, 32}}, 15 | {primitive.Point8{1, 128}, primitive.Point8{64, 223}}, 16 | 17 | //Cross Left 18 | {primitive.Point8{110, 74}, primitive.Point8{94, 89}}, 19 | {primitive.Point8{132, 128}, primitive.Point8{94, 89}}, 20 | {primitive.Point8{132, 128}, primitive.Point8{94, 165}}, 21 | {primitive.Point8{110, 181}, primitive.Point8{94, 165}}, 22 | 23 | //Cross Middle 24 | {primitive.Point8{110, 74}, primitive.Point8{148, 112}}, 25 | {primitive.Point8{186, 74}, primitive.Point8{148, 112}}, 26 | 27 | {primitive.Point8{110, 181}, primitive.Point8{148, 143}}, 28 | {primitive.Point8{186, 181}, primitive.Point8{148, 143}}, 29 | 30 | //Cross Right 31 | {primitive.Point8{186, 74}, primitive.Point8{201, 89}}, 32 | {primitive.Point8{163, 128}, primitive.Point8{201, 89}}, 33 | {primitive.Point8{163, 128}, primitive.Point8{201, 165}}, 34 | {primitive.Point8{186, 181}, primitive.Point8{201, 165}}, 35 | 36 | //Right 37 | {primitive.Point8{237, 32}, primitive.Point8{254, 47}}, 38 | {primitive.Point8{254, 47}, primitive.Point8{254, 205}}, 39 | {primitive.Point8{254, 205}, primitive.Point8{237, 223}}, 40 | }, 41 | } 42 | 43 | func Backspace(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 44 | primitive.FillShape(g.Display, x, y, w, w, &backspaceShape, c) 45 | } 46 | -------------------------------------------------------------------------------- /primitive/hsv.go: -------------------------------------------------------------------------------- 1 | package primitive 2 | 3 | func max(a, b uint8) uint8 { 4 | if a > b { 5 | return a 6 | } else { 7 | return b 8 | } 9 | } 10 | 11 | func min(a, b uint8) uint8 { 12 | if a < b { 13 | return a 14 | } else { 15 | return b 16 | } 17 | } 18 | 19 | //go:noinline 20 | func ToHSV(r, g, b uint8) (h, s, v uint8) { 21 | 22 | max := int(max(max(r, g), b)) 23 | min := int(min(min(r, g), b)) 24 | 25 | v = uint8(max) 26 | if v == 0 { 27 | h = 0 28 | s = 0 29 | return 30 | } 31 | 32 | diff := max - min 33 | 34 | s = uint8((255 * diff) / max) 35 | 36 | if s == 0 { 37 | h = 0 38 | return 39 | } 40 | 41 | if max == int(r) { 42 | h = uint8(0 + (43*(int(g)-int(b)))/diff) 43 | } else if max == int(g) { 44 | h = uint8(85 + (43*(int(b)-int(r)))/diff) 45 | } else if max == int(b) { 46 | h = uint8(171 + (43*(int(r)-int(g)))/diff) 47 | } 48 | 49 | return 50 | } 51 | 52 | //go:noinline 53 | func ToRGB(h, s, v uint8) (r, g, b uint8) { 54 | 55 | if s == 0 { 56 | return v, v, v 57 | } 58 | 59 | region := int(h) / 43 60 | 61 | remainder := (int(h) - (region * 43)) * 6 62 | 63 | p := uint8((int(v) * (255 - int(s))) >> 8) 64 | q := uint8((int(v) * (255 - ((int(s) * remainder) >> 8))) >> 8) 65 | t := uint8((int(v) * (255 - ((int(s) * (255 - remainder)) >> 8))) >> 8) 66 | 67 | switch region { 68 | case 0: 69 | r, g, b = v, t, p 70 | case 1: 71 | r, g, b = q, v, p 72 | case 2: 73 | r, g, b = p, v, t 74 | case 3: 75 | r, g, b = p, q, v 76 | case 4: 77 | r, g, b = t, p, v 78 | default: 79 | r, g, b = v, p, q 80 | } 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /component/event.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/spearson78/tinygui/event" 5 | ) 6 | 7 | type EventAction uint8 8 | 9 | const ( 10 | Ignore EventAction = 0 11 | Invalidate EventAction = 1 12 | Update EventAction = 2 13 | Click EventAction = 4 14 | Drag EventAction = 8 15 | DragEnd EventAction = 16 16 | ) 17 | 18 | //go:noinline 19 | func HandleEvent(guiEvent *event.Event, disabled bool, drag bool, noExternalState bool, x, y, w, h int16) (eventAction EventAction, rx int16, ry int16) { 20 | 21 | //stack.UpdateStackPointer("HandleEvent") 22 | 23 | if (guiEvent.Type & event.Click) != 0 { 24 | if !disabled { 25 | if int16(guiEvent.X) > x && int16(guiEvent.X) < (x+w) && int16(guiEvent.Y) > y && int16(guiEvent.Y) < (y+h) { 26 | eventAction |= Click 27 | rx = int16(guiEvent.X) - x 28 | ry = int16(guiEvent.Y) - y 29 | } 30 | } 31 | } else if (guiEvent.Type & (event.Drag | event.DragEnd)) != 0 { 32 | 33 | if drag { 34 | if int16(guiEvent.X) > x && int16(guiEvent.X) < (x+w) && int16(guiEvent.Y) > y && int16(guiEvent.Y) < (y+h) { 35 | 36 | dragType := Drag 37 | if (guiEvent.Type & event.DragEnd) != 0 { 38 | dragType = DragEnd 39 | } 40 | 41 | eventAction |= dragType 42 | rx = int16(guiEvent.DragX) - x 43 | ry = int16(guiEvent.DragY) - y 44 | } 45 | } 46 | } 47 | 48 | if (guiEvent.Type & event.Invalidate) != 0 { 49 | eventAction |= Invalidate 50 | } 51 | 52 | if (guiEvent.Type & event.Update) != 0 { 53 | if !noExternalState { 54 | eventAction |= Update 55 | } 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /component/textfield.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/event" 8 | "github.com/spearson78/tinygui/primitive" 9 | ) 10 | 11 | type TextFieldStyle byte 12 | 13 | const ( 14 | Standard TextFieldStyle = iota 15 | Filled 16 | OutlinedTextField 17 | ) 18 | 19 | type TextFieldState struct { 20 | Text string 21 | } 22 | 23 | type TextFieldProps struct { 24 | ComponentPos 25 | ComponentSize 26 | Style TextFieldStyle 27 | Disabled bool 28 | Color color.RGBA 29 | } 30 | 31 | func NewTextFieldProps(g *tinygui.GuiContext) TextFieldProps { 32 | return TextFieldProps{ 33 | ComponentPos: ComponentPos{ 34 | X: 10, 35 | Y: 10, 36 | }, 37 | ComponentSize: ComponentSize{ 38 | W: 100, 39 | H: -1, 40 | }, 41 | Style: Standard, 42 | Disabled: false, 43 | Color: g.Theme.Text, 44 | } 45 | } 46 | 47 | //go:noinline 48 | func TextField(g *tinygui.GuiContext, state *TextFieldState, props *TextFieldProps) bool { 49 | 50 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, props.W, 30) 51 | if eventAction == Ignore || eventAction == Click { 52 | return false 53 | } 54 | 55 | if (g.Event.Type & event.Invalidate) != 0 { 56 | g.Display.FillRect(props.X, props.Y, props.W, 30, g.Theme.Background) 57 | g.Display.FillRect(props.X, props.Y+28, props.W, 1, props.Color) 58 | } else { 59 | g.Display.FillRect(props.X, props.Y, props.W, 27, g.Theme.Background) 60 | } 61 | 62 | primitive.WriteLine(g.Display, g.Theme.Font, props.X, int16(props.Y+21), state.Text, props.Color) 63 | 64 | return false 65 | } 66 | -------------------------------------------------------------------------------- /primitive/shape.go: -------------------------------------------------------------------------------- 1 | package primitive 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "tinygo.org/x/tinydraw" 8 | ) 9 | 10 | type Point8 struct { 11 | X uint8 12 | Y uint8 13 | } 14 | 15 | type LineSegment struct { 16 | A Point8 17 | B Point8 18 | } 19 | 20 | type Shape struct { 21 | Lines []LineSegment 22 | } 23 | 24 | func getLineIntersection(x0, y0, x1, y1, x2, y2, x3, y3 float32) (float32, bool) { 25 | 26 | s1_x := x1 - x0 27 | s1_y := y1 - y0 28 | s2_x := x3 - x2 29 | s2_y := y3 - y2 30 | 31 | s := (-s1_y*(x0-x2) + s1_x*(y0-y2)) / (-s2_x*s1_y + s1_x*s2_y) 32 | t := (s2_x*(y0-y2) - s2_y*(x0-x2)) / (-s2_x*s1_y + s1_x*s2_y) 33 | 34 | if s >= 0 && s <= 1 && t >= 0 && t <= 1 { 35 | // Collision detected 36 | x := x0 + (t * s1_x) 37 | return x, true 38 | } else { 39 | return 0, false 40 | } 41 | } 42 | 43 | //go:noinline 44 | func FillShape(d tinygui.Displayer, x, y, w, h int16, s *Shape, c color.RGBA) { 45 | 46 | wscale := float32(255) / float32(w) 47 | hscale := float32(255) / float32(h) 48 | for py := int16(0); py < h; py++ { 49 | x1 := int16(-1) 50 | x2 := int16(-1) 51 | for _, line := range s.Lines { 52 | scaledY := float32(py) * hscale 53 | ix, intersect := getLineIntersection(0, scaledY, 255, scaledY, float32(line.A.X), float32(line.A.Y), float32(line.B.X), float32(line.B.Y)) 54 | if intersect { 55 | if x1 == -1 { 56 | x1 = x + int16(ix/wscale) 57 | if x2 != -1 { 58 | if x1-x2 == 1 { 59 | x1++ 60 | } 61 | } 62 | } else { 63 | x2 = x + int16(ix/wscale) 64 | tinydraw.LineEx(d, x1, y+py, x2, y+py, c) 65 | x1 = -1 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/spearson78/tinygui 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/sago35/tinydisplay v0.0.0-20211221140237-b980dfd11c01 7 | github.com/shopspring/decimal v1.3.1 8 | github.com/spearson78/tinyamifont v0.0.0-20220201082506-452b1c82d8bd 9 | tinygo.org/x/drivers v0.19.0 10 | tinygo.org/x/tinydraw v0.0.0-20220125063109-43cae6615eb5 11 | ) 12 | 13 | require ( 14 | fyne.io/fyne/v2 v2.1.2 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect 17 | github.com/fsnotify/fsnotify v1.5.1 // indirect 18 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect 19 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect 20 | github.com/godbus/dbus/v5 v5.0.6 // indirect 21 | github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 // indirect 24 | github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 // indirect 25 | github.com/stretchr/testify v1.7.0 // indirect 26 | github.com/yuin/goldmark v1.4.4 // indirect 27 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect 28 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 29 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect 30 | golang.org/x/text v0.3.7 // indirect 31 | gopkg.in/yaml.v2 v2.4.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 33 | ) 34 | 35 | replace tinygo.org/x/drivers => github.com/spearson78/drivers v0.18.1-0.20220201074301-10484ed1d809 36 | 37 | replace tinygo.org/x/tinydraw => github.com/spearson78/tinydraw v0.0.0-20220201143000-b08d64c952ed 38 | -------------------------------------------------------------------------------- /cmd/demo/rp2040.go: -------------------------------------------------------------------------------- 1 | //go:build rp2040 2 | // +build rp2040 3 | 4 | package main 5 | 6 | import ( 7 | "device/rp" 8 | "image/color" 9 | "machine" 10 | 11 | "github.com/spearson78/tinygui" 12 | "github.com/spearson78/tinygui/adapter" 13 | "tinygo.org/x/drivers/ssd1289" 14 | "tinygo.org/x/drivers/touch" 15 | "tinygo.org/x/drivers/xpt2046" 16 | ) 17 | 18 | func initTempSensor() { 19 | 20 | rp.ADC.CS.SetBits(rp.ADC_CS_TS_EN) 21 | } 22 | 23 | func ReadTemp() float32 { 24 | 25 | rp.ADC.CS.SetBits(uint32(4) << rp.ADC_CS_AINSEL_Pos) 26 | rp.ADC.CS.SetBits(rp.ADC_CS_START_ONCE) 27 | 28 | for !rp.ADC.CS.HasBits(rp.ADC_CS_READY) { 29 | } 30 | 31 | // rp2040 uses 12-bit sampling, so scale to 16-bit 32 | raw := uint16(rp.ADC.RESULT.Get()) 33 | 34 | conversion_factor := float32(3.3) / float32(1<<12) 35 | result := float32(raw) * conversion_factor 36 | temp := float32(27) - (result-float32(0.706))/float32(0.001721) 37 | 38 | return temp 39 | } 40 | 41 | func initHardware() (tinygui.Displayer, touch.Pointer) { 42 | 43 | /* 44 | bus := ssd1289.NewPinBus([16]machine.Pin{ 45 | machine.GP4, 46 | machine.GP5, 47 | machine.GP6, 48 | machine.GP7, 49 | machine.GP8, 50 | machine.GP9, 51 | machine.GP10, 52 | machine.GP11, 53 | machine.GP12, 54 | machine.GP13, 55 | machine.GP14, 56 | machine.GP15, 57 | machine.GP16, 58 | machine.GP17, 59 | machine.GP18, 60 | machine.GP19, 61 | }) 62 | */ 63 | bus := ssd1289.NewRP2040Bus(machine.GP4) 64 | 65 | utft := ssd1289.New(machine.GP0, machine.GP1, machine.GP2, machine.GP3, bus) 66 | utft.Configure() 67 | utft.FillDisplay(color.RGBA{250, 0, 0, 255}) 68 | 69 | touch := xpt2046.New(machine.GP20, machine.GP21, machine.GP22, machine.GP26, machine.GP27) 70 | touch.Configure(&xpt2046.Config{ 71 | Precision: 10, 72 | }) 73 | rotated := adapter.UpsideDown(&utft, &touch) 74 | 75 | machine.InitADC() 76 | initTempSensor() 77 | 78 | machine.LED.Configure(machine.PinConfig{Mode: machine.PinOutput}) 79 | 80 | return &rotated, &rotated 81 | } 82 | -------------------------------------------------------------------------------- /component/iconbutton.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/icon" 8 | "github.com/spearson78/tinygui/primitive" 9 | "tinygo.org/x/tinydraw" 10 | ) 11 | 12 | type IconButtonProps struct { 13 | ComponentPos 14 | ComponentSize 15 | Disabled bool 16 | Color color.RGBA 17 | Icon icon.Icon 18 | } 19 | 20 | type IconButtonState struct { 21 | animateState int8 22 | } 23 | 24 | func NewIconButtonProps(g *tinygui.GuiContext) IconButtonProps { 25 | return IconButtonProps{ 26 | ComponentPos: ComponentPos{ 27 | X: 10, 28 | Y: 10, 29 | }, 30 | ComponentSize: ComponentSize{ 31 | W: 30, 32 | H: -1, 33 | }, 34 | Disabled: false, 35 | Color: g.Theme.DefaultColor, 36 | Icon: icon.Add, 37 | } 38 | } 39 | 40 | //go:noinline 41 | func IconButton(g *tinygui.GuiContext, state *IconButtonState, props *IconButtonProps) bool { 42 | 43 | w := props.W 44 | h := w 45 | centerOffset := w / 2 46 | clicked := false 47 | 48 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, true, props.X, props.Y, props.W, h) 49 | if eventAction == Ignore { 50 | return false 51 | } 52 | if (eventAction & Click) != 0 { 53 | state.animateState = 10 54 | clicked = true 55 | g.UpdateState() 56 | } 57 | 58 | if state.animateState >= 0 { 59 | fillColor := g.Theme.Background 60 | if state.animateState > 0 { 61 | fillColor = primitive.Desaturate(props.Color, 76) 62 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*25)) 63 | } 64 | 65 | tinydraw.FilledCircleEx(g.Display, props.X+centerOffset, props.Y+centerOffset, (w/2)-1, fillColor) 66 | state.animateState-- 67 | g.InvalidateRect(props.X, props.Y, props.X+w, props.Y+h, false) 68 | } 69 | 70 | darkColor := props.Color 71 | if props.Disabled { 72 | darkColor = g.Theme.DisabedColor 73 | } 74 | 75 | iconOffset := w / 4 76 | props.Icon(g, props.X+iconOffset+1, props.Y+iconOffset+1, (w / 2), darkColor) 77 | 78 | return clicked 79 | 80 | } 81 | -------------------------------------------------------------------------------- /primitive/primitives.go: -------------------------------------------------------------------------------- 1 | package primitive 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinyamifont" 7 | "github.com/spearson78/tinygui" 8 | "tinygo.org/x/drivers" 9 | "tinygo.org/x/tinydraw" 10 | ) 11 | 12 | //go:noinline 13 | func OutlineBox(d tinygui.Displayer, x, y, w, h int16, c color.RGBA) { 14 | tinydraw.LineEx(d, x+1, y, x+w-2, y, c) 15 | tinydraw.LineEx(d, x, y+1, x, y+h-2, c) 16 | tinydraw.LineEx(d, x+w-1, y+1, x+w-1, y+h-2, c) 17 | tinydraw.LineEx(d, x+1, y+h-1, x+w-2, y+h-1, c) 18 | } 19 | 20 | //go:noinline 21 | func FilledBox(d tinygui.Displayer, x, y, w, h int16, c color.RGBA) { 22 | tinydraw.LineEx(d, x+1, y, x+w-2, y, c) 23 | tinydraw.FilledRectangleEx(d, x, y+1, w, h-2, c) 24 | tinydraw.LineEx(d, x+1, y+h-1, x+w-2, y+h-1, c) 25 | } 26 | 27 | //go:noinline 28 | func OutlineBoxWithShadow(d tinygui.Displayer, x, y, w, h int16, c, s1, s2 color.RGBA) { 29 | OutlineBox(d, x, y, w, h-2, c) 30 | tinydraw.LineEx(d, x+1, y+h-2, x+w-2, y+h-2, s1) 31 | tinydraw.LineEx(d, x+2, y+h-1, x+w-4, y+h-1, s2) 32 | } 33 | 34 | //go:noinline 35 | func FilledBoxWithShadow(d tinygui.Displayer, x, y, w, h int16, c, s1, s2 color.RGBA) { 36 | FilledBox(d, x, y, w, h-2, c) 37 | tinydraw.LineEx(d, x+1, y+h-2, x+w-2, y+h-2, s1) 38 | tinydraw.LineEx(d, x+2, y+h-1, x+w-4, y+h-1, s2) 39 | } 40 | 41 | //go:noinline 42 | func FilledCircle(d tinygui.Displayer, x, y, w int16, c color.RGBA) { 43 | 44 | r := (w / 2) - 2 45 | tinydraw.FilledCircleEx(d, x+r+2, y+r, r, c) 46 | } 47 | 48 | //go:noinline 49 | func FilledCircleWithShadow(d tinygui.Displayer, x, y, w int16, c, s1, s2 color.RGBA) { 50 | r := (w / 2) - 2 51 | 52 | tinydraw.Circle(d, x+r+2, y+r+2, r, s2) 53 | tinydraw.Circle(d, x+r+2, y+r+1, r, s1) 54 | tinydraw.FilledCircleEx(d, x+r+2, y+r, r, c) 55 | } 56 | 57 | //go:noinline 58 | func WriteLine(display drivers.Displayer, font *tinyamifont.Font, x int16, y int16, str string, c color.RGBA) { 59 | tinyamifont.PrintString(font, display, str, x, y, c, tinyamifont.Regular) 60 | } 61 | 62 | //go:noinline 63 | func LineWidth(font *tinyamifont.Font, str string) (innerWidth uint32, outboxWidth uint32) { 64 | w := tinyamifont.LineWidth(font, str, tinyamifont.Regular) 65 | return uint32(w), uint32(w) 66 | } 67 | -------------------------------------------------------------------------------- /icon/calc.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/primitive" 8 | ) 9 | 10 | var calcShape = primitive.Shape{ 11 | Lines: []primitive.LineSegment{ 12 | 13 | //Top Left 14 | {primitive.Point8{1, 30}, primitive.Point8{30, 1}}, 15 | //Middle Left 16 | {primitive.Point8{1, 30}, primitive.Point8{1, 225}}, 17 | 18 | //Bottom Left 19 | {primitive.Point8{1, 225}, primitive.Point8{30, 254}}, 20 | 21 | //Subtract Left 22 | {primitive.Point8{47, 69}, primitive.Point8{47, 91}}, 23 | {primitive.Point8{119, 69}, primitive.Point8{119, 91}}, 24 | 25 | //Multiply 26 | {primitive.Point8{145, 60}, primitive.Point8{159, 45}}, 27 | {primitive.Point8{145, 100}, primitive.Point8{163, 80}}, 28 | {primitive.Point8{159, 115}, primitive.Point8{145, 100}}, 29 | {primitive.Point8{163, 80}, primitive.Point8{145, 60}}, 30 | 31 | {primitive.Point8{159, 45}, primitive.Point8{179, 65}}, 32 | {primitive.Point8{179, 95}, primitive.Point8{159, 115}}, 33 | {primitive.Point8{179, 65}, primitive.Point8{199, 45}}, 34 | {primitive.Point8{199, 115}, primitive.Point8{179, 95}}, 35 | {primitive.Point8{195, 80}, primitive.Point8{215, 100}}, 36 | {primitive.Point8{215, 60}, primitive.Point8{195, 80}}, 37 | {primitive.Point8{199, 45}, primitive.Point8{215, 60}}, 38 | {primitive.Point8{215, 100}, primitive.Point8{199, 115}}, 39 | 40 | //Plus 41 | {primitive.Point8{73, 137}, primitive.Point8{73, 166}}, 42 | {primitive.Point8{95, 137}, primitive.Point8{95, 166}}, 43 | {primitive.Point8{44, 166}, primitive.Point8{44, 186}}, 44 | {primitive.Point8{122, 166}, primitive.Point8{122, 186}}, 45 | {primitive.Point8{73, 187}, primitive.Point8{73, 216}}, 46 | {primitive.Point8{95, 187}, primitive.Point8{95, 216}}, 47 | 48 | //Equals 49 | {primitive.Point8{144, 148}, primitive.Point8{144, 169}}, 50 | {primitive.Point8{215, 148}, primitive.Point8{215, 169}}, 51 | 52 | {primitive.Point8{144, 183}, primitive.Point8{144, 205}}, 53 | {primitive.Point8{215, 183}, primitive.Point8{215, 205}}, 54 | 55 | //TOP right 56 | {primitive.Point8{225, 1}, primitive.Point8{254, 30}}, 57 | //MIDDLE RIGHT 58 | {primitive.Point8{254, 30}, primitive.Point8{254, 225}}, 59 | 60 | //Bottom Right 61 | {primitive.Point8{225, 254}, primitive.Point8{254, 225}}, 62 | }, 63 | } 64 | 65 | func Calc(g *tinygui.GuiContext, x, y, w int16, c color.RGBA) { 66 | 67 | primitive.FillShape(g.Display, x, y, w, w, &calcShape, c) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /component/floatingactionbutton.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/icon" 8 | "github.com/spearson78/tinygui/primitive" 9 | ) 10 | 11 | type FloatingActionButtonStyle byte 12 | 13 | const ( 14 | IconStyle FloatingActionButtonStyle = iota 15 | Extended 16 | ) 17 | 18 | type FloatingActionButtonProps struct { 19 | ComponentPos 20 | ComponentSize 21 | Style FloatingActionButtonStyle 22 | Disabled bool 23 | Color color.RGBA 24 | Icon icon.Icon 25 | Label string //Ony used in Extended style 26 | } 27 | 28 | type FloatingActionButtonState struct { 29 | animateState int8 30 | } 31 | 32 | func NewFloatingActionButtonProps(g *tinygui.GuiContext) FloatingActionButtonProps { 33 | return FloatingActionButtonProps{ 34 | ComponentPos: ComponentPos{ 35 | X: 10, 36 | Y: 10, 37 | }, 38 | ComponentSize: ComponentSize{ 39 | W: 40, 40 | H: -1, 41 | }, 42 | Style: IconStyle, 43 | Label: "", 44 | Disabled: false, 45 | Color: g.Theme.DefaultColor, 46 | Icon: icon.Add, 47 | } 48 | } 49 | 50 | //go:noinline 51 | func FloatingActionButton(g *tinygui.GuiContext, state *FloatingActionButtonState, props *FloatingActionButtonProps) bool { 52 | 53 | w := props.W 54 | h := props.W 55 | clicked := false 56 | 57 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, true, props.X, props.Y, props.W, h) 58 | if eventAction == Ignore { 59 | return false 60 | } 61 | if (eventAction & Click) != 0 { 62 | state.animateState = 10 63 | clicked = true 64 | g.UpdateState() 65 | } 66 | 67 | darkColor := props.Color 68 | lightColor := g.Theme.Background 69 | if props.Disabled { 70 | darkColor = g.Theme.DisabedColor 71 | lightColor = color.RGBA{166, 167, 168, 255} 72 | } 73 | 74 | if state.animateState > 0 { 75 | 76 | fillColor := darkColor 77 | darkColor = primitive.Desaturate(darkColor, 76) 78 | darkColor = primitive.TransitionColor(darkColor, fillColor, uint8(state.animateState*25)) 79 | state.animateState-- 80 | g.InvalidateRect(props.X, props.Y, props.X+w, props.Y+h, false) 81 | } 82 | 83 | textColor := lightColor 84 | 85 | if !props.Disabled { 86 | shadow1 := g.Theme.Shadow1 87 | shadow2 := g.Theme.Shadow2 88 | 89 | primitive.FilledCircleWithShadow(g.Display, props.X, props.Y, props.W, darkColor, shadow1, shadow2) 90 | } else { 91 | primitive.FilledCircle(g.Display, props.X, props.Y, props.W, darkColor) 92 | } 93 | 94 | iconWidth := props.W / 2 // int16(float32(radius) * float32(0.9)) 95 | centreOffset := (w/2 - iconWidth/2) 96 | if centreOffset < 0 { 97 | centreOffset = 4 98 | } 99 | 100 | props.Icon(g, props.X+centreOffset, props.Y+centreOffset-1, iconWidth, textColor) 101 | 102 | return clicked 103 | 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Gui 2 | 3 | A minimal material design based UI toolkit for Tiny Go projects. 4 | 5 | **Warning: This library is in an alpha state there is currently no API stability guarantee.** 6 | 7 | # Why 8 | 9 | Microcontrollers have limited ram and flash storage a tyical UI toolkit will will require the construction of a component tree at run time that consumes ram. This component tree often includes both the structural definition of the UI and the state of the UI components. Through careful coding a component tree could be constructed as a compile time constant but such a UI would not be very dynamic. 10 | 11 | # How 12 | 13 | A Tiny Gui is instead created by rendering UI components in sequence programmatically in this way the component structure is guaranted to be in flash memory and through the usage of normal control flow statements (if,for) complex dynamic UIs can be constructed. This is often referred to as an immediate mode gui. 14 | 15 | TinyGui additionaly performs zero allocations during rendering. The only memory usage is the stack during rendering and the minimal state for the components. When not actively rendering the only memory usage is the state of the components. 16 | 17 | # Basic Example 18 | 19 | This example will toggle an LED when a button is clicked. 20 | 21 | ![Toggle LED Screenshot](examples/toggleled/ToggleLedExample.png) 22 | 23 | ``` 24 | package toggleled 25 | 26 | import ( 27 | "github.com/spearson78/tinygui" 28 | "github.com/spearson78/tinygui/component" 29 | 30 | "machine" 31 | ) 32 | 33 | //State variable to track whether LED is on or off 34 | var LedState bool 35 | 36 | //State for Toggle Button 37 | var btnToggleLed component.ButtonState 38 | 39 | //Gui Function GuiContext is provided by Tiny Gui 40 | func ToggleLedGui(g *tinygui.GuiContext) { 41 | 42 | //Create button props including default values based on current theme 43 | btnToggleProps := component.NewButtonProps(g) 44 | 45 | //Set position and Size of the button 46 | btnToggleProps.X = 10 47 | btnToggleProps.Y = 10 48 | btnToggleProps.W = 220 49 | btnToggleProps.H = 50 50 | 51 | //Set the label of the button 52 | btnToggleProps.Label = "TOGGLE LED" 53 | 54 | //Set the style of the button 55 | btnToggleProps.Style = component.Contained 56 | 57 | //Render the Button and handle the click 58 | if component.Button(g, &btnToggleLed, &btnToggleProps) { 59 | //component.Button returns true if the button was clicked 60 | LedState = !LedState 61 | machine.LED.Set(LedState) 62 | } 63 | } 64 | ``` 65 | 66 | See the examples and cmd folders for more examples 67 | 68 | # Screenshots 69 | 70 | ![Calculator Screenshot](examples/calc/CalcExample.png) 71 | 72 | ![Touch Calibration Screenshot](examples/calibrate/CalibrateExample.png) 73 | 74 | ![Temperature Dashboard Screenshot](examples/temp/TempExample.png) 75 | 76 | ![Other Components Screenshot](examples/test/TestExample.png) 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /component/switch.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/event" 8 | "github.com/spearson78/tinygui/primitive" 9 | "tinygo.org/x/tinydraw" 10 | ) 11 | 12 | type SwitchProps struct { 13 | ComponentPos 14 | Label string 15 | Color color.RGBA 16 | Disabled bool 17 | } 18 | 19 | type SwitchState struct { 20 | animateState int8 21 | Checked bool 22 | } 23 | 24 | func NewSwitchProps(g *tinygui.GuiContext) SwitchProps { 25 | return SwitchProps{ 26 | ComponentPos: ComponentPos{ 27 | X: 10, 28 | Y: 10, 29 | }, 30 | Label: "Switch", 31 | Disabled: false, 32 | Color: g.Theme.DefaultColor, 33 | } 34 | } 35 | 36 | //go:noinline 37 | func Switch(g *tinygui.GuiContext, state *SwitchState, props *SwitchProps) bool { 38 | 39 | clicked := false 40 | 41 | _, ow := primitive.LineWidth(g.Theme.Font, props.Label) 42 | w := int16(48) + int16(ow) 43 | 44 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, w, 30) 45 | if eventAction == Ignore { 46 | return false 47 | } 48 | if (eventAction & Click) != 0 { 49 | state.Checked = !state.Checked 50 | if state.Checked { 51 | state.animateState = 10 52 | } 53 | clicked = true 54 | g.UpdateState() 55 | } 56 | 57 | if (g.Event.Type & event.Invalidate) != 0 { 58 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, w, int16(24), g.Theme.Background) 59 | } else { 60 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, 48, int16(24), g.Theme.Background) 61 | } 62 | 63 | //Check box 64 | var switchColor = g.Theme.Background 65 | var backColor = g.Theme.Border 66 | if state.Checked { 67 | switchColor = props.Color 68 | } 69 | 70 | if props.Disabled { 71 | backColor = primitive.Desaturate(backColor, 76) 72 | switchColor = primitive.Desaturate(switchColor, 76) 73 | } 74 | 75 | switchX := int16(0) 76 | if state.Checked { 77 | switchX = 20 78 | } 79 | 80 | if state.animateState > 0 { 81 | animColor := primitive.Desaturate(switchColor, 76) 82 | animColor = primitive.TransitionColor(animColor, g.Theme.Background, uint8(state.animateState*25)) 83 | primitive.FilledCircle(g.Display, props.X+switchX-3, props.Y+5-3, 26, animColor) 84 | state.animateState-- 85 | 86 | g.InvalidateRect(props.X+switchX-4, props.Y, props.X+switchX+28, props.Y+24, false) 87 | } 88 | 89 | primitive.FilledBox(g.Display, props.X+10, props.Y+5+5, 20, 10, backColor) 90 | primitive.FilledCircleWithShadow(g.Display, props.X+switchX, props.Y+5, 20, switchColor, g.Theme.Shadow1, g.Theme.Shadow2) 91 | 92 | if (g.Event.Type & event.Invalidate) != 0 { 93 | //Label 94 | colText := g.Theme.Text 95 | if props.Disabled { 96 | colText = g.Theme.DisabedColor 97 | } 98 | 99 | primitive.WriteLine(g.Display, g.Theme.Font, props.X+48, props.Y+20, props.Label, colText) 100 | } 101 | 102 | return clicked 103 | } 104 | -------------------------------------------------------------------------------- /component/checkbox.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/event" 8 | "github.com/spearson78/tinygui/icon" 9 | "github.com/spearson78/tinygui/primitive" 10 | "tinygo.org/x/tinydraw" 11 | ) 12 | 13 | type CheckBoxProps struct { 14 | ComponentPos 15 | Label string 16 | Color color.RGBA 17 | Disabled bool 18 | Icon icon.Icon 19 | } 20 | 21 | type CheckBoxState struct { 22 | animateState int8 23 | Checked bool 24 | } 25 | 26 | func NewCheckBoxProps(g *tinygui.GuiContext) CheckBoxProps { 27 | return CheckBoxProps{ 28 | ComponentPos: ComponentPos{ 29 | X: 10, 30 | Y: 10, 31 | }, 32 | Label: "", 33 | Disabled: false, 34 | Color: g.Theme.DefaultColor, 35 | Icon: icon.Checkmark, 36 | } 37 | } 38 | 39 | //go:noinline 40 | func CheckBox(g *tinygui.GuiContext, state *CheckBoxState, props *CheckBoxProps) bool { 41 | 42 | clicked := false 43 | 44 | _, ow := primitive.LineWidth(g.Theme.Font, props.Label) 45 | w := int16(ow) 46 | h := int16(20) 47 | 48 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, w, h) 49 | if eventAction == Ignore { 50 | return false 51 | } 52 | if (eventAction & Click) != 0 { 53 | state.animateState = 10 54 | state.Checked = !state.Checked 55 | clicked = true 56 | g.UpdateState() 57 | } 58 | 59 | colChecked := g.Theme.Background 60 | colBackground := g.Theme.Background 61 | 62 | if (g.Event.Type & event.Invalidate) != 0 { 63 | //Clear out old widget 64 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, int16(w), int16(h), colBackground) 65 | } else { 66 | tinydraw.FilledRectangleEx(g.Display, props.X+1, props.Y+6, 16, 16, colBackground) 67 | } 68 | 69 | //Check box 70 | var boxColor = g.Theme.Border 71 | 72 | if props.Disabled { 73 | boxColor = g.Theme.DefaultColor 74 | } else if state.Checked { 75 | boxColor = props.Color 76 | } 77 | 78 | if state.animateState > 0 { 79 | fillColor := primitive.Desaturate(props.Color, 76) 80 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*25)) 81 | 82 | tinydraw.FilledCircleEx(g.Display, props.X+8, props.Y+13, 15, fillColor) 83 | state.animateState-- 84 | g.InvalidateRect(props.X-8, props.Y-4, props.X+24, props.Y+28, false) 85 | } 86 | 87 | if !state.Checked { 88 | primitive.OutlineBox(g.Display, props.X, props.Y+5, 15+2, 15+2, boxColor) 89 | } else { 90 | primitive.FilledBox(g.Display, props.X, props.Y+5, 15+2, 15+2, boxColor) 91 | props.Icon(g, props.X+2, props.Y+7, 13, colChecked) 92 | } 93 | 94 | if (g.Event.Type & event.Invalidate) != 0 { 95 | //Label 96 | colText := g.Theme.Text 97 | if props.Disabled { 98 | colText = boxColor 99 | } 100 | 101 | primitive.WriteLine(g.Display, g.Theme.Font, props.X+24, props.Y+20, props.Label, colText) 102 | } 103 | 104 | return clicked 105 | } 106 | -------------------------------------------------------------------------------- /component/slider.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/primitive" 8 | "tinygo.org/x/tinydraw" 9 | ) 10 | 11 | type SliderState struct { 12 | animateState int8 13 | Value float32 14 | } 15 | 16 | type SliderProps struct { 17 | ComponentPos 18 | ComponentSize 19 | Color color.RGBA 20 | Disabled bool 21 | } 22 | 23 | func NewSliderProps(g *tinygui.GuiContext) SliderProps { 24 | return SliderProps{ 25 | ComponentPos: ComponentPos{ 26 | X: 10, 27 | Y: 10, 28 | }, 29 | ComponentSize: ComponentSize{ 30 | W: 100, 31 | H: -1, 32 | }, 33 | Color: g.Theme.DefaultColor, 34 | Disabled: false, 35 | } 36 | } 37 | 38 | //go:noinline 39 | func Slider(g *tinygui.GuiContext, state *SliderState, props *SliderProps) bool { 40 | 41 | clicked := false 42 | colBackground := g.Theme.Background 43 | 44 | h := int16(30) 45 | 46 | eventAction, clickX, _ := HandleEvent(&g.Event, props.Disabled, true, false, props.X, props.Y, props.W, h) 47 | if eventAction == Ignore { 48 | return false 49 | } 50 | if (eventAction & (Click | Drag | DragEnd)) != 0 { 51 | if eventAction == Drag { 52 | state.animateState = 11 53 | } else { 54 | state.animateState = 10 55 | } 56 | state.Value = float32(clickX-16) / float32(props.W-32) 57 | if state.Value < 0 { 58 | state.Value = 0 59 | } else if state.Value > 1 { 60 | state.Value = 1 61 | } 62 | clicked = true 63 | g.UpdateState() 64 | } 65 | 66 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, 16, h, colBackground) 67 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, props.W, 14, colBackground) 68 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y+19, props.W, 13, colBackground) 69 | tinydraw.FilledRectangleEx(g.Display, props.X+props.W-16, props.Y, 16, h, colBackground) 70 | 71 | //Slider bar 72 | 73 | sliderWidth := int16(state.Value * float32(props.W-32)) 74 | sliderLeftCol := props.Color 75 | sliderRightCol := primitive.Desaturate(sliderLeftCol, 76) 76 | 77 | if state.animateState >= 0 { 78 | 79 | if state.animateState < 11 { 80 | g.InvalidateRect(props.X+sliderWidth+16-16, props.Y, props.X+sliderWidth+32, props.Y+h+1, false) 81 | state.animateState-- 82 | } 83 | 84 | fillColor := g.Theme.Background 85 | if state.animateState > 0 { 86 | fillColor = primitive.Desaturate(sliderLeftCol, 51) 87 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*23)) 88 | } 89 | 90 | tinydraw.FilledCircleEx(g.Display, props.X+sliderWidth+16, props.Y+16, 15, fillColor) 91 | } 92 | 93 | primitive.FilledBox(g.Display, props.X+16, props.Y+14, sliderWidth+9, 5, sliderLeftCol) 94 | primitive.FilledBox(g.Display, props.X+sliderWidth+16, props.Y+14, props.W-sliderWidth-32, 5, sliderRightCol) 95 | 96 | tinydraw.FilledCircleEx(g.Display, props.X+sliderWidth+16, props.Y+16, 9, sliderLeftCol) 97 | 98 | return clicked 99 | } 100 | -------------------------------------------------------------------------------- /component/radiobutton.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/event" 8 | "github.com/spearson78/tinygui/primitive" 9 | "tinygo.org/x/tinydraw" 10 | ) 11 | 12 | type RadioButtonGroup uint8 13 | 14 | var DefaultRadioButtonGroup RadioButtonGroup 15 | 16 | type RadioButtonProps struct { 17 | X int16 18 | Y int16 19 | Label string 20 | Color color.RGBA 21 | Disabled bool 22 | Group *RadioButtonGroup 23 | CheckedState uint8 24 | } 25 | 26 | type RadioButtonState struct { 27 | animateState int8 28 | } 29 | 30 | func NewRadioButtonProps(g *tinygui.GuiContext) RadioButtonProps { 31 | return RadioButtonProps{ 32 | X: 10, 33 | Y: 10, 34 | Label: "", 35 | Color: g.Theme.DefaultColor, 36 | Disabled: false, 37 | Group: &DefaultRadioButtonGroup, 38 | CheckedState: 1, 39 | } 40 | } 41 | 42 | //go:noinline 43 | func RadioButton(g *tinygui.GuiContext, state *RadioButtonState, props *RadioButtonProps) bool { 44 | 45 | clicked := false 46 | colBackground := g.Theme.Background 47 | boxColor := g.Theme.Border 48 | 49 | _, ow := primitive.LineWidth(g.Theme.Font, props.Label) 50 | w := int16(ow) 51 | h := int16(20) 52 | 53 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, w, h) 54 | if eventAction == Ignore { 55 | return false 56 | } 57 | if (eventAction & Click) != 0 { 58 | state.animateState = 10 59 | *props.Group = RadioButtonGroup(props.CheckedState) 60 | clicked = true 61 | g.UpdateState() 62 | } 63 | 64 | checked := *props.Group == RadioButtonGroup(props.CheckedState) 65 | 66 | if props.Disabled { 67 | boxColor = g.Theme.DisabedColor 68 | } else if checked { 69 | boxColor = props.Color 70 | } 71 | 72 | //Clear out old widget 73 | if (g.Event.Type & event.Invalidate) != 0 { 74 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, int16(w), int16(h), colBackground) 75 | } 76 | 77 | if state.animateState > 0 { 78 | fillColor := g.Theme.Background 79 | fillColor = primitive.Desaturate(props.Color, 76) 80 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*25)) 81 | 82 | tinydraw.FilledCircleEx(g.Display, props.X+10, props.Y+10, 15, fillColor) 83 | state.animateState-- 84 | g.InvalidateRect(props.X-5, props.Y-5, props.X+25, props.Y+25, false) 85 | } 86 | 87 | //Radio Button 88 | tinydraw.FilledCircleEx(g.Display, props.X+10, props.Y+10, 10, boxColor) 89 | tinydraw.FilledCircleEx(g.Display, props.X+10, props.Y+10, 8, colBackground) 90 | 91 | if checked { 92 | tinydraw.FilledCircleEx(g.Display, props.X+10, props.Y+10, 5, boxColor) 93 | } 94 | 95 | if (g.Event.Type & event.Invalidate) != 0 { 96 | colText := g.Theme.Text 97 | if props.Disabled { 98 | colText = boxColor 99 | } 100 | 101 | //Label 102 | primitive.WriteLine(g.Display, g.Theme.Font, int16(props.X+24), int16(props.Y+17), props.Label, colText) 103 | } 104 | 105 | return clicked 106 | } 107 | -------------------------------------------------------------------------------- /examples/calibrate/calibrate.go: -------------------------------------------------------------------------------- 1 | package calibrate 2 | 3 | import ( 4 | "github.com/spearson78/tinygui" 5 | "github.com/spearson78/tinygui/event" 6 | "github.com/spearson78/tinygui/primitive" 7 | "tinygo.org/x/tinydraw" 8 | ) 9 | 10 | type calibrationState struct { 11 | state uint8 12 | topLeftX uint16 13 | topLeftY uint16 14 | bottomRightX uint16 15 | bottomRightY uint16 16 | 17 | xPixelSize uint16 18 | xOffset uint16 19 | yPixelSize uint16 20 | yOffset uint16 21 | } 22 | 23 | func IsCalibrated() bool { 24 | return CalibrationState.state == 2 25 | } 26 | 27 | func TouchCalibration(x int, y int) (int32, int32) { 28 | if CalibrationState.state != 2 { 29 | return int32(x), int32(y) 30 | } else { 31 | rx := (int32(x) / int32(CalibrationState.xPixelSize)) - int32(CalibrationState.xOffset) 32 | ry := (int32(y) / int32(CalibrationState.yPixelSize)) - int32(CalibrationState.yOffset) 33 | 34 | return rx, ry 35 | } 36 | } 37 | 38 | var CalibrationState = calibrationState{} 39 | 40 | func handleCalibrateEvent(g *tinygui.GuiContext, evt event.Event, px, py int16) (rx, ry uint16, clicked bool) { 41 | fillColor := g.Theme.DefaultColor 42 | 43 | if (evt.Type & (event.DragEnd | event.Click)) != 0 { 44 | fillColor = g.Theme.SuccessColor 45 | clicked = true 46 | rx = evt.X 47 | ry = evt.Y 48 | } 49 | 50 | tinydraw.FilledCircleEx(g.Display, px, py, 10, fillColor) 51 | 52 | return 53 | } 54 | 55 | //go:noinline 56 | func Gui(g *tinygui.GuiContext) { 57 | 58 | w, h := g.Display.Size() 59 | 60 | switch CalibrationState.state { 61 | case 0: 62 | px := int16(20) 63 | py := int16(20) 64 | rx, ry, clicked := handleCalibrateEvent(g, g.Event, px, py) 65 | if clicked { 66 | CalibrationState.state++ 67 | CalibrationState.topLeftX = rx 68 | CalibrationState.topLeftY = ry 69 | g.UpdateState() 70 | } 71 | case 1: 72 | px := w - 20 73 | py := h - 20 74 | rx, ry, clicked := handleCalibrateEvent(g, g.Event, px, py) 75 | if clicked { 76 | CalibrationState.state++ 77 | CalibrationState.bottomRightX = rx 78 | CalibrationState.bottomRightY = ry 79 | 80 | CalibrationState.xPixelSize = uint16((int32(CalibrationState.bottomRightX) - int32(CalibrationState.topLeftX)) / int32(w-40)) 81 | CalibrationState.xOffset = ((CalibrationState.topLeftX) / CalibrationState.xPixelSize) - 20 82 | 83 | CalibrationState.yPixelSize = uint16((int32(CalibrationState.bottomRightY) - int32(CalibrationState.topLeftY)) / int32(h-40)) 84 | CalibrationState.yOffset = ((CalibrationState.topLeftY) / CalibrationState.yPixelSize) - 20 85 | 86 | } 87 | case 2: 88 | if (g.Event.Type & event.Click) != 0 { 89 | tinydraw.FilledCircleEx(g.Display, int16(g.Event.X), int16(g.Event.Y), 10, g.Theme.SecondaryColor) 90 | } 91 | 92 | } 93 | 94 | if (g.Event.Type & event.Invalidate) != 0 { 95 | txt := "Touch Calibration" 96 | _, ow := primitive.LineWidth(g.Theme.Font, txt) 97 | 98 | txtX := (w / 2) - (int16(ow) / 2) 99 | txtY := (h / 2) + (int16(g.Theme.Font.Ysize) / 2) 100 | 101 | primitive.WriteLine(g.Display, g.Theme.Font, txtX, txtY, txt, g.Theme.Text) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /examples/temp/temperatureGui.go: -------------------------------------------------------------------------------- 1 | package temp 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | 7 | "github.com/spearson78/tinygui" 8 | "github.com/spearson78/tinygui/component" 9 | "github.com/spearson78/tinygui/icon" 10 | "github.com/spearson78/tinygui/theme" 11 | ) 12 | 13 | var txtTempDisplayState = component.DashCardState{ 14 | Value: "0", 15 | ActionIcon: icon.Edit, 16 | ActionLabel: "Fahrenheit", 17 | } 18 | 19 | var lineGraphState = component.DashLineGraphState{ 20 | Values: []float32{24, 24, 24, 24, 24, 24, 24}, 21 | } 22 | 23 | var tempCelsius = true 24 | var lastTemp = float32(20) 25 | var graphUpdateCounter = int64(0) 26 | 27 | //go:noinline 28 | func DoSensor(g *tinygui.TinyGui, t *theme.Theme, readTemp func() float32) { 29 | temp := readTemp() 30 | 31 | if math.Abs(float64(lastTemp)-float64(temp)) >= 1.0 { 32 | 33 | t := float64(temp) 34 | suffix := "°C" 35 | if !tempCelsius { 36 | t = (t * (9 / 5)) + 32 37 | suffix = "°F" 38 | } 39 | 40 | tempStr := strconv.FormatFloat(t, 'f', 0, 64) 41 | txtTempDisplayState.Value = tempStr + suffix 42 | g.UpdateState() 43 | lastTemp = temp 44 | 45 | } 46 | 47 | graphUpdateCounter++ 48 | if graphUpdateCounter%10 == 0 { 49 | 50 | lineGraphState.Values[0] = lineGraphState.Values[1] 51 | lineGraphState.Values[1] = lineGraphState.Values[2] 52 | lineGraphState.Values[2] = lineGraphState.Values[3] 53 | lineGraphState.Values[3] = lineGraphState.Values[4] 54 | lineGraphState.Values[4] = lineGraphState.Values[5] 55 | lineGraphState.Values[5] = lineGraphState.Values[6] 56 | lineGraphState.Values[6] = temp 57 | 58 | percentChange := ((lineGraphState.Values[6] - lineGraphState.Values[0]) / lineGraphState.Values[6]) * float32(100.0) 59 | if percentChange >= 0.0 { 60 | lineGraphState.SubTitle = strconv.FormatFloat(float64(percentChange), 'f', 2, 64) + "% Increase" 61 | lineGraphState.SubTitleColor = t.SuccessColor 62 | } else { 63 | lineGraphState.SubTitle = strconv.FormatFloat(math.Abs(float64(percentChange)), 'f', 2, 64) + "% Decrease" 64 | lineGraphState.SubTitleColor = t.ErrorColor 65 | } 66 | 67 | g.UpdateState() 68 | 69 | } 70 | } 71 | 72 | var tempXLabels = []string{"6", "5", "4", "3", "2", "1", "0"} 73 | 74 | //go:noinline 75 | func TemperatureSensorGui(g *tinygui.GuiContext) { 76 | 77 | dashCardProps := component.NewDashCardProps(g) 78 | dashCardProps.X = 5 79 | dashCardProps.Y = 10 80 | dashCardProps.Label = "Temp" 81 | 82 | if component.DashCard(g, &txtTempDisplayState, &dashCardProps) { 83 | tempCelsius = !tempCelsius 84 | if tempCelsius { 85 | txtTempDisplayState.ActionLabel = "Fahrenheit" 86 | } else { 87 | txtTempDisplayState.ActionLabel = "Celsius" 88 | } 89 | lastTemp = 0 90 | } 91 | 92 | dashLineProps := component.NewDashLineGraphProps(g) 93 | dashLineProps.X = 10 94 | dashLineProps.Y = 85 95 | dashLineProps.Color = g.Theme.SuccessColor 96 | dashLineProps.YStart = 16 97 | dashLineProps.YEnd = 30 98 | dashLineProps.YStep = 2 99 | dashLineProps.Label = "Temperature" 100 | dashLineProps.XLabels = tempXLabels 101 | if component.DashLineGraph(g, &lineGraphState, &dashLineProps) { 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /component/dashcard.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/event" 8 | "github.com/spearson78/tinygui/icon" 9 | "github.com/spearson78/tinygui/primitive" 10 | "tinygo.org/x/tinydraw" 11 | ) 12 | 13 | type DashCardState struct { 14 | animateState int8 15 | Value string 16 | ActionIcon icon.Icon 17 | ActionLabel string 18 | } 19 | 20 | type DashCardProps struct { 21 | ComponentPos 22 | ComponentSize 23 | BadgeIcon icon.Icon 24 | Label string 25 | Disabled bool 26 | Color color.RGBA 27 | } 28 | 29 | func NewDashCardProps(g *tinygui.GuiContext) DashCardProps { 30 | return DashCardProps{ 31 | ComponentPos: ComponentPos{ 32 | X: 10, 33 | Y: 10, 34 | }, 35 | ComponentSize: ComponentSize{ 36 | W: 110, 37 | H: -1, 38 | }, 39 | Label: "Click", 40 | Disabled: false, 41 | BadgeIcon: icon.Announcement, 42 | Color: g.Theme.DefaultColor, 43 | } 44 | } 45 | 46 | //go:noinline 47 | func DashCard(g *tinygui.GuiContext, state *DashCardState, props *DashCardProps) bool { 48 | 49 | _, labelWidth := primitive.LineWidth(g.Theme.SecondaryFont, props.Label) 50 | _, valueWidth := primitive.LineWidth(g.Theme.Font, state.Value) 51 | 52 | clicked := false 53 | 54 | h := int16(70) 55 | 56 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, props.W, h) 57 | if eventAction == Ignore { 58 | return false 59 | } 60 | if (eventAction & Click) != 0 { 61 | state.animateState = 10 62 | clicked = true 63 | g.UpdateState() 64 | } 65 | 66 | border := g.Theme.Border 67 | white := g.Theme.Background 68 | 69 | if (g.Event.Type & event.Invalidate) != 0 { 70 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, props.W, h, white) 71 | 72 | primitive.OutlineBoxWithShadow(g.Display, props.X, props.Y+10, props.W, h-10, border, g.Theme.Shadow1, g.Theme.Shadow2) 73 | 74 | primitive.FilledBoxWithShadow(g.Display, props.X+10, props.Y, 40, 42, props.Color, g.Theme.Shadow1, g.Theme.Shadow2) 75 | 76 | tinydraw.LineEx(g.Display, props.X+10, props.Y+48, props.X+props.W-10, props.Y+48, border) 77 | props.BadgeIcon(g, props.X+20, props.Y+10, 20, white) 78 | primitive.WriteLine(g.Display, g.Theme.SecondaryFont, props.X+props.W-int16(labelWidth)-10, props.Y+18, props.Label, border) 79 | 80 | } else { 81 | tinydraw.FilledRectangleEx(g.Display, props.X+10+41, props.Y+24, props.W-52, 17, g.Theme.Background) 82 | } 83 | 84 | buttonBack := g.Theme.Background 85 | if state.animateState > 0 { 86 | 87 | buttonBack = primitive.Desaturate(border, 76) 88 | buttonBack = primitive.TransitionColor(buttonBack, g.Theme.Background, uint8(state.animateState*25)) 89 | } 90 | 91 | tinydraw.FilledRectangleEx(g.Display, props.X+1, props.Y+49, props.W-2, 18, buttonBack) 92 | 93 | if state.animateState > 0 { 94 | g.InvalidateRect(props.X+1, props.Y+49, props.X+1+props.W-2, props.Y+49+18, false) 95 | state.animateState-- 96 | } 97 | 98 | primitive.WriteLine(g.Display, g.Theme.Font, props.X+props.W-int16(valueWidth)-10, props.Y+38, state.Value, g.Theme.Text) 99 | state.ActionIcon(g, props.X+11, props.Y+52, 10, border) 100 | primitive.WriteLine(g.Display, g.Theme.SecondaryFont, props.X+11+10+3, props.Y+60, state.ActionLabel, border) 101 | 102 | return clicked 103 | } 104 | -------------------------------------------------------------------------------- /component/bottomnavigation.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/icon" 8 | "github.com/spearson78/tinygui/primitive" 9 | ) 10 | 11 | type BottomNavigationState struct { 12 | animateState int8 13 | lastSelected uint8 14 | Selected uint8 15 | } 16 | 17 | type BottomNavigationOption struct { 18 | Icon icon.Icon 19 | Label string 20 | } 21 | 22 | type BottomNavigationProps struct { 23 | ComponentPos 24 | ComponentSize 25 | Options []BottomNavigationOption 26 | Disabled bool 27 | Color color.RGBA 28 | PermaLabel bool 29 | } 30 | 31 | var defaultBottomNavOptions = []BottomNavigationOption{ 32 | {Icon: icon.Calc, Label: "Calc"}, 33 | {Icon: icon.Timeline, Label: "Data"}, 34 | {Icon: icon.Announcement, Label: "Test"}, 35 | } 36 | 37 | func NewBottomNavigationProps(g *tinygui.GuiContext) BottomNavigationProps { 38 | displayWidth, _ := g.Display.Size() 39 | 40 | return BottomNavigationProps{ 41 | ComponentPos: ComponentPos{ 42 | X: 10, 43 | Y: 10, 44 | }, 45 | ComponentSize: ComponentSize{ 46 | W: displayWidth, 47 | H: -1, 48 | }, 49 | Options: defaultBottomNavOptions, 50 | Disabled: false, 51 | Color: g.Theme.DefaultColor, 52 | PermaLabel: false, 53 | } 54 | } 55 | 56 | func BottomNavigation(g *tinygui.GuiContext, state *BottomNavigationState, props *BottomNavigationProps) bool { 57 | 58 | optionWidth := props.W / int16(len(props.Options)) 59 | 60 | clicked := false 61 | 62 | h := int16(40) 63 | 64 | draw := true 65 | 66 | eventAction, clickX, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, props.W, h) 67 | if eventAction == Ignore { 68 | return false 69 | } 70 | if (eventAction & Click) != 0 { 71 | state.animateState = 10 72 | state.Selected = uint8(clickX / optionWidth) 73 | clicked = true 74 | g.UpdateState() 75 | } 76 | if (eventAction & Update) != 0 { 77 | if state.lastSelected == state.Selected { 78 | draw = false 79 | } 80 | } 81 | 82 | state.lastSelected = state.Selected 83 | 84 | if draw { 85 | 86 | x := props.X 87 | iconWidth := int16(16) 88 | iconOffset := (optionWidth / 2) - (iconWidth / 2) 89 | 90 | for i, option := range props.Options { 91 | 92 | col := g.Theme.Border 93 | 94 | if i == int(state.Selected) { 95 | 96 | col = props.Color 97 | 98 | if state.animateState > 0 { 99 | fillColor := primitive.Desaturate(props.Color, 76) 100 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*25)) 101 | 102 | g.Display.FillRect(x, props.Y, optionWidth, h, fillColor) 103 | 104 | if state.animateState > 0 { 105 | g.InvalidateRect(x, props.Y, x+optionWidth, props.Y+h, false) 106 | } 107 | 108 | state.animateState-- 109 | } else { 110 | g.Display.FillRect(x, props.Y, optionWidth, h, g.Theme.Background) 111 | } 112 | } else { 113 | g.Display.FillRect(x, props.Y, optionWidth, h, g.Theme.Background) 114 | } 115 | 116 | if props.PermaLabel || i == int(state.Selected) { 117 | option.Icon(g, x+iconOffset, props.Y+5, 16, col) 118 | _, ow := primitive.LineWidth(g.Theme.Font, option.Label) 119 | textOffset := (optionWidth / 2) - int16(ow/2) 120 | primitive.WriteLine(g.Display, g.Theme.Font, x+textOffset, props.Y+35, option.Label, col) 121 | } else { 122 | option.Icon(g, x+iconOffset, props.Y+15, 16, col) 123 | } 124 | 125 | x = x + optionWidth 126 | } 127 | } 128 | 129 | return clicked 130 | 131 | } 132 | -------------------------------------------------------------------------------- /component/dashlinegraph.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | "strconv" 6 | 7 | "github.com/spearson78/tinygui" 8 | "github.com/spearson78/tinygui/event" 9 | "github.com/spearson78/tinygui/primitive" 10 | "tinygo.org/x/tinydraw" 11 | ) 12 | 13 | type DashLineGraphState struct { 14 | Values []float32 15 | SubTitle string 16 | SubTitleColor color.RGBA 17 | //ActionIcon IconFunc 18 | //ActionLabel string 19 | } 20 | 21 | type DashLineGraphProps struct { 22 | ComponentPos 23 | ComponentSize 24 | Label string 25 | XLabels []string 26 | YStart int16 27 | YEnd int16 28 | YStep int16 29 | Disabled bool 30 | Color color.RGBA 31 | } 32 | 33 | var defaultDashLineGraphXLabels = []string{"0", "10", "20", "30", "40", "50", "60", "70", "80", "90", "100"} 34 | 35 | func NewDashLineGraphProps(g *tinygui.GuiContext) DashLineGraphProps { 36 | return DashLineGraphProps{ 37 | ComponentPos: ComponentPos{ 38 | X: 10, 39 | Y: 10, 40 | }, 41 | ComponentSize: ComponentSize{ 42 | W: 220, 43 | H: -1, 44 | }, 45 | Disabled: false, 46 | Color: g.Theme.DefaultColor, 47 | YStart: 0, 48 | YEnd: 100, 49 | YStep: 10, 50 | Label: "Line Graph", 51 | XLabels: defaultDashLineGraphXLabels, 52 | } 53 | } 54 | 55 | //go:noinline 56 | func DashLineGraph(g *tinygui.GuiContext, state *DashLineGraphState, props *DashLineGraphProps) bool { 57 | 58 | border := g.Theme.Border 59 | white := g.Theme.Background 60 | axisColor := white 61 | lineColor := white 62 | 63 | graphStartY := int16(118) 64 | graphHeight := int16(110) 65 | graphYScale := float32(graphHeight) / (float32(props.YEnd) - float32(props.YStart)) 66 | h := graphStartY + 40 67 | 68 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, false, props.X, props.Y, props.W, h) 69 | if eventAction == Ignore { 70 | return false 71 | } 72 | if (eventAction & Click) != 0 { 73 | return false 74 | } 75 | 76 | graphStartX := int16(26) 77 | graphWidth := props.W - graphStartX - 15 78 | grapXScale := float32(graphWidth) / float32(len(props.XLabels)-1) 79 | 80 | if (g.Event.Type & event.Invalidate) != 0 { 81 | tinydraw.FilledRectangleEx(g.Display, props.X, props.Y, props.W, h, white) 82 | 83 | primitive.OutlineBoxWithShadow(g.Display, props.X, props.Y+10, props.W, h, border, g.Theme.Shadow1, g.Theme.Shadow2) 84 | 85 | primitive.FilledBoxWithShadow(g.Display, props.X+10, props.Y, props.W-20, 130, props.Color, g.Theme.Shadow1, g.Theme.Shadow2) 86 | 87 | yCounter := float32(0) 88 | for yLabelInt := props.YStart; yLabelInt <= props.YEnd; yLabelInt += props.YStep { 89 | yLabelOffset := yCounter * graphYScale 90 | yLabelStr := strconv.FormatInt(int64(yLabelInt), 10) 91 | 92 | primitive.WriteLine(g.Display, g.Theme.SecondaryFont, props.X+12, props.Y+graphStartY-int16(yLabelOffset), yLabelStr, axisColor) 93 | yCounter += float32(props.YStep) 94 | } 95 | 96 | for x := 0; x < len(props.XLabels); x++ { 97 | xLabelOffset := int16(float32(x) * grapXScale) 98 | xLabelStr := props.XLabels[x] 99 | 100 | primitive.WriteLine(g.Display, g.Theme.SecondaryFont, props.X+graphStartX+xLabelOffset, props.Y+graphStartY+7, xLabelStr, axisColor) 101 | } 102 | 103 | primitive.WriteLine(g.Display, g.Theme.Font, props.X+10, props.Y+graphStartY+30, props.Label, g.Theme.Text) 104 | 105 | } else { 106 | tinydraw.FilledRectangleEx(g.Display, props.X+graphStartX-2, props.Y+graphStartY-graphHeight-2, graphWidth+5, graphHeight+5, props.Color) 107 | tinydraw.FilledRectangleEx(g.Display, props.X+10, props.Y+graphStartY+34, props.W-20, 7, g.Theme.Background) 108 | 109 | } 110 | 111 | tinydraw.LineEx(g.Display, props.X+graphStartX, props.Y+graphStartY, props.X+graphStartX, props.Y+graphStartY-graphHeight, axisColor) 112 | tinydraw.LineEx(g.Display, props.X+graphStartX, props.Y+graphStartY, props.X+graphStartX+graphWidth, props.Y+graphStartY, axisColor) 113 | 114 | lastXPos := int16(0) 115 | lastYPos := int16(0) 116 | for x := 0; x < len(state.Values); x++ { 117 | xPos := props.X + graphStartX + int16(float32(x)*grapXScale) 118 | yPos := props.Y + graphStartY - int16(float32(state.Values[x]-float32(props.YStart))*graphYScale) 119 | tinydraw.FilledCircleEx(g.Display, xPos, yPos, 2, lineColor) 120 | 121 | if x > 0 { 122 | tinydraw.LineEx(g.Display, lastXPos, lastYPos, xPos, yPos, lineColor) 123 | } 124 | 125 | lastXPos = xPos 126 | lastYPos = yPos 127 | } 128 | 129 | primitive.WriteLine(g.Display, g.Theme.SecondaryFont, props.X+10, props.Y+graphStartY+40, state.SubTitle, state.SubTitleColor) 130 | 131 | return false 132 | } 133 | -------------------------------------------------------------------------------- /tinygui.go: -------------------------------------------------------------------------------- 1 | package tinygui 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/spearson78/tinygui/event" 7 | "github.com/spearson78/tinygui/theme" 8 | 9 | "tinygo.org/x/drivers/touch" 10 | ) 11 | 12 | type Component func(g *GuiContext) 13 | 14 | type TouchCalibration func(x int, y int) (int32, int32) 15 | 16 | type TouchSource interface { 17 | Read() (int32, int32, bool) 18 | } 19 | 20 | type TinyGui struct { 21 | guiContext GuiContext 22 | clip ClippedDisplay 23 | tch touch.Pointer 24 | touchCalibration TouchCalibration 25 | 26 | touchInProgress bool 27 | dragInProgress bool 28 | touchStartX int32 29 | touchStartY int32 30 | touchLastX int32 31 | touchLastY int32 32 | } 33 | 34 | func New(d Displayer, t touch.Pointer, theme theme.Theme, touchCalibration TouchCalibration) *TinyGui { 35 | tinyGui := TinyGui{ 36 | guiContext: GuiContext{ 37 | Theme: theme, 38 | TriggerInvalidate: true, 39 | ClearDisplay: true, 40 | InvalidX0: -1, 41 | InvalidY0: -1, 42 | InvalidX1: -1, 43 | InvalidY1: -1, 44 | }, 45 | clip: ClippedDisplay{D: d}, 46 | tch: t, 47 | touchCalibration: touchCalibration, 48 | } 49 | 50 | tinyGui.guiContext.Display = &tinyGui.clip 51 | 52 | return &tinyGui 53 | } 54 | 55 | func (t *TinyGui) Init() { 56 | } 57 | 58 | func abs(x int32) int32 { 59 | if x < 0 { 60 | return -x 61 | } 62 | return x 63 | } 64 | 65 | const dragSensitivity = int32(3) 66 | 67 | func (t *TinyGui) readTouch() (int32, int32, bool) { 68 | tp := t.tch.ReadTouchPoint() 69 | touching := tp.Z != 0 70 | 71 | if touching { 72 | x, y := t.touchCalibration(tp.X, tp.Y) 73 | return x, y, touching 74 | } 75 | return 0, 0, false 76 | } 77 | 78 | func (t *TinyGui) UpdateState() { 79 | t.guiContext.TriggerUpdateState = true 80 | } 81 | 82 | //go:noinline 83 | func (t *TinyGui) DoTouchNonBlocking() *GuiContext { 84 | t.clip.ClipRect = image.Rectangle{} 85 | 86 | t.guiContext.Event.Type = event.None 87 | 88 | if t.touchInProgress { 89 | 90 | //Continue Drag 91 | x, y, touching := t.readTouch() 92 | 93 | if !touching { 94 | if t.dragInProgress { 95 | t.guiContext.Event.Type |= event.DragEnd 96 | } else { 97 | t.guiContext.Event.Type |= event.Click 98 | } 99 | t.touchInProgress = false 100 | t.dragInProgress = false 101 | } else { 102 | 103 | deltaX := abs(t.touchLastX - x) 104 | deltaY := abs(t.touchLastY - y) 105 | dragSens := dragSensitivity 106 | if !t.dragInProgress { 107 | dragSens *= 3 108 | } 109 | if deltaX > dragSens || deltaY > dragSens { 110 | t.dragInProgress = true 111 | t.guiContext.Event.Type |= event.Drag 112 | t.touchLastX = x 113 | t.touchLastY = y 114 | } 115 | } 116 | 117 | if t.guiContext.Event.Type != event.None { 118 | 119 | t.guiContext.Event.X = uint16(t.touchStartX) 120 | t.guiContext.Event.Y = uint16(t.touchStartY) 121 | t.guiContext.Event.DragX = uint16(t.touchLastX) 122 | t.guiContext.Event.DragY = uint16(t.touchLastY) 123 | } 124 | } else { 125 | 126 | x, y, touching := t.readTouch() 127 | if touching { 128 | //Start of touch maybe a click 129 | t.touchInProgress = true 130 | t.touchStartX = x 131 | t.touchStartY = y 132 | t.touchLastX = x 133 | t.touchLastY = y 134 | } 135 | } 136 | 137 | if t.guiContext.TriggerUpdateState { 138 | t.guiContext.TriggerUpdateState = false 139 | if t.guiContext.TriggerInvalidate && t.guiContext.InvalidX0 == -1 { 140 | t.guiContext.TriggerInvalidate = false 141 | t.guiContext.Event.Type |= event.Invalidate 142 | 143 | w, h := t.clip.D.Size() 144 | if t.guiContext.ClearDisplay { 145 | t.clip.FillRect(0, 0, w-1, h-1, t.guiContext.Theme.Background) 146 | t.guiContext.ClearDisplay = false 147 | } 148 | 149 | } else { 150 | t.guiContext.Event.Type |= event.Update 151 | } 152 | 153 | } else if t.guiContext.TriggerInvalidate && (t.guiContext.InvalidX0 == -1 || t.guiContext.Event.Type == event.None) { 154 | 155 | w, h := t.clip.D.Size() 156 | if t.guiContext.InvalidX0 != -1 { 157 | t.clip.ClipRect = image.Rect(int(t.guiContext.InvalidX0), int(t.guiContext.InvalidY0), int(t.guiContext.InvalidX1+1), int(t.guiContext.InvalidY1+1)) 158 | t.guiContext.InvalidX0 = -1 159 | t.guiContext.InvalidY0 = -1 160 | t.guiContext.InvalidX1 = -1 161 | t.guiContext.InvalidY1 = -1 162 | } 163 | 164 | if t.guiContext.ClearDisplay { 165 | t.clip.FillRect(0, 0, w-1, h-1, t.guiContext.Theme.Background) 166 | t.guiContext.ClearDisplay = false 167 | } 168 | 169 | t.guiContext.TriggerInvalidate = false 170 | t.guiContext.Event.Type |= event.Invalidate 171 | } 172 | 173 | if t.guiContext.Event.Type != event.None { 174 | return &t.guiContext 175 | } else { 176 | return nil 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /examples/test/testGui.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/component" 8 | "github.com/spearson78/tinygui/icon" 9 | "github.com/spearson78/tinygui/layout" 10 | ) 11 | 12 | var btn1 component.ButtonState 13 | var btn2 component.ButtonState 14 | var btn3 component.ButtonState 15 | var btn4 component.IconButtonState 16 | var ledOn component.CheckBoxState 17 | var fab1State component.FloatingActionButtonState 18 | var sliderState = component.SliderState{Value: 0.5} 19 | var sliderText = component.TextFieldState{Text: "0"} 20 | var txtCounterState = component.DashCardState{ 21 | Value: "0", 22 | ActionIcon: icon.Add, 23 | ActionLabel: "Reset", 24 | } 25 | var counter = int64(0) 26 | var switchState = component.SwitchState{} 27 | 28 | /* 29 | var fab2State tinygui.FloatingActionButtonState 30 | var radio1State tinygui.RadioButtonState 31 | var radio2State tinygui.RadioButtonState 32 | var radio3State tinygui.RadioButtonState 33 | 34 | */ 35 | //go:noinline 36 | func TestGui(g *tinygui.GuiContext) { 37 | 38 | btnProps := component.NewButtonProps(g) 39 | btnProps.ComponentSize = component.ComponentSize{ 40 | W: 110, 41 | H: 30, 42 | } 43 | 44 | grid := layout.Grid(5, 5, btnProps.ComponentSize.W, btnProps.ComponentSize.H+5, 2) 45 | 46 | btnProps.ComponentPos = grid.NextCell() 47 | btnProps.Label = "ON" 48 | btnProps.Style = component.Contained 49 | btnProps.StartIcon = icon.Add 50 | if component.Button(g, &btn1, &btnProps) { 51 | ledOn.Checked = true 52 | g.UpdateState() 53 | } 54 | 55 | icnProps := component.NewIconButtonProps(g) 56 | icnProps.ComponentPos = grid.EndRow() 57 | icnProps.Icon = icon.Edit 58 | if component.IconButton(g, &btn4, &icnProps) { 59 | ledOn.Checked = false 60 | g.UpdateState() 61 | } 62 | 63 | btnProps.ComponentPos = grid.NextCell() 64 | btnProps.Label = "Off" 65 | btnProps.Style = component.Outlined 66 | btnProps.StartIcon = nil 67 | btnProps.Color = g.Theme.SecondaryColor 68 | if component.Button(g, &btn3, &btnProps) { 69 | ledOn.Checked = false 70 | g.UpdateState() 71 | } 72 | 73 | chkBoxProps := component.NewCheckBoxProps(g) 74 | chkBoxProps.ComponentPos = grid.EndRow() 75 | chkBoxProps.Icon = icon.Checkmark 76 | chkBoxProps.Label = "TOGGLE" 77 | if component.CheckBox(g, &ledOn, &chkBoxProps) { 78 | } 79 | 80 | btnProps.ComponentPos = grid.NextCell() 81 | btnProps.Label = "ON2" 82 | btnProps.Style = component.Text 83 | btnProps.StartIcon = nil 84 | btnProps.EndIcon = icon.Edit 85 | btnProps.Color = g.Theme.SecondaryColor 86 | if component.Button(g, &btn2, &btnProps) { 87 | ledOn.Checked = true 88 | g.UpdateState() 89 | } 90 | 91 | txtProps := component.NewTextFieldProps(g) 92 | txtProps.ComponentPos = grid.EndRow() 93 | txtProps.ComponentSize = btnProps.ComponentSize 94 | if component.TextField(g, &sliderText, &txtProps) { 95 | } 96 | 97 | sldProps := component.NewSliderProps(g) 98 | sldProps.ComponentPos = grid.EndRow() 99 | sldProps.ComponentSize.W = 220 100 | if component.Slider(g, &sliderState, &sldProps) { 101 | sliderText.Text = strconv.FormatFloat(float64(sliderState.Value), 'f', 2, 64) 102 | } 103 | 104 | swtchProps := component.NewSwitchProps(g) 105 | swtchProps.ComponentPos = grid.EndRow() 106 | if component.Switch(g, &switchState, &swtchProps) { 107 | } 108 | 109 | dshProps := component.NewDashCardProps(g) 110 | dshProps.ComponentPos = grid.EndRow() 111 | dshProps.Color = g.Theme.SecondaryColor 112 | dshProps.Label = "Count" 113 | 114 | if component.DashCard(g, &txtCounterState, &dshProps) { 115 | counter = 0 116 | tempStr := strconv.FormatInt(counter, 10) 117 | txtCounterState.Value = tempStr 118 | g.UpdateState() 119 | } 120 | 121 | fltBtnProps := component.NewFloatingActionButtonProps(g) 122 | fltBtnProps.X = 195 123 | fltBtnProps.Y = 235 124 | fltBtnProps.Icon = icon.Edit 125 | if component.FloatingActionButton(g, &fab1State, &fltBtnProps) { 126 | counter++ 127 | tempStr := strconv.FormatInt(counter, 10) 128 | txtCounterState.Value = tempStr 129 | g.UpdateState() 130 | } 131 | 132 | /* 133 | 134 | 135 | 136 | 137 | 138 | if tinygui.RadioButton(g, &radio1State, 139 | tinygui.Pos(10, 105), 140 | tinygui.Label("RADIO 1"), 141 | tinygui.RadioButtonCheckedState(1), 142 | ) { 143 | } 144 | 145 | if tinygui.RadioButton(g, &radio2State, 146 | tinygui.Pos(10, 135), 147 | tinygui.Label("RADIO 2"), 148 | tinygui.Color(g.Theme.SecondaryColor), 149 | tinygui.RadioButtonCheckedState(2), 150 | ) { 151 | } 152 | 153 | if tinygui.RadioButton(g, &radio3State, 154 | tinygui.Pos(10, 165), 155 | tinygui.Label("RADIO 3"), 156 | tinygui.Disabled(true), 157 | tinygui.RadioButtonCheckedState(3), 158 | ) { 159 | } 160 | 161 | 162 | 163 | if tinygui.FloatingActionButton(g, &fab2State, 164 | tinygui.Pos(150, 100), 165 | tinygui.Color(g.Theme.SecondaryColor), 166 | tinygui.Icon(tinygui.Edit), 167 | ) { 168 | ledOn.Checked = !ledOn.Checked 169 | g.UpdateState() 170 | } 171 | 172 | tinygui.DashCard(g, &txtTempDisplayState, 173 | tinygui.Pos(10, 230), 174 | ) 175 | 176 | //machine.LED.Set(ledOn.Checked) 177 | */ 178 | 179 | } 180 | -------------------------------------------------------------------------------- /component/button.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/spearson78/tinygui" 7 | "github.com/spearson78/tinygui/icon" 8 | "github.com/spearson78/tinygui/primitive" 9 | 10 | "tinygo.org/x/tinydraw" 11 | ) 12 | 13 | type ButtonStyle byte 14 | 15 | const ( 16 | Text ButtonStyle = iota 17 | Contained 18 | Outlined 19 | ) 20 | 21 | type ButtonState struct { 22 | animateState int8 23 | } 24 | 25 | type ButtonProps struct { 26 | ComponentPos 27 | ComponentSize 28 | Label string 29 | Style ButtonStyle 30 | Disabled bool 31 | Color color.RGBA 32 | DisableElevation bool 33 | StartIcon icon.Icon 34 | EndIcon icon.Icon 35 | } 36 | 37 | // ATTEMPT 2 38 | 39 | func NewButtonProps(g *tinygui.GuiContext) ButtonProps { 40 | return ButtonProps{ 41 | ComponentPos: ComponentPos{ 42 | X: 10, 43 | Y: 10, 44 | }, 45 | ComponentSize: ComponentSize{ 46 | W: -1, 47 | H: 30, 48 | }, 49 | Label: "Click", 50 | Style: Text, 51 | Disabled: false, 52 | DisableElevation: false, 53 | StartIcon: nil, 54 | EndIcon: nil, 55 | Color: g.Theme.DefaultColor, 56 | } 57 | } 58 | 59 | //go:noinline 60 | func btnText(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps, w int16) color.RGBA { 61 | darkColor := props.Color 62 | //lightColor := g.Theme.Background 63 | if props.Disabled { 64 | darkColor = g.Theme.DefaultColor 65 | //lightColor = color.RGBA{166, 167, 168, 255} 66 | } 67 | 68 | //textColor := lightColor 69 | 70 | textColor := darkColor 71 | 72 | if state.animateState >= 0 { 73 | if state.animateState == 0 { 74 | darkColor = g.Theme.Background 75 | } else { 76 | darkColor = primitive.Desaturate(props.Color, 76) 77 | darkColor = primitive.TransitionColor(darkColor, g.Theme.Background, uint8(state.animateState*25)) 78 | } 79 | 80 | primitive.FilledBox(g.Display, props.X, props.Y, w, props.H-2, darkColor) 81 | 82 | if state.animateState > 0 { 83 | g.InvalidateRect(props.X, props.Y, props.X+w, props.Y+props.H, false) 84 | } 85 | 86 | state.animateState-- 87 | } 88 | 89 | return textColor 90 | 91 | } 92 | 93 | //go:noinline 94 | func btnContained(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps, w int16) color.RGBA { 95 | darkColor := props.Color 96 | lightColor := g.Theme.Background 97 | if props.Disabled { 98 | darkColor = g.Theme.DefaultColor 99 | lightColor = color.RGBA{166, 167, 168, 255} 100 | } 101 | 102 | fillColor := darkColor 103 | 104 | if state.animateState > 0 { 105 | fillColor = primitive.Desaturate(fillColor, 76) 106 | fillColor = primitive.TransitionColor(fillColor, darkColor, uint8(state.animateState*25)) 107 | state.animateState-- 108 | g.InvalidateRect(props.X, props.Y, props.X+w, props.Y+props.H, false) 109 | } 110 | 111 | shadow1 := g.Theme.Shadow1 112 | shadow2 := g.Theme.Shadow2 113 | if !props.DisableElevation && !props.Disabled { 114 | primitive.FilledBoxWithShadow(g.Display, props.X, props.Y, w, props.H, fillColor, shadow1, shadow2) 115 | } else { 116 | primitive.FilledBox(g.Display, props.X, props.Y, w, props.H-2, fillColor) 117 | } 118 | 119 | return lightColor 120 | } 121 | 122 | //go:noinline 123 | func btnOutlined(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps, w int16) color.RGBA { 124 | 125 | darkColor := props.Color 126 | if props.Disabled { 127 | darkColor = g.Theme.DefaultColor 128 | } 129 | 130 | if state.animateState > 0 { 131 | fillColor := darkColor 132 | if state.animateState == 1 { 133 | fillColor = g.Theme.Background 134 | } else { 135 | 136 | fillColor = primitive.Desaturate(fillColor, 76) 137 | fillColor = primitive.TransitionColor(fillColor, g.Theme.Background, uint8(state.animateState*25)) 138 | } 139 | 140 | tinydraw.FilledRectangleEx(g.Display, int16(props.X+1), int16(props.Y+1), int16(w-1), int16(props.H-3), fillColor) 141 | 142 | g.InvalidateRect(props.X, props.Y, props.X+w, props.Y+props.H, false) 143 | 144 | state.animateState-- 145 | } 146 | 147 | //-2 for the shadow on contained buttons 148 | primitive.OutlineBox(g.Display, props.X, props.Y, w, props.H-2, darkColor) 149 | 150 | return darkColor 151 | 152 | } 153 | 154 | //go:noinline 155 | func btnLabel(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps, textAndIconWidth int16, textColor color.RGBA, centreY int16, w int16) int16 { 156 | 157 | if props.StartIcon != nil { 158 | textAndIconWidth += 20 159 | } 160 | 161 | if props.EndIcon != nil { 162 | textAndIconWidth += 20 163 | } 164 | 165 | textStartXOffset := (w / 2) - (textAndIconWidth / 2) 166 | 167 | if props.StartIcon != nil { 168 | textStartXOffset += 20 169 | } 170 | 171 | if props.Label != "" { 172 | primitive.WriteLine(g.Display, g.Theme.Font, props.X+textStartXOffset, props.Y+(centreY+5), props.Label, textColor) 173 | } 174 | 175 | return textStartXOffset 176 | } 177 | 178 | //go:noinline 179 | func btnIcons(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps, textStartXOffset int16, ow int16, textColor color.RGBA, centreY int16) { 180 | 181 | if props.StartIcon != nil { 182 | props.StartIcon(g, props.X+textStartXOffset-17, props.Y+(centreY-9), 15, textColor) 183 | } 184 | 185 | if props.EndIcon != nil { 186 | props.EndIcon(g, props.X+textStartXOffset+int16(ow)+2, props.Y+(centreY-9), 15, textColor) 187 | } 188 | 189 | } 190 | 191 | //go:noinline 192 | func Button(g *tinygui.GuiContext, state *ButtonState, props *ButtonProps) bool { 193 | 194 | ow := uint32(0) 195 | if props.Label != "" { 196 | _, ow = primitive.LineWidth(g.Theme.Font, props.Label) 197 | } 198 | 199 | w := props.W 200 | 201 | if w == -1 { 202 | w = int16(ow) + 10 203 | if props.StartIcon != nil { 204 | w += 20 205 | } 206 | if props.EndIcon != nil { 207 | w += 20 208 | } 209 | } 210 | 211 | clicked := false 212 | 213 | eventAction, _, _ := HandleEvent(&g.Event, props.Disabled, false, true, props.X, props.Y, w, props.H) 214 | 215 | if eventAction == Ignore { 216 | return false 217 | } 218 | if (eventAction & Click) != 0 { 219 | state.animateState = 10 220 | clicked = true 221 | g.UpdateState() 222 | } 223 | textColor := g.Theme.Text 224 | 225 | switch props.Style { 226 | case Text: 227 | textColor = btnText(g, state, props, w) 228 | case Contained: 229 | textColor = btnContained(g, state, props, w) 230 | 231 | case Outlined: 232 | textColor = btnOutlined(g, state, props, w) 233 | } 234 | 235 | centreY := (props.H / 2) 236 | textStartXOffset := btnLabel(g, state, props, int16(ow), textColor, centreY, w) 237 | btnIcons(g, state, props, textStartXOffset, int16(ow), textColor, centreY) 238 | 239 | return clicked 240 | 241 | } 242 | -------------------------------------------------------------------------------- /examples/calc/calcGui.go: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "github.com/spearson78/tinygui" 6 | "github.com/spearson78/tinygui/component" 7 | "github.com/spearson78/tinygui/icon" 8 | "github.com/spearson78/tinygui/layout" 9 | ) 10 | 11 | var btnSeven component.ButtonState 12 | var btnEignt component.ButtonState 13 | var btnNine component.ButtonState 14 | 15 | var btnFour component.ButtonState 16 | var btnFive component.ButtonState 17 | var btnSix component.ButtonState 18 | 19 | var btnOne component.ButtonState 20 | var btnTwo component.ButtonState 21 | var btnThree component.ButtonState 22 | 23 | var btnZero component.ButtonState 24 | var btnDot component.ButtonState 25 | var btnNegate component.ButtonState 26 | 27 | var btnDiv component.ButtonState 28 | var btnMul component.ButtonState 29 | var btnSub component.ButtonState 30 | var btnAdd component.ButtonState 31 | var btnEquals component.ButtonState 32 | 33 | var btnClear component.ButtonState 34 | var btnDel component.ButtonState 35 | 36 | var txtResultDisplay component.TextFieldState 37 | 38 | var calcShift = int32(1) 39 | var valShift = int32(0) 40 | var accumulator = decimal.Decimal{} 41 | var calcValue = decimal.Decimal{} 42 | 43 | type Operator byte 44 | 45 | const ( 46 | Equals Operator = iota 47 | Div 48 | Mul 49 | Sub 50 | Add 51 | ) 52 | 53 | var op Operator 54 | 55 | func applyLastOperator(newOp Operator) { 56 | switch op { 57 | case Equals: 58 | accumulator = calcValue 59 | case Div: 60 | accumulator = accumulator.Div(calcValue) 61 | case Mul: 62 | accumulator = accumulator.Mul(calcValue) 63 | case Sub: 64 | accumulator = accumulator.Sub(calcValue) 65 | case Add: 66 | accumulator = accumulator.Add(calcValue) 67 | } 68 | calcValue = decimal.Decimal{} 69 | calcShift = int32(1) 70 | valShift = int32(0) 71 | op = newOp 72 | } 73 | 74 | func doNumericButton(val int32) { 75 | calcValue = calcValue.Shift(calcShift) 76 | calcValue = calcValue.Add(decimal.NewFromInt32(val).Shift(valShift)) 77 | if valShift != 0 { 78 | valShift-- 79 | } 80 | } 81 | 82 | //go:noinline 83 | func calcRow1(g *tinygui.GuiContext, btnGrid *layout.GridLayout, size component.ComponentSize) { 84 | 85 | btnProps := component.NewButtonProps(g) 86 | btnProps.ComponentSize = size 87 | btnProps.Style = component.Text 88 | 89 | btnProps.ComponentPos = btnGrid.NextCell() 90 | btnProps.Label = "7" 91 | if component.Button(g, &btnSeven, &btnProps) { 92 | doNumericButton(7) 93 | } 94 | 95 | btnProps.ComponentPos = btnGrid.NextCell() 96 | btnProps.Label = "8" 97 | if component.Button(g, &btnEignt, &btnProps) { 98 | doNumericButton(8) 99 | } 100 | 101 | btnProps.ComponentPos = btnGrid.NextCell() 102 | btnProps.Label = "9" 103 | if component.Button(g, &btnNine, &btnProps) { 104 | doNumericButton(9) 105 | } 106 | 107 | btnProps.ComponentPos = btnGrid.NextCell() 108 | btnProps.Label = "/" 109 | btnProps.Color = g.Theme.Text 110 | if component.Button(g, &btnDiv, &btnProps) { 111 | applyLastOperator(Div) 112 | } 113 | 114 | btnProps.ComponentPos = btnGrid.EndRow() 115 | btnProps.Style = component.Contained 116 | btnProps.Label = "C" 117 | btnProps.Color = g.Theme.ErrorColor 118 | btnProps.DisableElevation = true 119 | if component.Button(g, &btnClear, &btnProps) { 120 | op = Equals 121 | accumulator = decimal.Decimal{} 122 | calcValue = decimal.Decimal{} 123 | calcShift = int32(1) 124 | valShift = int32(0) 125 | } 126 | } 127 | 128 | //go:noinline 129 | func calcRow2(g *tinygui.GuiContext, btnGrid *layout.GridLayout, size component.ComponentSize) { 130 | 131 | btnProps := component.NewButtonProps(g) 132 | btnProps.ComponentSize = size 133 | btnProps.Style = component.Text 134 | 135 | btnProps.ComponentPos = btnGrid.NextCell() 136 | btnProps.Label = "4" 137 | if component.Button(g, &btnFour, &btnProps) { 138 | doNumericButton(4) 139 | } 140 | 141 | btnProps.ComponentPos = btnGrid.NextCell() 142 | btnProps.Label = "5" 143 | if component.Button(g, &btnFive, &btnProps) { 144 | doNumericButton(5) 145 | } 146 | 147 | btnProps.ComponentPos = btnGrid.NextCell() 148 | btnProps.Label = "6" 149 | if component.Button(g, &btnSix, &btnProps) { 150 | doNumericButton(6) 151 | } 152 | 153 | btnProps.ComponentPos = btnGrid.NextCell() 154 | btnProps.Label = "X" 155 | btnProps.Color = g.Theme.Text 156 | if component.Button(g, &btnMul, &btnProps) { 157 | applyLastOperator(Mul) 158 | } 159 | 160 | btnProps.ComponentPos = btnGrid.EndRow() 161 | btnProps.Label = "" 162 | btnProps.Color = g.Theme.ErrorColor 163 | btnProps.Style = component.Contained 164 | btnProps.StartIcon = icon.Backspace 165 | btnProps.DisableElevation = true 166 | if component.Button(g, &btnDel, &btnProps) { 167 | if valShift != 0 { 168 | valShift++ 169 | calcValue = calcValue.Truncate(-(valShift + 1)) 170 | if valShift == -1 { 171 | calcShift = int32(1) 172 | valShift = int32(0) 173 | } 174 | } else { 175 | calcValue = calcValue.Shift(-1).Truncate(0) 176 | } 177 | } 178 | } 179 | 180 | //go:noinline 181 | func calcRow3(g *tinygui.GuiContext, btnGrid *layout.GridLayout, size component.ComponentSize) { 182 | 183 | btnProps := component.NewButtonProps(g) 184 | btnProps.ComponentSize = size 185 | btnProps.Style = component.Text 186 | 187 | btnProps.ComponentPos = btnGrid.NextCell() 188 | btnProps.Label = "1" 189 | if component.Button(g, &btnOne, &btnProps) { 190 | doNumericButton(1) 191 | } 192 | 193 | btnProps.ComponentPos = btnGrid.NextCell() 194 | btnProps.Label = "2" 195 | if component.Button(g, &btnTwo, &btnProps) { 196 | doNumericButton(2) 197 | } 198 | 199 | btnProps.ComponentPos = btnGrid.NextCell() 200 | btnProps.Label = "3" 201 | if component.Button(g, &btnThree, &btnProps) { 202 | doNumericButton(3) 203 | } 204 | 205 | btnProps.ComponentPos = btnGrid.NextCell() 206 | btnProps.Label = "-" 207 | btnProps.Color = g.Theme.Text 208 | if component.Button(g, &btnSub, &btnProps) { 209 | applyLastOperator(Sub) 210 | } 211 | 212 | btnProps.ComponentPos = btnGrid.EndRow() 213 | btnProps.ComponentSize.H = btnProps.ComponentSize.H * 2 214 | btnProps.Label = "=" 215 | btnProps.Color = g.Theme.DefaultColor 216 | btnProps.Style = component.Contained 217 | btnProps.DisableElevation = true 218 | if component.Button(g, &btnEquals, &btnProps) { 219 | applyLastOperator(Equals) 220 | } 221 | } 222 | 223 | //go:noinline 224 | func calcRow4(g *tinygui.GuiContext, btnGrid *layout.GridLayout, size component.ComponentSize) { 225 | 226 | btnProps := component.NewButtonProps(g) 227 | btnProps.ComponentSize = size 228 | btnProps.Style = component.Text 229 | 230 | btnProps.ComponentPos = btnGrid.NextCell() 231 | btnProps.Label = "+/-" 232 | btnProps.Color = g.Theme.Text 233 | if component.Button(g, &btnNegate, &btnProps) { 234 | if calcValue.Equal(decimal.Zero) { 235 | accumulator = accumulator.Neg() 236 | } else { 237 | calcValue = calcValue.Neg() 238 | } 239 | } 240 | 241 | btnProps.ComponentPos = btnGrid.NextCell() 242 | btnProps.Label = "0" 243 | btnProps.Color = g.Theme.DefaultColor 244 | if component.Button(g, &btnZero, &btnProps) { 245 | doNumericButton(0) 246 | } 247 | 248 | btnProps.ComponentPos = btnGrid.NextCell() 249 | btnProps.Label = "." 250 | btnProps.Color = g.Theme.Text 251 | if component.Button(g, &btnDot, &btnProps) { 252 | if valShift == 0 { 253 | calcShift = 0 254 | valShift = -1 255 | } 256 | } 257 | 258 | btnProps.ComponentPos = btnGrid.NextCell() 259 | btnProps.Label = "+" 260 | if component.Button(g, &btnAdd, &btnProps) { 261 | applyLastOperator(Add) 262 | } 263 | } 264 | 265 | //go:noinline 266 | func calcDisplay(g *tinygui.GuiContext) { 267 | if calcValue.Equal(decimal.Zero) { 268 | txtResultDisplay.Text = accumulator.String() 269 | } else { 270 | txtResultDisplay.Text = calcValue.String() 271 | } 272 | 273 | txtProps := component.NewTextFieldProps(g) 274 | txtProps.X = 5 275 | txtProps.Y = 5 276 | txtProps.W = 230 277 | component.TextField(g, &txtResultDisplay, &txtProps) 278 | } 279 | 280 | //go:noinline 281 | func CalcGui(g *tinygui.GuiContext) { 282 | 283 | btnSize := component.ComponentSize{ 284 | W: 46, 285 | H: 61, 286 | } 287 | 288 | btnGrid := layout.Grid(0, 35, btnSize.W, btnSize.H, 1) 289 | 290 | calcRow1(g, &btnGrid, btnSize) 291 | calcRow2(g, &btnGrid, btnSize) 292 | calcRow3(g, &btnGrid, btnSize) 293 | calcRow4(g, &btnGrid, btnSize) 294 | calcDisplay(g) 295 | } 296 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | fyne.io/fyne/v2 v2.1.1/go.mod h1:c1vwI38Ebd0dAdxVa6H1Pj6/+cK1xtDy61+I31g+s14= 2 | fyne.io/fyne/v2 v2.1.2 h1:avp9CvLAUdvE7fDMtH1tVKyjxEWHWcpow6aI6L7Kvvw= 3 | fyne.io/fyne/v2 v2.1.2/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 6 | github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= 7 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 8 | github.com/bgould/http v0.0.0-20190627042742-d268792bdee7/go.mod h1:BTqvVegvwifopl4KTEDth6Zezs9eR+lCWhvGKvkxJHE= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 14 | github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= 15 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= 16 | github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= 17 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 18 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 19 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 20 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 21 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU= 22 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= 23 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= 24 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= 25 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 26 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be h1:Z28GdQBfKOL8tNHjvaDn3wHDO7AzTRkmAXvHvnopp98= 27 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 28 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c= 29 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 30 | github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 31 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 32 | github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= 33 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 34 | github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= 35 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 36 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= 37 | github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= 38 | github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c h1:JGCm/+tJ9gC6THUxooTldS+CUDsba0qvkvU3DHklqW8= 39 | github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= 40 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= 42 | github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= 43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 45 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 46 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 47 | github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc= 48 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 50 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 51 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 56 | github.com/sago35/tinydisplay v0.0.0-20211221140237-b980dfd11c01 h1:VkQxJNtY5YZ8Vz323OJJzVYcIgHV3vU9NnEAlbevdJQ= 57 | github.com/sago35/tinydisplay v0.0.0-20211221140237-b980dfd11c01/go.mod h1:Jn5GaTExb2r06xEPbM1GOVsbMj0n+79g8csEDgrNgDM= 58 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 59 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 60 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 61 | github.com/spearson78/drivers v0.18.1-0.20220201074301-10484ed1d809 h1:WjwRtVGEaYbFOZboRTYvPwrjI2y5EaPx+cqp4x8sBIo= 62 | github.com/spearson78/drivers v0.18.1-0.20220201074301-10484ed1d809/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= 63 | github.com/spearson78/tinyamifont v0.0.0-20220201082506-452b1c82d8bd h1:O5s3aNtwinEaoCk59r3QnH90sjL6FL0fjNNdqzS5fSA= 64 | github.com/spearson78/tinyamifont v0.0.0-20220201082506-452b1c82d8bd/go.mod h1:lX0cK+okiwTatmdAzKa56EUdfOeZA/a6NNcfCkF06EQ= 65 | github.com/spearson78/tinydraw v0.0.0-20220201143000-b08d64c952ed h1:DsJ/a5+E8Kcze6Dsi+ytaU5SkC3NkM33XgEpI5arDT0= 66 | github.com/spearson78/tinydraw v0.0.0-20220201143000-b08d64c952ed/go.mod h1:fRXPiOiG19gTp5ws29fvhGEhSYkqrV1+Hbo+0FYrrMo= 67 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 68 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 69 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= 70 | github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= 71 | github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 h1:XPYXKIuH/n5zpUoEWk2jWV/SjEMNYmqDYmTgbjmhtaI= 72 | github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= 73 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= 74 | github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= 75 | github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 h1:YR16ysw3I1bqwtEcYV9dpvhHEe7j55hIClkLoAqY31I= 76 | github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 79 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 80 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 81 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 84 | github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 85 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 86 | github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E= 87 | github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 88 | github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= 89 | github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 92 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= 93 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 94 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= 95 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 96 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 99 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 100 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= 101 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 102 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 103 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 104 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 115 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= 117 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 119 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 120 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 121 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 122 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 124 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 125 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 127 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 128 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 134 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 135 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 136 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 138 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 140 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 141 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 142 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 143 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 144 | tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 145 | tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 146 | tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= 147 | tinygo.org/x/drivers v0.17.1/go.mod h1:+uFfVgSjxRPqsnalFrcQse/Tmhoxwl9AJmJIVuRbuRo= 148 | tinygo.org/x/drivers v0.18.0 h1:cJ1G84Dc1Id1a5VTZwhWYZ42EMIwf0Qy9JykExJKz3Q= 149 | tinygo.org/x/drivers v0.18.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= 150 | tinygo.org/x/drivers v0.19.0 h1:x/TIC8SFWeGViJvcYO1ATjgwSNF7hHN2ouAyjMUXI2Q= 151 | tinygo.org/x/drivers v0.19.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= 152 | tinygo.org/x/tinydraw v0.0.0-20200416172542-c30d6d84353c/go.mod h1:ygmD8mKwhhF6HLXIs4FCe5JTYurTD2w32cmymeaYrEw= 153 | tinygo.org/x/tinydraw v0.0.0-20220125063109-43cae6615eb5 h1:f7tLkrlmTpbuA/KS3r2i3E+CpfpzCwGuULe/uOlQMu0= 154 | tinygo.org/x/tinydraw v0.0.0-20220125063109-43cae6615eb5/go.mod h1:aZT1S4Bf6Hj+B5OPIs28Hk8TxXZBiWBnh9Z64PeB4DE= 155 | tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM= 156 | tinygo.org/x/tinyfs v0.1.0/go.mod h1:ysc8Y92iHfhTXeyEM9+c7zviUQ4fN9UCFgSOFfMWv20= 157 | tinygo.org/x/tinyterm v0.1.0/go.mod h1:/DDhNnGwNF2/tNgHywvyZuCGnbH3ov49Z/6e8LPLRR4= 158 | --------------------------------------------------------------------------------