feat(wayland): map xdg toplevels and apply core window commands
- commit
- e9aeb28461a83b499b6a08966c1503e43c5f4241
- author
- brice · · 2026-09-23 12:18 UTC
- parent
- f46a2b0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +332 -27
M internal/adapters/wayland/server.go +97 -17
| @@ -2,13 +2,15 @@ package wayland | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "encoding/binary" | |
| 5 | 6 | "fmt" | |
| 6 | - | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | 7 | "os" | |
| 8 | - | "time" | |
| 9 | - | "sync" | |
| 8 | + | "sync" | |
| 9 | + | "time" | |
| 10 | 10 | ||
| 11 | 11 | "github.com/bnema/nefertty/internal/ports" | |
| 12 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 13 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 12 | 14 | "github.com/bnema/purego-libwayland/server" | |
| 13 | 15 | "github.com/bnema/zerowrap" | |
| 14 | 16 | "golang.org/x/sys/unix" | |
| @@ -18,21 +20,27 @@ type Options struct { | |||
| 18 | 20 | RuntimeDir string | |
| 19 | 21 | OutputWidth, OutputHeight int | |
| 20 | 22 | } | |
| 23 | + | ||
| 24 | + | // Channels carries client notifications and commands. Events must be buffered. | |
| 21 | 25 | type Channels struct { | |
| 22 | 26 | Events chan<- ports.ClientEvent | |
| 23 | 27 | Commands <-chan ports.ClientCommand | |
| 24 | 28 | } | |
| 25 | 29 | type Server struct { | |
| 26 | - | display *server.Display | |
| 27 | - | name string | |
| 28 | - | cleanup func() | |
| 29 | - | log zerowrap.Logger | |
| 30 | - | channels Channels | |
| 31 | - | awaiting []*wayland.Callback | |
| 32 | - | frames uint64 | |
| 33 | - | started time.Time | |
| 34 | - | surfaces map[*server.Resource]*surface | |
| 35 | - | serial uint32 | |
| 30 | + | display *server.Display | |
| 31 | + | name string | |
| 32 | + | cleanup func() | |
| 33 | + | log zerowrap.Logger | |
| 34 | + | channels Channels | |
| 35 | + | awaiting []*wayland.Callback | |
| 36 | + | frames uint64 | |
| 37 | + | started time.Time | |
| 38 | + | surfaces map[*server.Resource]*surface | |
| 39 | + | serial uint32 | |
| 40 | + | ctx context.Context | |
| 41 | + | windows map[ports.WindowID]*window | |
| 42 | + | nextWindow ports.WindowID | |
| 43 | + | focused ports.WindowID | |
| 36 | 44 | } | |
| 37 | 45 | ||
| 38 | 46 | func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |
| @@ -55,7 +63,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 55 | 63 | cleanup() | |
| 56 | 64 | return nil, err | |
| 57 | 65 | } | |
| 58 | - | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface)} | |
| 66 | + | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), windows: make(map[ports.WindowID]*window), nextWindow: 1} | |
| 59 | 67 | if err = registerGlobals(d, opts, s); err != nil { | |
| 60 | 68 | cleanup() | |
| 61 | 69 | return nil, err | |
| @@ -67,8 +75,34 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 67 | 75 | defer s.cleanup() | |
| 68 | 76 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | |
| 69 | 77 | s.started = time.Now() | |
| 78 | + | s.ctx = ctx | |
| 70 | 79 | var wg sync.WaitGroup | |
| 71 | - | wg.Add(1) | |
| 80 | + | wg.Add(2) | |
| 81 | + | go func() { | |
| 82 | + | defer wg.Done() | |
| 83 | + | for { | |
| 84 | + | select { | |
| 85 | + | case <-ctx.Done(): | |
| 86 | + | return | |
| 87 | + | case <-s.display.Stopped(): | |
| 88 | + | return | |
| 89 | + | case cmd, ok := <-s.channels.Commands: | |
| 90 | + | if !ok { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | if ctx.Err() != nil { | |
| 94 | + | return | |
| 95 | + | } | |
| 96 | + | if !s.display.Do(func() { | |
| 97 | + | if ctx.Err() == nil { | |
| 98 | + | s.apply(cmd) | |
| 99 | + | } | |
| 100 | + | }) { | |
| 101 | + | return | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | }() | |
| 72 | 106 | go func() { | |
| 73 | 107 | defer wg.Done() | |
| 74 | 108 | ticker := time.NewTicker(16 * time.Millisecond) | |
| @@ -77,11 +111,13 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 77 | 111 | select { | |
| 78 | 112 | case <-ctx.Done(): | |
| 79 | 113 | return | |
| 114 | + | case <-s.display.Stopped(): | |
| 115 | + | return | |
| 80 | 116 | case <-ticker.C: | |
| 81 | 117 | if ctx.Err() != nil { | |
| 82 | 118 | return | |
| 83 | 119 | } | |
| 84 | - | s.display.Do(func() { | |
| 120 | + | if !s.display.Do(func() { | |
| 85 | 121 | if ctx.Err() != nil { | |
| 86 | 122 | return | |
| 87 | 123 | } | |
| @@ -95,7 +131,9 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 95 | 131 | cb.Destroy() | |
| 96 | 132 | s.frames++ | |
| 97 | 133 | } | |
| 98 | - | }) | |
| 134 | + | }) { | |
| 135 | + | return | |
| 136 | + | } | |
| 99 | 137 | } | |
| 100 | 138 | } | |
| 101 | 139 | }() | |
| @@ -103,3 +141,45 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 103 | 141 | wg.Wait() | |
| 104 | 142 | return err | |
| 105 | 143 | } | |
| 144 | + | ||
| 145 | + | func (s *Server) emit(ev ports.ClientEvent) { | |
| 146 | + | if s.channels.Events == nil { | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | select { | |
| 150 | + | case s.channels.Events <- ev: | |
| 151 | + | case <-s.ctx.Done(): | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func (s *Server) apply(cmd ports.ClientCommand) { | |
| 156 | + | switch c := cmd.(type) { | |
| 157 | + | case ports.FocusWindow: | |
| 158 | + | s.focused = c.ID | |
| 159 | + | case ports.ForwardKey: | |
| 160 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("forward key") | |
| 161 | + | case ports.ConfigureWindow: | |
| 162 | + | w := s.windows[c.ID] | |
| 163 | + | if w == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | |
| 164 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | |
| 165 | + | return | |
| 166 | + | } | |
| 167 | + | var states []byte | |
| 168 | + | if c.Fullscreen { | |
| 169 | + | states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen)) | |
| 170 | + | } | |
| 171 | + | if c.Activated { | |
| 172 | + | states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated)) | |
| 173 | + | } | |
| 174 | + | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states) | |
| 175 | + | s.serial++ | |
| 176 | + | w.xdg.resource.SendConfigure(s.serial) | |
| 177 | + | case ports.CloseWindow: | |
| 178 | + | w := s.windows[c.ID] | |
| 179 | + | if w == nil || !w.toplevel.Resource.Alive() { | |
| 180 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | |
| 181 | + | return | |
| 182 | + | } | |
| 183 | + | w.toplevel.SendClose() | |
| 184 | + | } | |
| 185 | + | } | |
| @@ -2,13 +2,15 @@ package wayland | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "encoding/binary" | |||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | - | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 7 | "os" | 7 | "os" | ||
| 8 | - | "time" | 8 | + | "sync" |
| 9 | - | "sync" | 9 | + | "time" |
| 10 | 10 | ||||
| 11 | "github.com/bnema/nefertty/internal/ports" | 11 | "github.com/bnema/nefertty/internal/ports" | ||
| 12 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 13 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 12 | "github.com/bnema/purego-libwayland/server" | 14 | "github.com/bnema/purego-libwayland/server" | ||
| 13 | "github.com/bnema/zerowrap" | 15 | "github.com/bnema/zerowrap" | ||
| 14 | "golang.org/x/sys/unix" | 16 | "golang.org/x/sys/unix" | ||
| @@ -18,21 +20,27 @@ type Options struct { | |||||
| 18 | RuntimeDir string | 20 | RuntimeDir string | ||
| 19 | OutputWidth, OutputHeight int | 21 | OutputWidth, OutputHeight int | ||
| 20 | } | 22 | } | ||
| 23 | + | ||||
| 24 | + | // Channels carries client notifications and commands. Events must be buffered. | |||
| 21 | type Channels struct { | 25 | type Channels struct { | ||
| 22 | Events chan<- ports.ClientEvent | 26 | Events chan<- ports.ClientEvent | ||
| 23 | Commands <-chan ports.ClientCommand | 27 | Commands <-chan ports.ClientCommand | ||
| 24 | } | 28 | } | ||
| 25 | type Server struct { | 29 | type Server struct { | ||
| 26 | - | display *server.Display | 30 | + | display *server.Display |
| 27 | - | name string | 31 | + | name string |
| 28 | - | cleanup func() | 32 | + | cleanup func() |
| 29 | - | log zerowrap.Logger | 33 | + | log zerowrap.Logger |
| 30 | - | channels Channels | 34 | + | channels Channels |
| 31 | - | awaiting []*wayland.Callback | 35 | + | awaiting []*wayland.Callback |
| 32 | - | frames uint64 | 36 | + | frames uint64 |
| 33 | - | started time.Time | 37 | + | started time.Time |
| 34 | - | surfaces map[*server.Resource]*surface | 38 | + | surfaces map[*server.Resource]*surface |
| 35 | - | serial uint32 | 39 | + | serial uint32 |
| 40 | + | ctx context.Context | |||
| 41 | + | windows map[ports.WindowID]*window | |||
| 42 | + | nextWindow ports.WindowID | |||
| 43 | + | focused ports.WindowID | |||
| 36 | } | 44 | } | ||
| 37 | 45 | ||||
| 38 | func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | 46 | func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | ||
| @@ -55,7 +63,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 55 | cleanup() | 63 | cleanup() | ||
| 56 | return nil, err | 64 | return nil, err | ||
| 57 | } | 65 | } | ||
| 58 | - | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface)} | 66 | + | s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), windows: make(map[ports.WindowID]*window), nextWindow: 1} |
| 59 | if err = registerGlobals(d, opts, s); err != nil { | 67 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 60 | cleanup() | 68 | cleanup() | ||
| 61 | return nil, err | 69 | return nil, err | ||
| @@ -67,8 +75,34 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 67 | defer s.cleanup() | 75 | defer s.cleanup() | ||
| 68 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | 76 | s.log.Info().Str("socket", s.name).Msg("starting wayland") | ||
| 69 | s.started = time.Now() | 77 | s.started = time.Now() | ||
| 78 | + | s.ctx = ctx | |||
| 70 | var wg sync.WaitGroup | 79 | var wg sync.WaitGroup | ||
| 71 | - | wg.Add(1) | 80 | + | wg.Add(2) |
| 81 | + | go func() { | |||
| 82 | + | defer wg.Done() | |||
| 83 | + | for { | |||
| 84 | + | select { | |||
| 85 | + | case <-ctx.Done(): | |||
| 86 | + | return | |||
| 87 | + | case <-s.display.Stopped(): | |||
| 88 | + | return | |||
| 89 | + | case cmd, ok := <-s.channels.Commands: | |||
| 90 | + | if !ok { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | if ctx.Err() != nil { | |||
| 94 | + | return | |||
| 95 | + | } | |||
| 96 | + | if !s.display.Do(func() { | |||
| 97 | + | if ctx.Err() == nil { | |||
| 98 | + | s.apply(cmd) | |||
| 99 | + | } | |||
| 100 | + | }) { | |||
| 101 | + | return | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | }() | |||
| 72 | go func() { | 106 | go func() { | ||
| 73 | defer wg.Done() | 107 | defer wg.Done() | ||
| 74 | ticker := time.NewTicker(16 * time.Millisecond) | 108 | ticker := time.NewTicker(16 * time.Millisecond) | ||
| @@ -77,11 +111,13 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 77 | select { | 111 | select { | ||
| 78 | case <-ctx.Done(): | 112 | case <-ctx.Done(): | ||
| 79 | return | 113 | return | ||
| 114 | + | case <-s.display.Stopped(): | |||
| 115 | + | return | |||
| 80 | case <-ticker.C: | 116 | case <-ticker.C: | ||
| 81 | if ctx.Err() != nil { | 117 | if ctx.Err() != nil { | ||
| 82 | return | 118 | return | ||
| 83 | } | 119 | } | ||
| 84 | - | s.display.Do(func() { | 120 | + | if !s.display.Do(func() { |
| 85 | if ctx.Err() != nil { | 121 | if ctx.Err() != nil { | ||
| 86 | return | 122 | return | ||
| 87 | } | 123 | } | ||
| @@ -95,7 +131,9 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 95 | cb.Destroy() | 131 | cb.Destroy() | ||
| 96 | s.frames++ | 132 | s.frames++ | ||
| 97 | } | 133 | } | ||
| 98 | - | }) | 134 | + | }) { |
| 135 | + | return | |||
| 136 | + | } | |||
| 99 | } | 137 | } | ||
| 100 | } | 138 | } | ||
| 101 | }() | 139 | }() | ||
| @@ -103,3 +141,45 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 103 | wg.Wait() | 141 | wg.Wait() | ||
| 104 | return err | 142 | return err | ||
| 105 | } | 143 | } | ||
| 144 | + | ||||
| 145 | + | func (s *Server) emit(ev ports.ClientEvent) { | |||
| 146 | + | if s.channels.Events == nil { | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | select { | |||
| 150 | + | case s.channels.Events <- ev: | |||
| 151 | + | case <-s.ctx.Done(): | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 156 | + | switch c := cmd.(type) { | |||
| 157 | + | case ports.FocusWindow: | |||
| 158 | + | s.focused = c.ID | |||
| 159 | + | case ports.ForwardKey: | |||
| 160 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("forward key") | |||
| 161 | + | case ports.ConfigureWindow: | |||
| 162 | + | w := s.windows[c.ID] | |||
| 163 | + | if w == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | |||
| 164 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | |||
| 165 | + | return | |||
| 166 | + | } | |||
| 167 | + | var states []byte | |||
| 168 | + | if c.Fullscreen { | |||
| 169 | + | states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateFullscreen)) | |||
| 170 | + | } | |||
| 171 | + | if c.Activated { | |||
| 172 | + | states = binary.LittleEndian.AppendUint32(states, uint32(xdgshell.ToplevelStateActivated)) | |||
| 173 | + | } | |||
| 174 | + | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states) | |||
| 175 | + | s.serial++ | |||
| 176 | + | w.xdg.resource.SendConfigure(s.serial) | |||
| 177 | + | case ports.CloseWindow: | |||
| 178 | + | w := s.windows[c.ID] | |||
| 179 | + | if w == nil || !w.toplevel.Resource.Alive() { | |||
| 180 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | |||
| 181 | + | return | |||
| 182 | + | } | |||
| 183 | + | w.toplevel.SendClose() | |||
| 184 | + | } | |||
| 185 | + | } | |||
M internal/adapters/wayland/server_test.go +174 -0
| @@ -1,11 +1,14 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 5 | 7 | "os" | |
| 6 | 8 | "os/exec" | |
| 7 | 9 | "path/filepath" | |
| 8 | 10 | "strings" | |
| 11 | + | "sync" | |
| 9 | 12 | "testing" | |
| 10 | 13 | "time" | |
| 11 | 14 | ||
| @@ -115,3 +118,174 @@ func TestWestonFrames(t *testing.T) { | |||
| 115 | 118 | } | |
| 116 | 119 | } | |
| 117 | 120 | } | |
| 121 | + | ||
| 122 | + | // lifecycleServer starts a display with buffered command and event channels. | |
| 123 | + | func lifecycleServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) { | |
| 124 | + | t.Helper() | |
| 125 | + | dir := t.TempDir() | |
| 126 | + | events := make(chan ports.ClientEvent, 16) | |
| 127 | + | commands := make(chan ports.ClientCommand, 16) | |
| 128 | + | s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland")) | |
| 129 | + | if err != nil { | |
| 130 | + | t.Fatal(err) | |
| 131 | + | } | |
| 132 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 133 | + | done := make(chan error, 1) | |
| 134 | + | go func() { done <- s.Run(ctx) }() | |
| 135 | + | t.Cleanup(func() { | |
| 136 | + | cancel() | |
| 137 | + | select { | |
| 138 | + | case err := <-done: | |
| 139 | + | if err != nil { | |
| 140 | + | t.Errorf("Run: %v", err) | |
| 141 | + | } | |
| 142 | + | case <-time.After(2 * time.Second): | |
| 143 | + | t.Error("Run hung") | |
| 144 | + | } | |
| 145 | + | }) | |
| 146 | + | return s, events, commands, dir | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func lifecycleClient(t *testing.T, tool string, s *Server, dir string, extra []string, args ...string) (*exec.Cmd, <-chan error, *lockedBuffer) { | |
| 150 | + | t.Helper() | |
| 151 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 152 | + | cmd := exec.CommandContext(ctx, tool, args...) | |
| 153 | + | cmd.Env = append(append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName()), extra...) | |
| 154 | + | output := &lockedBuffer{} | |
| 155 | + | cmd.Stderr = output | |
| 156 | + | if err := cmd.Start(); err != nil { | |
| 157 | + | cancel() | |
| 158 | + | t.Fatal(err) | |
| 159 | + | } | |
| 160 | + | done := make(chan error, 1) | |
| 161 | + | finished := make(chan struct{}) | |
| 162 | + | go func() { done <- cmd.Wait(); close(finished) }() | |
| 163 | + | t.Cleanup(func() { | |
| 164 | + | cancel() | |
| 165 | + | _ = cmd.Process.Kill() | |
| 166 | + | select { | |
| 167 | + | case <-finished: | |
| 168 | + | case <-time.After(2 * time.Second): | |
| 169 | + | t.Error("client hung") | |
| 170 | + | } | |
| 171 | + | }) | |
| 172 | + | return cmd, done, output | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | type lockedBuffer struct { | |
| 176 | + | mu sync.Mutex | |
| 177 | + | b bytes.Buffer | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | func (b *lockedBuffer) Write(p []byte) (int, error) { | |
| 181 | + | b.mu.Lock() | |
| 182 | + | defer b.mu.Unlock() | |
| 183 | + | return b.b.Write(p) | |
| 184 | + | } | |
| 185 | + | func (b *lockedBuffer) String() string { b.mu.Lock(); defer b.mu.Unlock(); return b.b.String() } | |
| 186 | + | ||
| 187 | + | func waitEvent(t *testing.T, events <-chan ports.ClientEvent, duration time.Duration) ports.ClientEvent { | |
| 188 | + | t.Helper() | |
| 189 | + | select { | |
| 190 | + | case ev := <-events: | |
| 191 | + | return ev | |
| 192 | + | case <-time.After(duration): | |
| 193 | + | t.Fatal("event timeout") | |
| 194 | + | return nil | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | func mapped(t *testing.T, events <-chan ports.ClientEvent, duration time.Duration) ports.WindowMapped { | |
| 198 | + | t.Helper() | |
| 199 | + | ev := waitEvent(t, events, duration) | |
| 200 | + | w, ok := ev.(ports.WindowMapped) | |
| 201 | + | if !ok { | |
| 202 | + | t.Fatalf("expected mapped, got %T", ev) | |
| 203 | + | } | |
| 204 | + | return w | |
| 205 | + | } | |
| 206 | + | func unmapped(t *testing.T, events <-chan ports.ClientEvent, id ports.WindowID) { | |
| 207 | + | t.Helper() | |
| 208 | + | ev := waitEvent(t, events, 3*time.Second) | |
| 209 | + | w, ok := ev.(ports.WindowUnmapped) | |
| 210 | + | if !ok || w.ID != id { | |
| 211 | + | t.Fatalf("expected unmapped %d, got %#v", id, ev) | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | func TestWindowClose(t *testing.T) { | |
| 216 | + | tool, err := exec.LookPath("weston-simple-shm") | |
| 217 | + | if err != nil { | |
| 218 | + | t.Skip("weston-simple-shm not installed") | |
| 219 | + | } | |
| 220 | + | s, events, commands, dir := lifecycleServer(t) | |
| 221 | + | _, done, output := lifecycleClient(t, tool, s, dir, nil) | |
| 222 | + | w := mapped(t, events, 5*time.Second) | |
| 223 | + | commands <- ports.CloseWindow{ID: w.ID} | |
| 224 | + | select { | |
| 225 | + | case err := <-done: | |
| 226 | + | if err != nil { | |
| 227 | + | t.Errorf("client: %v: %s", err, output.String()) | |
| 228 | + | } | |
| 229 | + | case <-time.After(5 * time.Second): | |
| 230 | + | t.Fatal("client did not close") | |
| 231 | + | } | |
| 232 | + | unmapped(t, events, w.ID) | |
| 233 | + | } | |
| 234 | + | func TestWindowConfigure(t *testing.T) { | |
| 235 | + | tool, err := exec.LookPath("weston-simple-shm") | |
| 236 | + | if err != nil { | |
| 237 | + | t.Skip("weston-simple-shm not installed") | |
| 238 | + | } | |
| 239 | + | s, events, commands, dir := lifecycleServer(t) | |
| 240 | + | _, _, output := lifecycleClient(t, tool, s, dir, []string{"WAYLAND_DEBUG=client"}) | |
| 241 | + | w := mapped(t, events, 5*time.Second) | |
| 242 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 640, Height: 480, Activated: true} | |
| 243 | + | deadline := time.After(3 * time.Second) | |
| 244 | + | tick := time.NewTicker(10 * time.Millisecond) | |
| 245 | + | defer tick.Stop() | |
| 246 | + | for { | |
| 247 | + | select { | |
| 248 | + | case <-deadline: | |
| 249 | + | t.Fatalf("missing configure(640, 480: %s", output.String()) | |
| 250 | + | case <-tick.C: | |
| 251 | + | if strings.Contains(output.String(), "configure(640, 480") { | |
| 252 | + | return | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | func TestWindowKilled(t *testing.T) { | |
| 258 | + | tool, err := exec.LookPath("weston-simple-shm") | |
| 259 | + | if err != nil { | |
| 260 | + | t.Skip("weston-simple-shm not installed") | |
| 261 | + | } | |
| 262 | + | s, events, _, dir := lifecycleServer(t) | |
| 263 | + | cmd, _, _ := lifecycleClient(t, tool, s, dir, nil) | |
| 264 | + | w := mapped(t, events, 5*time.Second) | |
| 265 | + | if err := cmd.Process.Kill(); err != nil { | |
| 266 | + | t.Fatal(err) | |
| 267 | + | } | |
| 268 | + | unmapped(t, events, w.ID) | |
| 269 | + | } | |
| 270 | + | func TestFootWindow(t *testing.T) { | |
| 271 | + | tool, err := exec.LookPath("foot") | |
| 272 | + | if err != nil { | |
| 273 | + | t.Skip("foot not installed") | |
| 274 | + | } | |
| 275 | + | s, events, _, dir := lifecycleServer(t) | |
| 276 | + | _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "PATH=" + os.Getenv("PATH")}, "-c", "/dev/null", "sh", "-c", "sleep 3") | |
| 277 | + | select { | |
| 278 | + | case ev := <-events: | |
| 279 | + | w, ok := ev.(ports.WindowMapped) | |
| 280 | + | if !ok { | |
| 281 | + | t.Fatalf("expected mapped, got %T", ev) | |
| 282 | + | } | |
| 283 | + | if w.AppID != "foot" { | |
| 284 | + | t.Fatalf("app ID: %q", w.AppID) | |
| 285 | + | } | |
| 286 | + | case err := <-done: | |
| 287 | + | t.Skipf("foot unavailable: %v: %s", err, output.String()) | |
| 288 | + | case <-time.After(10 * time.Second): | |
| 289 | + | t.Skipf("foot did not map: %s", output.String()) | |
| 290 | + | } | |
| 291 | + | } | |
| @@ -1,11 +1,14 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "context" | 5 | "context" | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 5 | "os" | 7 | "os" | ||
| 6 | "os/exec" | 8 | "os/exec" | ||
| 7 | "path/filepath" | 9 | "path/filepath" | ||
| 8 | "strings" | 10 | "strings" | ||
| 11 | + | "sync" | |||
| 9 | "testing" | 12 | "testing" | ||
| 10 | "time" | 13 | "time" | ||
| 11 | 14 | ||||
| @@ -115,3 +118,174 @@ func TestWestonFrames(t *testing.T) { | |||||
| 115 | } | 118 | } | ||
| 116 | } | 119 | } | ||
| 117 | } | 120 | } | ||
| 121 | + | ||||
| 122 | + | // lifecycleServer starts a display with buffered command and event channels. | |||
| 123 | + | func lifecycleServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) { | |||
| 124 | + | t.Helper() | |||
| 125 | + | dir := t.TempDir() | |||
| 126 | + | events := make(chan ports.ClientEvent, 16) | |||
| 127 | + | commands := make(chan ports.ClientCommand, 16) | |||
| 128 | + | s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland")) | |||
| 129 | + | if err != nil { | |||
| 130 | + | t.Fatal(err) | |||
| 131 | + | } | |||
| 132 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 133 | + | done := make(chan error, 1) | |||
| 134 | + | go func() { done <- s.Run(ctx) }() | |||
| 135 | + | t.Cleanup(func() { | |||
| 136 | + | cancel() | |||
| 137 | + | select { | |||
| 138 | + | case err := <-done: | |||
| 139 | + | if err != nil { | |||
| 140 | + | t.Errorf("Run: %v", err) | |||
| 141 | + | } | |||
| 142 | + | case <-time.After(2 * time.Second): | |||
| 143 | + | t.Error("Run hung") | |||
| 144 | + | } | |||
| 145 | + | }) | |||
| 146 | + | return s, events, commands, dir | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func lifecycleClient(t *testing.T, tool string, s *Server, dir string, extra []string, args ...string) (*exec.Cmd, <-chan error, *lockedBuffer) { | |||
| 150 | + | t.Helper() | |||
| 151 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 152 | + | cmd := exec.CommandContext(ctx, tool, args...) | |||
| 153 | + | cmd.Env = append(append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName()), extra...) | |||
| 154 | + | output := &lockedBuffer{} | |||
| 155 | + | cmd.Stderr = output | |||
| 156 | + | if err := cmd.Start(); err != nil { | |||
| 157 | + | cancel() | |||
| 158 | + | t.Fatal(err) | |||
| 159 | + | } | |||
| 160 | + | done := make(chan error, 1) | |||
| 161 | + | finished := make(chan struct{}) | |||
| 162 | + | go func() { done <- cmd.Wait(); close(finished) }() | |||
| 163 | + | t.Cleanup(func() { | |||
| 164 | + | cancel() | |||
| 165 | + | _ = cmd.Process.Kill() | |||
| 166 | + | select { | |||
| 167 | + | case <-finished: | |||
| 168 | + | case <-time.After(2 * time.Second): | |||
| 169 | + | t.Error("client hung") | |||
| 170 | + | } | |||
| 171 | + | }) | |||
| 172 | + | return cmd, done, output | |||
| 173 | + | } | |||
| 174 | + | ||||
| 175 | + | type lockedBuffer struct { | |||
| 176 | + | mu sync.Mutex | |||
| 177 | + | b bytes.Buffer | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | func (b *lockedBuffer) Write(p []byte) (int, error) { | |||
| 181 | + | b.mu.Lock() | |||
| 182 | + | defer b.mu.Unlock() | |||
| 183 | + | return b.b.Write(p) | |||
| 184 | + | } | |||
| 185 | + | func (b *lockedBuffer) String() string { b.mu.Lock(); defer b.mu.Unlock(); return b.b.String() } | |||
| 186 | + | ||||
| 187 | + | func waitEvent(t *testing.T, events <-chan ports.ClientEvent, duration time.Duration) ports.ClientEvent { | |||
| 188 | + | t.Helper() | |||
| 189 | + | select { | |||
| 190 | + | case ev := <-events: | |||
| 191 | + | return ev | |||
| 192 | + | case <-time.After(duration): | |||
| 193 | + | t.Fatal("event timeout") | |||
| 194 | + | return nil | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | func mapped(t *testing.T, events <-chan ports.ClientEvent, duration time.Duration) ports.WindowMapped { | |||
| 198 | + | t.Helper() | |||
| 199 | + | ev := waitEvent(t, events, duration) | |||
| 200 | + | w, ok := ev.(ports.WindowMapped) | |||
| 201 | + | if !ok { | |||
| 202 | + | t.Fatalf("expected mapped, got %T", ev) | |||
| 203 | + | } | |||
| 204 | + | return w | |||
| 205 | + | } | |||
| 206 | + | func unmapped(t *testing.T, events <-chan ports.ClientEvent, id ports.WindowID) { | |||
| 207 | + | t.Helper() | |||
| 208 | + | ev := waitEvent(t, events, 3*time.Second) | |||
| 209 | + | w, ok := ev.(ports.WindowUnmapped) | |||
| 210 | + | if !ok || w.ID != id { | |||
| 211 | + | t.Fatalf("expected unmapped %d, got %#v", id, ev) | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | func TestWindowClose(t *testing.T) { | |||
| 216 | + | tool, err := exec.LookPath("weston-simple-shm") | |||
| 217 | + | if err != nil { | |||
| 218 | + | t.Skip("weston-simple-shm not installed") | |||
| 219 | + | } | |||
| 220 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 221 | + | _, done, output := lifecycleClient(t, tool, s, dir, nil) | |||
| 222 | + | w := mapped(t, events, 5*time.Second) | |||
| 223 | + | commands <- ports.CloseWindow{ID: w.ID} | |||
| 224 | + | select { | |||
| 225 | + | case err := <-done: | |||
| 226 | + | if err != nil { | |||
| 227 | + | t.Errorf("client: %v: %s", err, output.String()) | |||
| 228 | + | } | |||
| 229 | + | case <-time.After(5 * time.Second): | |||
| 230 | + | t.Fatal("client did not close") | |||
| 231 | + | } | |||
| 232 | + | unmapped(t, events, w.ID) | |||
| 233 | + | } | |||
| 234 | + | func TestWindowConfigure(t *testing.T) { | |||
| 235 | + | tool, err := exec.LookPath("weston-simple-shm") | |||
| 236 | + | if err != nil { | |||
| 237 | + | t.Skip("weston-simple-shm not installed") | |||
| 238 | + | } | |||
| 239 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 240 | + | _, _, output := lifecycleClient(t, tool, s, dir, []string{"WAYLAND_DEBUG=client"}) | |||
| 241 | + | w := mapped(t, events, 5*time.Second) | |||
| 242 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 640, Height: 480, Activated: true} | |||
| 243 | + | deadline := time.After(3 * time.Second) | |||
| 244 | + | tick := time.NewTicker(10 * time.Millisecond) | |||
| 245 | + | defer tick.Stop() | |||
| 246 | + | for { | |||
| 247 | + | select { | |||
| 248 | + | case <-deadline: | |||
| 249 | + | t.Fatalf("missing configure(640, 480: %s", output.String()) | |||
| 250 | + | case <-tick.C: | |||
| 251 | + | if strings.Contains(output.String(), "configure(640, 480") { | |||
| 252 | + | return | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | } | |||
| 256 | + | } | |||
| 257 | + | func TestWindowKilled(t *testing.T) { | |||
| 258 | + | tool, err := exec.LookPath("weston-simple-shm") | |||
| 259 | + | if err != nil { | |||
| 260 | + | t.Skip("weston-simple-shm not installed") | |||
| 261 | + | } | |||
| 262 | + | s, events, _, dir := lifecycleServer(t) | |||
| 263 | + | cmd, _, _ := lifecycleClient(t, tool, s, dir, nil) | |||
| 264 | + | w := mapped(t, events, 5*time.Second) | |||
| 265 | + | if err := cmd.Process.Kill(); err != nil { | |||
| 266 | + | t.Fatal(err) | |||
| 267 | + | } | |||
| 268 | + | unmapped(t, events, w.ID) | |||
| 269 | + | } | |||
| 270 | + | func TestFootWindow(t *testing.T) { | |||
| 271 | + | tool, err := exec.LookPath("foot") | |||
| 272 | + | if err != nil { | |||
| 273 | + | t.Skip("foot not installed") | |||
| 274 | + | } | |||
| 275 | + | s, events, _, dir := lifecycleServer(t) | |||
| 276 | + | _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "PATH=" + os.Getenv("PATH")}, "-c", "/dev/null", "sh", "-c", "sleep 3") | |||
| 277 | + | select { | |||
| 278 | + | case ev := <-events: | |||
| 279 | + | w, ok := ev.(ports.WindowMapped) | |||
| 280 | + | if !ok { | |||
| 281 | + | t.Fatalf("expected mapped, got %T", ev) | |||
| 282 | + | } | |||
| 283 | + | if w.AppID != "foot" { | |||
| 284 | + | t.Fatalf("app ID: %q", w.AppID) | |||
| 285 | + | } | |||
| 286 | + | case err := <-done: | |||
| 287 | + | t.Skipf("foot unavailable: %v: %s", err, output.String()) | |||
| 288 | + | case <-time.After(10 * time.Second): | |||
| 289 | + | t.Skipf("foot did not map: %s", output.String()) | |||
| 290 | + | } | |||
| 291 | + | } | |||
M internal/adapters/wayland/xdg.go +61 -10
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 4 | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 5 | 6 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 6 | 7 | "github.com/bnema/purego-libwayland/server" | |
| @@ -55,24 +56,63 @@ type xdgSurface struct { | |||
| 55 | 56 | server *Server | |
| 56 | 57 | surface *surface | |
| 57 | 58 | configured bool | |
| 59 | + | acked bool | |
| 60 | + | window *window | |
| 58 | 61 | serial uint32 | |
| 59 | 62 | } | |
| 60 | 63 | ||
| 61 | 64 | func (x *xdgSurface) Destroy(*xdgshell.Surface) {} | |
| 65 | + | ||
| 66 | + | type window struct { | |
| 67 | + | id ports.WindowID | |
| 68 | + | toplevel *xdgshell.Toplevel | |
| 69 | + | xdg *xdgSurface | |
| 70 | + | appID, title string | |
| 71 | + | mapped bool | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func (w *window) unmap() { | |
| 75 | + | if !w.mapped { | |
| 76 | + | return | |
| 77 | + | } | |
| 78 | + | w.mapped = false | |
| 79 | + | w.xdg.configured = false | |
| 80 | + | w.xdg.acked = false | |
| 81 | + | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 82 | + | } | |
| 62 | 83 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 63 | - | t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{x}) | |
| 84 | + | if x.window != nil { | |
| 85 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "toplevel already constructed") | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | w := &window{id: x.server.nextWindow, xdg: x} | |
| 89 | + | t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w}) | |
| 64 | 90 | if err != nil { | |
| 65 | 91 | return | |
| 66 | 92 | } | |
| 93 | + | w.toplevel = t | |
| 94 | + | x.window = w | |
| 95 | + | x.server.nextWindow++ | |
| 96 | + | x.server.windows[w.id] = w | |
| 67 | 97 | x.surface.role = func(buffer bool) { | |
| 68 | - | if !buffer && !x.configured { | |
| 98 | + | if x.surface.destroyed { | |
| 99 | + | w.unmap() | |
| 100 | + | return | |
| 101 | + | } | |
| 102 | + | if !buffer && !w.mapped && !x.configured { | |
| 69 | 103 | x.configured = true | |
| 70 | 104 | x.server.serial++ | |
| 71 | 105 | t.SendConfigure(0, 0, nil) | |
| 72 | 106 | r.SendConfigure(x.server.serial) | |
| 107 | + | } else if buffer && !w.mapped && x.acked { | |
| 108 | + | w.mapped = true | |
| 109 | + | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID}) | |
| 110 | + | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Msg("window mapped") | |
| 111 | + | } else if !buffer && w.mapped { | |
| 112 | + | w.unmap() | |
| 73 | 113 | } | |
| 74 | 114 | } | |
| 75 | - | t.OnDestroy = func() { x.surface.role = nil } | |
| 115 | + | t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil } | |
| 76 | 116 | } | |
| 77 | 117 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | |
| 78 | 118 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | |
| @@ -80,14 +120,17 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac | |||
| 80 | 120 | } | |
| 81 | 121 | } | |
| 82 | 122 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | |
| 83 | - | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { x.serial = serial } | |
| 123 | + | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { | |
| 124 | + | x.serial = serial | |
| 125 | + | x.acked = true | |
| 126 | + | } | |
| 84 | 127 | ||
| 85 | - | type top struct{ x *xdgSurface } | |
| 128 | + | type top struct{ w *window } | |
| 86 | 129 | ||
| 87 | 130 | func (top) Destroy(*xdgshell.Toplevel) {} | |
| 88 | 131 | func (top) SetParent(*xdgshell.Toplevel, *xdgshell.Toplevel) {} | |
| 89 | - | func (top) SetTitle(*xdgshell.Toplevel, string) {} | |
| 90 | - | func (top) SetAppId(*xdgshell.Toplevel, string) {} | |
| 132 | + | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { t.w.title = title } | |
| 133 | + | func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) { t.w.appID = appID } | |
| 91 | 134 | func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {} | |
| 92 | 135 | func (top) Move(*xdgshell.Toplevel, *wayland.Seat, uint32) {} | |
| 93 | 136 | func (top) Resize(*xdgshell.Toplevel, *wayland.Seat, uint32, uint32) {} | |
| @@ -95,9 +138,17 @@ func (top) SetMaxSize(*xdgshell.Toplevel, int32, int32) | |||
| 95 | 138 | func (top) SetMinSize(*xdgshell.Toplevel, int32, int32) {} | |
| 96 | 139 | func (top) SetMaximized(*xdgshell.Toplevel) {} | |
| 97 | 140 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | |
| 98 | - | func (top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {} | |
| 99 | - | func (top) UnsetFullscreen(*xdgshell.Toplevel) {} | |
| 100 | - | func (top) SetMinimized(*xdgshell.Toplevel) {} | |
| 141 | + | func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) { | |
| 142 | + | if t.w.mapped { | |
| 143 | + | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true}) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | func (t top) UnsetFullscreen(*xdgshell.Toplevel) { | |
| 147 | + | if t.w.mapped { | |
| 148 | + | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id}) | |
| 149 | + | } | |
| 150 | + | } | |
| 151 | + | func (top) SetMinimized(*xdgshell.Toplevel) {} | |
| 101 | 152 | ||
| 102 | 153 | type popup struct{} | |
| 103 | 154 | ||
| @@ -1,6 +1,7 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 4 | "github.com/bnema/purego-libwayland/protocol/wayland" | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 5 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | 6 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | ||
| 6 | "github.com/bnema/purego-libwayland/server" | 7 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -55,24 +56,63 @@ type xdgSurface struct { | |||||
| 55 | server *Server | 56 | server *Server | ||
| 56 | surface *surface | 57 | surface *surface | ||
| 57 | configured bool | 58 | configured bool | ||
| 59 | + | acked bool | |||
| 60 | + | window *window | |||
| 58 | serial uint32 | 61 | serial uint32 | ||
| 59 | } | 62 | } | ||
| 60 | 63 | ||||
| 61 | func (x *xdgSurface) Destroy(*xdgshell.Surface) {} | 64 | func (x *xdgSurface) Destroy(*xdgshell.Surface) {} | ||
| 65 | + | ||||
| 66 | + | type window struct { | |||
| 67 | + | id ports.WindowID | |||
| 68 | + | toplevel *xdgshell.Toplevel | |||
| 69 | + | xdg *xdgSurface | |||
| 70 | + | appID, title string | |||
| 71 | + | mapped bool | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func (w *window) unmap() { | |||
| 75 | + | if !w.mapped { | |||
| 76 | + | return | |||
| 77 | + | } | |||
| 78 | + | w.mapped = false | |||
| 79 | + | w.xdg.configured = false | |||
| 80 | + | w.xdg.acked = false | |||
| 81 | + | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |||
| 82 | + | } | |||
| 62 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 83 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 63 | - | t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{x}) | 84 | + | if x.window != nil { |
| 85 | + | r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "toplevel already constructed") | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | w := &window{id: x.server.nextWindow, xdg: x} | |||
| 89 | + | t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w}) | |||
| 64 | if err != nil { | 90 | if err != nil { | ||
| 65 | return | 91 | return | ||
| 66 | } | 92 | } | ||
| 93 | + | w.toplevel = t | |||
| 94 | + | x.window = w | |||
| 95 | + | x.server.nextWindow++ | |||
| 96 | + | x.server.windows[w.id] = w | |||
| 67 | x.surface.role = func(buffer bool) { | 97 | x.surface.role = func(buffer bool) { | ||
| 68 | - | if !buffer && !x.configured { | 98 | + | if x.surface.destroyed { |
| 99 | + | w.unmap() | |||
| 100 | + | return | |||
| 101 | + | } | |||
| 102 | + | if !buffer && !w.mapped && !x.configured { | |||
| 69 | x.configured = true | 103 | x.configured = true | ||
| 70 | x.server.serial++ | 104 | x.server.serial++ | ||
| 71 | t.SendConfigure(0, 0, nil) | 105 | t.SendConfigure(0, 0, nil) | ||
| 72 | r.SendConfigure(x.server.serial) | 106 | r.SendConfigure(x.server.serial) | ||
| 107 | + | } else if buffer && !w.mapped && x.acked { | |||
| 108 | + | w.mapped = true | |||
| 109 | + | x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID}) | |||
| 110 | + | x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Msg("window mapped") | |||
| 111 | + | } else if !buffer && w.mapped { | |||
| 112 | + | w.unmap() | |||
| 73 | } | 113 | } | ||
| 74 | } | 114 | } | ||
| 75 | - | t.OnDestroy = func() { x.surface.role = nil } | 115 | + | t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil } |
| 76 | } | 116 | } | ||
| 77 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | 117 | func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) { | ||
| 78 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | 118 | if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil { | ||
| @@ -80,14 +120,17 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac | |||||
| 80 | } | 120 | } | ||
| 81 | } | 121 | } | ||
| 82 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | 122 | func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {} | ||
| 83 | - | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { x.serial = serial } | 123 | + | func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) { |
| 124 | + | x.serial = serial | |||
| 125 | + | x.acked = true | |||
| 126 | + | } | |||
| 84 | 127 | ||||
| 85 | - | type top struct{ x *xdgSurface } | 128 | + | type top struct{ w *window } |
| 86 | 129 | ||||
| 87 | func (top) Destroy(*xdgshell.Toplevel) {} | 130 | func (top) Destroy(*xdgshell.Toplevel) {} | ||
| 88 | func (top) SetParent(*xdgshell.Toplevel, *xdgshell.Toplevel) {} | 131 | func (top) SetParent(*xdgshell.Toplevel, *xdgshell.Toplevel) {} | ||
| 89 | - | func (top) SetTitle(*xdgshell.Toplevel, string) {} | 132 | + | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { t.w.title = title } |
| 90 | - | func (top) SetAppId(*xdgshell.Toplevel, string) {} | 133 | + | func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) { t.w.appID = appID } |
| 91 | func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {} | 134 | func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {} | ||
| 92 | func (top) Move(*xdgshell.Toplevel, *wayland.Seat, uint32) {} | 135 | func (top) Move(*xdgshell.Toplevel, *wayland.Seat, uint32) {} | ||
| 93 | func (top) Resize(*xdgshell.Toplevel, *wayland.Seat, uint32, uint32) {} | 136 | func (top) Resize(*xdgshell.Toplevel, *wayland.Seat, uint32, uint32) {} | ||
| @@ -95,9 +138,17 @@ func (top) SetMaxSize(*xdgshell.Toplevel, int32, int32) | |||||
| 95 | func (top) SetMinSize(*xdgshell.Toplevel, int32, int32) {} | 138 | func (top) SetMinSize(*xdgshell.Toplevel, int32, int32) {} | ||
| 96 | func (top) SetMaximized(*xdgshell.Toplevel) {} | 139 | func (top) SetMaximized(*xdgshell.Toplevel) {} | ||
| 97 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | 140 | func (top) UnsetMaximized(*xdgshell.Toplevel) {} | ||
| 98 | - | func (top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {} | 141 | + | func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) { |
| 99 | - | func (top) UnsetFullscreen(*xdgshell.Toplevel) {} | 142 | + | if t.w.mapped { |
| 100 | - | func (top) SetMinimized(*xdgshell.Toplevel) {} | 143 | + | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true}) |
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | func (t top) UnsetFullscreen(*xdgshell.Toplevel) { | |||
| 147 | + | if t.w.mapped { | |||
| 148 | + | t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id}) | |||
| 149 | + | } | |||
| 150 | + | } | |||
| 151 | + | func (top) SetMinimized(*xdgshell.Toplevel) {} | |||
| 101 | 152 | ||||
| 102 | type popup struct{} | 153 | type popup struct{} | ||
| 103 | 154 | ||||