ref: 84a10235c84a8b3c8039d6e5d24c5d172acafd6d
author: glenda <glenda@9ouveau>
date: Sun May 11 09:49:44 EDT 2025
initial commit
--- /dev/null
+++ b/README
@@ -1,0 +1,6 @@
+# xui (very wip)
+
+Simple ui that is heavily inspired by duit.
+
+Not usable yet though and the API isn't stable either.
+
--- /dev/null
+++ b/anim/anim.go
@@ -1,0 +1,85 @@
+package anim
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "time"
+ "xui"
+ "xui/events"
+ "xui/internal/color"
+ "xui/layout"
+ "xui/space"
+)
+
+type Interface interface {
+}
+
+type Anim struct {
+ Orig image.Point
+ x xui.Interface
+ images []*memdraw.Image
+ current *memdraw.Image
+ delay []int
+ delaySum []int
+ Margin space.Sp
+}
+
+func NewFromSeq(x xui.Interface, images []*memdraw.Image, delay []int) *Anim {
+ a := &Anim{
+ x: x,
+ images: images,
+ delay: delay,
+ }
+ a.delaySum = make([]int, len(a.delay))
+ for i := 0; i < len(a.delay); i++ {
+ a.delaySum[i] = a.delay[i]
+ if i > 0 {
+ a.delaySum[i] += a.delaySum[i-1]
+ }
+ }
+
+ var err error
+ a.current, err = memdraw.AllocImage(a.images[0].R, draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(a.current, draw.Transparent)
+
+ return a
+}
+
+func (a Anim) CurrentFrame(unixNano int64) int {
+ m := a.delaySum[len(a.delay)-1]
+ aTime := int((unixNano/1e7) % int64(m))
+ for i, dc := range a.delaySum {
+ if aTime <= dc {
+ return i
+ }
+ }
+ return 0
+}
+
+func (a Anim) Event(events.Interface) {
+}
+
+func (a Anim) Render() *memdraw.Image {
+ t := time.Now().UnixNano()
+ f := a.CurrentFrame(t)
+ //orig := xy
+
+ a.current.Draw(a.images[f].R, a.images[f], image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ return a.current
+}
+
+func (a Anim) Focus() {
+}
+
+func (a Anim) Layout() layout.Interface {
+ return layout.Inline{}
+}
+
+func (a Anim) Geom() (r image.Rectangle, margin space.Sp) {
+ return a.images[0].R, a.Margin
+}
+
--- /dev/null
+++ b/box/box.go
@@ -1,0 +1,196 @@
+package box
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ //"log"
+ "sync"
+ "xui/element"
+ "xui/events"
+ "xui/events/mouse"
+ "xui/internal/color"
+ "xui/layout"
+ "xui/space"
+)
+
+type Interface = element.Interface
+
+type Dir int
+
+const (
+ Horizontal Dir = iota + 1
+ Vertical
+)
+
+type Box struct {
+ Elements []element.Interface
+ Rs []image.Rectangle
+ Dir
+ Wrap bool
+ boxImg *memdraw.Image
+
+ mouseEntered []bool
+
+ // mouseXY for hover focus
+ mouseXY image.Point
+
+ // Optional parameters
+ Width int
+ Height int
+ color.Colorset
+
+ Margin space.Sp
+ Border space.Sp
+ Padding space.Sp
+}
+
+// rMax is optional.
+//
+// R can otherwise be determined recursively by
+// calling els Geometry and applying the layout.
+//
+// ...can be dynamically resized though
+// (similar to Go slices)
+func New(els []element.Interface) *Box {
+ return &Box{
+ Elements: els,
+ Rs: make([]image.Rectangle, len(els)),
+ mouseEntered: make([]bool, len(els)),
+ }
+}
+
+func (b *Box) Event(evOrig events.Interface) {
+ if mev, ok := evOrig.(mouse.Event); ok {
+ b.mouseXY = mev.Point
+ }
+ for i, el := range b.Elements {
+ switch tevOrig := evOrig.(type) {
+ case mouse.Event:
+ var ev mouse.Event
+
+ ev.Point = tevOrig.Point
+ ev.Buttons = tevOrig.Buttons
+ ev.Msec = tevOrig.Msec
+
+ if tevOrig.Point.In(b.Rs[i]) {
+ if !b.mouseEntered[i] {
+ b.mouseEntered[i] = true
+ ev.Type |= mouse.Enter
+ }
+ if tevOrig.Type != 0 {
+ ev.Type |= tevOrig.Type
+ }
+ } else {
+ if b.mouseEntered[i] {
+ b.mouseEntered[i] = false
+ ev.Type |= mouse.Leave
+ }
+ }
+ if ev.Type != 0 {
+ el.Event(ev)
+ }
+ default:
+ if b.mouseXY.In(b.Rs[i]) {
+ el.Event(tevOrig)
+ }
+ }
+ }
+}
+
+func (b *Box) Render() *memdraw.Image {
+ if b.boxImg == nil {
+ b.layoutBoxImg()
+ }
+
+ // 2. Render
+ ims := make([]*memdraw.Image, len(b.Elements))
+ wg := sync.WaitGroup{}
+ for i, el := range b.Elements {
+ wg.Add(1)
+ go func(ii int) {
+ ims[ii] = el.Render() //b.boxImg, b.Rs[i].Min)
+ wg.Done()
+ }(i)
+ }
+ wg.Wait()
+
+ for i, im := range ims {
+ //log.Printf("Render: ims[%d].R=%+v", i, ims[i].R)
+ rIm := im.R.Add(b.Rs[i].Min).Add(b.Padding.TopLeft())
+ b.boxImg.Draw(rIm, im, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ }
+ //log.Printf("Box.Render: surface=%v", surface)
+ //log.Printf("b.boxImg.R=%v", b.boxImg.R)
+ return b.boxImg
+}
+
+func (b *Box) layoutBoxImg() {
+ // 0. Validations
+
+ // 1. Layout
+ var dxy image.Point
+ for i, el := range b.Elements {
+ rEl, marginEl := el.Geom()
+ b.Rs[i] = rEl.Add(dxy)
+ b.Rs[i] = b.Rs[i].Add(marginEl.TopLeft())
+ switch b.Dir {
+ case Horizontal:
+ //log.Printf("horiz.")
+ dxy = dxy.Add(image.Point{X: rEl.Dx()+marginEl.Left.Val})
+ if i > 0 {
+ _, marginLast := b.Elements[i-1].Geom()
+ dxy = dxy.Add(image.Point{X: marginLast.Right.Val})
+ }
+ case Vertical:
+ //log.Printf("vert.")
+ fallthrough
+ default:
+ dxy = dxy.Add(image.Point{Y: rEl.Dy()+marginEl.Top.Val})
+ if i > 0 {
+ _, marginLast := b.Elements[i-1].Geom()
+ dxy = dxy.Add(image.Point{Y: marginLast.Bottom.Val})
+ }
+ }
+ }
+
+ if b.boxImg == nil {
+ var err error
+ var r image.Rectangle
+ if b.Width != 0 && b.Height != 0 {
+ r = image.Rect(0, 0, b.Width, b.Height)
+ } else if len(b.Elements) != 0 {
+ r = image.Rectangle{
+ //Min: b.Rs[0].Min,
+ Max: b.Rs[len(b.Rs)-1].Max.Add(b.Rs[0].Min).
+ Add(b.Padding.Size()),
+ }
+ }
+ // Allocate image
+ b.boxImg, err = memdraw.AllocImage(r, draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ if b.Colorset.Normal.Background != nil {
+ //log.Printf("b.Background.R=%v", b.Background.R)
+ b.boxImg.Draw(r, b.Colorset.Normal.Background, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ } else {
+ memdraw.FillColor(b.boxImg, draw.Transparent)
+ }
+ }
+}
+
+func (b Box) Focus() {
+}
+
+func (b Box) Layout() layout.Interface {
+ return layout.Inline{}
+}
+
+func (b *Box) Geom() (r image.Rectangle, margin space.Sp) {
+ if b.boxImg == nil {
+ b.layoutBoxImg()
+ }
+ return b.boxImg.R, b.Margin
+}
+
--- /dev/null
+++ b/box/box_test.go
@@ -1,0 +1,93 @@
+package box
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ imagecolor "image/color"
+ "testing"
+ "xui/element"
+ "xui/internal/color"
+ "xui/label"
+ "xui/space"
+ "xui/xuitest"
+)
+
+func TestMarginPadding(t *testing.T) {
+ memdraw.Init()
+
+ inner := New([]element.Interface{})
+ inner.Width = 130
+ inner.Height = 70
+ inner.Colorset.Normal.Background = color.MustMake(draw.Red)
+ inner.Margin = space.New(7)
+
+ t.Logf("inner.Margin=%v", inner.Margin)
+
+ outer := New([]element.Interface{inner})
+ outer.Colorset.Normal.Background = color.MustMake(draw.Green)
+ outer.Margin = space.New(3)
+ outer.Padding = space.New(11)
+ img := outer.Render()
+ t.Logf("TestMarginPadding: img.R=%v", img.R)
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ t.Logf("info=%v", info.BboxByColor)
+ xuitest.Dump(img)
+ bboxGreen := info.BboxByColor[imagecolor.RGBA{G:255,A:255}]
+ if bboxGreen.Dx() != (130+2*7+2*11) || bboxGreen.Dy() != (70+2*7+2*11) {
+ t.Fail()
+ }
+}
+
+func TestRenderRect(t *testing.T) {
+ memdraw.Init()
+
+ b := New([]element.Interface{})
+ b.Width = 130
+ b.Height = 70
+ b.Colorset.Normal.Background = color.MustMake(draw.Black)
+ img := b.Render()
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ t.Logf("info=%v", info.Bbox)
+ if info.Bbox.Dx() != 130 || info.Bbox.Dy() != 70 {
+ t.Fail()
+ }
+}
+
+func TestRenderLabel(t *testing.T) {
+ memdraw.Init()
+
+ l := label.New(image.ZP, "hello")
+
+ imgTestLabel := l.Render()
+ infoTestLabel, err := xuitest.Analyze(imgTestLabel)
+ if err != nil {
+ panic(err.Error())
+ }
+ t.Logf("infoTestLabel=%v", infoTestLabel.Bbox)
+
+ rScreen := image.Rect(0, 0, 800, 600)
+
+ b := New([]element.Interface{l})
+
+ img := b.Render()
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ t.Logf("info=%v", info.Bbox)
+ if !info.Bbox.In(rScreen) {
+ t.Fatalf("bbox outside screen rect")
+ }
+
+ if !infoTestLabel.Bbox.Eq(info.Bbox) {
+ t.Fail()
+ }
+}
+
--- /dev/null
+++ b/button/button.go
@@ -1,0 +1,114 @@
+package button
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "xui"
+ "xui/events"
+ "xui/events/mouse"
+ "xui/internal/color"
+ "xui/internal/font"
+ "xui/internal/geom"
+ "xui/space"
+ "xui/layout"
+)
+
+type Interface interface {
+}
+
+type Button struct {
+ Orig image.Point
+ Text string
+ x xui.Interface
+ textColor *memdraw.Image
+ borderColor *memdraw.Image
+ borderHoverColor *memdraw.Image
+
+ textImg *memdraw.Image
+ btnImg *memdraw.Image
+ hoverImg *memdraw.Image
+
+ hover bool
+
+ Margin space.Sp
+
+ cb func(ev events.Interface, userData any)
+ cbUserData any
+}
+
+func New(x xui.Interface, orig image.Point, text string) (b *Button) {
+ b = &Button{}
+ b.Orig = orig
+ b.Text = text
+ b.x = x
+
+ var err error
+ b.textImg, err = font.String(text)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ b.btnImg, err = memdraw.AllocImage(b.textImg.R.Inset(-10).Add(image.Point{10,10}), draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(b.btnImg, draw.Opaque)
+
+ b.hoverImg, err = memdraw.AllocImage(b.textImg.R.Inset(-10).Add(image.Point{10,10}), draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(b.hoverImg, draw.Opaque)
+
+ b.borderColor = color.Border
+ b.borderHoverColor = color.Hover.Border
+
+ b.btnImg.Draw(b.textImg.R.Add(image.Point{7,13}), b.textImg, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ geom.DrawRoundedBorder(b.btnImg, b.btnImg.R, b.borderColor)
+
+ b.hoverImg.Draw(b.textImg.R.Add(image.Point{7,13}), b.textImg, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ geom.DrawRoundedBorder(b.hoverImg, b.hoverImg.R, b.borderHoverColor)
+
+ return
+}
+
+func (b *Button) Event(ev events.Interface) {
+ mev, ok := ev.(mouse.Event)
+ if !ok { return }
+
+ if mev.Type == mouse.Enter {
+ b.hover = true
+ } else if mev.Type == mouse.Leave {
+ b.hover = false
+ }
+
+ if b.cb != nil {
+ b.cb(mev, b.cbUserData)
+ }
+}
+
+func (b Button) Render() *memdraw.Image {
+ if b.hover {
+ return b.hoverImg
+ } else {
+ return b.btnImg
+ }
+}
+
+func (b Button) Focus() {
+}
+
+func (b Button) Layout() layout.Interface {
+ return layout.Inline{}
+}
+
+func (b Button) Geom() (r image.Rectangle, margin space.Sp) {
+ return b.btnImg.R, b.Margin
+}
+
+func (b *Button) SetCallback(cb func(ev events.Interface, userData any), userData any) {
+ b.cb = cb
+ b.cbUserData = userData
+}
+
binary files /dev/null b/cmd/hello/animated-computer-image-0064.gif differ
--- /dev/null
+++ b/cmd/hello/hello.go
@@ -1,0 +1,95 @@
+package main
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+ "image"
+ imagedraw "image/draw"
+ "image/gif"
+ "log"
+ "os"
+ "xui"
+ "xui/anim"
+ "xui/box"
+ "xui/button"
+ "xui/element"
+ "xui/events"
+ "xui/events/mouse"
+ "xui/field"
+ "xui/label"
+ "xui/space"
+)
+
+func Main() (err error) {
+ memdraw.Init()
+
+ x, err := xui.New()
+
+ if err != nil {
+ return
+ }
+
+
+ l := label.New(image.Point{10, 5}, "Hello, world!!") //, c)
+ l.Margin = space.New(5, 10)
+
+ f, err := os.Open("animated-computer-image-0064.gif")
+ //f, err := os.Open("g3Ys.gif")
+ if err != nil { return }
+ gifImg, err := gif.DecodeAll(f)
+ if err != nil { return }
+
+ animImg := make([]*memdraw.Image, len(gifImg.Image))
+ rGifImg := image.Rectangle{
+ Max: image.Point{gifImg.Config.Width, gifImg.Config.Height},
+ }
+ for i, im := range gifImg.Image {
+ ni, err := memdraw.AllocImage(rGifImg, draw.ABGR32)
+ if err != nil {
+ return fmt.Errorf("allocimage: %s", err)
+ }
+
+ // Stolen from duit.ReadImage
+ var rgba *image.RGBA
+ b := im.Bounds()
+ rgba = image.NewRGBA(rGifImg)
+ imagedraw.Draw(rgba, rgba.Bounds(), im, b.Min, imagedraw.Src)
+ _, err = memdraw.Load(ni, rGifImg, rgba.Pix, false)
+ if err != nil {
+ return fmt.Errorf("load image_: %w", err)
+ }
+
+ animImg[i] = ni
+ }
+
+ a := anim.NewFromSeq(x, animImg, gifImg.Delay)
+ a.Margin = x.Space(5, 10)
+
+ btn := button.New(x, image.ZP, "click right here")
+ btn.SetCallback(func(ev events.Interface, _ any) {
+ if mev, ok := ev.(mouse.Event); ok && mev.Type & mouse.Click > 0 {
+ log.Printf("clicked!")
+ }
+ }, nil)
+ btn.Margin = x.Space(5, 10)
+
+ fl := field.New(x, image.ZP, " ", x.Rect(0,0, 150, 50))
+ fl.Margin = x.Space(5, 10)
+
+ b := box.New([]element.Interface{
+ a, l, btn, fl,
+ })
+ //b.Dir = box.Horizontal
+ x.SetRoot(b)
+
+ x.Loop()
+
+ return
+}
+
+func main() {
+ if err := Main(); err != nil {
+ log.Fatalf("%v", err)
+ }
+}
--- /dev/null
+++ b/cmd/pixtest/pixtest.go
@@ -1,0 +1,83 @@
+package main
+
+import (
+ "flag"
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+ "image"
+ "log"
+ "time"
+)
+
+const (
+ width = 17
+ height = 13
+)
+
+func main() {
+ usemem := flag.Bool("usemem", false, "use memdraw")
+ screxp := flag.Bool("screxp", true, "export 'unload' pix after drawing directly to screen")
+ flag.Parse()
+
+ errch := make(chan error, 1)
+ rectStr := fmt.Sprintf("%dx%d", width, height)
+ log.Printf("rectStr=%s", rectStr)
+ display, err := draw.Init(errch, "", "hello", "800x600")
+ if err != nil { panic(err.Error()) }
+
+ r := image.Rectangle{Max: image.Point{X: width, Y: height}}
+ buf := make([]byte, 1024*1024*32)
+
+ if *screxp {
+ log.Printf("export from actual screen")
+ img, err := display.AllocImage(r, draw.ARGB32, false, draw.Blue)
+ if err != nil { panic(err.Error()) }
+ display.ScreenImage.Draw(r, img, nil, image.ZP)
+ display.Flush()
+
+ nbuf, err := display.ScreenImage.Unload(r, buf)
+ if err != nil { panic(err.Error()) }
+ buf = buf[:nbuf]
+
+ log.Printf("17x13x4=%v", 17*13*4)
+ log.Printf("buf len=%v", len(buf))
+ log.Printf("buf=%+x", buf)
+
+ _, err = display.ScreenImage.Load(r.Add(image.Point{100, 100}), buf)
+ if err != nil { panic(err.Error()) }
+ display.Flush()
+
+ <-time.After(time.Hour)
+ return
+ }
+
+ if *usemem {
+ log.Printf("using memdraw")
+ img, err := memdraw.AllocImage(r, display.ScreenImage.Pix)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(img, draw.Black)
+ nbuf, err := memdraw.Unload(img, r, buf)
+ if err != nil { panic(err.Error()) }
+ buf = buf[:nbuf]
+ } else {
+ log.Printf("using screen directly")
+ img, err := display.AllocImage(r, display.ScreenImage.Pix, false, draw.Black)
+ if err != nil { panic(err.Error()) }
+ nbuf, err := img.Unload(r, buf)
+ if err != nil { panic(err.Error()) }
+ buf = buf[:nbuf]
+ }
+
+ log.Printf("17x13x4=%v", 17*13*4)
+ log.Printf("buf len=%v", len(buf))
+ log.Printf("buf=%+x", buf)
+
+ _, err = display.ScreenImage.Load(r, buf)
+ if err != nil { panic(err.Error()) }
+ display.Flush()
+
+ <-time.After(time.Hour)
+}
--- /dev/null
+++ b/element/element.go
@@ -1,0 +1,25 @@
+package element
+
+import (
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "xui/events"
+ "xui/layout"
+ "xui/space"
+)
+
+type Interface interface {
+ Event(events.Interface)
+ Render() *memdraw.Image
+ Focus()
+ // Layout hints about how to arrange this element
+ Layout() layout.Interface
+ // Coordinates relative to parent element unless layout expects different coordinate system
+ //
+ // Other possible coordinates:
+ // - global (relative to window)
+ // - global scroll (relative to scrolled window)
+ // - inline (relative to neighbour)
+ // - parent (relative to (scrolled) parent element)
+ Geom() (r image.Rectangle, margin space.Sp)
+}
--- /dev/null
+++ b/events/events.go
@@ -1,0 +1,3 @@
+package events
+
+type Interface interface {}
--- /dev/null
+++ b/events/keyboard/keyboard.go
@@ -1,0 +1,15 @@
+package keyboard
+
+type Type int
+
+const (
+ Down Type = 1 << iota
+ Up
+ Pressed
+)
+
+type Event struct {
+ Type
+
+ Key rune
+}
--- /dev/null
+++ b/events/mouse/mouse.go
@@ -1,0 +1,25 @@
+package mouse
+
+import "image"
+
+type Type int
+
+const (
+ Enter Type = 1 << iota
+ Leave
+ Click
+)
+
+// Mouse Point in local coordinates though
+//
+// E.g. for click event on Button relative to button
+// - thus for mouse leave it can be negative
+type Event struct {
+ Type
+
+ image.Point
+
+ Buttons int
+ // timestamp in ms
+ Msec uint32
+}
--- /dev/null
+++ b/field/field.go
@@ -1,0 +1,167 @@
+package field
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ //"fmt"
+ "image"
+ "log"
+ "sync"
+ "xui"
+ "xui/events"
+ "xui/events/keyboard"
+ "xui/events/mouse"
+ "xui/internal/color"
+ "xui/internal/font"
+ "xui/internal/geom"
+ "xui/layout"
+ "xui/space"
+)
+
+const (
+ Backspace rune = 8
+)
+
+type Interface interface {
+}
+
+type Field struct {
+ mu sync.RWMutex
+
+ Orig image.Point
+ image.Rectangle
+ x xui.Interface
+
+ Text string
+ color.Colorset
+
+ textImg *memdraw.Image
+ borderImg *memdraw.Image
+ hoverImg *memdraw.Image
+
+ hover bool
+
+ cb func(ev events.Interface, userData any)
+ cbUserData any
+
+ Margin space.Sp
+}
+
+func New(x xui.Interface, orig image.Point, text string, r image.Rectangle) (f *Field) {
+ f = &Field{}
+ f.Orig = orig
+ f.Text = text
+ f.Rectangle = r
+ f.x = x
+
+ f.Colorset.Normal.Border = color.Border
+ f.Colorset.Hover.Border = color.Hover.Border
+
+ return
+}
+
+func (f *Field) Event(ev events.Interface) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ switch tev := ev.(type) {
+ case mouse.Event:
+ if tev.Type == mouse.Enter {
+ f.hover = true
+ } else if tev.Type == mouse.Leave {
+ f.hover = false
+ }
+
+ if f.cb != nil {
+ f.cb(tev, f.cbUserData)
+ }
+ case keyboard.Event:
+ log.Printf("key pressed: %+v", tev)
+
+ switch tev.Key {
+ case Backspace:
+ if len(f.Text) > 0 {
+ f.Text = f.Text[:len(f.Text)-1]
+ }
+ default:
+ f.Text += string([]byte{byte(tev.Key)})
+ }
+ //log.Printf("event: call updateTextImgs")
+ f.updateTextImgs()
+ }
+}
+
+func (f *Field) Render() *memdraw.Image {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ if f.borderImg == nil {
+ //log.Printf("render: call updateTextImgs")
+ f.updateTextImgs()
+ }
+
+ if f.hover {
+ return f.hoverImg
+ } else {
+ return f.borderImg
+ }
+}
+
+func (f *Field) updateTextImgs() {
+
+ var err error
+ //log.Printf("updateTextImgs: call font.String")
+ f.textImg, err = font.String(f.Text)
+ if err != nil {
+ panic(err.Error())
+ }
+ r := f.Rectangle//f.textImg.R.Intersect(f.Rectangle)
+
+ f.borderImg, err = memdraw.AllocImage(f.Rectangle.Inset(-f.x.Scale(5)).Add(f.x.Pt(5, 5)), draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(f.borderImg, draw.Opaque)
+
+ f.hoverImg, err = memdraw.AllocImage(f.Rectangle.Inset(-f.x.Scale(5)).Add(f.x.Pt(5, 5)), draw.ABGR32)
+ if err != nil {
+ panic(err.Error())
+ }
+ memdraw.FillColor(f.hoverImg, draw.Opaque)
+
+ rr := r
+ rr.Min = r.Min.Add(f.x.Pt(3, 7))
+
+ f.borderImg.Draw(rr, f.textImg, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ geom.DrawRoundedBorder(f.borderImg, f.Rectangle, f.Colorset.Normal.Border)
+
+ f.hoverImg.Draw(rr, f.textImg, image.ZP, color.EmptyMask, image.ZP, draw.SoverD)
+ geom.DrawRoundedBorder(f.hoverImg, f.Rectangle, f.Colorset.Hover.Border)
+
+ geom.DrawCursor(f.hoverImg, r, f.textImg, f.Colorset.Hover.Border)
+}
+
+func (f Field) Focus() {
+}
+
+func (f Field) Layout() layout.Interface {
+ return layout.Inline{}
+}
+
+func (f *Field) Geom() (r image.Rectangle, margin space.Sp) {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ if f.borderImg == nil {
+ //log.Printf("geom: call updateTextImgs")
+ f.updateTextImgs()
+ }
+ return f.borderImg.R, f.Margin
+}
+
+func (f *Field) SetCallback(cb func(ev events.Interface, userData any), userData any) {
+ f.cb = cb
+ f.cbUserData = userData
+}
+
+
--- /dev/null
+++ b/field/field_test.go
@@ -1,0 +1,35 @@
+package field
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "testing"
+ "xui/events/keyboard"
+ "xui/xuitest"
+)
+
+func TestRender(t *testing.T) {
+ memdraw.Init()
+ rScreen := image.Rect(0, 0, 800, 600)
+ rField := image.Rect(0, 0, 300, 100)
+ img, err := memdraw.AllocImage(rScreen, draw.ABGR32)
+ if err != nil {
+ t.Fail()
+ }
+ memdraw.FillColor(img, draw.White)
+ f := New(&xuitest.Xui{}, image.ZP, "", rField)
+
+ for i := 0; i < 20; i++ {
+ f.Event(keyboard.Event{Key: 'a'})
+ img := f.Render()
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ t.Logf("info=%v", info.Bbox)
+ if !info.Bbox.In(rField) {
+ t.Fatalf("bbox outside field rect")
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+++ b/go.mod
@@ -1,0 +1,12 @@
+module xui
+
+go 1.22.0
+
+//replace 9fans.net/go v0.0.0-00010101000000-000000000000 => github.com/psilva261/go v0.0.0-20210805155101-6b9925e0d807
+
+replace 9fans.net/go v0.0.2 => github.com/psilva261/go v0.0.0-20211226113545-7b63d477c61e
+
+require (
+ 9fans.net/go v0.0.2
+ golang.org/x/image v0.0.0-20190802002840-cff245a6509b
+)
--- /dev/null
+++ b/go.sum
@@ -1,0 +1,37 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/psilva261/go v0.0.0-20210805155101-6b9925e0d807 h1:JJqdNuwkTdbAe8hxrTaBBbX5TTk88gJrOhGigxoQb7c=
+github.com/psilva261/go v0.0.0-20210805155101-6b9925e0d807/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM=
+github.com/psilva261/go v0.0.0-20211226113545-7b63d477c61e h1:HAu3fZPB/xDdzAy0atpSn3e+QHmbl/d8sTiT1SP4bPU=
+github.com/psilva261/go v0.0.0-20211226113545-7b63d477c61e/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp v0.0.0-20210405174845-4513512abef3/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
+golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
--- /dev/null
+++ b/internal/color/color.go
@@ -1,0 +1,48 @@
+package color
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+)
+
+var (
+ EmptyMask, Border *memdraw.Image
+
+ Hover Colors
+)
+
+type Colors struct {
+ Text *memdraw.Image
+ Background *memdraw.Image
+ Border *memdraw.Image
+}
+
+type Colorset struct {
+ Normal, Hover Colors
+}
+
+func init() {
+ EmptyMask = MustMake(draw.Opaque)
+ Border = MustMake(0xbbbbbbff)
+ Hover.Border = MustMake(0xdc7232ff)
+}
+
+func MustMake(val draw.Color) (img *memdraw.Image) {
+ img, err := Make(val)
+ if err != nil {
+ panic(err.Error())
+ }
+ return
+}
+
+func Make(val draw.Color) (img *memdraw.Image, err error) {
+ img, err = memdraw.AllocImage(draw.Rect(0, 0, 1, 1), draw.ABGR32)
+ if err != nil {
+ return nil, fmt.Errorf("allocc image: %w", err)
+ }
+ img.Flags |= memdraw.Frepl
+ img.Clipr = draw.Rect(-0x3FFFFFF, -0x3FFFFFF, 0x3FFFFFF, 0x3FFFFFF)
+ memdraw.FillColor(img, val)
+ return
+}
--- /dev/null
+++ b/internal/font/font.go
@@ -1,0 +1,68 @@
+package font
+
+import (
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/plan9font"
+ "golang.org/x/image/math/fixed"
+ "image"
+ "io/ioutil"
+ "log"
+ "path"
+ "path/filepath"
+ imagedraw "image/draw"
+)
+
+func String(text string) (textImg *memdraw.Image, err error) {
+ readFile := func(name string) ([]byte, error) {
+ return ioutil.ReadFile(filepath.FromSlash(path.Join(FontDir(), name)))
+ }
+ fontData, err := readFile(FontFile())
+ if err != nil {
+ log.Fatal(err)
+ }
+ face, err := plan9font.ParseFont(fontData, readFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ ascent := face.Metrics().Ascent.Ceil()
+
+ w := 0
+ for _, r := range text {
+ bounds, adv, _ := face.GlyphBounds(r)
+ _ = bounds
+ w += adv.Ceil()
+ }
+ r := image.Rect(0, 0, w, 2*ascent)
+ if r.Dx() == 0 {
+ r.Max.X = r.Min.X + 1
+ }
+ if r.Dy() == 0 {
+ r.Max.Y = r.Min.Y + 1
+ }
+ dst := image.NewRGBA(r)
+ imagedraw.Draw(dst, dst.Bounds(), image.White, image.Point{}, imagedraw.Src)
+ d := &font.Drawer{
+ Dst: dst,
+ Src: image.Black,
+ Face: face,
+ Dot: fixed.P(0, ascent),
+ }
+ d.DrawString(text)
+
+ log.Printf("String: dst.Bounds()=%+v", dst.Bounds())
+
+ textImg, err = memdraw.AllocImage(dst.Bounds(), draw.ABGR32)
+ if err != nil {
+ panic(fmt.Errorf("allocimage: %s", err).Error())
+ }
+
+ _, err = memdraw.Load(textImg, dst.Bounds(), dst.Pix, false)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ return
+}
--- /dev/null
+++ b/internal/font/font_plan9.go
@@ -1,0 +1,9 @@
+package font
+
+func FontDir() string {
+ return "/lib/font/bit/lucida"
+}
+
+func FontFile() string {
+ return "unicode.9.font"
+}
--- /dev/null
+++ b/internal/font/font_test.go
@@ -1,0 +1,51 @@
+package font
+
+import (
+ //"9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "image/color"
+ "testing"
+ "xui/xuitest"
+)
+
+func TestString(t *testing.T) {
+ memdraw.Init()
+ colors, dashBbox := testString(t, "----")
+ t.Logf("colors=%+v", colors)
+ t.Logf("dashBbox=%+v", dashBbox)
+ if dashBbox.Dx() < 5 || dashBbox.Dy() < 1 {
+ t.Fail()
+ }
+ colors, underBbox := testString(t, "____")
+ t.Logf("colors=%+v", colors)
+ t.Logf("underBbox=%+v", underBbox)
+ if underBbox.Dx() < 5 || underBbox.Dy() < 1 {
+ t.Fail()
+ }
+ if underBbox.Min.X >= dashBbox.Min.X {
+ t.Fail()
+ }
+}
+
+func TestStringEmpty(t *testing.T) {
+ memdraw.Init()
+ _, dashBbox := testString(t, "")
+ t.Logf("dashBbox=%v", dashBbox)
+}
+
+func testString(t *testing.T, text string) (map[color.Color]int, image.Rectangle) {
+ img, err := String(text)
+ if err != nil {
+ t.Fail()
+ }
+
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ colors := info.Colors
+ bbox := info.Bbox
+
+ return colors, bbox
+}
\ No newline at end of file
--- /dev/null
+++ b/internal/font/font_unix.go
@@ -1,0 +1,15 @@
+//go:build !plan9
+package font
+
+import (
+ "os"
+ "path"
+)
+
+func FontDir() string {
+ return path.Join(os.Getenv("PLAN9"), "/font/luc")
+}
+
+func FontFile() string {
+ return "unicode.18.font"
+}
--- /dev/null
+++ b/internal/geom/image.go
@@ -1,0 +1,14 @@
+package geom
+
+import (
+ "image"
+ "log"
+
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+)
+
+func MakeImage(r image.Rectangle, val draw.Color) (img *memdraw.Image, err error) {
+ log.Printf("not implemented")
+ return
+}
--- /dev/null
+++ b/internal/geom/lines.go
@@ -1,0 +1,60 @@
+package geom
+
+// Original code from github.com/mjl-/duit
+//
+// Copyright 2018 Mechiel Lukkien mechiel@ueber.net
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import (
+ "image"
+
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+)
+
+// draw border with rounded corners, on the inside of `r`.
+func DrawRoundedBorder(img *memdraw.Image, r image.Rectangle, color *memdraw.Image) {
+ radius := 3
+ x0 := r.Min.X
+ x1 := r.Max.X - 1
+ y0 := r.Min.Y
+ y1 := r.Max.Y - 1
+ tl := image.Pt(x0+radius, y0+radius)
+ bl := image.Pt(x0+radius, y1-radius)
+ br := image.Pt(x1-radius, y1-radius)
+ tr := image.Pt(x1-radius, y0+radius)
+ memdraw.Arc(img, tl, radius, radius, 0, color, image.ZP, 90, 90, draw.SoverD)
+ memdraw.Arc(img, bl, radius, radius, 0, color, image.ZP, 180, 90, draw.SoverD)
+ memdraw.Arc(img, br, radius, radius, 0, color, image.ZP, 270, 90, draw.SoverD)
+ memdraw.Arc(img, tr, radius, radius, 0, color, image.ZP, 0, 90, draw.SoverD)
+ memdraw.Line(img, image.Pt(x0, y0+radius), image.Pt(x0, y1-radius), 0, 0, 0, color, image.ZP, draw.SoverD)
+ memdraw.Line(img, image.Pt(x0+radius, y1), image.Pt(x1-radius, y1), 0, 0, 0, color, image.ZP, draw.SoverD)
+ memdraw.Line(img, image.Pt(x1, y1-radius), image.Pt(x1, y0+radius), 0, 0, 0, color, image.ZP, draw.SoverD)
+ memdraw.Line(img, image.Pt(x1-radius, y0), image.Pt(x0+radius, y0), 0, 0, 0, color, image.ZP, draw.SoverD)
+}
+
+func DrawCursor(dst *memdraw.Image, bounds image.Rectangle, text *memdraw.Image, color *memdraw.Image) {
+ h := text.R.Dy()/2
+ p1 := text.R.Max.Add(image.Pt(h/5, -h/2))
+ p0 := p1.Add(image.Pt(0, -h))
+ if p0.X >= bounds.Dx() {
+ return
+ }
+ memdraw.Line(dst, p0, p1, draw.EndSquare, draw.EndSquare, 1, color, image.ZP, draw.SoverD)
+}
+
--- /dev/null
+++ b/label/label.go
@@ -1,0 +1,54 @@
+package label
+
+import (
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "xui/events"
+ "xui/internal/font"
+ "xui/layout"
+ "xui/space"
+)
+
+type Interface interface {
+}
+
+type Label struct {
+ Orig image.Point
+ Text string
+ textColor *memdraw.Image
+
+ textImg *memdraw.Image
+
+ Margin space.Sp
+}
+
+func New(orig image.Point, text string) (l Label) {
+ l.Orig = orig
+ l.Text = text
+
+ var err error
+ l.textImg, err = font.String(text)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ return
+}
+
+func (l Label) Event(events.Interface) {
+}
+
+func (l Label) Render() *memdraw.Image {
+ return l.textImg
+}
+
+func (l Label) Focus() {
+}
+
+func (l Label) Layout() layout.Interface {
+ return layout.Inline{}
+}
+
+func (l Label) Geom() (r image.Rectangle, margin space.Sp) {
+ return l.textImg.R, l.Margin
+}
--- /dev/null
+++ b/label/label_test.go
@@ -1,0 +1,35 @@
+package label
+
+import (
+ "9fans.net/go/draw/memdraw"
+ "image"
+ "image/color"
+ "testing"
+ "xui/xuitest"
+)
+
+func TestRender(t *testing.T) {
+ colors := testRender(t, "aaaa")
+ black := color.RGBA{R: 0, G: 0, B: 0, A: 255}
+ n4 := colors[black]
+ colors = testRender(t, "aaaaaaaa")
+ n8 := colors[black]
+ if n8/n4 != 2 {
+ t.Fail()
+ }
+}
+
+func testRender(t *testing.T, text string) map[color.Color]int {
+ memdraw.Init()
+ l := New(image.Pt(0, 0), text)
+ img := l.Render()
+
+ info, err := xuitest.Analyze(img)
+ if err != nil {
+ panic(err.Error())
+ }
+ colors := info.Colors
+ //bbox := info.Bbox
+
+ return colors
+}
--- /dev/null
+++ b/layout/layout.go
@@ -1,0 +1,20 @@
+package layout
+
+import (
+ "image"
+)
+
+type Interface interface {
+ Arrange([]Element)
+}
+
+type Element interface {
+ Geom() (orig, extent image.Point)
+}
+
+type Inline struct {
+}
+
+func (inl Inline) Arrange([]Element) {}
+
+
--- /dev/null
+++ b/space/offset/offset.go
@@ -1,0 +1,13 @@
+package offset
+
+type Opts int
+
+const (
+ Auto = 1 << (iota+1)
+ Unset
+)
+
+type Of struct {
+ Val int
+ Opts
+}
--- /dev/null
+++ b/space/space.go
@@ -1,0 +1,52 @@
+package space
+
+import (
+ "image"
+ "xui/space/offset"
+)
+
+type Sp struct {
+ Top, Right, Bottom, Left offset.Of
+}
+
+func New(vals... int) (s Sp) {
+ switch len(vals) {
+ case 4:
+ s.Top = offset.Of{Val: vals[0]}
+ s.Right = offset.Of{Val: vals[1]}
+ s.Bottom = offset.Of{Val: vals[2]}
+ s.Left = offset.Of{Val: vals[3]}
+ case 3:
+ s.Top = offset.Of{Val: vals[0]}
+ s.Right = offset.Of{Val: vals[1]}
+ s.Bottom = offset.Of{Val: vals[2]}
+ s.Left = offset.Of{Val: vals[1]}
+ case 2:
+ s.Top = offset.Of{Val: vals[0]}
+ s.Right = offset.Of{Val: vals[1]}
+ s.Bottom = offset.Of{Val: vals[0]}
+ s.Left = offset.Of{Val: vals[1]}
+ case 1:
+ s.Top = offset.Of{Val: vals[0]}
+ s.Right = offset.Of{Val: vals[0]}
+ s.Bottom = offset.Of{Val: vals[0]}
+ s.Left = offset.Of{Val: vals[0]}
+ }
+ return
+}
+
+func (s Sp) TopLeft() image.Point {
+ return image.Pt(s.Left.Val, s.Top.Val)
+}
+
+func (s Sp) Dx() int {
+ return s.Left.Val+s.Right.Val
+}
+
+func (s Sp) Dy() int {
+ return s.Top.Val+s.Bottom.Val
+}
+
+func (s Sp) Size() image.Point {
+ return image.Pt(s.Dx(), s.Dy())
+}
--- /dev/null
+++ b/xui.go
@@ -1,0 +1,207 @@
+package xui
+
+import (
+ "xui/element"
+ "9fans.net/go/draw"
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+ "image"
+ "io"
+ "log"
+ "sync"
+ "time"
+ "xui/events/keyboard"
+ "xui/events/mouse"
+ //"xui/internal/color"
+ "xui/space"
+)
+
+const (
+ scrollStep = 40
+)
+
+type Interface interface {
+ R() image.Rectangle
+ SetRoot(element.Interface)
+ Render()
+ Loop()
+
+ Scale(n int) int
+
+ // Create properly scaled points, rectangles and spaces
+
+ Pt(x, y int) image.Point
+ Rect(x0, y0, x1, y1 int) image.Rectangle
+ Space(vals... int) space.Sp
+}
+
+type Xui struct {
+ mu sync.Mutex
+ keyctl *draw.Keyboardctl
+ mousectl *draw.Mousectl
+ errch chan error
+ root element.Interface
+ display *draw.Display
+ surface *memdraw.Image
+ bgLayer *memdraw.Image
+
+ rootXY image.Point
+}
+
+func (x *Xui) SetRoot(el element.Interface) {
+ x.mu.Lock()
+ defer x.mu.Unlock()
+
+ x.root = el
+}
+
+var buf = make([]byte, 1024*1024*32)
+
+func (x *Xui) Render() {
+ x.mu.Lock()
+ defer x.mu.Unlock()
+
+ if x.root != nil {
+ //log.Printf("x.Render: x.bgLayer=%v, color.EmptyMask=%v", x.bgLayer, color.EmptyMask)
+ //log.Printf("x.Render: x.surface.R=%v, x.bgLayer.R=%v, color.EmptyMask.R=%v", x.surface.R, x.bgLayer.R, color.EmptyMask.R)
+ x.surface.Draw(x.bgLayer.R, x.bgLayer, image.ZP, nil, image.ZP, draw.SoverD)
+
+ im := x.root.Render() //x.surface, x.rootXY)
+ x.surface.Draw(im.R.Add(x.rootXY), im, image.ZP, nil, image.ZP, draw.SoverD)
+
+ nbuf, err := memdraw.Unload(x.surface, x.surface.R, buf)
+ if err != nil { panic(err.Error()) }
+
+ data := buf[:nbuf]
+ _, err = x.display.ScreenImage.Load(x.surface.R, data)
+ if err != nil {
+ panic(err.Error())
+ }
+
+ x.display.Flush()
+ }
+}
+
+func (x *Xui) R() image.Rectangle {
+ return x.display.ScreenImage.R
+}
+
+func (x *Xui) Scale(n int) int {
+ return x.display.Scale(n)
+}
+
+func (x *Xui) Pt(x0, y0 int) image.Point {
+ return image.Point{
+ x.Scale(x0),
+ x.Scale(y0),
+ }
+}
+
+func (x *Xui) Rect(x0, y0, x1, y1 int) image.Rectangle {
+ return image.Rectangle{
+ x.Pt(x0, y0),
+ x.Pt(x1, y1),
+ }
+}
+
+func (x *Xui) Space(vals... int) space.Sp {
+ scaled := make([]int, len(vals))
+ for i := 0; i < len(vals); i++ {
+ scaled[i] = x.Scale(vals[i])
+ }
+ return space.New(vals...)
+}
+
+func New() (Interface, error) {
+
+ errch := make(chan error, 1)
+ display, err := draw.Init(errch, "", "hello", "800x600")
+ if err != nil {
+ return nil, err
+ }
+ memdraw.Init()
+ _ = display
+
+ x := &Xui{
+ errch: errch,
+ keyctl: display.InitKeyboard(),
+ mousectl: display.InitMouse(),
+ display: display,
+ }
+
+ x.surface, err = memdraw.AllocImage(x.display.ScreenImage.R, x.display.ScreenImage.Pix)
+ if err != nil {
+ return nil, fmt.Errorf("alloc image: %w", err)
+ }
+
+ x.bgLayer, err = memdraw.AllocImage(x.display.ScreenImage.R, draw.ABGR32)
+ if err != nil {
+ return nil, fmt.Errorf("alloc image: %w", err)
+ }
+ memdraw.FillColor(x.bgLayer, draw.White)
+
+ return x, nil
+}
+
+func (x *Xui) Loop() {
+ go func() {
+ for {
+ (func() {
+ <-time.After(10*time.Millisecond)
+ x.Render()
+ })()
+ }
+ }()
+ for {
+ select {
+ case m := <-x.mousectl.C:
+ if m.Buttons == 8 {
+ x.rootXY.Y += scrollStep
+ } else if m.Buttons == 16 {
+ x.rootXY.Y -= scrollStep
+ }
+ go func() {
+ x.mu.Lock()
+ defer x.mu.Unlock()
+
+ var ev mouse.Event
+
+ if m.Buttons&1 > 0 {
+ ev.Type |= mouse.Click
+ }
+
+
+ if x.root != nil {
+ ev.Point = m.Point.Sub(x.rootXY)
+ ev.Buttons = m.Buttons
+ ev.Msec = m.Msec
+ x.root.Event(ev)
+ }
+ }()
+ case k := <-x.keyctl.C:
+ go func() {
+ x.mu.Lock()
+ defer x.mu.Unlock()
+
+ log.Printf("KEY %v", k)
+
+ ev := keyboard.Event{}
+ ev.Type = keyboard.Pressed
+ ev.Key = k
+ if x.root != nil {
+ x.root.Event(ev)
+ }
+ }()
+ case <-x.mousectl.Resize:
+ log.Printf("resize")
+ case err := <-x.errch:
+ log.Printf("errch: %v", err)
+ if err == io.EOF {
+ log.Printf("returning")
+ return
+ }
+ case <-time.After(10*time.Millisecond):
+ //x.Render()
+ }
+ }
+}
--- /dev/null
+++ b/xuitest/xuitest.go
@@ -1,0 +1,146 @@
+package xuitest
+
+import (
+ "9fans.net/go/draw/memdraw"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
+ "os"
+ "xui/element"
+ "xui/space"
+)
+
+type Xui struct {
+ image.Rectangle
+}
+
+func (xui *Xui) R() image.Rectangle {
+ return xui.Rectangle
+}
+
+func (xui *Xui) Scale(n int) int {
+ return n
+}
+
+func (x *Xui) Pt(x0, y0 int) image.Point {
+ return image.Point{
+ x.Scale(x0),
+ x.Scale(y0),
+ }
+}
+
+func (x *Xui) Rect(x0, y0, x1, y1 int) image.Rectangle {
+ return image.Rectangle{
+ x.Pt(x0, y0),
+ x.Pt(x1, y1),
+ }
+}
+
+func (x *Xui) Space(vals... int) space.Sp {
+ scaled := make([]int, len(vals))
+ for i := 0; i < len(vals); i++ {
+ scaled[i] = x.Scale(vals[i])
+ }
+ return space.New(vals...)
+}
+
+func (xui *Xui) SetRoot(element.Interface) {
+}
+
+func (xui *Xui) Render() {
+}
+
+func (xui *Xui) Loop() {
+}
+
+type Info struct {
+ Colors map[color.Color]int
+ Bbox image.Rectangle
+ BboxByColor map[color.Color]*image.Rectangle
+}
+
+func Analyze(img *memdraw.Image) (Info, error) {
+ rgba, err := ToRGBA(img)
+ if err != nil {
+ return Info{}, fmt.Errorf("to rgba: %w", err)
+ }
+
+ colors := make(map[color.Color]int)
+ black := color.RGBA{R:0, G:0, B:0, A:255}
+ newRect := image.Rectangle{
+ Min: image.Point{1e6, 1e6},
+ Max: image.Point{-1e6, -1e6},
+ }
+ bbox := newRect
+ bboxByColor := make(map[color.Color]*image.Rectangle)
+ for x := img.R.Min.X; x < img.R.Max.X; x++ {
+ for y := img.R.Min.Y; y < img.R.Max.Y; y++ {
+ c := rgba.At(x, y)
+ colors[c] = colors[c]+1
+ if bboxByColor[c] == nil {
+ r := newRect
+ bboxByColor[c] = &r
+ }
+ if c == black {
+ if bbox.Min.X > x {
+ bbox.Min.X = x
+ }
+ if bbox.Max.X < x+1 {
+ bbox.Max.X = x+1
+ }
+ if bbox.Min.Y > y {
+ bbox.Min.Y = y
+ }
+ if bbox.Max.Y < y+1 {
+ bbox.Max.Y = y+1
+ }
+ }
+ if bboxByColor[c].Min.X > x {
+ bboxByColor[c].Min.X = x
+ }
+ if bboxByColor[c].Max.X < x+1 {
+ bboxByColor[c].Max.X = x+1
+ }
+ if bboxByColor[c].Min.Y > y {
+ bboxByColor[c].Min.Y = y
+ }
+ if bboxByColor[c].Max.Y < y+1 {
+ bboxByColor[c].Max.Y = y+1
+ }
+ }
+ }
+
+ info := Info{
+ Colors: colors,
+ Bbox: bbox,
+ BboxByColor: bboxByColor,
+ }
+
+ return info, nil
+}
+
+func Dump(img *memdraw.Image) {
+ rgba, err := ToRGBA(img)
+ if err != nil {
+ if err != nil { panic(err.Error()) }
+ }
+
+ f, err := os.Create("xui.png")
+ if err != nil { panic(err.Error()) }
+ defer f.Close()
+ err = png.Encode(f, rgba)
+ if err != nil { panic(err.Error()) }
+}
+
+func ToRGBA(img *memdraw.Image) (rgba *image.RGBA, err error) {
+ rgba = image.NewRGBA/*32*/(img.R)
+ rgba.Pix = make([]byte, 10*1024*1024)
+ n, err := memdraw.Unload(img, img.R, rgba.Pix)
+ if err != nil {
+ return nil, fmt.Errorf("unload: %w", err)
+ }
+ rgba.Pix = rgba.Pix[:n]
+
+ return
+}
--
⑨