chore(v2): vendor dependencies for offline/China builds
go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor so go run/build work without hitting proxy.golang.org (blocked/slow in China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and GOOS=windows go build -mod=vendor ./internal/ui both pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+991
@@ -0,0 +1,991 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/gpu"
|
||||
"gioui.org/internal/debug"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
// Option configures a window.
|
||||
type Option func(unit.Metric, *Config)
|
||||
|
||||
// Window represents an operating system window.
|
||||
//
|
||||
// The zero-value Window is useful; the GUI window is created and shown the first
|
||||
// time the [Event] method is called. On iOS or Android, the first Window represents
|
||||
// the window previously created by the platform.
|
||||
//
|
||||
// More than one Window is not supported on iOS, Android, WebAssembly.
|
||||
type Window struct {
|
||||
initialOpts []Option
|
||||
initialActions []system.Action
|
||||
|
||||
ctx context
|
||||
gpu gpu.GPU
|
||||
// timer tracks the delayed invalidate goroutine.
|
||||
timer struct {
|
||||
// quit is shuts down the goroutine.
|
||||
quit chan struct{}
|
||||
// update the invalidate time.
|
||||
update chan time.Time
|
||||
}
|
||||
|
||||
animating bool
|
||||
hasNextFrame bool
|
||||
nextFrame time.Time
|
||||
// viewport is the latest frame size with insets applied.
|
||||
viewport image.Rectangle
|
||||
// metric is the metric from the most recent frame.
|
||||
metric unit.Metric
|
||||
queue input.Router
|
||||
cursor pointer.Cursor
|
||||
decorations struct {
|
||||
op.Ops
|
||||
// enabled tracks the Decorated option as
|
||||
// given to the Option method. It may differ
|
||||
// from Config.Decorated depending on platform
|
||||
// capability.
|
||||
enabled bool
|
||||
Config
|
||||
height unit.Dp
|
||||
currentHeight int
|
||||
*material.Theme
|
||||
*widget.Decorations
|
||||
}
|
||||
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 input.SemanticID
|
||||
prevTree []input.SemanticNode
|
||||
tree []input.SemanticNode
|
||||
ids map[input.SemanticID]input.SemanticNode
|
||||
}
|
||||
imeState editorState
|
||||
driver driver
|
||||
// gpuErr tracks the GPU error that is to be reported when
|
||||
// the window is closed.
|
||||
gpuErr error
|
||||
|
||||
// invMu protects mayInvalidate.
|
||||
invMu sync.Mutex
|
||||
mayInvalidate bool
|
||||
|
||||
// coalesced tracks the most recent events waiting to be delivered
|
||||
// to the client.
|
||||
coalesced eventSummary
|
||||
// frame tracks the most recent frame event.
|
||||
lastFrame struct {
|
||||
sync bool
|
||||
size image.Point
|
||||
off image.Point
|
||||
deco op.CallOp
|
||||
}
|
||||
}
|
||||
|
||||
type eventSummary struct {
|
||||
wakeup bool
|
||||
cfg *ConfigEvent
|
||||
view *ViewEvent
|
||||
frame *frameEvent
|
||||
framePending bool
|
||||
destroy *DestroyEvent
|
||||
}
|
||||
|
||||
type callbacks struct {
|
||||
w *Window
|
||||
}
|
||||
|
||||
func decoHeightOpt(h unit.Dp) Option {
|
||||
return func(m unit.Metric, c *Config) {
|
||||
c.decoHeight = h
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error {
|
||||
signal := func() {
|
||||
if sigChan != nil {
|
||||
// We're done with frame, let the client continue.
|
||||
sigChan <- struct{}{}
|
||||
// Signal at most once.
|
||||
sigChan = nil
|
||||
}
|
||||
}
|
||||
defer signal()
|
||||
for {
|
||||
if w.gpu == nil && !w.nocontext {
|
||||
var err error
|
||||
if w.ctx == nil {
|
||||
w.ctx, err = w.driver.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 errors.Is(err, gpu.ErrDeviceLost) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.ctx != nil {
|
||||
if err := w.ctx.Lock(); err != nil {
|
||||
w.destroyGPU()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if w.gpu == nil && !w.nocontext {
|
||||
gpu, err := gpu.New(w.ctx.API())
|
||||
if err != nil {
|
||||
w.ctx.Unlock()
|
||||
w.destroyGPU()
|
||||
return err
|
||||
}
|
||||
w.gpu = gpu
|
||||
}
|
||||
if w.gpu != nil {
|
||||
if err := w.frame(frame, size); err != nil {
|
||||
w.ctx.Unlock()
|
||||
if errors.Is(err, errOutOfDate) {
|
||||
// GPU surface needs refreshing.
|
||||
sync = true
|
||||
continue
|
||||
}
|
||||
w.destroyGPU()
|
||||
if errors.Is(err, gpu.ErrDeviceLost) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.queue.Frame(frame)
|
||||
// Let the client continue as soon as possible, in particular before
|
||||
// a potentially blocking Present.
|
||||
signal()
|
||||
var err error
|
||||
if w.gpu != nil {
|
||||
err = w.ctx.Present()
|
||||
w.ctx.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
|
||||
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
|
||||
}
|
||||
return w.gpu.Frame(frame, target, viewport)
|
||||
}
|
||||
|
||||
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
|
||||
w.coalesced.framePending = false
|
||||
wrapper := &w.decorations.Ops
|
||||
off := op.Offset(w.lastFrame.off).Push(wrapper)
|
||||
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
|
||||
off.Pop()
|
||||
w.lastFrame.deco.Add(wrapper)
|
||||
if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil {
|
||||
w.destroyGPU()
|
||||
w.gpuErr = err
|
||||
w.driver.Perform(system.ActionClose)
|
||||
return
|
||||
}
|
||||
w.updateState()
|
||||
w.updateCursor()
|
||||
}
|
||||
|
||||
func (w *Window) updateState() {
|
||||
for k := range w.semantic.ids {
|
||||
delete(w.semantic.ids, k)
|
||||
}
|
||||
w.semantic.uptodate = false
|
||||
q := &w.queue
|
||||
switch q.TextInputState() {
|
||||
case input.TextInputOpen:
|
||||
w.driver.ShowTextInput(true)
|
||||
case input.TextInputClose:
|
||||
w.driver.ShowTextInput(false)
|
||||
}
|
||||
if hint, ok := q.TextInputHint(); ok {
|
||||
w.driver.SetInputHint(hint)
|
||||
}
|
||||
if mime, txt, ok := q.WriteClipboard(); ok {
|
||||
w.driver.WriteClipboard(mime, txt)
|
||||
}
|
||||
if q.ClipboardRequested() {
|
||||
w.driver.ReadClipboard()
|
||||
}
|
||||
oldState := w.imeState
|
||||
newState := oldState
|
||||
newState.EditorState = q.EditorState()
|
||||
if newState != oldState {
|
||||
w.imeState = newState
|
||||
w.driver.EditorStateChanged(oldState, newState)
|
||||
}
|
||||
if t, ok := q.WakeupTime(); ok {
|
||||
w.setNextFrame(t)
|
||||
}
|
||||
w.updateAnimation()
|
||||
}
|
||||
|
||||
// Invalidate the window such that a [FrameEvent] will be generated immediately.
|
||||
// If the window is inactive, an unspecified event is sent instead.
|
||||
//
|
||||
// Note that Invalidate is intended for externally triggered updates, such as a
|
||||
// response from a network request. The [op.InvalidateCmd] command is more efficient
|
||||
// for animation.
|
||||
//
|
||||
// Invalidate is safe for concurrent use.
|
||||
func (w *Window) Invalidate() {
|
||||
w.invMu.Lock()
|
||||
defer w.invMu.Unlock()
|
||||
if w.mayInvalidate {
|
||||
w.mayInvalidate = false
|
||||
w.driver.Invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
// Option applies the options to the window. The options are hints; the platform is
|
||||
// free to ignore or adjust them.
|
||||
func (w *Window) Option(opts ...Option) {
|
||||
if len(opts) == 0 {
|
||||
return
|
||||
}
|
||||
if w.driver == nil {
|
||||
w.initialOpts = append(w.initialOpts, opts...)
|
||||
return
|
||||
}
|
||||
w.Run(func() {
|
||||
cnf := Config{Decorated: w.decorations.enabled}
|
||||
for _, opt := range opts {
|
||||
opt(w.metric, &cnf)
|
||||
}
|
||||
w.decorations.enabled = cnf.Decorated
|
||||
decoHeight := w.decorations.height
|
||||
if !w.decorations.enabled {
|
||||
decoHeight = 0
|
||||
}
|
||||
opts = append(opts, decoHeightOpt(decoHeight))
|
||||
w.driver.Configure(opts)
|
||||
w.setNextFrame(time.Time{})
|
||||
w.updateAnimation()
|
||||
})
|
||||
}
|
||||
|
||||
// Run f in the same thread as the native window event loop, and wait for f to
|
||||
// return or the window to close. If the window has not yet been created,
|
||||
// Run calls f directly.
|
||||
//
|
||||
// Note that most programs should not call Run; configuring a Window with
|
||||
// [CustomRenderer] is a notable exception.
|
||||
func (w *Window) Run(f func()) {
|
||||
if w.driver == nil {
|
||||
f()
|
||||
return
|
||||
}
|
||||
done := make(chan struct{})
|
||||
w.driver.Run(func() {
|
||||
defer close(done)
|
||||
f()
|
||||
})
|
||||
<-done
|
||||
}
|
||||
|
||||
func (w *Window) updateAnimation() {
|
||||
if w.driver == nil {
|
||||
return
|
||||
}
|
||||
animate := false
|
||||
if w.hasNextFrame {
|
||||
if dt := time.Until(w.nextFrame); dt <= 0 {
|
||||
animate = true
|
||||
} else {
|
||||
// Schedule redraw.
|
||||
w.scheduleInvalidate(w.nextFrame)
|
||||
}
|
||||
}
|
||||
if animate != w.animating {
|
||||
w.animating = animate
|
||||
w.driver.SetAnimating(animate)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Window) scheduleInvalidate(t time.Time) {
|
||||
if w.timer.quit == nil {
|
||||
w.timer.quit = make(chan struct{})
|
||||
w.timer.update = make(chan time.Time)
|
||||
go func() {
|
||||
var timer *time.Timer
|
||||
for {
|
||||
var timeC <-chan time.Time
|
||||
if timer != nil {
|
||||
timeC = timer.C
|
||||
}
|
||||
select {
|
||||
case <-w.timer.quit:
|
||||
w.timer.quit <- struct{}{}
|
||||
return
|
||||
case t := <-w.timer.update:
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
timer = time.NewTimer(time.Until(t))
|
||||
case <-timeC:
|
||||
w.Invalidate()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
w.timer.update <- t
|
||||
}
|
||||
|
||||
func (w *Window) setNextFrame(at time.Time) {
|
||||
if !w.hasNextFrame || at.Before(w.nextFrame) {
|
||||
w.hasNextFrame = true
|
||||
w.nextFrame = at
|
||||
}
|
||||
}
|
||||
|
||||
func (c *callbacks) SetDriver(d driver) {
|
||||
if d == nil {
|
||||
panic("nil driver")
|
||||
}
|
||||
c.w.invMu.Lock()
|
||||
defer c.w.invMu.Unlock()
|
||||
c.w.driver = d
|
||||
}
|
||||
|
||||
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
|
||||
c.w.processFrame(frame, ack)
|
||||
}
|
||||
|
||||
func (c *callbacks) ProcessEvent(e event.Event) bool {
|
||||
return c.w.processEvent(e)
|
||||
}
|
||||
|
||||
// SemanticRoot returns the ID of the semantic root.
|
||||
func (c *callbacks) SemanticRoot() input.SemanticID {
|
||||
c.w.updateSemantics()
|
||||
return c.w.semantic.root
|
||||
}
|
||||
|
||||
// LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root.
|
||||
func (c *callbacks) LookupSemantic(semID input.SemanticID) (input.SemanticNode, bool) {
|
||||
c.w.updateSemantics()
|
||||
n, found := c.w.semantic.ids[semID]
|
||||
return n, found
|
||||
}
|
||||
|
||||
func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.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) (input.SemanticID, bool) {
|
||||
c.w.updateSemantics()
|
||||
return c.w.queue.SemanticAt(pos)
|
||||
}
|
||||
|
||||
func (c *callbacks) EditorState() editorState {
|
||||
return c.w.imeState
|
||||
}
|
||||
|
||||
func (c *callbacks) SetComposingRegion(r key.Range) {
|
||||
c.w.imeState.compose = r
|
||||
c.w.driver.ProcessEvent(key.CompositionEvent(r))
|
||||
}
|
||||
|
||||
func (c *callbacks) EditorInsert(text string) {
|
||||
sel := c.w.imeState.Selection.Range
|
||||
c.EditorReplace(sel, text)
|
||||
start := min(sel.End, sel.Start)
|
||||
sel.Start = start + utf8.RuneCountInString(text)
|
||||
sel.End = sel.Start
|
||||
c.SetEditorSelection(sel)
|
||||
}
|
||||
|
||||
func (c *callbacks) EditorReplace(r key.Range, text string) {
|
||||
c.w.imeState.Replace(r, text)
|
||||
c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text})
|
||||
c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range))
|
||||
}
|
||||
|
||||
func (c *callbacks) SetEditorSelection(r key.Range) {
|
||||
c.w.imeState.Selection.Range = r
|
||||
c.w.driver.ProcessEvent(key.SelectionEvent(r))
|
||||
}
|
||||
|
||||
func (c *callbacks) SetEditorSnippet(r key.Range) {
|
||||
if sn := c.EditorState().Snippet.Range; sn == r {
|
||||
// No need to expand.
|
||||
return
|
||||
}
|
||||
c.w.driver.ProcessEvent(key.SnippetEvent(r))
|
||||
}
|
||||
|
||||
func (w *Window) moveFocus(dir key.FocusDirection) {
|
||||
w.queue.MoveFocus(dir)
|
||||
if _, handled := w.queue.WakeupTime(); handled {
|
||||
w.queue.RevealFocus(w.viewport)
|
||||
} else {
|
||||
var v image.Point
|
||||
switch dir {
|
||||
case key.FocusRight:
|
||||
v = image.Pt(+1, 0)
|
||||
case key.FocusLeft:
|
||||
v = image.Pt(-1, 0)
|
||||
case key.FocusDown:
|
||||
v = image.Pt(0, +1)
|
||||
case key.FocusUp:
|
||||
v = image.Pt(0, -1)
|
||||
default:
|
||||
return
|
||||
}
|
||||
const scrollABit = unit.Dp(50)
|
||||
dist := v.Mul(int(w.metric.Dp(scrollABit)))
|
||||
w.queue.ScrollFocus(dist)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *callbacks) ClickFocus() {
|
||||
c.w.queue.ClickFocus()
|
||||
c.w.setNextFrame(time.Time{})
|
||||
c.w.updateAnimation()
|
||||
}
|
||||
|
||||
func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
|
||||
return c.w.queue.ActionAt(p)
|
||||
}
|
||||
|
||||
func (w *Window) destroyGPU() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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 *[]input.SemanticID, n input.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 (c *callbacks) Invalidate() {
|
||||
c.w.setNextFrame(time.Time{})
|
||||
c.w.updateAnimation()
|
||||
// Guarantee a wakeup, even when not animating.
|
||||
c.w.processEvent(wakeupEvent{})
|
||||
}
|
||||
|
||||
func (c *callbacks) nextEvent() (event.Event, bool) {
|
||||
return c.w.nextEvent()
|
||||
}
|
||||
|
||||
func (w *Window) nextEvent() (event.Event, bool) {
|
||||
s := &w.coalesced
|
||||
defer func() {
|
||||
// Every event counts as a wakeup.
|
||||
s.wakeup = false
|
||||
}()
|
||||
switch {
|
||||
case s.framePending:
|
||||
// If the user didn't call FrameEvent.Event, process
|
||||
// an empty frame.
|
||||
w.processFrame(new(op.Ops), nil)
|
||||
case s.view != nil:
|
||||
e := *s.view
|
||||
s.view = nil
|
||||
return e, true
|
||||
case s.destroy != nil:
|
||||
e := *s.destroy
|
||||
// Clear pending events after DestroyEvent is delivered.
|
||||
*s = eventSummary{}
|
||||
return e, true
|
||||
case s.cfg != nil:
|
||||
e := *s.cfg
|
||||
s.cfg = nil
|
||||
return e, true
|
||||
case s.frame != nil:
|
||||
e := *s.frame
|
||||
s.frame = nil
|
||||
s.framePending = true
|
||||
return e.FrameEvent, true
|
||||
case s.wakeup:
|
||||
return wakeupEvent{}, true
|
||||
}
|
||||
w.invMu.Lock()
|
||||
defer w.invMu.Unlock()
|
||||
w.mayInvalidate = w.driver != nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (w *Window) processEvent(e event.Event) bool {
|
||||
switch e2 := e.(type) {
|
||||
case wakeupEvent:
|
||||
w.coalesced.wakeup = true
|
||||
case frameEvent:
|
||||
if e2.Size == (image.Point{}) {
|
||||
panic(errors.New("internal error: zero-sized Draw"))
|
||||
}
|
||||
w.metric = e2.Metric
|
||||
w.hasNextFrame = false
|
||||
e2.Frame = w.driver.Frame
|
||||
e2.Source = w.queue.Source()
|
||||
// Prepare the decorations and update the frame insets.
|
||||
viewport := image.Rectangle{
|
||||
Min: image.Point{
|
||||
X: e2.Metric.Dp(e2.Insets.Left),
|
||||
Y: e2.Metric.Dp(e2.Insets.Top),
|
||||
},
|
||||
Max: image.Point{
|
||||
X: e2.Size.X - e2.Metric.Dp(e2.Insets.Right),
|
||||
Y: e2.Size.Y - e2.Metric.Dp(e2.Insets.Bottom),
|
||||
},
|
||||
}
|
||||
// Scroll to focus if viewport is shrinking in any dimension.
|
||||
if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y {
|
||||
w.queue.RevealFocus(viewport)
|
||||
}
|
||||
w.viewport = viewport
|
||||
wrapper := &w.decorations.Ops
|
||||
wrapper.Reset()
|
||||
m := op.Record(wrapper)
|
||||
offset := w.decorate(e2.FrameEvent, wrapper)
|
||||
w.lastFrame.deco = m.Stop()
|
||||
w.lastFrame.size = e2.Size
|
||||
w.lastFrame.sync = e2.Sync
|
||||
w.lastFrame.off = offset
|
||||
e2.Size = e2.Size.Sub(offset)
|
||||
w.coalesced.frame = &e2
|
||||
case DestroyEvent:
|
||||
if w.gpuErr != nil {
|
||||
e2.Err = w.gpuErr
|
||||
}
|
||||
w.destroyGPU()
|
||||
w.invMu.Lock()
|
||||
w.mayInvalidate = false
|
||||
w.driver = nil
|
||||
w.invMu.Unlock()
|
||||
if q := w.timer.quit; q != nil {
|
||||
q <- struct{}{}
|
||||
<-q
|
||||
}
|
||||
w.coalesced.destroy = &e2
|
||||
case ViewEvent:
|
||||
if !e2.Valid() && w.gpu != nil {
|
||||
w.ctx.Lock()
|
||||
w.gpu.Release()
|
||||
w.gpu = nil
|
||||
w.ctx.Unlock()
|
||||
}
|
||||
w.coalesced.view = &e2
|
||||
case ConfigEvent:
|
||||
w.decorations.Decorations.Maximized = e2.Config.Mode == Maximized
|
||||
wasFocused := w.decorations.Config.Focused
|
||||
w.decorations.Config = e2.Config
|
||||
e2.Config = w.effectiveConfig()
|
||||
w.coalesced.cfg = &e2
|
||||
if f := w.decorations.Config.Focused; f != wasFocused {
|
||||
w.queue.Queue(key.FocusEvent{Focus: f})
|
||||
}
|
||||
t, handled := w.queue.WakeupTime()
|
||||
if handled {
|
||||
w.setNextFrame(t)
|
||||
w.updateAnimation()
|
||||
}
|
||||
return handled
|
||||
case event.Event:
|
||||
focusDir := key.FocusDirection(-1)
|
||||
if e, ok := e2.(key.Event); ok && e.State == key.Press {
|
||||
isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android"
|
||||
switch {
|
||||
case e.Name == key.NameTab && e.Modifiers == 0:
|
||||
focusDir = key.FocusForward
|
||||
case e.Name == key.NameTab && e.Modifiers == key.ModShift:
|
||||
focusDir = key.FocusBackward
|
||||
case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile:
|
||||
focusDir = key.FocusUp
|
||||
case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile:
|
||||
focusDir = key.FocusDown
|
||||
case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile:
|
||||
focusDir = key.FocusLeft
|
||||
case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile:
|
||||
focusDir = key.FocusRight
|
||||
}
|
||||
}
|
||||
e := e2
|
||||
if focusDir != -1 {
|
||||
e = input.SystemEvent{Event: e}
|
||||
}
|
||||
w.queue.Queue(e)
|
||||
t, handled := w.queue.WakeupTime()
|
||||
if focusDir != -1 && !handled {
|
||||
w.moveFocus(focusDir)
|
||||
t, handled = w.queue.WakeupTime()
|
||||
}
|
||||
w.updateCursor()
|
||||
if handled {
|
||||
w.setNextFrame(t)
|
||||
w.updateAnimation()
|
||||
}
|
||||
return handled
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Event blocks until an event is received from the window, such as
|
||||
// [FrameEvent], or until [Invalidate] is called. The window is created
|
||||
// and shown the first time Event is called.
|
||||
func (w *Window) Event() event.Event {
|
||||
if w.driver == nil {
|
||||
w.init()
|
||||
}
|
||||
if w.driver == nil {
|
||||
e, ok := w.nextEvent()
|
||||
if !ok {
|
||||
panic("window initialization failed without a DestroyEvent")
|
||||
}
|
||||
return e
|
||||
}
|
||||
return w.driver.Event()
|
||||
}
|
||||
|
||||
func (w *Window) init() {
|
||||
debug.Parse()
|
||||
// Measure decoration height.
|
||||
deco := new(widget.Decorations)
|
||||
theme := material.NewTheme()
|
||||
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
|
||||
decoStyle := material.Decorations(theme, deco, 0, "")
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
// Measure in Dp.
|
||||
Metric: unit.Metric{},
|
||||
}
|
||||
// Allow plenty of space.
|
||||
gtx.Constraints.Max.Y = 200
|
||||
dims := decoStyle.Layout(gtx)
|
||||
decoHeight := unit.Dp(dims.Size.Y)
|
||||
defaultOptions := []Option{
|
||||
Size(800, 600),
|
||||
Title("Gio"),
|
||||
Decorated(true),
|
||||
decoHeightOpt(decoHeight),
|
||||
}
|
||||
options := append(defaultOptions, w.initialOpts...)
|
||||
w.initialOpts = nil
|
||||
var cnf Config
|
||||
cnf.apply(unit.Metric{}, options)
|
||||
|
||||
w.nocontext = cnf.CustomRenderer
|
||||
w.decorations.Theme = theme
|
||||
w.decorations.Decorations = deco
|
||||
w.decorations.enabled = cnf.Decorated
|
||||
w.decorations.height = decoHeight
|
||||
w.imeState.compose = key.Range{Start: -1, End: -1}
|
||||
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
|
||||
newWindow(&callbacks{w}, options)
|
||||
for _, acts := range w.initialActions {
|
||||
w.Perform(acts)
|
||||
}
|
||||
w.initialActions = nil
|
||||
}
|
||||
|
||||
func (w *Window) updateCursor() {
|
||||
if c := w.queue.Cursor(); c != w.cursor {
|
||||
w.cursor = c
|
||||
w.driver.SetCursor(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Window) fallbackDecorate() bool {
|
||||
cnf := w.decorations.Config
|
||||
return w.decorations.enabled && !cnf.Decorated && cnf.Mode != Fullscreen && !w.nocontext
|
||||
}
|
||||
|
||||
// decorate the window if enabled and returns the corresponding Insets.
|
||||
func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
|
||||
if !w.fallbackDecorate() {
|
||||
return image.Pt(0, 0)
|
||||
}
|
||||
deco := w.decorations.Decorations
|
||||
allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize |
|
||||
system.ActionClose | system.ActionMove
|
||||
style := material.Decorations(w.decorations.Theme, deco, allActions, w.decorations.Config.Title)
|
||||
// Update the decorations based on the current window mode.
|
||||
var actions system.Action
|
||||
switch m := w.decorations.Config.Mode; m {
|
||||
case Windowed:
|
||||
actions |= system.ActionUnmaximize
|
||||
case Minimized:
|
||||
actions |= system.ActionMinimize
|
||||
case Maximized:
|
||||
actions |= system.ActionMaximize
|
||||
case Fullscreen:
|
||||
actions |= system.ActionFullscreen
|
||||
default:
|
||||
panic(fmt.Errorf("unknown WindowMode %v", m))
|
||||
}
|
||||
gtx := layout.Context{
|
||||
Ops: o,
|
||||
Now: e.Now,
|
||||
Source: e.Source,
|
||||
Metric: e.Metric,
|
||||
Constraints: layout.Exact(e.Size),
|
||||
}
|
||||
// Update the window based on the actions on the decorations.
|
||||
opts, acts := splitActions(deco.Update(gtx))
|
||||
if len(opts) > 0 {
|
||||
w.driver.Configure(opts)
|
||||
}
|
||||
if acts != 0 {
|
||||
w.driver.Perform(acts)
|
||||
}
|
||||
style.Layout(gtx)
|
||||
// Offset to place the frame content below the decorations.
|
||||
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
|
||||
if w.decorations.currentHeight != decoHeight {
|
||||
w.decorations.currentHeight = decoHeight
|
||||
w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()}
|
||||
}
|
||||
return image.Pt(0, decoHeight)
|
||||
}
|
||||
|
||||
func (w *Window) effectiveConfig() Config {
|
||||
cnf := w.decorations.Config
|
||||
cnf.Size.Y -= w.decorations.currentHeight
|
||||
cnf.Decorated = w.decorations.enabled || cnf.Decorated
|
||||
return cnf
|
||||
}
|
||||
|
||||
// splitActions splits options from actions and return them and the remaining
|
||||
// actions.
|
||||
func splitActions(actions system.Action) ([]Option, system.Action) {
|
||||
var opts []Option
|
||||
walkActions(actions, func(action system.Action) {
|
||||
switch action {
|
||||
case system.ActionMinimize:
|
||||
opts = append(opts, Minimized.Option())
|
||||
case system.ActionMaximize:
|
||||
opts = append(opts, Maximized.Option())
|
||||
case system.ActionUnmaximize:
|
||||
opts = append(opts, Windowed.Option())
|
||||
case system.ActionFullscreen:
|
||||
opts = append(opts, Fullscreen.Option())
|
||||
default:
|
||||
return
|
||||
}
|
||||
actions &^= action
|
||||
})
|
||||
return opts, actions
|
||||
}
|
||||
|
||||
// Perform the actions on the window.
|
||||
func (w *Window) Perform(actions system.Action) {
|
||||
opts, acts := splitActions(actions)
|
||||
w.Option(opts...)
|
||||
if acts == 0 {
|
||||
return
|
||||
}
|
||||
if w.driver == nil {
|
||||
w.initialActions = append(w.initialActions, acts)
|
||||
return
|
||||
}
|
||||
w.Run(func() {
|
||||
w.driver.Perform(actions)
|
||||
})
|
||||
}
|
||||
|
||||
// Title sets the title of the window.
|
||||
func Title(t string) Option {
|
||||
return func(_ unit.Metric, cnf *Config) {
|
||||
cnf.Title = t
|
||||
}
|
||||
}
|
||||
|
||||
// Size sets the size of the window. The mode will be changed to Windowed.
|
||||
func Size(w, h unit.Dp) Option {
|
||||
if w <= 0 {
|
||||
panic("width must be larger than or equal to 0")
|
||||
}
|
||||
if h <= 0 {
|
||||
panic("height must be larger than or equal to 0")
|
||||
}
|
||||
return func(m unit.Metric, cnf *Config) {
|
||||
cnf.Mode = Windowed
|
||||
cnf.Size = image.Point{
|
||||
X: m.Dp(w),
|
||||
Y: m.Dp(h),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MaxSize sets the maximum size of the window.
|
||||
func MaxSize(w, h unit.Dp) Option {
|
||||
if w <= 0 {
|
||||
panic("width must be larger than or equal to 0")
|
||||
}
|
||||
if h <= 0 {
|
||||
panic("height must be larger than or equal to 0")
|
||||
}
|
||||
return func(m unit.Metric, cnf *Config) {
|
||||
cnf.MaxSize = image.Point{
|
||||
X: m.Dp(w),
|
||||
Y: m.Dp(h),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MinSize sets the minimum size of the window.
|
||||
func MinSize(w, h unit.Dp) Option {
|
||||
if w <= 0 {
|
||||
panic("width must be larger than or equal to 0")
|
||||
}
|
||||
if h <= 0 {
|
||||
panic("height must be larger than or equal to 0")
|
||||
}
|
||||
return func(m unit.Metric, cnf *Config) {
|
||||
cnf.MinSize = image.Point{
|
||||
X: m.Dp(w),
|
||||
Y: m.Dp(h),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Caller must assume responsibility for rendering which includes
|
||||
// initializing the render backend, swapping the framebuffer and
|
||||
// handling frame pacing.
|
||||
func CustomRenderer(custom bool) Option {
|
||||
return func(_ unit.Metric, cnf *Config) {
|
||||
cnf.CustomRenderer = custom
|
||||
}
|
||||
}
|
||||
|
||||
// Decorated controls whether Gio and/or the platform are responsible
|
||||
// for drawing window decorations. Providing false indicates that
|
||||
// the application will either be undecorated or will draw its own decorations.
|
||||
func Decorated(enabled bool) Option {
|
||||
return func(_ unit.Metric, cnf *Config) {
|
||||
cnf.Decorated = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// TopMost windows will be rendered above all other non-top-most windows.
|
||||
//
|
||||
// TopMost windows are supported on macOS, Windows.
|
||||
func TopMost(enabled bool) Option {
|
||||
return func(_ unit.Metric, cnf *Config) {
|
||||
cnf.TopMost = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// flushEvent is sent to detect when the user program
|
||||
// has completed processing of all prior events. Its an
|
||||
// [io/event.Event] but only for internal use.
|
||||
type flushEvent struct{}
|
||||
|
||||
func (t flushEvent) ImplementsEvent() {}
|
||||
|
||||
// theFlushEvent avoids allocating garbage when sending
|
||||
// flushEvents.
|
||||
var theFlushEvent flushEvent
|
||||
Reference in New Issue
Block a user