aboutsummaryrefslogtreecommitdiffstats
path: root/gui/widget
diff options
context:
space:
mode:
Diffstat (limited to 'gui/widget')
-rw-r--r--gui/widget/concurrent_face.go51
-rw-r--r--gui/widget/focus.go70
-rw-r--r--gui/widget/text.go78
-rw-r--r--gui/widget/widget.go170
4 files changed, 369 insertions, 0 deletions
diff --git a/gui/widget/concurrent_face.go b/gui/widget/concurrent_face.go
new file mode 100644
index 0000000..98db572
--- /dev/null
+++ b/gui/widget/concurrent_face.go
@@ -0,0 +1,51 @@
+package widget
+
+import (
+ "image"
+ "sync"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/math/fixed"
+)
+
+type concurrentFace struct {
+ mu sync.Mutex
+ face font.Face
+}
+
+func (cf *concurrentFace) Close() error {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Close()
+}
+
+func (cf *concurrentFace) Glyph(dot fixed.Point26_6, r rune) (
+ dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Glyph(dot, r)
+}
+
+func (cf *concurrentFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.GlyphBounds(r)
+}
+
+func (cf *concurrentFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.GlyphAdvance(r)
+}
+
+func (cf *concurrentFace) Kern(r0, r1 rune) fixed.Int26_6 {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Kern(r0, r1)
+}
+
+func (cf *concurrentFace) Metrics() font.Metrics {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Metrics()
+}
diff --git a/gui/widget/focus.go b/gui/widget/focus.go
new file mode 100644
index 0000000..e2d1074
--- /dev/null
+++ b/gui/widget/focus.go
@@ -0,0 +1,70 @@
+package widget
+
+import "image"
+
+// Focus keeps track of the currently selected widget.
+// A widget receives true when it gains focus and false when it loses focus.
+type Focus struct {
+ Widgets [][]chan bool
+ p image.Point // coordinates of currently focused widget
+}
+
+func NewFocus(rows []int) Focus {
+ f := Focus{
+ make([][]chan bool, len(rows)),
+ image.Point{},
+ }
+ for i := range f.Widgets {
+ f.Widgets[i] = make([]chan bool, rows[i])
+ for j := range f.Widgets[i] {
+ f.Widgets[i][j] = make(chan bool)
+ }
+ }
+ return f
+}
+
+func (f *Focus) Close() {
+ for i := range f.Widgets {
+ for j := range f.Widgets[i] {
+ close(f.Widgets[i][j])
+ }
+ }
+}
+
+func (f *Focus) Focus(focus bool) {
+ f.Widgets[f.p.Y][f.p.X] <- focus
+}
+
+func (f *Focus) Left() {
+ f.Focus(false)
+ if f.p.X <= 0 {
+ f.p.X = len(f.Widgets[f.p.Y]) - 1
+ } else {
+ f.p.X--
+ }
+ f.Focus(true)
+}
+
+func (f *Focus) Right() {
+ f.Focus(false)
+ f.p.X = (f.p.X + 1) % len(f.Widgets[f.p.Y])
+ f.Focus(true)
+}
+
+func (f *Focus) Up() {
+ f.Focus(false)
+ if f.p.Y <= 0 {
+ f.p.Y = len(f.Widgets) - 1
+ } else {
+ f.p.Y--
+ }
+ f.p.X = min(f.p.X, len(f.Widgets[f.p.Y])-1)
+ f.Focus(true)
+}
+
+func (f *Focus) Down() {
+ f.Focus(false)
+ f.p.Y = (f.p.Y + 1) % len(f.Widgets)
+ f.p.X = min(f.p.X, len(f.Widgets[f.p.Y])-1)
+ f.Focus(true)
+}
diff --git a/gui/widget/text.go b/gui/widget/text.go
new file mode 100644
index 0000000..1b40096
--- /dev/null
+++ b/gui/widget/text.go
@@ -0,0 +1,78 @@
+package widget
+
+import (
+ "log"
+ "sync"
+
+ "image"
+ "image/color"
+ "image/draw"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/opentype"
+ "golang.org/x/image/math/fixed"
+)
+
+var (
+ FONT = goregular.TTF
+ FONT_SIZE = 15
+ DPI = 72
+ PAD = 3
+)
+
+var face *concurrentFace
+
+func init() {
+ fnt, err := opentype.Parse(FONT)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fce, err := opentype.NewFace(fnt, &opentype.FaceOptions{
+ Size: float64(FONT_SIZE),
+ DPI: float64(DPI),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ face = &concurrentFace{sync.Mutex{}, fce}
+}
+
+func TextSize(text string) image.Point {
+ bounds := textBounds([]byte(text), font.Drawer{Face: face})
+ return image.Point{bounds.Max.X - bounds.Min.X + 2*PAD, bounds.Max.Y - bounds.Min.Y + 2*PAD}
+}
+
+func drawText(text []byte, dst draw.Image, r image.Rectangle, fg, bg color.Color) {
+ drawer := font.Drawer{
+ Src: &image.Uniform{fg},
+ Face: face,
+ Dot: fixed.P(0, 0),
+ }
+
+ // background
+ draw.Draw(dst, r, &image.Uniform{bg}, image.ZP, draw.Src)
+
+ // text image
+ bounds := textBounds(text, drawer)
+ textImg := image.NewRGBA(bounds)
+ draw.Draw(textImg, bounds, &image.Uniform{bg}, image.ZP, draw.Src)
+ drawer.Dst = textImg
+ drawer.DrawBytes(text)
+
+ // draw text image over background
+ leftCentre := image.Pt(bounds.Min.X, (bounds.Min.Y+bounds.Max.Y)/2)
+ target := image.Pt(r.Max.X-bounds.Max.X-PAD, (r.Min.Y+r.Max.Y)/2)
+ delta := target.Sub(leftCentre)
+ draw.Draw(dst, bounds.Add(delta).Intersect(r), drawer.Dst, bounds.Min, draw.Src)
+}
+
+func textBounds(text []byte, drawer font.Drawer) image.Rectangle {
+ b, _ := drawer.BoundBytes(text)
+ return image.Rect(
+ b.Min.X.Floor(),
+ b.Min.Y.Floor(),
+ b.Max.X.Ceil(),
+ b.Max.Y.Ceil(),
+ )
+}
diff --git a/gui/widget/widget.go b/gui/widget/widget.go
new file mode 100644
index 0000000..ae09160
--- /dev/null
+++ b/gui/widget/widget.go
@@ -0,0 +1,170 @@
+package widget
+
+import (
+ "cmp"
+ "fmt"
+ "sync"
+
+ xdraw "golang.org/x/image/draw"
+ "image"
+ "image/color"
+ "image/draw"
+
+ "volute/gui"
+ "volute/gui/win"
+)
+
+var (
+ FOCUS_COLOR = color.RGBA{179, 217, 255, 255}
+ GREEN = color.RGBA{51, 102, 0, 255}
+ BLACK = color.Gray{0}
+ WHITE = color.Gray{255}
+)
+
+func Label(text string, r image.Rectangle, env gui.Env, wg *sync.WaitGroup) {
+ defer wg.Done()
+ defer close(env.Draw())
+
+ redraw := func(drw draw.Image) image.Rectangle {
+ drawText([]byte(text), drw, r, BLACK, WHITE)
+ return r
+ }
+
+ env.Draw() <- redraw
+ for event := range env.Events() {
+ switch event := event.(type) {
+ case win.WiFocus:
+ if event.Focused {
+ env.Draw() <- redraw
+ }
+ }
+ }
+}
+
+func Input(val chan<- uint, r image.Rectangle, focusChan <-chan bool, env gui.Env, wg *sync.WaitGroup) {
+ defer wg.Done()
+ defer close(env.Draw())
+ defer close(val)
+
+ redraw := func(text []byte, focus bool) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ if focus {
+ drawText(text, drw, r, GREEN, FOCUS_COLOR)
+ } else {
+ drawText(text, drw, r, GREEN, WHITE)
+ }
+ return r
+ }
+ }
+ text := []byte{'0'}
+ focus := false
+
+ env.Draw() <- redraw(text, focus)
+Loop:
+ for {
+ select {
+ case focus = <-focusChan:
+ env.Draw() <- redraw(text, focus)
+ case event, ok := <-env.Events():
+ if !ok { // channel closed
+ break Loop
+ }
+ switch event := event.(type) {
+ case win.WiFocus:
+ if event.Focused {
+ env.Draw() <- redraw(text, focus)
+ }
+ case win.KbType:
+ if focus && isDigit(event.Rune) {
+ text = fmt.Appendf(text, "%c", event.Rune)
+ env.Draw() <- redraw(text, focus)
+ val <- atoi(text)
+ }
+ case win.KbDown:
+ if focus && event.Key == win.KeyBackspace && len(text) > 0 {
+ text = text[:len(text)-1]
+ env.Draw() <- redraw(text, focus)
+ val <- atoi(text)
+ }
+ }
+ }
+ }
+}
+
+func Output(val <-chan float64, r image.Rectangle, env gui.Env, wg *sync.WaitGroup) {
+ defer wg.Done()
+ defer close(env.Draw())
+
+ redraw := func(v float64) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ drawText([]byte(fmt.Sprintf("%.3f", v)), drw, r, BLACK, WHITE)
+ return r
+ }
+ }
+ var v float64 = 0.0
+
+ env.Draw() <- redraw(v)
+Loop:
+ for {
+ select {
+ case v = <-val:
+ env.Draw() <- redraw(v)
+ case event, ok := <-env.Events():
+ if !ok { // channel closed
+ break Loop
+ }
+ if event, ok := event.(win.WiFocus); ok && event.Focused {
+ env.Draw() <- redraw(v)
+ }
+ }
+ }
+}
+
+func Image(imChan <-chan image.Image, r image.Rectangle, env gui.Env, wg *sync.WaitGroup) {
+ defer wg.Done()
+ defer close(env.Draw())
+
+ interp := xdraw.ApproxBiLinear
+ redraw := func(im image.Image) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ interp.Scale(drw, r, im, im.Bounds(), draw.Src, nil)
+ return r
+ }
+ }
+ var im image.Image = image.NewGray(r)
+
+ for {
+ select {
+ case im = <-imChan:
+ env.Draw() <- redraw(im)
+ case event, ok := <-env.Events():
+ if !ok {
+ return
+ }
+ if event, ok := event.(win.WiFocus); ok && event.Focused {
+ env.Draw() <- redraw(im)
+ }
+ }
+ }
+}
+
+func isDigit(r rune) bool {
+ return '0' <= r && r <= '9'
+}
+
+func contains[T cmp.Ordered](slc []T, v T) bool {
+ for i := range slc {
+ if slc[i] == v {
+ return true
+ }
+ }
+ return false
+}
+
+func atoi(s []byte) uint {
+ var n uint = 0
+ for _, d := range s {
+ n = n*10 + uint(d-'0')
+ }
+ return n
+}