Complete the Gio v0.10.2 GUI migration (working on Android).

- Event loop: call w.Event() and FrameEvent.Frame() on the same
  goroutine; no channel bridge (the bridge broke frame rendering).
- clip: use clip.UniformRRect(...).Push(gtx.Ops) / Pop() for real
  clipping in layoutRRect (old .Add(ops) painted unclipped).
- Window clear color is white in v0.10.2.
- Fonts: always provide gofont; on Android use
  WithCollection(gofont.Collection()).
- Button labels: explicitly set Color (zero-value NRGBA has A=0,
  i.e. transparent, so labels were invisible).
- Filter: detect changes by comparing the editor text, since
  material.Editor.Layout consumes the editor's input events
  internally and a later FilterEd.Update(gtx) never sees them.
- idPage: persist the manually entered ID with store.SetID (on
  Android nativeIdentities is unimplemented, so there are no id
  buttons to click).
- Use U+2026 (gofont) instead of U+22EE (not in gofont) for the
  menu button.
- Split getConfDir/noidLabelText out into impl_linux.go for the
  Linux build.
This commit is contained in:
Greg Pomerantz 2026-08-21 13:00:25 -04:00
parent af1b878308
commit e7218b1efd
7 changed files with 216 additions and 175 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@ cmd/passgo/passgo
cmd/passgo-gui/passgo-gui
nohup.out
*.apk
*.idsig
/passgo-gui

View File

