diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 5a8e44d..9f9606e 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -44,7 +44,7 @@ type App struct { // mu protects the following fields. mu sync.Mutex // netState is the most recent network state. - netState NetworkState + netState BackendState // browseURL is set whenever the backend wants to // browse. browseURL *string @@ -54,14 +54,14 @@ type App struct { type clientState struct { browseURL string - net NetworkState + backend BackendState // query is the search query, in lowercase. query string Peers []UIPeer } -type NetworkState struct { +type BackendState struct { State ipn.State NetworkMap *controlclient.NetworkMap LostInternet bool @@ -160,9 +160,12 @@ func (a *App) runBackend() error { notifications <- n }) }() - var cfg *router.Config - var state NetworkState - var service jni.Object + var ( + cfg *router.Config + state BackendState + service jni.Object + signingIn bool + ) for { select { case err := <-startErr: @@ -215,6 +218,7 @@ func (a *App) runBackend() error { a.notify(state) } if u := n.BrowseToURL; u != nil { + signingIn = false a.setURL(*u) } if m := n.NetMap; m != nil { @@ -234,7 +238,10 @@ func (a *App) runBackend() error { case e := <-a.backend: switch e := e.(type) { case ReauthEvent: - go b.backend.StartLoginInteractive() + if !signingIn { + go b.backend.StartLoginInteractive() + signingIn = true + } case LogoutEvent: go b.backend.Logout() case ConnectEvent: @@ -374,7 +381,7 @@ func (a *App) notifyVPNClosed() { } } -func (a *App) notify(state NetworkState) { +func (a *App) notify(state BackendState) { a.mu.Lock() a.netState = state a.mu.Unlock() @@ -420,11 +427,12 @@ func (a *App) runUI() error { a.request(ConnectEvent{Enable: false}) case <-a.updates: a.mu.Lock() - oldState := state.net.State - state.net = a.netState + oldState := state.backend.State + state.backend = a.netState if a.browseURL != nil { state.browseURL = *a.browseURL a.browseURL = nil + ui.signinType = noSignin } if a.prefs != nil { ui.enabled.Value = a.prefs.WantRunning @@ -434,7 +442,7 @@ func (a *App) runUI() error { a.updateState(peer, state) w.Invalidate() if peer != 0 { - newState := state.net.State + newState := state.backend.State // Start VPN if we just logged in. if oldState <= ipn.Stopped && newState > ipn.Stopped { if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil { @@ -444,7 +452,7 @@ func (a *App) runUI() error { } case peer = <-onPeerCreated: w.Invalidate() - if state.net.State > ipn.Stopped { + if state.backend.State > ipn.Stopped { if err := a.callVoidMethod(peer, "prepareVPN", "()V"); err != nil { return err } @@ -459,7 +467,7 @@ func (a *App) runUI() error { return nil }) case <-vpnPrepared: - if state.net.State > ipn.Stopped { + if state.backend.State > ipn.Stopped { if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil { return err } @@ -502,7 +510,7 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) { } state.Peers = nil - netMap := state.net.NetworkMap + netMap := state.backend.NetworkMap if netMap == nil { return } @@ -534,7 +542,7 @@ func (a *App) updateState(javaPeer jni.Object, state *clientState) { name = strings.ToUpper(name) peers = append(peers, UIPeer{Owner: u, Name: name}) } - myID := state.net.NetworkMap.User + myID := state.backend.NetworkMap.User sort.Slice(peers, func(i, j int) bool { lhs, rhs := peers[i], peers[j] if lu, ru := lhs.Owner, rhs.Owner; ru != lu { diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go index 5598a94..1282504 100644 --- a/cmd/tailscale/ui.go +++ b/cmd/tailscale/ui.go @@ -51,6 +51,8 @@ type UI struct { // googleSignin is the button for native Google Sign-in googleSignin widget.Clickable + signinType signinType + self widget.Clickable peers []widget.Clickable @@ -86,6 +88,8 @@ type UI struct { events []UIEvent } +type signinType uint8 + // An UIPeer is either a peer or a section header // with the user information. type UIPeer struct { @@ -108,6 +112,12 @@ const ( keyShowIntro = "ui.showintro" ) +const ( + noSignin signinType = iota + webSignin + googleSignin +) + type ( C = layout.Context D = layout.Dimensions @@ -175,7 +185,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat ui.menu.show = !ui.menu.show } - netmap := state.net.NetworkMap + netmap := state.backend.NetworkMap var localName, localAddr string var expiry time.Time if netmap != nil { @@ -186,12 +196,16 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat } } - if ui.googleSignin.Clicked() { - ui.events = append(ui.events, GoogleAuthEvent{}) - } + if ui.signinType == noSignin { + if ui.googleSignin.Clicked() { + ui.signinType = googleSignin + ui.events = append(ui.events, GoogleAuthEvent{}) + } - if ui.webSignin.Clicked() { - ui.events = append(ui.events, ReauthEvent{}) + if ui.webSignin.Clicked() { + ui.signinType = webSignin + ui.events = append(ui.events, ReauthEvent{}) + } } if ui.menuClicked(&ui.menu.copy) && localAddr != "" { @@ -215,7 +229,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat const numHeaders = 5 n := numHeaders + len(state.Peers) - needsLogin := state.net.State == ipn.NeedsLogin + needsLogin := state.backend.State == ipn.NeedsLogin ui.root.Layout(gtx, n, func(gtx C, idx int) D { var in layout.Inset if idx == n-1 { @@ -226,14 +240,14 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat return in.Layout(gtx, func(gtx C) D { switch idx { case 0: - return ui.layoutTop(gtx, sysIns, &state.net) + return ui.layoutTop(gtx, sysIns, &state.backend) case 1: - if netmap == nil || state.net.State < ipn.Stopped { + if netmap == nil || state.backend.State < ipn.Stopped { return D{} } return ui.layoutLocal(gtx, sysIns, localName, localAddr) case 2: - if state.net.State < ipn.Stopped { + if state.backend.State < ipn.Stopped { return D{} } return ui.layoutSearchbar(gtx, sysIns) @@ -241,9 +255,9 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat if !needsLogin { return D{} } - return ui.layoutSignIn(gtx) + return ui.layoutSignIn(gtx, &state.backend) case 4: - if needsLogin || !state.net.LostInternet { + if needsLogin || !state.backend.LostInternet { return D{} } return ui.layoutDisconnected(gtx) @@ -309,7 +323,7 @@ func (d *Dismiss) Dismissed(gtx layout.Context) bool { } // layoutSignIn lays out the sign in button(s). -func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions { +func (ui *UI) layoutSignIn(gtx layout.Context, state *BackendState) layout.Dimensions { return layout.Inset{Top: unit.Dp(48), Left: unit.Dp(48), Right: unit.Dp(48)}.Layout(gtx, func(gtx C) D { const ( textColor = 0x555555 @@ -328,24 +342,26 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions { signin := material.ButtonLayout(ui.theme, &ui.googleSignin) signin.Background = rgb(white) - return border.Layout(gtx, func(gtx C) D { - return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { - return signin.Layout(gtx, func(gtx C) D { - gtx.Constraints.Max.Y = gtx.Px(unit.Dp(48)) - return layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx C) D { - return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D { - return drawImage(gtx, ui.icons.google, unit.Dp(16)) - }) - }), - layout.Rigid(func(gtx C) D { - return layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D { - l := material.Body2(ui.theme, "Sign in with Google") - l.Color = rgb(textColor) - return l.Layout(gtx) - }) - }), - ) + return ui.withLoader(gtx, ui.signinType == googleSignin, func(gtx C) D { + return border.Layout(gtx, func(gtx C) D { + return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { + return signin.Layout(gtx, func(gtx C) D { + gtx.Constraints.Max.Y = gtx.Px(unit.Dp(48)) + return layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx C) D { + return layout.Inset{Right: unit.Dp(4)}.Layout(gtx, func(gtx C) D { + return drawImage(gtx, ui.icons.google, unit.Dp(16)) + }) + }), + layout.Rigid(func(gtx C) D { + return layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10)}.Layout(gtx, func(gtx C) D { + l := material.Body2(ui.theme, "Sign in with Google") + l.Color = rgb(textColor) + return l.Layout(gtx) + }) + }), + ) + }) }) }) }) @@ -356,13 +372,15 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions { if !enableGoogleSignin { label = "Sign in" } - return border.Layout(gtx, func(gtx C) D { - return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { - signin := material.Button(ui.theme, &ui.webSignin, label) - signin.Background = rgb(signinColor) - signin.Color = rgb(textColor) - signin.Background = rgb(white) - return signin.Layout(gtx) + return ui.withLoader(gtx, ui.signinType == webSignin, func(gtx C) D { + return border.Layout(gtx, func(gtx C) D { + return layout.UniformInset(unit.Px(2)).Layout(gtx, func(gtx C) D { + signin := material.Button(ui.theme, &ui.webSignin, label) + signin.Background = rgb(signinColor) + signin.Color = rgb(textColor) + signin.Background = rgb(white) + return signin.Layout(gtx) + }) }) }) }), @@ -370,6 +388,27 @@ func (ui *UI) layoutSignIn(gtx layout.Context) layout.Dimensions { }) } +func (ui *UI) withLoader(gtx layout.Context, loading bool, w layout.Widget) layout.Dimensions { + cons := gtx.Constraints + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(func(gtx C) D { + gtx.Constraints = cons + return w(gtx) + }), + layout.Stacked(func(gtx C) D { + if !loading { + return D{} + } + return layout.Inset{Left: unit.Dp(16)}.Layout(gtx, func(gtx C) D { + gtx.Constraints.Min = image.Point{ + X: gtx.Px(unit.Dp(16)), + } + return material.Loader(ui.theme).Layout(gtx) + }) + }), + ) +} + // layoutDisconnected lays out the "please connect to the internet" // message. func (ui *UI) layoutDisconnected(gtx layout.Context) layout.Dimensions { @@ -624,7 +663,7 @@ func (ui *UI) layoutSection(gtx layout.Context, sysIns system.Insets, title stri } // layoutTop lays out the top controls: toggle, status and menu dots. -func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *NetworkState) layout.Dimensions { +func (ui *UI) layoutTop(gtx layout.Context, sysIns system.Insets, state *BackendState) layout.Dimensions { in := layout.Inset{ Top: unit.Dp(16), Bottom: unit.Dp(16), diff --git a/go.mod b/go.mod index a7fcd31..af9aeac 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b - gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb + gioui.org v0.0.0-20200709135439-29f820caaac9 gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43 github.com/go-bindata/go-bindata v3.1.2+incompatible github.com/tailscale/wireguard-go v0.0.0-20200615180905-687c10194779 diff --git a/go.sum b/go.sum index 30b327f..9632736 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b h1:J9r7EuPdhvBTafg34EqrObAm/bDEaDh7LvhKJPGficE= eliasnaur.com/font v0.0.0-20200617114307-e02d32decb4b/go.mod h1:CYwJpIhpzVfoHpFXGlXjSx9mXMWtHt4XXmZb6RjumRc= gioui.org v0.0.0-20200622101735-5368743478e0/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= -gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb h1:+jJBzbEtW03w0+fAfhPCwmN0/8xN1CJ94lbfV2eSKhs= -gioui.org v0.0.0-20200630184435-6ef1ff7cfbfb/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= +gioui.org v0.0.0-20200709135439-29f820caaac9 h1:dFKWoxIzFlW2e8WTMWYOvGDAosSruMHnS66UBpzdYeU= +gioui.org v0.0.0-20200709135439-29f820caaac9/go.mod h1:jiUwifN9cRl/zmco43aAqh0aV+s9GbhG13KcD+gEpkU= gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43 h1:Wj8OoCIw06dNSSSPAAipnmcG7dbFn+7Et9IY37e1HBU= gioui.org/cmd v0.0.0-20200622185735-5bd0ecea5e43/go.mod h1:KrsGUGWoPetiRyuDmOd/GTNCBFi2u4UbESTFXZ5YqXY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=