diff options
Diffstat (limited to 'vendor/gioui.org/app/window.go')
-rw-r--r-- | vendor/gioui.org/app/window.go | 734 |
1 files changed, 516 insertions, 218 deletions
diff --git a/vendor/gioui.org/app/window.go b/vendor/gioui.org/app/window.go index 2c9860c..ada92fb 100644 --- a/vendor/gioui.org/app/window.go +++ b/vendor/gioui.org/app/window.go @@ -6,10 +6,14 @@ import ( "errors" "fmt" "image" + "image/color" + "runtime" "time" - "gioui.org/app/internal/window" + "gioui.org/f32" + "gioui.org/gpu" "gioui.org/io/event" + "gioui.org/io/pointer" "gioui.org/io/profile" "gioui.org/io/router" "gioui.org/io/system" @@ -19,24 +23,32 @@ import ( _ "gioui.org/app/internal/log" ) -// WindowOption configures a Window. -type Option func(opts *window.Options) +// Option configures a window. +type Option func(unit.Metric, *Config) // Window represents an operating system window. type Window struct { - driver window.Driver - loop *renderLoop + ctx context + gpu gpu.GPU // driverFuncs is a channel of functions to run when // the Window has a valid driver. - driverFuncs chan func() - - out chan event.Event - in chan event.Event - ack chan struct{} - invalidates chan struct{} - frames chan *op.Ops - frameAck chan struct{} + driverFuncs chan func(d driver) + // wakeups wakes up the native event loop to send a + // WakeupEvent that flushes driverFuncs. + wakeups chan struct{} + // wakeupFuncs is sent wakeup functions when the driver changes. + wakeupFuncs chan func() + // redraws is notified when a redraw is requested by the client. + redraws chan struct{} + // immediateRedraws is like redraw but doesn't need a wakeup. + immediateRedraws chan struct{} + // scheduledRedraws is sent the most recent delayed redraw time. + scheduledRedraws chan time.Time + + out chan event.Event + frames chan *op.Ops + frameAck chan struct{} // dead is closed when the window is destroyed. dead chan struct{} @@ -46,13 +58,33 @@ type Window struct { nextFrame time.Time delayedDraw *time.Timer - queue queue + queue queue + cursor pointer.CursorName callbacks callbacks + + nocontext bool + + // semantic data, lazily evaluated if requested by a backend to speed up + // the cases where semantic data is not needed. + semantic struct { + // uptodate tracks whether the fields below are up to date. + uptodate bool + root router.SemanticID + prevTree []router.SemanticNode + tree []router.SemanticNode + ids map[router.SemanticID]router.SemanticNode + } +} + +type semanticResult struct { + found bool + node router.SemanticNode } type callbacks struct { w *Window + d driver } // queue is an event.Queue implementation that distributes system events @@ -61,12 +93,6 @@ type queue struct { q router.Router } -// driverEvent is sent when a new native driver -// is available for the Window. -type driverEvent struct { - driver window.Driver -} - // Pre-allocate the ack event to avoid garbage. var ackEvent event.Event @@ -74,35 +100,37 @@ var ackEvent event.Event // options. The options are hints; the platform is free to // ignore or adjust them. // -// If the current program is running on iOS and Android, +// If the current program is running on iOS or Android, // NewWindow returns the window previously created by the // platform. // // Calling NewWindow more than once is not supported on // iOS, Android, WebAssembly. func NewWindow(options ...Option) *Window { - opts := &window.Options{ - Width: unit.Dp(800), - Height: unit.Dp(600), - Title: "Gio", - } - - for _, o := range options { - o(opts) + defaultOptions := []Option{ + Size(unit.Dp(800), unit.Dp(600)), + Title("Gio"), } + options = append(defaultOptions, options...) + var cnf Config + cnf.apply(unit.Metric{}, options) w := &Window{ - in: make(chan event.Event), - out: make(chan event.Event), - ack: make(chan struct{}), - invalidates: make(chan struct{}, 1), - frames: make(chan *op.Ops), - frameAck: make(chan struct{}), - driverFuncs: make(chan func()), - dead: make(chan struct{}), + out: make(chan event.Event), + immediateRedraws: make(chan struct{}, 0), + redraws: make(chan struct{}, 1), + scheduledRedraws: make(chan time.Time, 1), + frames: make(chan *op.Ops), + frameAck: make(chan struct{}), + driverFuncs: make(chan func(d driver), 1), + wakeups: make(chan struct{}, 1), + wakeupFuncs: make(chan func()), + dead: make(chan struct{}), + nocontext: cnf.CustomRenderer, } + w.semantic.ids = make(map[router.SemanticID]router.SemanticNode) w.callbacks.w = w - go w.run(opts) + go w.run(options) return w } @@ -111,141 +139,260 @@ func (w *Window) Events() <-chan event.Event { return w.out } -// update updates the Window. Paint operations updates the -// window contents, input operations declare input handlers, -// and so on. The supplied operations list completely replaces -// the window state from previous calls. +// update updates the window contents, input operations declare input handlers, +// and so on. The supplied operations list completely replaces the window state +// from previous calls. func (w *Window) update(frame *op.Ops) { w.frames <- frame <-w.frameAck } -func (w *Window) validateAndProcess(frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error { +func (w *Window) validateAndProcess(d driver, frameStart time.Time, size image.Point, sync bool, frame *op.Ops) error { for { - if w.loop != nil { - if err := w.loop.Flush(); err != nil { + if w.gpu == nil && !w.nocontext { + var err error + if w.ctx == nil { + w.ctx, err = d.NewContext() + if err != nil { + return err + } + sync = true + } + } + if sync && w.ctx != nil { + if err := w.ctx.Refresh(); err != nil { + if errors.Is(err, errOutOfDate) { + // Surface couldn't be created for transient reasons. Skip + // this frame and wait for the next. + return nil + } w.destroyGPU() - if err == window.ErrDeviceLost { + if errors.Is(err, gpu.ErrDeviceLost) { continue } return err } } - if w.loop == nil { - var ctx window.Context - ctx, err := w.driver.NewContext() - if err != nil { + if w.gpu == nil && !w.nocontext { + if err := w.ctx.Lock(); err != nil { + w.destroyGPU() return err } - w.loop, err = newLoop(ctx) + gpu, err := gpu.New(w.ctx.API()) + w.ctx.Unlock() if err != nil { - ctx.Release() + w.destroyGPU() return err } + w.gpu = gpu } - w.processFrame(frameStart, size, frame) - if sync { - if err := w.loop.Flush(); err != nil { + if w.gpu != nil { + if err := w.render(frame, size); err != nil { + if errors.Is(err, errOutOfDate) { + // GPU surface needs refreshing. + sync = true + continue + } w.destroyGPU() - if err == window.ErrDeviceLost { + if errors.Is(err, gpu.ErrDeviceLost) { continue } return err } } + w.processFrame(d, frameStart, frame) return nil } } -func (w *Window) processFrame(frameStart time.Time, size image.Point, frame *op.Ops) { - sync := w.loop.Draw(size, frame) +func (w *Window) render(frame *op.Ops, viewport image.Point) error { + if err := w.ctx.Lock(); err != nil { + return err + } + defer w.ctx.Unlock() + if runtime.GOOS == "js" { + // Use transparent black when Gio is embedded, to allow mixing of Gio and + // foreign content below. + w.gpu.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) + } else { + w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + } + target, err := w.ctx.RenderTarget() + if err != nil { + return err + } + if err := w.gpu.Frame(frame, target, viewport); err != nil { + return err + } + return w.ctx.Present() +} + +func (w *Window) processFrame(d driver, frameStart time.Time, frame *op.Ops) { w.queue.q.Frame(frame) + for k := range w.semantic.ids { + delete(w.semantic.ids, k) + } + w.semantic.uptodate = false switch w.queue.q.TextInputState() { case router.TextInputOpen: - w.driver.ShowTextInput(true) + d.ShowTextInput(true) case router.TextInputClose: - w.driver.ShowTextInput(false) + d.ShowTextInput(false) + } + if hint, ok := w.queue.q.TextInputHint(); ok { + d.SetInputHint(hint) + } + if txt, ok := w.queue.q.WriteClipboard(); ok { + w.WriteClipboard(txt) + } + if w.queue.q.ReadClipboard() { + w.ReadClipboard() } - if w.queue.q.Profiling() { + if w.queue.q.Profiling() && w.gpu != nil { frameDur := time.Since(frameStart) frameDur = frameDur.Truncate(100 * time.Microsecond) q := 100 * time.Microsecond - timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.loop.Summary()) - w.queue.q.Add(profile.Event{Timings: timings}) + timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q), w.gpu.Profile()) + w.queue.q.Queue(profile.Event{Timings: timings}) } if t, ok := w.queue.q.WakeupTime(); ok { w.setNextFrame(t) } - w.updateAnimation() - // Wait for the GPU goroutine to finish processing frame. - <-sync + w.updateAnimation(d) } -// Invalidate the window such that a FrameEvent will be generated -// immediately. If the window is inactive, the event is sent when the -// window becomes active. +// Invalidate the window such that a FrameEvent will be generated immediately. +// If the window is inactive, the event is sent when the window becomes active. +// +// Note that Invalidate is intended for externally triggered updates, such as a +// response from a network request. InvalidateOp is more efficient for animation +// and similar internal updates. +// // Invalidate is safe for concurrent use. func (w *Window) Invalidate() { select { - case w.invalidates <- struct{}{}: + case w.immediateRedraws <- struct{}{}: + return default: } + select { + case w.redraws <- struct{}{}: + w.wakeup() + default: + } +} + +// Option applies the options to the window. +func (w *Window) Option(opts ...Option) { + w.driverDefer(func(d driver) { + d.Configure(opts) + }) } // ReadClipboard initiates a read of the clipboard in the form -// of a system.ClipboardEvent. Multiple reads may be coalescedd +// of a clipboard.Event. Multiple reads may be coalesced // to a single event. func (w *Window) ReadClipboard() { - w.driverDo(func() { - w.driver.ReadClipboard() + w.driverDefer(func(d driver) { + d.ReadClipboard() }) } // WriteClipboard writes a string to the clipboard. func (w *Window) WriteClipboard(s string) { - w.driverDo(func() { - w.driver.WriteClipboard(s) + w.driverDefer(func(d driver) { + d.WriteClipboard(s) + }) +} + +// SetCursorName changes the current window cursor to name. +func (w *Window) SetCursorName(name pointer.CursorName) { + w.driverDefer(func(d driver) { + d.SetCursor(name) }) } // Close the window. The window's event loop should exit when it receives // system.DestroyEvent. // -// Currently, only macOS, Windows and X11 drivers implement this functionality, +// Currently, only macOS, Windows, X11 and Wayland drivers implement this functionality, // all others are stubbed. func (w *Window) Close() { - w.driverDo(func() { - w.driver.Close() + w.driverDefer(func(d driver) { + d.Close() }) } -// driverDo calls f as soon as the window has a valid driver attached, -// or does nothing if the window is destroyed while waiting. -func (w *Window) driverDo(f func()) { - go func() { - select { - case w.driverFuncs <- f: - case <-w.dead: - } - }() +// Maximize the window. +// Note: only implemented on Windows, macOS and X11. +func (w *Window) Maximize() { + w.driverDefer(func(d driver) { + d.Maximize() + }) } -func (w *Window) updateAnimation() { - animate := false - if w.delayedDraw != nil { - w.delayedDraw.Stop() - w.delayedDraw = nil +// Center the window. +// Note: only implemented on Windows, macOS and X11. +func (w *Window) Center() { + w.driverDefer(func(d driver) { + d.Center() + }) +} + +// Run f in the same thread as the native window event loop, and wait for f to +// return or the window to close. Run is guaranteed not to deadlock if it is +// invoked during the handling of a ViewEvent, system.FrameEvent, +// system.StageEvent; call Run in a separate goroutine to avoid deadlock in all +// other cases. +// +// Note that most programs should not call Run; configuring a Window with +// CustomRenderer is a notable exception. +func (w *Window) Run(f func()) { + done := make(chan struct{}) + w.driverDefer(func(d driver) { + defer close(done) + f() + }) + select { + case <-done: + case <-w.dead: } +} + +// driverDefer is like Run but can be run from any context. It doesn't wait +// for f to return. +func (w *Window) driverDefer(f func(d driver)) { + select { + case w.driverFuncs <- f: + w.wakeup() + case <-w.dead: + } +} + +func (w *Window) updateAnimation(d driver) { + animate := false if w.stage >= system.StageRunning && w.hasNextFrame { if dt := time.Until(w.nextFrame); dt <= 0 { animate = true } else { - w.delayedDraw = time.NewTimer(dt) + // Schedule redraw. + select { + case <-w.scheduledRedraws: + default: + } + w.scheduledRedraws <- w.nextFrame } } if animate != w.animating { w.animating = animate - w.driver.SetAnimating(animate) + d.SetAnimating(animate) + } +} + +func (w *Window) wakeup() { + select { + case w.wakeups <- struct{}{}: + default: } } @@ -256,164 +403,289 @@ func (w *Window) setNextFrame(at time.Time) { } } -func (c *callbacks) SetDriver(d window.Driver) { - c.Event(driverEvent{d}) +func (c *callbacks) SetDriver(d driver) { + c.d = d + var wakeup func() + if d != nil { + wakeup = d.Wakeup + } + c.w.wakeupFuncs <- wakeup } func (c *callbacks) Event(e event.Event) { - select { - case c.w.in <- e: - <-c.w.ack - case <-c.w.dead: + if c.d == nil { + panic("event while no driver active") } + c.w.processEvent(c.d, e) + c.w.updateState(c.d) +} + +// SemanticRoot returns the ID of the semantic root. +func (c *callbacks) SemanticRoot() router.SemanticID { + c.w.updateSemantics() + return c.w.semantic.root } -func (w *Window) waitAck() { - // Send a dummy event; when it gets through we - // know the application has processed the previous event. - w.out <- ackEvent +// LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root. +func (c *callbacks) LookupSemantic(semID router.SemanticID) (router.SemanticNode, bool) { + c.w.updateSemantics() + n, found := c.w.semantic.ids[semID] + return n, found } -// Prematurely destroy the window and wait for the native window -// destroy event. -func (w *Window) destroy(err error) { - w.destroyGPU() - // Ack the current event. - w.ack <- struct{}{} - w.out <- system.DestroyEvent{Err: err} - close(w.dead) - for e := range w.in { - w.ack <- struct{}{} - if _, ok := e.(system.DestroyEvent); ok { +func (c *callbacks) AppendSemanticDiffs(diffs []router.SemanticID) []router.SemanticID { + c.w.updateSemantics() + if tree := c.w.semantic.prevTree; len(tree) > 0 { + c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0]) + } + return diffs +} + +func (c *callbacks) SemanticAt(pos f32.Point) (router.SemanticID, bool) { + c.w.updateSemantics() + return c.w.queue.q.SemanticAt(pos) +} + +func (w *Window) waitAck(d driver) { + for { + select { + case f := <-w.driverFuncs: + f(d) + case w.out <- ackEvent: + // A dummy event went through, so we know the application has processed the previous event. return + case <-w.immediateRedraws: + // Invalidate was called during frame processing. + w.setNextFrame(time.Time{}) } } } func (w *Window) destroyGPU() { - if w.loop != nil { - w.loop.Release() - w.loop = nil + if w.gpu != nil { + w.ctx.Lock() + w.gpu.Release() + w.ctx.Unlock() + w.gpu = nil + } + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil } } // waitFrame waits for the client to either call FrameEvent.Frame // or to continue event handling. It returns whether the client // called Frame or not. -func (w *Window) waitFrame() (*op.Ops, bool) { +func (w *Window) waitFrame(d driver) (*op.Ops, bool) { + for { + select { + case f := <-w.driverFuncs: + f(d) + case frame := <-w.frames: + // The client called FrameEvent.Frame. + return frame, true + case w.out <- ackEvent: + // The client ignored FrameEvent and continued processing + // events. + return nil, false + case <-w.immediateRedraws: + // Invalidate was called during frame processing. + w.setNextFrame(time.Time{}) + } + } +} + +// updateSemantics refreshes the semantics tree, the id to node map and the ids of +// updated nodes. +func (w *Window) updateSemantics() { + if w.semantic.uptodate { + return + } + w.semantic.uptodate = true + w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree + w.semantic.tree = w.queue.q.AppendSemantics(w.semantic.tree[:0]) + w.semantic.root = w.semantic.tree[0].ID + for _, n := range w.semantic.tree { + w.semantic.ids[n.ID] = n + } +} + +// collectSemanticDiffs traverses the previous semantic tree, noting changed nodes. +func (w *Window) collectSemanticDiffs(diffs *[]router.SemanticID, n router.SemanticNode) { + newNode, exists := w.semantic.ids[n.ID] + // Ignore deleted nodes, as their disappearance will be reported through an + // ancestor node. + if !exists { + return + } + diff := newNode.Desc != n.Desc || len(n.Children) != len(newNode.Children) + for i, ch := range n.Children { + if !diff { + newCh := newNode.Children[i] + diff = ch.ID != newCh.ID + } + w.collectSemanticDiffs(diffs, ch) + } + if diff { + *diffs = append(*diffs, n.ID) + } +} + +func (w *Window) updateState(d driver) { + for { + select { + case f := <-w.driverFuncs: + f(d) + case <-w.redraws: + w.setNextFrame(time.Time{}) + w.updateAnimation(d) + default: + return + } + } +} + +func (w *Window) processEvent(d driver, e event.Event) { select { - case frame := <-w.frames: - // The client called FrameEvent.Frame. - return frame, true - case w.out <- ackEvent: - // The client ignored FrameEvent and continued processing - // events. - return nil, false + case <-w.dead: + return + default: + } + switch e2 := e.(type) { + case system.StageEvent: + if e2.Stage < system.StageRunning { + if w.gpu != nil { + w.ctx.Lock() + w.gpu.Release() + w.gpu = nil + w.ctx.Unlock() + } + } + w.stage = e2.Stage + w.updateAnimation(d) + w.out <- e + w.waitAck(d) + case frameEvent: + if e2.Size == (image.Point{}) { + panic(errors.New("internal error: zero-sized Draw")) + } + if w.stage < system.StageRunning { + // No drawing if not visible. + break + } + frameStart := time.Now() + w.hasNextFrame = false + e2.Frame = w.update + e2.Queue = &w.queue + w.out <- e2.FrameEvent + frame, gotFrame := w.waitFrame(d) + err := w.validateAndProcess(d, frameStart, e2.Size, e2.Sync, frame) + if gotFrame { + // We're done with frame, let the client continue. + w.frameAck <- struct{}{} + } + if err != nil { + w.destroyGPU() + w.out <- system.DestroyEvent{Err: err} + close(w.dead) + close(w.out) + break + } + w.updateCursor() + case *system.CommandEvent: + w.out <- e + w.waitAck(d) + case system.DestroyEvent: + w.destroyGPU() + w.out <- e2 + close(w.dead) + close(w.out) + case ViewEvent: + w.out <- e2 + w.waitAck(d) + case wakeupEvent: + case event.Event: + if w.queue.q.Queue(e2) { + w.setNextFrame(time.Time{}) + w.updateAnimation(d) + } + w.updateCursor() + w.out <- e } } -func (w *Window) run(opts *window.Options) { - defer close(w.in) - defer close(w.out) - if err := window.NewWindow(&w.callbacks, opts); err != nil { +func (w *Window) run(options []Option) { + if err := newWindow(&w.callbacks, options); err != nil { w.out <- system.DestroyEvent{Err: err} + close(w.dead) + close(w.out) return } + var wakeup func() + var timer *time.Timer for { - var driverFuncs chan func() - if w.driver != nil { - driverFuncs = w.driverFuncs - } - var timer <-chan time.Time - if w.delayedDraw != nil { - timer = w.delayedDraw.C + var ( + wakeups <-chan struct{} + timeC <-chan time.Time + ) + if wakeup != nil { + wakeups = w.wakeups + if timer != nil { + timeC = timer.C + } } select { - case <-timer: - w.setNextFrame(time.Time{}) - w.updateAnimation() - case <-w.invalidates: - w.setNextFrame(time.Time{}) - w.updateAnimation() - case f := <-driverFuncs: - f() - case e := <-w.in: - switch e2 := e.(type) { - case system.StageEvent: - if w.loop != nil { - if e2.Stage < system.StageRunning { - w.destroyGPU() - } else { - w.loop.Refresh() - } - } - w.stage = e2.Stage - w.updateAnimation() - w.out <- e - w.waitAck() - case window.FrameEvent: - if e2.Size == (image.Point{}) { - panic(errors.New("internal error: zero-sized Draw")) - } - if w.stage < system.StageRunning { - // No drawing if not visible. - break - } - frameStart := time.Now() - w.hasNextFrame = false - e2.Frame = w.update - e2.Queue = &w.queue - w.out <- e2.FrameEvent - if w.loop != nil { - if e2.Sync { - w.loop.Refresh() - } - } - frame, gotFrame := w.waitFrame() - err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame) - if gotFrame { - // We're done with frame, let the client continue. - w.frameAck <- struct{}{} - } - if err != nil { - w.destroyGPU() - w.destroy(err) - return - } - case *system.CommandEvent: - w.out <- e - w.waitAck() - case driverEvent: - w.driver = e2.driver - case system.DestroyEvent: - w.destroyGPU() - w.out <- e2 - w.ack <- struct{}{} - return - case event.Event: - if w.queue.q.Add(e2) { - w.setNextFrame(time.Time{}) - w.updateAnimation() - } - w.out <- e + case t := <-w.scheduledRedraws: + if timer != nil { + timer.Stop() } - w.ack <- struct{}{} + timer = time.NewTimer(time.Until(t)) + case <-w.dead: + return + case <-timeC: + select { + case w.redraws <- struct{}{}: + wakeup() + default: + } + case <-wakeups: + wakeup() + case wakeup = <-w.wakeupFuncs: } } } +func (w *Window) updateCursor() { + if c := w.queue.q.Cursor(); c != w.cursor { + w.cursor = c + w.SetCursorName(c) + } +} + +// Raise requests that the platform bring this window to the top of all open windows. +// Some platforms do not allow this except under certain circumstances, such as when +// a window from the same application already has focus. If the platform does not +// support it, this method will do nothing. +func (w *Window) Raise() { + w.driverDefer(func(d driver) { + d.Raise() + }) +} + func (q *queue) Events(k event.Tag) []event.Event { return q.q.Events(k) } // Title sets the title of the window. func Title(t string) Option { - return func(opts *window.Options) { - opts.Title = t + return func(_ unit.Metric, cnf *Config) { + cnf.Title = t } } -// Size sets the size of the window. +// Size sets the size of the window. The option is ignored +// in Fullscreen mode. func Size(w, h unit.Value) Option { if w.V <= 0 { panic("width must be larger than or equal to 0") @@ -421,9 +693,11 @@ func Size(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(opts *window.Options) { - opts.Width = w - opts.Height = h + return func(m unit.Metric, cnf *Config) { + cnf.Size = image.Point{ + X: m.Px(w), + Y: m.Px(h), + } } } @@ -435,9 +709,11 @@ func MaxSize(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(opts *window.Options) { - opts.MaxWidth = w - opts.MaxHeight = h + return func(m unit.Metric, cnf *Config) { + cnf.MaxSize = image.Point{ + X: m.Px(w), + Y: m.Px(h), + } } } @@ -449,10 +725,32 @@ func MinSize(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(opts *window.Options) { - opts.MinWidth = w - opts.MinHeight = h + return func(m unit.Metric, cnf *Config) { + cnf.MinSize = image.Point{ + X: m.Px(w), + Y: m.Px(h), + } } } -func (driverEvent) ImplementsEvent() {} +// StatusColor sets the color of the Android status bar. +func StatusColor(color color.NRGBA) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.StatusColor = color + } +} + +// NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers. +func NavigationColor(color color.NRGBA) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.NavigationColor = color + } +} + +// CustomRenderer controls whether the window contents is +// rendered by the client. If true, no GPU context is created. +func CustomRenderer(custom bool) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.CustomRenderer = custom + } +} |