# godcr review -rw-r--r-- 6.6 KiB View raw
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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