Life of a tap
state.Update, which asks the event loop for one redraw.The part about the screen showed the Event loopEvent loop: The one goroutine that polls for input and firmware messages every 20 ms and calls the app. It is the only one that draws.: one goroutine that polls the firmware every 20 ms and calls the app. The part about books showed how a page is laid out. This part is about the app itself: what it does with those calls. We start with the most common one, a tap, and follow it all the way to the panel.
Two constraints shape the whole path. Only one goroutine may draw, since the panel and the FramebufferFramebuffer: The block of memory (/dev/fb0) the panel controller reads pixels from. Writing to it changes nothing on screen until a refresh. cannot be shared. And the app is never only doing one thing: books download, covers load and pages are prepared in the background, all while you tap. So the app needs one place where everything it knows lives, and a rule for who may change it, and when.
Screens: one package each​
A ScreenScreen: One page of the app’s UI (library, series, reader…), a Go package that draws itself and handles its taps. is one page of the app's interface: the series list, a series' book
list, the reader, the cover view, the contents of a book. Each one is a Go package under
internal/app/screens/, and implements a two-method interface:
type Screen interface {
// Draws the screen into the canvas, whole, and registers its targets.
// Runs with the state's mutex held.
DrawLocked()
// Performs a command of the reader's buttons (Previous, Next, Back, Menu).
// Runs without the mutex.
Command(order widgets.Command)
}
Screens never import each other. The book list does not know the series list exists: to go
back, it calls state.ShowLocked(state.SeriesScreen), which only changes which screen is shown
and drops the targets of the one left. What another screen must learn, such as the progress of
a book just closed, goes through the services (catalog, openbook…), which every screen may
use. (The screens/list package is not a screen: it draws the header, rows and footer that
the series list and the book list share, and the contents screen borrows its header and footer.)
A screen may also implement a few optional interfaces, which the app checks with a type
assertion: touchable (the reader and the cover view decide what each touch does
themselves), swipeable (a swipe turns a list's pages), overlaid (a list's hold menu closes
on any tap) and selfRefreshing (the reader refreshes only the part of the panel that
changed).
The State and its one mutex​
Everything the app knows while it runs is in one struct, the StateState: Everything the app knows (current screen, books, downloads…), in one struct guarded by one mutex.
(state.State): the screen shown, its targets, the lists, the open book, the download queue,
the covers, and Repaint, what the next draw must do. Its first group of fields is guarded by
one MutexMutex: A lock: only one goroutine at a time may hold it, so they never change the same data at once.. A screen draws with that mutex held, so it always sees a consistent
state: never a list half updated by a download.
The rule for changing it is one function, state.Updatestate.Update: The only way another goroutine changes the state: take the lock, change it, and ask for one repaint if needed.:
func (app *State) Update(fullRefresh bool, change func() bool) {
app.mu.Lock()
repaint := false
if change() { // something visible changed
app.Repaint.FullRefresh = app.Repaint.FullRefresh || fullRefresh
repaint = !app.Repaint.Queued && !app.Repaint.Closing
app.Repaint.Queued = true
}
app.mu.Unlock()
if repaint {
pocketbook.Repaint()
}
}
Notice what Update does not do: draw. It only asks for a draw, by dropping a token in a
channel the event loop watches.
Targets: registered while drawing​
How does the app know what is under your finger? Every screen, while it draws a button, also registers a TargetTarget: A rectangle a screen registers while it draws, with what to do when it is tapped.: a rectangle, and what to do when it is tapped.
type Target struct {
Rect image.Rectangle
Tap func() // runs when a tap starts and ends in Rect
Hold func() // runs when a touch in Rect is held
Command Command // when set, a tap acts as that command instead
}
Since the targets are made by the very code that draws the buttons, they always match what the
panel shows. There is no separate table of coordinates to keep in sync. Draw clears them
before every redraw, and the screen registers them again as it draws.
The path of a tap​
Now the whole path. Tap a button on the reader below, and watch the three lanes and the
padlock. Two things to notice: the mutex is never held while a button's work runs, and nothing
reaches the panel until the event loop calls Draw.
Downloaded · page 12
In words:
Pointer(PointerDown)records where the touch started (touchStart). Nothing else.Pointer(PointerUp)takes the mutex and callstouchTargetLocked(start, end): if the screen istouchable, it decides; else, if the touch moved more than a fifth of the screen width sideways (and twice as far sideways as up or down) on aswipeablescreen, it is a swipe and becomesNextorPrevious; elseTargets.Hitreturns the first target the touch started and ended in. Then the mutex is released.run(tapped), without the mutex: a target with aCommandgoes throughcommand, just as a press of the reader's own button would; otherwise itsTapclosure runs.- The closure or command calls the services, which change the state with
state.Update. - If the change returns true,
UpdatesetsRepaint.Queuedand callspocketbook.Repaint()once. - The event loop sees the request and calls
Draw: lock, clear the targets, then eitherDrawAndRefreshLockedfor a self-refreshing screen (the reader), orClearScreen, the screen'sDrawLockedand a refresh: a full, flashing one iffullRefreshwas asked for or the draw did not come fromUpdate, a partialGC16one otherwise. Unlock.
Why release the mutex before running the target? Because what it runs takes the mutex itself,
through Update. Go's mutexes are not reentrant: a goroutine that locks a mutex it already
holds waits for itself, forever. That is the reason closures run "after the app's lock is
released, as they take it themselves", as targets.go says.
Many updates, one draw​
A download reports its progress many times a second. The cover loader finishes a cover, then
another. If each change meant a redraw, the panel would spend its life refreshing, and an e-ink
refresh is slow. So Update coalesces: only the Update that finds Repaint.Queued false calls
Repaint, and the channel behind Repaint holds one request anyway. One Draw then shows
every change made before it.
How Inkshelf does it​
application(internal/app/app.go) is thepocketbook.App:Init,Draw,Key,Pointer,System,Close, all called on the event loop's goroutine.Keymaps the reader's buttons towidgets.Commands (Next,Previous,Back,Menu); a tap on a target with aCommandtakes the same road.DrawcomputesfullRefresh := app.Repaint.FullRefresh || !app.Repaint.Queued: aDrawthatUpdatedid not ask for comes from the start or from the app coming back to the foreground, when the panel may show anything, so it refreshes the whole panel.
Going deeper
- Overlays. On a list, any tap first closes the hold menu or the A–Z jump, unless the target
is the one that opens them (
KeepsOverlays). That is anUpdatewhose change returns false when nothing was open, so it costs no redraw. A command over an open overlay only closes it: Back closes a menu rather than leaving the list. - Long presses.
PointerLonghit-tests the same way, from the start point, and runs the target'sHold. ThePointerUpthat follows does nothing. - Progress redraws. A download's progress callback is an
Updatewhose change,noteProgressLocked, returns true only on the screens that show it, for the first page, and then at most every 2 s (progressRepaint): each redraw takes the CPU from the download. - Closing.
ClosesetsRepaint.Closing, after whichUpdatenever asks for a repaint. - The emulator counts background work (
state.Work) so that a script's screenshot waits for the work the last tap started. The app itself never reads it.
Where it lives in the code​
internal/app/input.go · Pointer, touchTargetLocked, run, commandFrom a touch to the target or command it means.internal/app/app.go · DrawLocks, clears the targets, draws the screen shown and refreshes the panel.internal/app/screens.go · ScreenThe interface of a screen, and the optional ones (touchable, swipeable, overlaid, selfRefreshing).internal/app/state/state.go · Update, ShowLocked, RepaintThe one way to change the state, and the switch between screens.internal/app/widgets/targets.go · Target, Targets.HitRectangles registered while drawing, and the hit test.internal/device/pocketbook/app.go · RepaintDrops a request in a channel that holds one; the event loop calls Draw.
The full story: App threading.
Next: a tap is one goroutine's story, but several run at once. Threads and locks gives the rules that keep them apart.