From d463bb90ad629dfc8f1cd4f4b6b9590b0008832d Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Thu, 9 May 2024 17:32:54 -0400 Subject: tree widget --- gui/layout/split.go | 16 ++++++++- gui/text/concurrent_face.go | 51 ++++++++++++++++++++++++++++ gui/text/text.go | 78 +++++++++++++++++++++++++++++++++++++++++++ gui/widget/concurrent_face.go | 51 ---------------------------- gui/widget/text.go | 78 ------------------------------------------- gui/widget/widget.go | 73 ++++++++++++++++++++++++++++++++++++---- 6 files changed, 211 insertions(+), 136 deletions(-) create mode 100644 gui/text/concurrent_face.go create mode 100644 gui/text/text.go delete mode 100644 gui/widget/concurrent_face.go delete mode 100644 gui/widget/text.go (limited to 'gui') diff --git a/gui/layout/split.go b/gui/layout/split.go index db04225..455d74b 100644 --- a/gui/layout/split.go +++ b/gui/layout/split.go @@ -1,6 +1,10 @@ package layout -import "fmt" +import ( + "fmt" + + "volute/gui/text" +) // SplitFunc represents a way to split a space among a number of elements. // The length of the returned slice must be equal to the number of elements. @@ -24,3 +28,13 @@ func EvenSplit(elements int, width int) []int { } return ret } + +func TextRowSplit(elements int, space int) []int { + bounds := make([]int, elements) + height := text.Size("1").Y + for i := 0; i < elements && space > 0; i++ { + bounds[i] = min(height, space) + space -= bounds[i] + } + return bounds +} diff --git a/gui/text/concurrent_face.go b/gui/text/concurrent_face.go new file mode 100644 index 0000000..c865251 --- /dev/null +++ b/gui/text/concurrent_face.go @@ -0,0 +1,51 @@ +package text + +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/text/text.go b/gui/text/text.go new file mode 100644 index 0000000..dfe60e1 --- /dev/null +++ b/gui/text/text.go @@ -0,0 +1,78 @@ +package text + +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 Size(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 Draw(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/concurrent_face.go b/gui/widget/concurrent_face.go deleted file mode 100644 index 98db572..0000000 --- a/gui/widget/concurrent_face.go +++ /dev/null @@ -1,51 +0,0 @@ -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/text.go b/gui/widget/text.go deleted file mode 100644 index 1b40096..0000000 --- a/gui/widget/text.go +++ /dev/null @@ -1,78 +0,0 @@ -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 index 2e7c671..3fd9637 100644 --- a/gui/widget/widget.go +++ b/gui/widget/widget.go @@ -11,6 +11,8 @@ import ( "image/draw" "volute/gui" + "volute/gui/layout" + "volute/gui/text" "volute/gui/win" ) @@ -23,12 +25,71 @@ var ( interpolator = xdraw.ApproxBiLinear ) -func Label(text string, r image.Rectangle, env gui.Env, wg *sync.WaitGroup) { +type Node[T any] struct { + Label string + Value T + Children []Node[T] + + expanded bool +} + +func Tree[T any](trees []Node[T], r image.Rectangle, focus FocusSlave, mux *gui.Mux, wg *sync.WaitGroup) { + defer wg.Done() + + var nodes []string + for _, root := range trees { + nodes = append(nodes, flatten(root, 0)...) + } + + bounds := layout.Grid{ + Rows: populate(make([]int, len(nodes)), 1), + Background: color.Gray{255}, + Gap: 1, + Split: layout.EvenSplit, + SplitRows: layout.TextRowSplit, + Margin: 0, + Border: 0, + BorderColor: color.Gray{16}, + Flip: false, + }.Lay(r) + for i := range nodes { + wg.Add(1) + go Label(nodes[i], bounds[i], mux.MakeEnv(), wg) + } + + /* + globalFocus := focus; + localFocus := NewFocusMaster([]int{1, 1, 1}); + defer localFocus.Close() + */ + // TODO +} + +func flatten[T any](root Node[T], depth int) []string { + indent := string(populate(make([]byte, 2*depth), ' ')) + nodes := []string{indent + root.Label} + root.expanded = true // TODO: remove me + if root.expanded { + for _, c := range root.Children { + nodes = append(nodes, flatten(c, depth+1)...) + } + } + return nodes +} + +func populate[T any](arr []T, v T) []T { + for i := range arr { + arr[i] = v + } + return arr +} + +func Label(str 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) + text.Draw([]byte(str), drw, r, BLACK, WHITE) return r } @@ -93,12 +154,12 @@ Loop: } } -func inputDraw(text []byte, focused bool, r image.Rectangle) func(draw.Image) image.Rectangle { +func inputDraw(str []byte, focused bool, r image.Rectangle) func(draw.Image) image.Rectangle { return func(drw draw.Image) image.Rectangle { if focused { - drawText(text, drw, r, GREEN, FOCUS_COLOR) + text.Draw(str, drw, r, GREEN, FOCUS_COLOR) } else { - drawText(text, drw, r, GREEN, WHITE) + text.Draw(str, drw, r, GREEN, WHITE) } return r } @@ -128,7 +189,7 @@ Loop: func outputDraw(v float64, r image.Rectangle) func(draw.Image) image.Rectangle { return func(drw draw.Image) image.Rectangle { - drawText([]byte(fmt.Sprintf("%.3f", v)), drw, r, BLACK, WHITE) + text.Draw([]byte(fmt.Sprintf("%.3f", v)), drw, r, BLACK, WHITE) return r } } -- cgit v1.2.3