- Added a new configuration option for borderless windows in the app.Config struct. - Implemented drag functionality for borderless windows, allowing users to move the window by clicking and dragging anywhere on the surface. - Updated the X11 window creation to support borderless mode by setting the override_redirect attribute. - Introduced a new example demonstrating the usage of borderless windows with real-time drag capabilities. - Enhanced the label widget to showcase standard Material Design 3 font sizes in a new demo. These changes improve the user experience by providing a modern, flexible windowing option and enhancing the visual presentation of text elements.
981 lines
24 KiB
Go
981 lines
24 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mleku/fromage/f32"
|
|
"github.com/mleku/fromage/font/gofont"
|
|
"github.com/mleku/fromage/gpu"
|
|
"github.com/mleku/fromage/internal/debug"
|
|
"github.com/mleku/fromage/internal/ops"
|
|
"github.com/mleku/fromage/io/event"
|
|
"github.com/mleku/fromage/io/input"
|
|
"github.com/mleku/fromage/io/key"
|
|
"github.com/mleku/fromage/io/pointer"
|
|
"github.com/mleku/fromage/io/system"
|
|
"github.com/mleku/fromage/layout"
|
|
"github.com/mleku/fromage/op"
|
|
"github.com/mleku/fromage/text"
|
|
"github.com/mleku/fromage/unit"
|
|
"github.com/mleku/fromage/widget"
|
|
"github.com/mleku/fromage/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 {
|
|
// Clear with opaque black for Linux X11
|
|
w.gpu.Clear(color.NRGBA{A: 0xff, R: 0x00, G: 0x00, B: 0x00})
|
|
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
|
|
}
|
|
|
|
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 {
|
|
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:
|
|
focusDir = key.FocusUp
|
|
case e.Name == key.NameDownArrow && e.Modifiers == 0:
|
|
focusDir = key.FocusDown
|
|
case e.Name == key.NameLeftArrow && e.Modifiers == 0:
|
|
focusDir = key.FocusLeft
|
|
case e.Name == key.NameRightArrow && e.Modifiers == 0:
|
|
focusDir = key.FocusRight
|
|
}
|
|
}
|
|
// Always queue the original event first to ensure it's processed
|
|
w.queue.Queue(e2)
|
|
t, handled := w.queue.WakeupTime()
|
|
// Only handle focus navigation if the event wasn't consumed and it's a focus navigation event
|
|
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
|
|
}
|
|
}
|
|
|
|
// Borderless creates a window without any borders or decorations.
|
|
// The window will have no title bar and no window manager controls.
|
|
func Borderless(enabled bool) Option {
|
|
return func(_ unit.Metric, cnf *Config) {
|
|
cnf.Borderless = 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
|