1package core
2
3import (
4 "slices"
5
6 "github.com/bnema/neferwl/internal/ports"
7)
8
9// captureWorkspace is the workspace a capture session renders off screen
10// while it is not the one on screen. Its windows keep a real layout in the
11// workspace viewport: they are sized (configure Captured) and drawn into
12// Scene.CaptureScene, but they are hidden on the physical layout, so they
13// take no pointer input and no physical presentation feedback.
14//
15// Only the core goroutine touches it. The index map and its storage are
16// reused by every publish (cleared, never reallocated once warm).
17type captureWorkspace struct {
18 sc *screen
19 ws *Workspace // nil: no workspace is captured off screen
20 frame Rect // ws.Output: the viewport, in monitor coordinates
21 placements []Placement
22 index map[WindowID]int
23}
24
25// active reports whether a hidden workspace is captured.
26func (cw *captureWorkspace) active() bool { return cw != nil && cw.ws != nil }
27
28// reset forgets the captured workspace, keeping the storage.
29func (cw *captureWorkspace) reset() {
30 cw.sc, cw.ws, cw.frame = nil, nil, Rect{}
31 cw.placements = nil
32 clear(cw.index)
33}
34
35// load lays out ws in its own viewport. ws.Layout allocates the placements
36// (the baseline of the existing layout); the index reuses its map.
37func (cw *captureWorkspace) load(sc *screen, ws *Workspace) {
38 cw.sc, cw.ws, cw.frame = sc, ws, ws.Output
39 cw.placements = ws.Layout()
40 if cw.index == nil {
41 cw.index = make(map[WindowID]int, len(cw.placements))
42 }
43 clear(cw.index)
44 for i := range cw.placements {
45 cw.index[cw.placements[i].ID] = i
46 }
47}
48
49// placement is the real placement of a window of the captured workspace.
50func (cw *captureWorkspace) placement(id WindowID) *Placement {
51 if !cw.active() {
52 return nil
53 }
54 if i, ok := cw.index[id]; ok {
55 return &cw.placements[i]
56 }
57 return nil
58}
59
60// captureTrack follows the session view of this publish: it lays out the
61// captured workspace when it is not drawn as itself (another workspace is
62// on screen, or the overview shows it as a preview) and forgets it
63// otherwise. It runs
64// right after captureEvaluate, before the popups and configures that use it.
65func (c *Core) captureTrack(view *capView) {
66 // Tracking follows the decided view, not whether frames were taken: while
67 // the captured workspace slides in its own viewport its windows stay
68 // captured (not suspended) and its popups stay.
69 if view == nil || view.hidden == nil {
70 c.configures.cw.reset()
71 return
72 }
73 c.configures.cw.load(view.hiddenScr, view.hidden)
74}
75
76// captureConfigure picks how a physical placement p on sc is configured:
77// for a window of the captured workspace it returns the real placement and
78// the target sized for it, else nil and t unchanged. Pass both to
79// configures.nextWithCapture.
80func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Placement, configureTarget) {
81 cw := &c.configures.cw
82 if !cw.active() || cw.sc != sc {
83 return nil, t
84 }
85 cp := cw.placement(p.ID)
86 if cp == nil {
87 return nil, t
88 }
89 t.captureArea = cw.frame
90 if !p.Hidden {
91 // An overview preview keeps its real size and physical state.
92 return cp, t
93 }
94 t.view = whole(cw.frame) // the capture image has no panels
95 t.focused = false
96 t.client, t.imposed = Rect{}, false
97 if !cp.Hidden {
98 t.client, t.imposed = c.clientRect(*cp), cw.ws.imposedFloat(cp.ID)
99 }
100 return cp, t
101}
102
103// captureRect is where a window of the captured workspace, or a popup
104// hanging from one, is: its client area in monitor coordinates. Unlike
105// windowRect it knows hidden workspaces; it never serves input. A stale
106// captured workspace (its screen or workspace gone) has no geometry.
107func (c *Core) captureRect(id WindowID) (*screen, Rect, bool) {
108 cw := &c.configures.cw
109 if !cw.active() || !slices.Contains(c.screens, cw.sc) || !cw.sc.mon.has(cw.ws) {
110 return nil, Rect{}, false
111 }
112 if p := c.popups[id]; p != nil {
113 sc, pr, ok := c.captureRect(p.parent)
114 if !ok {
115 return nil, Rect{}, false
116 }
117 if cp := cw.placement(c.popupRoot(id)); cp != nil && cp.Peek {
118 return nil, Rect{}, false
119 }
120 return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true
121 }
122 cp := cw.placement(id)
123 if cp == nil || cp.Hidden || !cp.Rect.Overlaps(cw.frame) {
124 return nil, Rect{}, false
125 }
126 return cw.sc, c.clientRect(*cp), true
127}
128
129// popupRect is windowRect that also knows the captured workspace; physical
130// tells which one answered (a single physical lookup).
131func (c *Core) popupRect(id WindowID) (sc *screen, r Rect, physical, ok bool) {
132 if sc, r, ok := c.windowRect(id); ok {
133 return sc, r, true, true
134 }
135 sc, r, ok = c.captureRect(id)
136 return sc, r, false, ok
137}
138
139// captureScene is the captured workspace drawn for capture only: a scene the
140// size of its viewport, whose windows and lines are rebased on the viewport
141// origin. It has no layers and no capture of its own. nil without a
142// captured workspace.
143func (c *Core) captureScene(seq uint64) *ports.Scene {
144 cw := &c.configures.cw
145 if !cw.active() {
146 return nil
147 }
148 f := cw.frame
149 // The capture image has no panels: all of it is seen.
150 view := whole(f)
151 rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r }
152 s := &ports.Scene{
153 Security: c.security,
154 Output: cw.sc.name(), Seq: seq, OutputWidth: f.W, OutputHeight: f.H, WorkspaceClip: Rect{W: f.W, H: f.H},
155 Scale: cw.sc.scale, Background: c.cfg.Background.Color,
156 Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive},
157 Windows: make([]ports.SceneWindow, 0, len(cw.placements)),
158 Dim: floatDim(cw.placements, view, c.cfg.Floating.Dim),
159 }
160 // A hidden workspace lights nothing: its focus is not shown.
161 for _, p := range cw.placements {
162 w := ports.SceneWindow{ID: p.ID, Focused: false, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview}
163 if !p.Hidden {
164 w.Rect = rebase(p.Rect)
165 }
166 w.Dim = c.peekDim(p)
167 s.Windows = append(s.Windows, w)
168 }
169 s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false)
170 for i := range s.Separators {
171 s.Separators[i].Rect = rebase(s.Separators[i].Rect)
172 }
173 for _, id := range c.popupOrder {
174 p := c.popups[id]
175 if p == nil || !p.mapped {
176 continue
177 }
178 if _, r, ok := c.captureRect(id); ok {
179 s.Windows = append(s.Windows, ports.SceneWindow{ID: id, Rect: rebase(r), Popup: true})
180 }
181 }
182 return s
183}