@ -1,13 +1,13 @@
//+build android
//go:build android
package main
import (
"os"
"git.wow.st/gmp/passgo"
"gioui.org/app"
"gioui.org/io/event"
"git.wow.st/gmp/passgo"
)
var (
@ -22,9 +22,15 @@ func init() {
func handleEvent(e event.Event) {
switch e := e.(type) {
case app.ViewEvent:
case app.AndroidViewEvent:
// A zero View means the view is detached (e.g. the activity going
// away on a recents-wipe); there is nothing to register for a
// detached view. A re-attach arrives as a fresh event with a live
// view.
if e.View != 0 {
initPgp(e.View)
}
}
}
func initPgp(view uintptr) {

View File

@ -1,14 +1,14 @@
//+build darwin
//go:build darwin
package main
import (
"io"
"os"
"os/user"
"path"
"gioui.org/font/opentype"
"gioui.org/text"
)
var (
@ -22,22 +22,18 @@ func setFont() error {
return err
}
fnts, err := opentype.ParseCollectionReaderAt(f)
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log(Info, "Cannot read system font collection")
return err
}
fnts, err := opentype.ParseCollection(data)
if err != nil {
log(Info, "Cannot parse font collection")
return err
}
fnt, err := fnts.Font(0)
if err != nil {
log(Info, "Cannot get font from collection")
return err
}
collection = append(collection, text.FontFace{Font: text.Font{}, Face: fnt})
if err != nil {
log(Info, "Cannot access font from font collection")
return err
}
//font.Register(text.Font{}, face)
collection = append(collection, fnts...)
return nil
}

View File

@ -0,0 +1,29 @@
//go:build linux && !android
package main
import (
"os"
"gioui.org/app"
)
var (
noidLabelText = "No GPG ids available"
)
func getConfDir() (string, error) {
ret, err := app.DataDir()
if err != nil {
log(Error, "Cannot get data directory:", err)
return "", err
}
if _, err := os.Stat(ret); os.IsNotExist(err) {
err = os.MkdirAll(ret, 0700)
if err != nil {
log(Error, "Cannot create configuration directory ", ret)
return "", err
}
}
return ret, nil
}

View File

@ -1,4 +1,4 @@
//+build !android
//go:build !android
package main

View File

@ -1,5 +1,3 @@
// +build darwin linux
package main
import (
@ -16,8 +14,6 @@ import (
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
@ -43,7 +39,8 @@ type conf struct {
}
func main() {
if false { go func() {
if false {
go func() {
f, err := os.Create("cpuprofile")
if err != nil {
fmt.Printf("Can't create CPU profile\n")
@ -60,7 +57,8 @@ func main() {
pprof.StopCPUProfile()
f.Close()
fmt.Printf("CPU profile written\n")
}() }
}()
}
var fd *os.File
var err error
confDir, err = getConfDir()
@ -106,7 +104,7 @@ func main() {
passch = make(chan []byte)
go func() {
log(Info,"passgo.Identities()")
log(Info, "passgo.Identities()")
passgo.Identities()
}()
go Updater()
@ -196,14 +194,22 @@ func saveConf(fds ...*os.File) {
}
func eventLoop() {
if collection == nil {
collection = gofont.Collection()
}
var ops op.Ops
th = material.NewTheme(collection)
th = material.NewTheme()
th.TextSize = unit.Sp(fontSize)
w := app.NewWindow(
if len(collection) > 0 {
// Use the embedded Go font plus the platform fonts (e.g. the
// Unicode font installed on Darwin) instead of the system fonts.
th.Shaper = text.NewShaper(text.NoSystemFonts(),
text.WithCollection(append(gofont.Collection(), collection...)))
} else {
// Always provide the embedded Go font: the system font scan
// finds no fonts on Android, and a fontless shaper renders a
// blank screen.
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
}
w := new(app.Window)
w.Option(
app.Size(unit.Dp(250), unit.Dp(500)),
app.Title("passgo"))
@ -211,13 +217,12 @@ func eventLoop() {
var c1 layout.FlexChild // flex child for title bar
sysinset := &layout.Inset{}
margin := layout.UniformInset(unit.Dp(10))
title := material.Body1(th, "passgo")
dotsBtn := &Button{
Size: unit.Sp(fontSize),
Label: "\xe2\x8b\xae",
Label: "\u2026",
Alignment: text.Middle,
Color: black,
Background: gray,
@ -243,6 +248,7 @@ func eventLoop() {
var overlayStart time.Time
FilterEd := &widget.Editor{SingleLine: true}
var lastFilter string
updateBtns := func() {
passBtns = passBtns[:0]
pathnames = pathnames[:0]
@ -259,7 +265,7 @@ func eventLoop() {
Level: x.Level - 1,
Dir: true,
}
fmt.Printf("addd(): d/n = '%s'/'%s'\n", d,n)
fmt.Printf("addd(): d/n = '%s'/'%s'\n", d, n)
if dirs[d] != true {
addd(x)
}
@ -269,6 +275,7 @@ func eventLoop() {
passBtns = append(passBtns, &Button{
Size: unit.Sp(fontSize),
Label: lbl,
Color: black,
Background: gray,
})
pathnames = append(pathnames, x.Pathname)
@ -288,6 +295,7 @@ func eventLoop() {
passBtns = append(passBtns, &Button{
Size: unit.Sp(fontSize),
Label: lbl,
Color: black,
Background: gray,
})
pathnames = append(pathnames, x.Pathname)
@ -304,7 +312,7 @@ func eventLoop() {
idBtns := make([]*Button, 0)
updateIdBtns := func() {
log(Info,"passgo.Identities()")
log(Info, "passgo.Identities()")
ids, err := passgo.Identities()
if err != nil {
log(Info, err)
@ -335,7 +343,7 @@ func eventLoop() {
}
storeDirLabel := material.Label(th, unit.Sp(fontSize), "Store directory")
storeDirEd := &widget.Editor{ SingleLine: true}
storeDirEd := &widget.Editor{SingleLine: true}
storeDirEd.SetText(store.Dir)
saveBtn := &Button{
Size: unit.Sp(fontSize),
@ -361,7 +369,7 @@ func eventLoop() {
}
promptLabel := material.Label(th, unit.Sp(fontSize), "passphrase")
promptEd := &widget.Editor{ SingleLine: true, Submit: true }
promptEd := &widget.Editor{SingleLine: true, Submit: true}
okBtn := &Button{
Size: unit.Sp(fontSize),
Label: "ok",
@ -386,9 +394,9 @@ func eventLoop() {
insertLabel := material.Label(th, unit.Sp(fontSize), "Insert")
passnameLabel := material.Label(th, unit.Sp(fontSize), "password name:")
passnameEd := &widget.Editor{ SingleLine: true }
passnameEd := &widget.Editor{SingleLine: true}
passvalLabel := material.Label(th, unit.Sp(fontSize), "password value:")
passvalEd := &widget.Editor{ SingleLine: true, Submit: true }
passvalEd := &widget.Editor{SingleLine: true, Submit: true}
noidLabel := material.Label(th, unit.Sp(fontSize), noidLabelText)
idLabel := material.Label(th, unit.Sp(fontSize), "Select ID")
@ -433,6 +441,7 @@ func eventLoop() {
})
if xBtn.Clicked() {
FilterEd.SetText("")
lastFilter = ""
updateBtns()
}
return lpf1.Layout(gtx, c21, c22)
@ -440,9 +449,6 @@ func eventLoop() {
c3 := layout.Flexed(1.0, func(gtx C) D {
var ret D
mux.Lock()
if lst.Dragging() {
key.HideInputOp{}.Add(gtx.Ops)
}
switch {
case store.Empty:
confBtn.Layout(gtx)
@ -543,7 +549,11 @@ func eventLoop() {
if animating && x > end {
animOff()
}
if len(FilterEd.Events()) > 0 {
// material.Editor.Layout (above) already consumes the editor's
// input events, so we detect a filter change by comparing the
// current text against the previous frame's.
if t := FilterEd.Text(); t != lastFilter {
lastFilter = t
updateBtns()
}
return ret
@ -588,19 +598,14 @@ func eventLoop() {
c4 = layout.Rigid(func(gtx C) D {
return material.Editor(th, idEd, "id").Layout(gtx)
})
for _, e := range idEd.Events() {
switch e.(type) {
case widget.SubmitEvent:
log(Info, "Submit")
store.Id = idEd.Text()
page = listPage
}
}
c5 = layout.Rigid(func(gtx C) D {
return idSubmitBtn.Layout(gtx)
})
if idSubmitBtn.Clicked() {
store.Id = idEd.Text()
// Persist the ID so it survives a restart (the manual entry
// path has no keyring to re-derive it from).
store.SetID(store.Id)
page = listPage
}
c6 = layout.Rigid(func(gtx C) D {
@ -632,6 +637,15 @@ func eventLoop() {
}
return ret
})
if ev, ok := idEd.Update(gtx); ok {
if _, isSubmit := ev.(widget.SubmitEvent); isSubmit {
log(Info, "Submit")
store.Id = idEd.Text()
store.SetID(store.Id)
w.Invalidate()
page = listPage
}
}
return flex.Layout(gtx, c1, c2, c3, c4, c5, c6, c7)
}
@ -644,7 +658,7 @@ func eventLoop() {
numBtn.Button = Button{Size: unit.Sp(fontSize), Label: "#"}
symBtn.Select()
numBtn.Select()
lenEd := &widget.Editor{ SingleLine: true, Alignment: text.End }
lenEd := &widget.Editor{SingleLine: true, Alignment: text.End}
lenEd.SetText("15")
lBtn := &Button{Size: unit.Sp(fontSize), Label: "<", Background: gray}
rBtn := &Button{Size: unit.Sp(fontSize), Label: ">", Background: gray}
@ -835,9 +849,8 @@ func eventLoop() {
promptPage = func(gtx C) D {
submit := false
for _, e := range promptEd.Events() {
switch e.(type) {
case widget.SubmitEvent:
if ev, ok := promptEd.Update(gtx); ok {
if _, isSubmit := ev.(widget.SubmitEvent); isSubmit {
log(Info, "Submit")
submit = true
}
@ -882,39 +895,36 @@ func eventLoop() {
ms := &runtime.MemStats{}
x := 0
var mallocs uint64
var idsInit bool
for {
// Window.Event and FrameEvent.Frame must be called from the same
// goroutine; drain the auxiliary channels non-blocking instead of
// bridging the window events through a channel.
select {
case <-updated:
log(Info, "UPDATE")
updateBtns()
w.Invalidate()
default:
}
select {
case <-anim.C:
w.Invalidate()
case e := <-w.Events():
x++
if x == 100 {
runtime.ReadMemStats(ms)
mallocs = ms.Mallocs
default:
}
switch e := e.(type) {
case system.DestroyEvent:
switch e := w.Event().(type) {
case app.DestroyEvent:
os.Exit(0)
case system.StageEvent:
if e.Stage == system.StageRunning {
go func() {
updateIdBtns()
}()
case app.FrameEvent:
x++
if !idsInit { // the app is running; refresh the ID list
idsInit = true
go updateIdBtns()
}
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
// NewContext resets ops and adjusts for e.Insets.
gtx := app.NewContext(&ops, e)
sysinset.Top = e.Insets.Top
sysinset.Bottom = e.Insets.Bottom
sysinset.Left = e.Insets.Left
sysinset.Right = e.Insets.Right
sysinset.Layout(gtx, func(gtx C) D {
return margin.Layout(gtx, func(gtx C) D {
margin.Layout(gtx, func(gtx C) D {
margincs = gtx.Constraints
c1 = layout.Rigid(func(gtx C) D {
ct2 := layout.Rigid(func(gtx C) D {
@ -932,7 +942,6 @@ func eventLoop() {
return page(gtx)
})
})
if dotsBtn.Clicked() {
log(Info, "Configure")
@ -945,7 +954,7 @@ func eventLoop() {
insName, insValue = "", ""
page = insertPage
}
e.Frame(gtx.Ops)
e.Frame(&ops)
default:
handleEvent(e)
}
@ -955,5 +964,4 @@ func eventLoop() {
fmt.Printf("mallocs: %d\n", ms.Mallocs-mallocs)
}
}
}
}

View File

@ -4,9 +4,8 @@ import (
"image"
"image/color"
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/io/event"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
@ -16,18 +15,18 @@ import (
)
var (
black = color.RGBA{A: 0xff, R: 0, G: 0, B: 0}
white = color.RGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}
gray = color.RGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xf0}
darkgray = color.RGBA{A: 0xff, R: 0xa0, G: 0xa0, B: 0xa0}
black = color.NRGBA{A: 0xff, R: 0, G: 0, B: 0}
white = color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}
gray = color.NRGBA{A: 0xff, R: 0xf0, G: 0xf0, B: 0xf0}
darkgray = color.NRGBA{A: 0xff, R: 0xa0, G: 0xa0, B: 0xa0}
)
type Overlay struct {
Size unit.Value
Size unit.Sp
Text string
Click gesture.Click
Color color.RGBA
Background color.RGBA
Color color.NRGBA
Background color.NRGBA
Alignment text.Alignment
}
@ -39,11 +38,9 @@ func (b *Overlay) Layout(gtx C) D {
l := material.Label(th, b.Size, b.Text)
ins := layout.UniformInset(unit.Dp(4))
l.Color = b.Color
ret := ins.Layout(gtx, func(gtx C) D {
return ins.Layout(gtx, func(gtx C) D {
return l.Layout(gtx)
})
pointer.Rect(image.Rect(0, 0, ret.Size.X, ret.Size.Y)).Add(gtx.Ops)
return ret
})
c1 := layout.Expanded(func(gtx C) D {
return layoutRRect(b.Background, gtx)
@ -54,55 +51,58 @@ func (b *Overlay) Layout(gtx C) D {
type SelButton struct {
Button
SelColor color.RGBA
SelColor color.NRGBA
Selected bool
}
type Button struct {
Size unit.Value
Size unit.Sp
Label string
Click gesture.Click
Color color.RGBA
Background color.RGBA
Color color.NRGBA
Background color.NRGBA
Alignment text.Alignment
clicked bool
}
func layoutRRect(col color.RGBA, gtx C) D {
r := float32(gtx.Px(unit.Dp(4)))
func layoutRRect(col color.NRGBA, gtx C) D {
r := gtx.Dp(4)
sz := image.Point{X: gtx.Constraints.Min.X, Y: gtx.Constraints.Min.Y}
w, h := float32(sz.X), float32(sz.Y)
rect := f32.Rectangle{
f32.Point{0, 0},
f32.Point{w, h},
}
//clip.RoundRect(gtx.Ops, rect, r, r, r, r)
clip.RRect{Rect: rect, NE: r, NW: r, SE: r, SW: r}.Add(gtx.Ops)
// Push the rounded-rect clip, paint, then restore the previous clip.
stk := clip.UniformRRect(image.Rectangle{Max: sz}, r).Push(gtx.Ops)
paint.ColorOp{Color: col}.Add(gtx.Ops)
paint.PaintOp{Rect: f32.Rectangle{Max: f32.Point{X: w, Y: h}}}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stk.Pop()
return layout.Dimensions{Size: sz}
}
func (b *Button) Layout(gtx C) D {
mwidth := gtx.Constraints.Min.X
b.clicked = false
for _, ev := range b.Click.Events(gtx) {
if ev.Type == gesture.TypeClick {
b.clicked = true
}
}
ins := layout.UniformInset(unit.Dp(1))
return ins.Layout(gtx, func(gtx C) D {
st := layout.Stack{}
c2 := layout.Stacked(func(gtx C) D {
l := material.Label(th, b.Size, b.Label)
l.Color = b.Color
ins := layout.UniformInset(unit.Dp(4))
//paint.ColorOp{Color: b.Color}.Add(ops)
ret := ins.Layout(gtx, func(gtx C) D {
return l.Layout(gtx)
})
pointer.Rect(image.Rect(0, 0, ret.Size.X, ret.Size.Y)).Add(gtx.Ops)
// Register the click handler for the label area.
stk := clip.Rect(image.Rectangle{Max: ret.Size}).Push(gtx.Ops)
event.Op(gtx.Ops, b)
b.Click.Add(gtx.Ops)
stk.Pop()
for {
ev, ok := b.Click.Update(gtx.Source)
if !ok {
break
}
if ev.Kind == gesture.KindClick {
b.clicked = true
}
}
return ret
})
c1 := layout.Expanded(func(gtx C) D {