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