godcr review, May 2021 The following is a high-level review of the godcr UI code, contained in the `ui` directory of the godcr repository[0]. Note that the review does not cover other aspects of the repository, such as security concerns or wallet operation. In particular, the `wallet` directory is not covered. == Top-level app.Window reference To maximize testability and to avoid touching global window state by accident, I recommend restricting access to the app.Window reference to top-level code, typically the event loop. The godcr/ui.Window struct contains a reference to the Gio app.Window. I suggest removing that and add *app.Window parameters to the `Loop` and `unhandled` methods. The godcr/ui.Window.refresh methods calls app.Window.Invalidate. I suggest using op.InvalidateOp and adding a layout.Context or op.Ops parameter to `refresh`. == Clipboard access godcr/ui.Window uses the raw app.Window.ReadClipboard and WriteClipboard methods for system clipboard access. Unless top-level clipboard access really is needed, I suggest using the gioui.org/op/clipboard API instead. It contains tagged ReadOp and WriteOp operations that can be used from any Gio widget, without access to an app.Window. Gio automatically takes care of multiplexing multiple clipboard readers and writers. == layout.Widget vs func(gtx C) D Running $ grep "func.*func\(gtx C\) D" $ grep "var.*func\(gtx C\) D" reveals multiple uses of `func(gtx C) D` as an explicit type. I suggest using the clearer `layout.Widget` type instead. == Brittle strings.Contains checks Running $ grep strings.Contains returns several instances of strings.Contains whose result may change for unrelated reasons. For example, the godcr/ui.pageCommon.walletAccountsHandler method contains the following three checks: if strings.Contains(page.wallAcctSelector.title, "Sending") { ... } if strings.Contains(page.wallAcctSelector.title, "Receiving") { ... } if strings.Contains(page.wallAcctSelector.title, "Purchasing") { ... } The title checks probably works today but may break in the future when, say, the wallet is localized to other languages. Some equals comparisons are also suspect, such as the if windex == 0 && page.wallAcctSelector.sendOption == "Address" check in pageCommon.walletAccountModalLayout: the sendOption value is user-visible. Error message checks such as the one in *pageCommon.walletAccountsHandler are another instance of this problem: strings.Contains(err.Error(), "invalid") I suggest using errors.Is as a more robust replacement. In this case, the error comes from strconv.ParseFloat where the concrete error type is always *strconv.NumError. There are brittle error equals comparisons as well. Example: if err.Error() == "exists" { errText = "Wallet name already exists" } == Explicit DPI conversions $ grep "\.PxPer returns a few instances of explicit use of the pixel-to-dp scale. The pageCommon.layoutNavDrawer converts from Dps to pixels: gtx.Constraints.Min.X = int(gtx.Metric.PxPerDp) * width However, PxPerDp may not be integral; a better version is gtx.Constraints.Min.X = int(gtx.Metric.PxPerDp * width) Even better is to change the type of width, navDrawerWidth, navDrawerMinimizedWidth to unit.Value, and use gtx.Constraints.Min.X = gtx.Px(width) A related issue is the godcr/ui/decredmaterial.pxf function; it is a copy of gioui.org/unit.Metric.Px except that it special-cases the zero scale. I think it is better to never construct zero-valued unit.Metrics and use the standard Px method instead. == Use of goroutines for UI tasks This is a common pattern for displaying a modal dialog: go func() { common.modalReceiver <- &modalLoad{ ... } }() I'm uncomfortable with the use of non-deterministic and racy goroutines for what is essentially a single-threaded concept: a list of modals waiting to be displayed. I suggest adding a function for adding a modal to a global list of modals. If a modal must be displayed as a result of an external event, use a separate channel for those events and construct the modals in the same goroutine as the rest of the UI code. == Smaller issues: I find the `margin` parameter of the godcr/ui/decredmaterial.Model.Layout method unclear: func (m *Modal) Layout(gtx layout.Context, widgets []func(gtx C) D, margin int) layout.Dimensions as used in the computation scaled := 3840 / float32(gtx.Constraints.Max.X) mg := unit.Px(float32(margin) / scaled) The file "compontents.go" has an extra "t" in its name. pageCommon methods use pointer receivers, but pageCommon.layoutNavDrawer does not. Further, functions such as godcr/ui.transactionRow take a pageCommon value: func transactionRow(gtx layout.Context, common pageCommon, row TransactionRow) layout.Dimensions It's more consistent and efficient to use *pageCommon values. Several layout.Widget wrappers are redundant. Example: layout.Rigid(func(gtx C) D { return leftWidget(gtx) }), layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D { return rightWidget(gtx) }) }), reduces to just layout.Rigid(leftWidget), layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, rightWidget) }), In general, a wrapper around a single return statement can often be omitted. The check if i != 32 { pg.seedEditors.editors[i+1].Editor.Focus() } in create_restore_page.go seems suspect; 32 is magic and its purpose unclear. There's a redundant cast in the same file: if pg.wal.LoadedWalletsCount() > int32(0) Same file, a typo: var msg = "You are about clearing ... The constants in modal_templates.go should be wrapped in a single const ( ) section. The godcr/ui.modalLoad.cancel field is an `interface{}` but it seems to me it could be tightened to `func()`. pageCommon.fetchExchangeValue returns a nil error when JSON decoding fails. Its only caller ignores the error, however. The goroutine in Window.updateStates seems racy. It also seems unnecessary. The timer callback in pageCommon.Layout sets page.toastLoad.text without synchronization with the UI goroutine: page.toastLoad.Timer(time.Second*3, func() { page.toastLoad.text = "" }) godcr/ui/decredmaterial.Badge.Layout contains DPI-dependent constants: min := image.Point{ X: 23, Y: 23, } The godcr/ui/decredmaterial.Editor.editor constructs up to two icons from vector data per frame if isPassword is true: icon := mustIcon(widget.NewIcon(icons.ActionVisibilityOff)) if e.Editor.Mask == '*' { icon = mustIcon(widget.NewIcon(icons.ActionVisibility)) } I suggest caching the icons. The "png" import in godcr/ui/decredmaterial/icon.go is irrelevant; the icon data is in IconVG format, not PNG. In general, side-effect imports in non-main packages are discouraged. [0] https://github.com/planetdecred/godcr