package ui import ( "image" "time" "gioui.org/app" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/unit" "gioui.org/widget/material" ) // toast is a transient top-of-window notification ("Copied", error // messages, etc). It is non-modal: drawing it does not consume pointer // input. Lifetime is governed by `until`; the next frame after the // deadline simply skips drawing. type toast struct { msg string until time.Time } const toastDuration = 1500 * time.Millisecond // Show stores the message and schedules an Invalidate after the toast // expires so the window redraws and removes it without waiting for the // next user interaction. func (t *toast) Show(msg string, w *app.Window) { t.msg = msg t.until = time.Now().Add(toastDuration) go func(deadline time.Time) { time.Sleep(time.Until(deadline) + 50*time.Millisecond) w.Invalidate() }(t.until) } // active reports whether the toast should be drawn this frame. func (t *toast) active() bool { return t.msg != "" && time.Now().Before(t.until) } // draw paints the toast as an overlay centered near the top of gtx. // Call AFTER laying out the rest of the frame so it stacks on top. func (t *toast) draw(gtx layout.Context, th *material.Theme) { if !t.active() { return } bg := activePalette.ToastBg fg := activePalette.ToastFg macro := op.Record(gtx.Ops) pad := layout.UniformInset(unit.Dp(10)) dims := pad.Layout(gtx, func(gtx layout.Context) layout.Dimensions { lbl := material.Body2(th, t.msg) lbl.Color = fg return lbl.Layout(gtx) }) call := macro.Stop() // Center horizontally near the top. x := (gtx.Constraints.Max.X - dims.Size.X) / 2 if x < 0 { x = 0 } y := gtx.Dp(unit.Dp(12)) stack := op.Offset(image.Point{X: x, Y: y}).Push(gtx.Ops) rrect := clip.RRect{ Rect: image.Rectangle{Max: dims.Size}, SE: gtx.Dp(unit.Dp(6)), SW: gtx.Dp(unit.Dp(6)), NE: gtx.Dp(unit.Dp(6)), NW: gtx.Dp(unit.Dp(6)), } bgArea := rrect.Push(gtx.Ops) paint.ColorOp{Color: bg}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) bgArea.Pop() call.Add(gtx.Ops) stack.Pop() }