gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/window.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package app 4 5 import ( 6 "errors" 7 "fmt" 8 "image" 9 "image/color" 10 "reflect" 11 "runtime" 12 "sync" 13 "time" 14 "unicode/utf8" 15 16 "gioui.org/f32" 17 "gioui.org/font/gofont" 18 "gioui.org/gpu" 19 "gioui.org/internal/debug" 20 "gioui.org/internal/ops" 21 "gioui.org/io/event" 22 "gioui.org/io/input" 23 "gioui.org/io/key" 24 "gioui.org/io/pointer" 25 "gioui.org/io/system" 26 "gioui.org/layout" 27 "gioui.org/op" 28 "gioui.org/text" 29 "gioui.org/unit" 30 "gioui.org/widget" 31 "gioui.org/widget/material" 32 ) 33 34 // Option configures a window. 35 type Option func(unit.Metric, *Config) 36 37 // Window represents an operating system window. 38 // 39 // The zero-value Window is useful, and calling any method on 40 // it creates and shows a new GUI window. On iOS or Android, 41 // the first Window represents the the window previously 42 // created by the platform. 43 // 44 // More than one Window is not supported on iOS, Android, 45 // WebAssembly. 46 type Window struct { 47 ctx context 48 gpu gpu.GPU 49 // timer tracks the delayed invalidate goroutine. 50 timer struct { 51 // quit is shuts down the goroutine. 52 quit chan struct{} 53 // update the invalidate time. 54 update chan time.Time 55 } 56 57 animating bool 58 hasNextFrame bool 59 nextFrame time.Time 60 // viewport is the latest frame size with insets applied. 61 viewport image.Rectangle 62 // metric is the metric from the most recent frame. 63 metric unit.Metric 64 queue input.Router 65 cursor pointer.Cursor 66 decorations struct { 67 op.Ops 68 // enabled tracks the Decorated option as 69 // given to the Option method. It may differ 70 // from Config.Decorated depending on platform 71 // capability. 72 enabled bool 73 Config 74 height unit.Dp 75 currentHeight int 76 *material.Theme 77 *widget.Decorations 78 } 79 nocontext bool 80 // semantic data, lazily evaluated if requested by a backend to speed up 81 // the cases where semantic data is not needed. 82 semantic struct { 83 // uptodate tracks whether the fields below are up to date. 84 uptodate bool 85 root input.SemanticID 86 prevTree []input.SemanticNode 87 tree []input.SemanticNode 88 ids map[input.SemanticID]input.SemanticNode 89 } 90 imeState editorState 91 driver driver 92 // basic is the driver interface that is needed even after the window is gone. 93 basic basicDriver 94 once sync.Once 95 // coalesced tracks the most recent events waiting to be delivered 96 // to the client. 97 coalesced eventSummary 98 // frame tracks the most recently frame event. 99 lastFrame struct { 100 sync bool 101 size image.Point 102 off image.Point 103 deco op.CallOp 104 } 105 } 106 107 type eventSummary struct { 108 wakeup bool 109 cfg *ConfigEvent 110 view *ViewEvent 111 frame *frameEvent 112 destroy *DestroyEvent 113 } 114 115 type callbacks struct { 116 w *Window 117 } 118 119 func decoHeightOpt(h unit.Dp) Option { 120 return func(m unit.Metric, c *Config) { 121 c.decoHeight = h 122 } 123 } 124 125 func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error { 126 signal := func() { 127 if sigChan != nil { 128 // We're done with frame, let the client continue. 129 sigChan <- struct{}{} 130 // Signal at most once. 131 sigChan = nil 132 } 133 } 134 defer signal() 135 for { 136 if w.gpu == nil && !w.nocontext { 137 var err error 138 if w.ctx == nil { 139 w.ctx, err = w.driver.NewContext() 140 if err != nil { 141 return err 142 } 143 sync = true 144 } 145 } 146 if sync && w.ctx != nil { 147 if err := w.ctx.Refresh(); err != nil { 148 if errors.Is(err, errOutOfDate) { 149 // Surface couldn't be created for transient reasons. Skip 150 // this frame and wait for the next. 151 return nil 152 } 153 w.destroyGPU() 154 if errors.Is(err, gpu.ErrDeviceLost) { 155 continue 156 } 157 return err 158 } 159 } 160 if w.ctx != nil { 161 if err := w.ctx.Lock(); err != nil { 162 w.destroyGPU() 163 return err 164 } 165 } 166 if w.gpu == nil && !w.nocontext { 167 gpu, err := gpu.New(w.ctx.API()) 168 if err != nil { 169 w.ctx.Unlock() 170 w.destroyGPU() 171 return err 172 } 173 w.gpu = gpu 174 } 175 if w.gpu != nil { 176 if err := w.frame(frame, size); err != nil { 177 w.ctx.Unlock() 178 if errors.Is(err, errOutOfDate) { 179 // GPU surface needs refreshing. 180 sync = true 181 continue 182 } 183 w.destroyGPU() 184 if errors.Is(err, gpu.ErrDeviceLost) { 185 continue 186 } 187 return err 188 } 189 } 190 w.queue.Frame(frame) 191 // Let the client continue as soon as possible, in particular before 192 // a potentially blocking Present. 193 signal() 194 var err error 195 if w.gpu != nil { 196 err = w.ctx.Present() 197 w.ctx.Unlock() 198 } 199 return err 200 } 201 } 202 203 func (w *Window) frame(frame *op.Ops, viewport image.Point) error { 204 if runtime.GOOS == "js" { 205 // Use transparent black when Gio is embedded, to allow mixing of Gio and 206 // foreign content below. 207 w.gpu.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) 208 } else { 209 w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) 210 } 211 target, err := w.ctx.RenderTarget() 212 if err != nil { 213 return err 214 } 215 return w.gpu.Frame(frame, target, viewport) 216 } 217 218 func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { 219 wrapper := &w.decorations.Ops 220 off := op.Offset(w.lastFrame.off).Push(wrapper) 221 ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) 222 off.Pop() 223 w.lastFrame.deco.Add(wrapper) 224 if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil { 225 w.destroyGPU() 226 w.driver.ProcessEvent(DestroyEvent{Err: err}) 227 return 228 } 229 w.updateState() 230 w.updateCursor() 231 } 232 233 func (w *Window) updateState() { 234 for k := range w.semantic.ids { 235 delete(w.semantic.ids, k) 236 } 237 w.semantic.uptodate = false 238 q := &w.queue 239 switch q.TextInputState() { 240 case input.TextInputOpen: 241 w.driver.ShowTextInput(true) 242 case input.TextInputClose: 243 w.driver.ShowTextInput(false) 244 } 245 if hint, ok := q.TextInputHint(); ok { 246 w.driver.SetInputHint(hint) 247 } 248 if mime, txt, ok := q.WriteClipboard(); ok { 249 w.driver.WriteClipboard(mime, txt) 250 } 251 if q.ClipboardRequested() { 252 w.driver.ReadClipboard() 253 } 254 oldState := w.imeState 255 newState := oldState 256 newState.EditorState = q.EditorState() 257 if newState != oldState { 258 w.imeState = newState 259 w.driver.EditorStateChanged(oldState, newState) 260 } 261 if t, ok := q.WakeupTime(); ok { 262 w.setNextFrame(t) 263 } 264 w.updateAnimation() 265 } 266 267 // Invalidate the window such that a [FrameEvent] will be generated immediately. 268 // If the window is inactive, an unspecified event is sent instead. 269 // 270 // Note that Invalidate is intended for externally triggered updates, such as a 271 // response from a network request. The [op.InvalidateCmd] command is more efficient 272 // for animation. 273 // 274 // Invalidate is safe for concurrent use. 275 func (w *Window) Invalidate() { 276 w.init() 277 w.basic.Invalidate() 278 } 279 280 // Option applies the options to the window. The options are hints; the platform is 281 // free to ignore or adjust them. 282 func (w *Window) Option(opts ...Option) { 283 if len(opts) == 0 { 284 return 285 } 286 w.init(opts...) 287 w.Run(func() { 288 cnf := Config{Decorated: w.decorations.enabled} 289 for _, opt := range opts { 290 opt(w.metric, &cnf) 291 } 292 w.decorations.enabled = cnf.Decorated 293 decoHeight := w.decorations.height 294 if !w.decorations.enabled { 295 decoHeight = 0 296 } 297 opts = append(opts, decoHeightOpt(decoHeight)) 298 w.driver.Configure(opts) 299 w.setNextFrame(time.Time{}) 300 w.updateAnimation() 301 }) 302 } 303 304 // Run f in the same thread as the native window event loop, and wait for f to 305 // return or the window to close. 306 // 307 // Note that most programs should not call Run; configuring a Window with 308 // [CustomRenderer] is a notable exception. 309 func (w *Window) Run(f func()) { 310 w.init() 311 if w.driver == nil { 312 return 313 } 314 done := make(chan struct{}) 315 w.driver.Run(func() { 316 defer close(done) 317 f() 318 }) 319 <-done 320 } 321 322 func (w *Window) updateAnimation() { 323 if w.driver == nil { 324 return 325 } 326 animate := false 327 if w.hasNextFrame { 328 if dt := time.Until(w.nextFrame); dt <= 0 { 329 animate = true 330 } else { 331 // Schedule redraw. 332 w.scheduleInvalidate(w.nextFrame) 333 } 334 } 335 if animate != w.animating { 336 w.animating = animate 337 w.driver.SetAnimating(animate) 338 } 339 } 340 341 func (w *Window) scheduleInvalidate(t time.Time) { 342 if w.timer.quit == nil { 343 w.timer.quit = make(chan struct{}) 344 w.timer.update = make(chan time.Time) 345 go func() { 346 var timer *time.Timer 347 for { 348 var timeC <-chan time.Time 349 if timer != nil { 350 timeC = timer.C 351 } 352 select { 353 case <-w.timer.quit: 354 w.timer.quit <- struct{}{} 355 return 356 case t := <-w.timer.update: 357 if timer != nil { 358 timer.Stop() 359 } 360 timer = time.NewTimer(time.Until(t)) 361 case <-timeC: 362 w.Invalidate() 363 } 364 } 365 }() 366 } 367 w.timer.update <- t 368 } 369 370 func (w *Window) setNextFrame(at time.Time) { 371 if !w.hasNextFrame || at.Before(w.nextFrame) { 372 w.hasNextFrame = true 373 w.nextFrame = at 374 } 375 } 376 377 func (c *callbacks) SetDriver(d basicDriver) { 378 c.w.basic = d 379 if d, ok := d.(driver); ok { 380 c.w.driver = d 381 } 382 } 383 384 func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) { 385 c.w.processFrame(frame, ack) 386 } 387 388 func (c *callbacks) ProcessEvent(e event.Event) bool { 389 return c.w.processEvent(e) 390 } 391 392 // SemanticRoot returns the ID of the semantic root. 393 func (c *callbacks) SemanticRoot() input.SemanticID { 394 c.w.updateSemantics() 395 return c.w.semantic.root 396 } 397 398 // LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root. 399 func (c *callbacks) LookupSemantic(semID input.SemanticID) (input.SemanticNode, bool) { 400 c.w.updateSemantics() 401 n, found := c.w.semantic.ids[semID] 402 return n, found 403 } 404 405 func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.SemanticID { 406 c.w.updateSemantics() 407 if tree := c.w.semantic.prevTree; len(tree) > 0 { 408 c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0]) 409 } 410 return diffs 411 } 412 413 func (c *callbacks) SemanticAt(pos f32.Point) (input.SemanticID, bool) { 414 c.w.updateSemantics() 415 return c.w.queue.SemanticAt(pos) 416 } 417 418 func (c *callbacks) EditorState() editorState { 419 return c.w.imeState 420 } 421 422 func (c *callbacks) SetComposingRegion(r key.Range) { 423 c.w.imeState.compose = r 424 } 425 426 func (c *callbacks) EditorInsert(text string) { 427 sel := c.w.imeState.Selection.Range 428 c.EditorReplace(sel, text) 429 start := sel.Start 430 if sel.End < start { 431 start = sel.End 432 } 433 sel.Start = start + utf8.RuneCountInString(text) 434 sel.End = sel.Start 435 c.SetEditorSelection(sel) 436 } 437 438 func (c *callbacks) EditorReplace(r key.Range, text string) { 439 c.w.imeState.Replace(r, text) 440 c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text}) 441 c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range)) 442 } 443 444 func (c *callbacks) SetEditorSelection(r key.Range) { 445 c.w.imeState.Selection.Range = r 446 c.w.driver.ProcessEvent(key.SelectionEvent(r)) 447 } 448 449 func (c *callbacks) SetEditorSnippet(r key.Range) { 450 if sn := c.EditorState().Snippet.Range; sn == r { 451 // No need to expand. 452 return 453 } 454 c.w.driver.ProcessEvent(key.SnippetEvent(r)) 455 } 456 457 func (w *Window) moveFocus(dir key.FocusDirection) { 458 w.queue.MoveFocus(dir) 459 if _, handled := w.queue.WakeupTime(); handled { 460 w.queue.RevealFocus(w.viewport) 461 } else { 462 var v image.Point 463 switch dir { 464 case key.FocusRight: 465 v = image.Pt(+1, 0) 466 case key.FocusLeft: 467 v = image.Pt(-1, 0) 468 case key.FocusDown: 469 v = image.Pt(0, +1) 470 case key.FocusUp: 471 v = image.Pt(0, -1) 472 default: 473 return 474 } 475 const scrollABit = unit.Dp(50) 476 dist := v.Mul(int(w.metric.Dp(scrollABit))) 477 w.queue.ScrollFocus(dist) 478 } 479 } 480 481 func (c *callbacks) ClickFocus() { 482 c.w.queue.ClickFocus() 483 c.w.setNextFrame(time.Time{}) 484 c.w.updateAnimation() 485 } 486 487 func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) { 488 return c.w.queue.ActionAt(p) 489 } 490 491 func (w *Window) destroyGPU() { 492 if w.gpu != nil { 493 w.ctx.Lock() 494 w.gpu.Release() 495 w.ctx.Unlock() 496 w.gpu = nil 497 } 498 if w.ctx != nil { 499 w.ctx.Release() 500 w.ctx = nil 501 } 502 } 503 504 // updateSemantics refreshes the semantics tree, the id to node map and the ids of 505 // updated nodes. 506 func (w *Window) updateSemantics() { 507 if w.semantic.uptodate { 508 return 509 } 510 w.semantic.uptodate = true 511 w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree 512 w.semantic.tree = w.queue.AppendSemantics(w.semantic.tree[:0]) 513 w.semantic.root = w.semantic.tree[0].ID 514 for _, n := range w.semantic.tree { 515 w.semantic.ids[n.ID] = n 516 } 517 } 518 519 // collectSemanticDiffs traverses the previous semantic tree, noting changed nodes. 520 func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.SemanticNode) { 521 newNode, exists := w.semantic.ids[n.ID] 522 // Ignore deleted nodes, as their disappearance will be reported through an 523 // ancestor node. 524 if !exists { 525 return 526 } 527 diff := newNode.Desc != n.Desc || len(n.Children) != len(newNode.Children) 528 for i, ch := range n.Children { 529 if !diff { 530 newCh := newNode.Children[i] 531 diff = ch.ID != newCh.ID 532 } 533 w.collectSemanticDiffs(diffs, ch) 534 } 535 if diff { 536 *diffs = append(*diffs, n.ID) 537 } 538 } 539 540 func (c *callbacks) Invalidate() { 541 c.w.setNextFrame(time.Time{}) 542 c.w.updateAnimation() 543 // Guarantee a wakeup, even when not animating. 544 c.w.processEvent(wakeupEvent{}) 545 } 546 547 func (c *callbacks) nextEvent() (event.Event, bool) { 548 s := &c.w.coalesced 549 // Every event counts as a wakeup. 550 defer func() { s.wakeup = false }() 551 switch { 552 case s.view != nil: 553 e := *s.view 554 s.view = nil 555 return e, true 556 case s.destroy != nil: 557 e := *s.destroy 558 // Clear pending events after DestroyEvent is delivered. 559 *s = eventSummary{} 560 return e, true 561 case s.cfg != nil: 562 e := *s.cfg 563 s.cfg = nil 564 return e, true 565 case s.frame != nil: 566 e := *s.frame 567 s.frame = nil 568 return e.FrameEvent, true 569 case s.wakeup: 570 return wakeupEvent{}, true 571 } 572 return nil, false 573 } 574 575 func (w *Window) processEvent(e event.Event) bool { 576 switch e2 := e.(type) { 577 case wakeupEvent: 578 w.coalesced.wakeup = true 579 case frameEvent: 580 if e2.Size == (image.Point{}) { 581 panic(errors.New("internal error: zero-sized Draw")) 582 } 583 w.metric = e2.Metric 584 w.hasNextFrame = false 585 e2.Frame = w.driver.Frame 586 e2.Source = w.queue.Source() 587 // Prepare the decorations and update the frame insets. 588 viewport := image.Rectangle{ 589 Min: image.Point{ 590 X: e2.Metric.Dp(e2.Insets.Left), 591 Y: e2.Metric.Dp(e2.Insets.Top), 592 }, 593 Max: image.Point{ 594 X: e2.Size.X - e2.Metric.Dp(e2.Insets.Right), 595 Y: e2.Size.Y - e2.Metric.Dp(e2.Insets.Bottom), 596 }, 597 } 598 // Scroll to focus if viewport is shrinking in any dimension. 599 if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y { 600 w.queue.RevealFocus(viewport) 601 } 602 w.viewport = viewport 603 wrapper := &w.decorations.Ops 604 wrapper.Reset() 605 m := op.Record(wrapper) 606 offset := w.decorate(e2.FrameEvent, wrapper) 607 w.lastFrame.deco = m.Stop() 608 w.lastFrame.size = e2.Size 609 w.lastFrame.sync = e2.Sync 610 w.lastFrame.off = offset 611 e2.Size = e2.Size.Sub(offset) 612 w.coalesced.frame = &e2 613 case DestroyEvent: 614 w.destroyGPU() 615 w.driver = nil 616 if q := w.timer.quit; q != nil { 617 q <- struct{}{} 618 <-q 619 } 620 w.coalesced.destroy = &e2 621 case ViewEvent: 622 if reflect.ValueOf(e2).IsZero() && w.gpu != nil { 623 w.ctx.Lock() 624 w.gpu.Release() 625 w.gpu = nil 626 w.ctx.Unlock() 627 } 628 w.coalesced.view = &e2 629 case ConfigEvent: 630 wasFocused := w.decorations.Config.Focused 631 w.decorations.Config = e2.Config 632 e2.Config = w.effectiveConfig() 633 w.coalesced.cfg = &e2 634 if f := w.decorations.Config.Focused; f != wasFocused { 635 w.queue.Queue(key.FocusEvent{Focus: f}) 636 } 637 t, handled := w.queue.WakeupTime() 638 if handled { 639 w.setNextFrame(t) 640 w.updateAnimation() 641 } 642 return handled 643 case event.Event: 644 focusDir := key.FocusDirection(-1) 645 if e, ok := e2.(key.Event); ok && e.State == key.Press { 646 isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" 647 switch { 648 case e.Name == key.NameTab && e.Modifiers == 0: 649 focusDir = key.FocusForward 650 case e.Name == key.NameTab && e.Modifiers == key.ModShift: 651 focusDir = key.FocusBackward 652 case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: 653 focusDir = key.FocusUp 654 case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: 655 focusDir = key.FocusDown 656 case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: 657 focusDir = key.FocusLeft 658 case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile: 659 focusDir = key.FocusRight 660 } 661 } 662 e := e2 663 if focusDir != -1 { 664 e = input.SystemEvent{Event: e} 665 } 666 w.queue.Queue(e) 667 t, handled := w.queue.WakeupTime() 668 if focusDir != -1 && !handled { 669 w.moveFocus(focusDir) 670 t, handled = w.queue.WakeupTime() 671 } 672 w.updateCursor() 673 if handled { 674 w.setNextFrame(t) 675 w.updateAnimation() 676 } 677 return handled 678 } 679 return true 680 } 681 682 // Event blocks until an event is received from the window, such as 683 // [FrameEvent], or until [Invalidate] is called. 684 func (w *Window) Event() event.Event { 685 w.init() 686 return w.basic.Event() 687 } 688 689 func (w *Window) init(initial ...Option) { 690 w.once.Do(func() { 691 debug.Parse() 692 // Measure decoration height. 693 deco := new(widget.Decorations) 694 theme := material.NewTheme() 695 theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular())) 696 decoStyle := material.Decorations(theme, deco, 0, "") 697 gtx := layout.Context{ 698 Ops: new(op.Ops), 699 // Measure in Dp. 700 Metric: unit.Metric{}, 701 } 702 // Allow plenty of space. 703 gtx.Constraints.Max.Y = 200 704 dims := decoStyle.Layout(gtx) 705 decoHeight := unit.Dp(dims.Size.Y) 706 defaultOptions := []Option{ 707 Size(800, 600), 708 Title("Gio"), 709 Decorated(true), 710 decoHeightOpt(decoHeight), 711 } 712 options := append(defaultOptions, initial...) 713 var cnf Config 714 cnf.apply(unit.Metric{}, options) 715 716 w.nocontext = cnf.CustomRenderer 717 w.decorations.Theme = theme 718 w.decorations.Decorations = deco 719 w.decorations.enabled = cnf.Decorated 720 w.decorations.height = decoHeight 721 w.imeState.compose = key.Range{Start: -1, End: -1} 722 w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) 723 newWindow(&callbacks{w}, options) 724 }) 725 } 726 727 func (w *Window) updateCursor() { 728 if c := w.queue.Cursor(); c != w.cursor { 729 w.cursor = c 730 w.driver.SetCursor(c) 731 } 732 } 733 734 func (w *Window) fallbackDecorate() bool { 735 cnf := w.decorations.Config 736 return w.decorations.enabled && !cnf.Decorated && cnf.Mode != Fullscreen && !w.nocontext 737 } 738 739 // decorate the window if enabled and returns the corresponding Insets. 740 func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point { 741 if !w.fallbackDecorate() { 742 return image.Pt(0, 0) 743 } 744 deco := w.decorations.Decorations 745 allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize | 746 system.ActionClose | system.ActionMove 747 style := material.Decorations(w.decorations.Theme, deco, allActions, w.decorations.Config.Title) 748 // Update the decorations based on the current window mode. 749 var actions system.Action 750 switch m := w.decorations.Config.Mode; m { 751 case Windowed: 752 actions |= system.ActionUnmaximize 753 case Minimized: 754 actions |= system.ActionMinimize 755 case Maximized: 756 actions |= system.ActionMaximize 757 case Fullscreen: 758 actions |= system.ActionFullscreen 759 default: 760 panic(fmt.Errorf("unknown WindowMode %v", m)) 761 } 762 deco.Perform(actions) 763 gtx := layout.Context{ 764 Ops: o, 765 Now: e.Now, 766 Source: e.Source, 767 Metric: e.Metric, 768 Constraints: layout.Exact(e.Size), 769 } 770 // Update the window based on the actions on the decorations. 771 opts, acts := splitActions(deco.Update(gtx)) 772 if len(opts) > 0 { 773 w.driver.Configure(opts) 774 } 775 if acts != 0 { 776 w.driver.Perform(acts) 777 } 778 style.Layout(gtx) 779 // Offset to place the frame content below the decorations. 780 decoHeight := gtx.Dp(w.decorations.Config.decoHeight) 781 if w.decorations.currentHeight != decoHeight { 782 w.decorations.currentHeight = decoHeight 783 w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()} 784 } 785 return image.Pt(0, decoHeight) 786 } 787 788 func (w *Window) effectiveConfig() Config { 789 cnf := w.decorations.Config 790 cnf.Size.Y -= w.decorations.currentHeight 791 cnf.Decorated = w.decorations.enabled || cnf.Decorated 792 return cnf 793 } 794 795 // splitActions splits options from actions and return them and the remaining 796 // actions. 797 func splitActions(actions system.Action) ([]Option, system.Action) { 798 var opts []Option 799 walkActions(actions, func(action system.Action) { 800 switch action { 801 case system.ActionMinimize: 802 opts = append(opts, Minimized.Option()) 803 case system.ActionMaximize: 804 opts = append(opts, Maximized.Option()) 805 case system.ActionUnmaximize: 806 opts = append(opts, Windowed.Option()) 807 case system.ActionFullscreen: 808 opts = append(opts, Fullscreen.Option()) 809 default: 810 return 811 } 812 actions &^= action 813 }) 814 return opts, actions 815 } 816 817 // Perform the actions on the window. 818 func (w *Window) Perform(actions system.Action) { 819 opts, acts := splitActions(actions) 820 w.Option(opts...) 821 if acts == 0 { 822 return 823 } 824 w.Run(func() { 825 w.driver.Perform(actions) 826 }) 827 } 828 829 // Title sets the title of the window. 830 func Title(t string) Option { 831 return func(_ unit.Metric, cnf *Config) { 832 cnf.Title = t 833 } 834 } 835 836 // Size sets the size of the window. The mode will be changed to Windowed. 837 func Size(w, h unit.Dp) Option { 838 if w <= 0 { 839 panic("width must be larger than or equal to 0") 840 } 841 if h <= 0 { 842 panic("height must be larger than or equal to 0") 843 } 844 return func(m unit.Metric, cnf *Config) { 845 cnf.Mode = Windowed 846 cnf.Size = image.Point{ 847 X: m.Dp(w), 848 Y: m.Dp(h), 849 } 850 } 851 } 852 853 // MaxSize sets the maximum size of the window. 854 func MaxSize(w, h unit.Dp) Option { 855 if w <= 0 { 856 panic("width must be larger than or equal to 0") 857 } 858 if h <= 0 { 859 panic("height must be larger than or equal to 0") 860 } 861 return func(m unit.Metric, cnf *Config) { 862 cnf.MaxSize = image.Point{ 863 X: m.Dp(w), 864 Y: m.Dp(h), 865 } 866 } 867 } 868 869 // MinSize sets the minimum size of the window. 870 func MinSize(w, h unit.Dp) Option { 871 if w <= 0 { 872 panic("width must be larger than or equal to 0") 873 } 874 if h <= 0 { 875 panic("height must be larger than or equal to 0") 876 } 877 return func(m unit.Metric, cnf *Config) { 878 cnf.MinSize = image.Point{ 879 X: m.Dp(w), 880 Y: m.Dp(h), 881 } 882 } 883 } 884 885 // StatusColor sets the color of the Android status bar. 886 func StatusColor(color color.NRGBA) Option { 887 return func(_ unit.Metric, cnf *Config) { 888 cnf.StatusColor = color 889 } 890 } 891 892 // NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers. 893 func NavigationColor(color color.NRGBA) Option { 894 return func(_ unit.Metric, cnf *Config) { 895 cnf.NavigationColor = color 896 } 897 } 898 899 // CustomRenderer controls whether the window contents is 900 // rendered by the client. If true, no GPU context is created. 901 // 902 // Caller must assume responsibility for rendering which includes 903 // initializing the render backend, swapping the framebuffer and 904 // handling frame pacing. 905 func CustomRenderer(custom bool) Option { 906 return func(_ unit.Metric, cnf *Config) { 907 cnf.CustomRenderer = custom 908 } 909 } 910 911 // Decorated controls whether Gio and/or the platform are responsible 912 // for drawing window decorations. Providing false indicates that 913 // the application will either be undecorated or will draw its own decorations. 914 func Decorated(enabled bool) Option { 915 return func(_ unit.Metric, cnf *Config) { 916 cnf.Decorated = enabled 917 } 918 } 919 920 // flushEvent is sent to detect when the user program 921 // has completed processing of all prior events. Its an 922 // [io/event.Event] but only for internal use. 923 type flushEvent struct{} 924 925 func (t flushEvent) ImplementsEvent() {} 926 927 // theFlushEvent avoids allocating garbage when sending 928 // flushEvents. 929 var theFlushEvent flushEvent