Australia
https://jackmordaunt.srht.site
Software Developer.
Ticket created by ~jackmordaunt on ~eliasnaur/gio
This might very well be intended behaviour, however it doesn't match prior versions of Gio. If it's intended feel free to close the ticket.
In short:
gtx.Disabled()
has typically been used to block user input. However in the current version of Gio it will block all animations as well, sincegtx.Execute(op.InvalidateCmd{})
is not processed on a disabled gtx.Here's a minimal reproducer:
package main import ( "log" "os" "gioui.org/app" "gioui.org/layout" "gioui.org/op" "gioui.org/widget/material" ) func main() { go func() { w := new(app.Window) w.Option(app.Title("playground")) if err := run(w); err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } func run(w *app.Window) error { var ( th = material.NewTheme() ops op.Ops ) for { switch ev := w.Event().(type) { case app.DestroyEvent: return ev.Err case app.FrameEvent: gtx := app.NewContext(&ops, ev) frame(gtx, th) ev.Frame(gtx.Ops) } } } type ( C = layout.Context D = layout.Dimensions ) func frame(gtx C, th *material.Theme) D { return layout.Center.Layout(gtx, func(gtx C) D { gtx = gtx.Disabled() return material.Loader(th).Layout(gtx) }) }
Ticket created by ~jackmordaunt on ~eliasnaur/gio
After https://todo.sr.ht/~eliasnaur/gio/600 has been fixed (thanks!), I've run into another bug.
My goal is to have a custom decorated window that allows for both moving the window with a drag (using
system.ActionMove
) and double-click to (un)maximize the window.To that end I have a
widget.Decorations
which implements the dragging, and expanded atop that I have agesture.Click
.If pass-through mode is active, only the decoration dragging works and double-clicks do nothing.
If pass-through mode is inactive, only the double-click logic works and the window cannot be dragged.
It seems that
system.ActionMove
doesn't respect pass-through mode.Here's a minimal reproducer:
package main import ( "fmt" "log" "os" "gioui.org/app" "gioui.org/gesture" "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" "gioui.org/widget" "gioui.org/widget/material" ) func main() { go func() { window := new(app.Window) err := run(window) if err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } type ( C = layout.Context D = layout.Dimensions ) func run(window *app.Window) error { window.Option(app.Decorated(false)) var ( theme = material.NewTheme() maximized bool click gesture.Click decorations widget.Decorations ops op.Ops ) for { switch e := window.Event().(type) { case app.DestroyEvent: return e.Err case app.FrameEvent: gtx := app.NewContext(&ops, e) layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return layout.Stack{}.Layout(gtx, layout.Stacked(func(gtx C) D { return material.Decorations(theme, &decorations, system.ActionClose|system.ActionMaximize|system.ActionMinimize, "hello").Layout(gtx) }), layout.Expanded(func(gtx C) D { defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() // If pass-through mode is active, only decoration dragging works. // If pass-through mode is disabled, only double-click logic works. defer pointer.PassOp{}.Push(gtx.Ops).Pop() click.Add(gtx.Ops) for { ev, ok := click.Update(gtx.Source) if !ok { break } if ev.Kind == gesture.KindClick && ev.NumClicks == 2 { if maximized { window.Perform(system.ActionUnmaximize) decorations.Perform(system.ActionUnmaximize) } else { window.Perform(system.ActionMaximize) decorations.Perform(system.ActionMaximize) } maximized = !maximized } } return D{Size: gtx.Constraints.Min} }), ) }), layout.Rigid(func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { return material.H4(theme, fmt.Sprintf("Is Maximized? %v", decorations.Maximized())).Layout(gtx) }) }), ) e.Frame(gtx.Ops) } } }
Comment by ~jackmordaunt on ~eliasnaur/gio
Environment: Windows 11, Go 1.22.4, Gio 0.7.0 (but also occurred on earlier versions eg 0.5.0).
Ticket created by ~jackmordaunt on ~eliasnaur/gio
Laying out
widget.Decorations
will produce a window that will maximize on double-click.The problem is that it won't un-maximize from there, and the
widget.Decorations
reflects the incorrect state, e.g.Maximized()
will return false.This is breaking apps that are trying to have their own double-click handling because the very first double-click is swallowed by this mechanism and not reflected in the state.
Here's a minimal reproducer:
package main import ( "fmt" "log" "os" "gioui.org/app" "gioui.org/layout" "gioui.org/op" "gioui.org/widget" "gioui.org/widget/material" ) func main() { go func() { window := new(app.Window) err := run(window) if err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } type ( C = layout.Context D = layout.Dimensions ) func run(window *app.Window) error { window.Option(app.Decorated(false)) theme := material.NewTheme() var decorations widget.Decorations var ops op.Ops for { switch e := window.Event().(type) { case app.DestroyEvent: return e.Err case app.FrameEvent: gtx := app.NewContext(&ops, e) layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return material.Decorations(theme, &decorations, 0, "hello").Layout(gtx) }), layout.Rigid(func(gtx C) D { return layout.Center.Layout(gtx, func(gtx C) D { return material.H4(theme, fmt.Sprintf("Is Maximized? %v", decorations.Maximized())).Layout(gtx) }) }), ) e.Frame(gtx.Ops) } } }
Comment by ~jackmordaunt on ~eliasnaur/gio
Curiously, this coincided with closing the window.
Ticket created by ~jackmordaunt on ~eliasnaur/gio
It appears there is a race between
app.window.Wakeup()
andapp.windowProc()
.================== WARNING: DATA RACE Read at 0x00c00aa88c80 by goroutine 4225: gioui.org/app.(*window).Wakeup() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:605 +0x47 gioui.org/app.driver.Wakeup-fm() <autogenerated>:1 +0x49 gioui.org/app.(*Window).run() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/window.go:976 +0x549 gioui.org/app.NewWindow.func1() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/window.go:188 +0x84 Previous write at 0x00c00aa88c80 by goroutine 4228: gioui.org/app.windowProc() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:310 +0x386 runtime.call32() C:/Program Files/Go/src/runtime/asm_amd64.s:748 +0x47 golang.org/x/sys/windows.(*LazyProc).Call() C:/Users/jackm/go/pkg/mod/golang.org/x/sys@v0.12.0/windows/dll_windows.go:348 +0xe4 gioui.org/app/internal/windows.DefWindowProc() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/internal/windows/windows.go:448 +0x1b3 gioui.org/app.windowProc() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:437 +0x2ded runtime.call32() C:/Program Files/Go/src/runtime/asm_amd64.s:748 +0x47 golang.org/x/sys/windows.(*LazyProc).Call() C:/Users/jackm/go/pkg/mod/golang.org/x/sys@v0.12.0/windows/dll_windows.go:348 +0xe4 gioui.org/app/internal/windows.DispatchMessage() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/internal/windows/windows.go:457 +0xd1 gioui.org/app.(*window).loop() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:584 +0x224 gioui.org/app.newWindow.func1() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:112 +0x632 Goroutine 4225 (running) created at: gioui.org/app.NewWindow() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/window.go:188 +0x16b6 git.sr.ht/~gioverse/skel/window.Windower.Run() C:/Users/jackm/go/pkg/mod/git.sr.ht/~gioverse/skel@v0.0.0-20230919201906-8a759b754d85/window/windower.go:118 +0x6c9 main.runClient() C:/Users/jackm/Source/desktop/cmd/client/main.go:315 +0x39a4 main.main.func1() C:/Users/jackm/Source/desktop/cmd/client/main.go:114 +0x104 Goroutine 4228 (running) created at: gioui.org/app.newWindow() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/os_windows.go:89 +0x1ef gioui.org/app.(*Window).run() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/window.go:942 +0xa4 gioui.org/app.NewWindow.func1() C:/Users/jackm/go/pkg/mod/gioui.org@v0.3.1-0.20230918142811-27193ae8e816/app/window.go:188 +0x84 ==================
Comment by ~jackmordaunt on ~eliasnaur/gio
Excellent, changes look good! Thanks Elias.
Ticket created by ~jackmordaunt on ~eliasnaur/gio
I believe a race condition causing a
send on closed channel
that gets triggered whenvalidateAndProcess
returns an error andprocessEvent
fails to wait for thew.dead
channel to close before processing another frame event.panic: send on closed channel goroutine 57 [running, locked to thread]: gioui.org/app.(*Window).processEvent(0xc000438000, {0x7ff7088881d0, 0xc00007ef00}, {0x7ff708876a60, 0xc008621980}) gioui.org@v0.0.0-20230425023356-bba91263b077/app/window.go:879 +0x987 gioui.org/app.(*callbacks).Event(0xc0004384d8, {0x7ff708876a60, 0xc008621980}) gioui.org@v0.0.0-20230425023356-bba91263b077/app/window.go:484 +0x2d2 gioui.org/app.(*window).draw(0xc00007ef00, 0x0) gioui.org@v0.0.0-20230425023356-bba91263b077/app/os_windows.go:604 +0x17c gioui.org/app.(*window).loop(0xc00007ef00) gioui.org@v0.0.0-20230425023356-bba91263b077/app/os_windows.go:554 +0x89 gioui.org/app.newWindow.func1() gioui.org@v0.0.0-20230425023356-bba91263b077/app/os_windows.go:118 +0x386 created by gioui.org/app.newWindow gioui.org@v0.0.0-20230425023356-bba91263b077/app/os_windows.go:95 +0x105
Thread 1.
// w.processEvent(...) if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil { w.destroyGPU() w.out <- system.DestroyEvent{Err: err} close(w.out) w.destroy <- struct{}{} // We falsely assume that this send means `w.dead` will be closed before next time we enter `processEvent`. break }Thread 2.
// w.run() case <-w.destroy: // Between the receive operation and the close operation is non-zero time wherein thread 1 can re-enter `processEvent` before we actually execute this channel close. close(w.dead)Here is some execution flow to help explain:
Thread 2 (blocked): case <-w.destroy Thread 1 (running): processEvent(system.FrameEvent) Thread 1 (running): validateAndProcess(...) -> error // oops, got error Thread 1 (running): w.out <- system.DestroyEvent Thread 1 (running): close(w.out) Thread 1 (running): w.destroy <- struct{}{} Thread 2 (receive): case <-w.destroy Thread 1 (running): break Thread 1 (running): processEvent(system.FrameEvent) Thread 1 (running): <-w.dead // not dead yet! Thread 2 (running): close(w.dead) // finally closed, but too late to protect us Thread 1 (running): w.out <- e2.FrameEvent // panic!!
processEvent
assumes that once it passes the dead check it can safely send on the out channel. In this case, we pass the dead check just before the dead channel is closed. Thus we continue processing the frame event under the false the assumption that the out channel is open, so we send on it causing a panic.
Ticket created by ~jackmordaunt on ~gioverse/chat
At the moment, panics are silently recovered. This means that messages are never delivered and the application has no way of detecting it.
We need to surface a way to handle panics inside workers.
In the case of a future, the panic can propagate to the handler as a plain error "panic processing worker: %w", for example.
Comment by ~jackmordaunt on ~eliasnaur/gio
Thanks for the tip Elias! I will investigate further based on this.