1package core
2
3import (
4 "context"
5 "slices"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// sceneFor builds the scene of screen i and sends the configures it sizes.
11// drawable reports that it can show the focus pulse on the focused window.
12func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) {
13 o := sc.mon.Output()
14 // view.frame is the viewport of the workspace on screen: the whole
15 // output unless it has a size override (never in the overview).
16 view := sc.mon.viewport()
17 var clip Rect
18 if view.frame != (Rect{W: o.W, H: o.H}) {
19 clip = view.frame
20 }
21 // layout is what is drawn; settled (same indexes) is where the
22 // windows are going, and alone sizes the configures.
23 layout, settled := sc.shown, sc.settledLayout
24 scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
25 if !sc.mon.previewing() {
26 // Tiles stop at the panels; the overview lays out previews over
27 // the whole output.
28 u := view.usable
29 scene.TileInset = ports.Insets{Top: u.Y, Left: u.X, Right: o.W - u.X - u.W, Bottom: o.H - u.Y - u.H}
30 }
31 var real map[WindowID]Placement
32 if sc.mon.previewing() {
33 // Local to this screen's build: reused, cleared each time.
34 if c.overviewReal == nil {
35 c.overviewReal = make(map[WindowID]Placement)
36 }
37 clear(c.overviewReal)
38 real = c.overviewReal
39 for w := range sc.mon.all() {
40 c.realBuf = w.layoutInto(c.realBuf)
41 for _, p := range c.realBuf {
42 real[p.ID] = p
43 }
44 }
45 }
46 scene.Dim = floatDim(layout, view, c.cfg.Floating.Dim)
47 if sc.mon.previewing() {
48 // Darken the wallpaper around the previews.
49 scene.Dim = c.cfg.Floating.Dim
50 scene.DimBehind = scene.Dim > 0
51 }
52 if d := c.drag; d != nil && d.target.screen == sc && d.target.kind != dropNone {
53 scene.DropHints = slices.Clone(d.target.hints)
54 }
55 if sc.mon.ov.open {
56 // Frame the selection and separate numbered and named row groups.
57 // The scene's own slice (immutable once sent), sized for both.
58 _, dividers := sc.mon.overviewRows()
59 var seps []ports.Separator
60 if len(dividers) > 0 {
61 seps = append(make([]ports.Separator, 0, len(dividers)+outlineLines), dividers...)
62 }
63 scene.Separators = overviewOutline(seps, layout, max(c.cfg.Border.Width, 2))
64 } else if sc.mon.switcherShown() {
65 // The switcher's cards: only the selection is framed.
66 scene.Separators = overviewOutline(nil, layout, max(c.cfg.Border.Width, 2))
67 } else {
68 // Only the focused output lights the focused window's lines.
69 scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen)
70 }
71 // A window alone on screen needs no pulse to show it has the focus.
72 alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, view) == 1
73 for k, p := range layout {
74 ps := settled[k]
75 // Only the focused output has an activated window.
76 focused := p.Focused && i == c.focusScreen
77 sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade}
78 if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) {
79 // A scale motion: the content follows the drawn size. A
80 // card in flight drawn at its size still needs Zoom 1:
81 // without it the renderer would shrink it by Preview.
82 sw.Zoom = p.Zoom
83 }
84 // A peek's veil is the configured one plus its animated offset.
85 sw.Dim = max(0, min(p.Dim+c.peekDim(p), 1))
86 if p.Leaving {
87 // A window fading out after it closed or hid: drawn, but
88 // hidden to everything else, its configures included.
89 sw.Hidden = false
90 scene.Windows = append(scene.Windows, sw)
91 // A window the workspace still holds (a hidden stash
92 // window) keeps its last configure: prune would forget it,
93 // and its next one would start from nothing. A closed one
94 // was forgotten at its unmap and stays so.
95 if w, _ := sc.mon.find(p.ID); w != nil {
96 c.configures.keep(p.ID)
97 }
98 continue
99 }
100 if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.previewing() && hasRoom(view, p) {
101 drawable = true
102 if p.ID == c.pulse.id {
103 sw.FocusEffect = pulse
104 }
105 }
106 scene.Windows = append(scene.Windows, sw)
107 t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID}
108 if !p.Hidden && p.Preview == 0 {
109 // Only a sized configure needs the client size.
110 t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID)
111 } else if t.captured && p.Hidden {
112 // A captured hidden window is sized like its capture.
113 t.client = capture.windowSz
114 } else if p.Preview > 0 && !p.Hidden {
115 if rp, ok := real[p.ID]; ok && !rp.Hidden {
116 t.realTiled = !rp.Floating
117 t.client = c.clientRect(rp)
118 }
119 }
120 cp, t := c.captureConfigure(sc, ps, t)
121 if v, send := c.configures.nextWithCapture(ps, t, cp); send {
122 if err := c.command(ctx, v); err != nil {
123 return ports.Scene{}, false, err
124 }
125 c.configures.mark(v)
126 }
127 }
128 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
129 if icon, ok := c.dataDragIconOn(i, sc); ok {
130 // The drag icon is over everything, popups and layers included.
131 scene.Windows = append(scene.Windows, icon)
132 }
133 scene.CaptureIndicators = c.captureIndicators(sc)
134 scene.Capture = c.captureSceneFor(sc, capture)
135 // The springs of this frame are advanced: a scene shown while one still
136 // runs wakes core when it flips; the one a spring settles on does not.
137 scene.Animating = c.sliding(sc.name())
138 // A scene carrying a capture image always gets a fresh Seq, and so
139 // does one of an animating screen (the one being stepped, or any
140 // when none is): the outputs drop a scene they already show, and a
141 // spring that starts at the previous scene, or rounds to it in its
142 // tail, would get no flip and wait for the fallback timer. So does the
143 // scene that flips Animating off: SameAs ignores the flag, and keeping
144 // the Seq of the animating scene would leave outputs showing it, and
145 // waking core on every later flip. Any other keeps its output's Seq
146 // while it draws the same, so an idle output is not recomposed.
147 withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr)
148 animating := scene.Animating && (only == nil || only == sc)
149 if !withCapture && !animating && sc.last.Animating == scene.Animating && sc.last.Seq != 0 && scene.SameAs(sc.last) {
150 scene.Seq = sc.last.Seq
151 } else {
152 c.seq++
153 scene.Seq = c.seq
154 }
155 if withCapture {
156 switch sc {
157 case capture.hiddenScr:
158 scene.CaptureScene = c.captureScene(scene.Seq)
159 case capture.windowScr:
160 scene.CaptureScene = c.captureWindowScene(capture, scene.Seq)
161 }
162 }
163 return scene, drawable, nil
164}