fix(core): order leaving tiles under the floats, reuse the settled buffer, clamp dim, snap the zoom on a re-flow
- A leaving tile is inserted before the first window that opens the floats, a leaving float appended; both layouts get the same index, so a closing tile never flashes over a float (TestLeavingPaintOrder). - withLeaving builds the settled layout with its leaving entries into a reused screen buffer: TestPublishLeavingAllocations pins 28 allocs per frame (two leaving + four moving windows per screen), budget 36. - Dim offsets are clamped into 0..1 on the shown placement and publish never emits a negative Dim. - A re-flow that retargets a running rect motion clears its scale: the content snaps to the settled size (the aspect may change), the fade goes on (TestRetargetClearsScaleKeepsFade). - floatDim counts leaving floats, scaled by 1-Fade, so the veil fades with the last float instead of dropping at once. - A leaving window's content settles at the 90 % zoom through show; its placement's Preview stays 0.
- commit
- 7d2748c03fe2eda7356f16b1e1eb2ae961df41e8
- author
- brice · · 2026-10-04 22:34 UTC
- parent
- a46389e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +244 -45
M Makefile +1 -1
| @@ -60,7 +60,7 @@ arch: | |||
| 60 | 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | |
| 61 | 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 62 | 62 | perf-check: | |
| 63 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | |
| 63 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | |
| 64 | 64 | check: vet test arch fakes-check log-check perf-check | |
| 65 | 65 | ||
| 66 | 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -60,7 +60,7 @@ arch: | |||||
| 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | ||
| 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 62 | perf-check: | 62 | perf-check: | ||
| 63 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | 63 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 |
| 64 | check: vet test arch fakes-check log-check perf-check | 64 | check: vet test arch fakes-check log-check perf-check | ||
| 65 | 65 | ||||
| 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
M internal/core/core.go +13 -13
| @@ -411,20 +411,23 @@ func onScreen(p Placement, o Rect) bool { | |||
| 411 | 411 | // floatDim is the veil opacity of a layout: dim only when a float is | |
| 412 | 412 | // drawn above the tiles, never for a demoted covering float alone nor for | |
| 413 | 413 | // an overview preview. | |
| 414 | + | // A leaving float (fading out after it closed or hid) keeps the veil, | |
| 415 | + | // scaled by what is left of it, so the veil fades with the last float. | |
| 414 | 416 | func floatDim(layout []Placement, o Rect, dim float64) float64 { | |
| 415 | - | shown := false | |
| 417 | + | k := 0.0 | |
| 416 | 418 | for _, p := range layout { | |
| 417 | - | if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) { | |
| 419 | + | if p.Floating && !p.Below && p.Preview == 0 && (onScreen(p, o) || p.Leaving && p.Rect.Overlaps(o)) { | |
| 418 | 420 | if p.Fullscreen { | |
| 419 | 421 | return 0 | |
| 420 | 422 | } | |
| 421 | - | shown = true | |
| 423 | + | if p.Leaving { | |
| 424 | + | k = max(k, 1-p.Fade) | |
| 425 | + | } else { | |
| 426 | + | k = 1 | |
| 427 | + | } | |
| 422 | 428 | } | |
| 423 | 429 | } | |
| 424 | - | if !shown { | |
| 425 | - | return 0 | |
| 426 | - | } | |
| 427 | - | return dim | |
| 430 | + | return dim * k | |
| 428 | 431 | } | |
| 429 | 432 | ||
| 430 | 433 | // visible reports whether the window, layer surface or popup is on screen | |
| @@ -605,13 +608,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 605 | 608 | ps := settled[k] | |
| 606 | 609 | // Only the focused output has an activated window. | |
| 607 | 610 | focused := p.Focused && i == c.focusScreen | |
| 608 | - | 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, Dim: p.Dim} | |
| 609 | - | if p.Zoom > 0 { | |
| 611 | + | 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, Dim: max(0, p.Dim)} | |
| 612 | + | if p.Zoom > 0 && p.Zoom < 1 { | |
| 610 | 613 | // A scale motion: the content follows the drawn size. | |
| 611 | - | sw.Preview = p.Zoom | |
| 612 | - | if sw.Preview >= 1 { | |
| 613 | - | sw.Preview = 0 | |
| 614 | - | } | |
| 614 | + | sw.Zoom = p.Zoom | |
| 615 | 615 | } | |
| 616 | 616 | if p.Peek { | |
| 617 | 617 | sw.Dim = min(sw.Dim+c.cfg.Stash.Dim, 1) | |
| @@ -411,20 +411,23 @@ func onScreen(p Placement, o Rect) bool { | |||||
| 411 | // floatDim is the veil opacity of a layout: dim only when a float is | 411 | // floatDim is the veil opacity of a layout: dim only when a float is | ||
| 412 | // drawn above the tiles, never for a demoted covering float alone nor for | 412 | // drawn above the tiles, never for a demoted covering float alone nor for | ||
| 413 | // an overview preview. | 413 | // an overview preview. | ||
| 414 | + | // A leaving float (fading out after it closed or hid) keeps the veil, | |||
| 415 | + | // scaled by what is left of it, so the veil fades with the last float. | |||
| 414 | func floatDim(layout []Placement, o Rect, dim float64) float64 { | 416 | func floatDim(layout []Placement, o Rect, dim float64) float64 { | ||
| 415 | - | shown := false | 417 | + | k := 0.0 |
| 416 | for _, p := range layout { | 418 | for _, p := range layout { | ||
| 417 | - | if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) { | 419 | + | if p.Floating && !p.Below && p.Preview == 0 && (onScreen(p, o) || p.Leaving && p.Rect.Overlaps(o)) { |
| 418 | if p.Fullscreen { | 420 | if p.Fullscreen { | ||
| 419 | return 0 | 421 | return 0 | ||
| 420 | } | 422 | } | ||
| 421 | - | shown = true | 423 | + | if p.Leaving { |
| 424 | + | k = max(k, 1-p.Fade) | |||
| 425 | + | } else { | |||
| 426 | + | k = 1 | |||
| 427 | + | } | |||
| 422 | } | 428 | } | ||
| 423 | } | 429 | } | ||
| 424 | - | if !shown { | 430 | + | return dim * k |
| 425 | - | return 0 | |||
| 426 | - | } | |||
| 427 | - | return dim | |||
| 428 | } | 431 | } | ||
| 429 | 432 | ||||
| 430 | // visible reports whether the window, layer surface or popup is on screen | 433 | // visible reports whether the window, layer surface or popup is on screen | ||
| @@ -605,13 +608,10 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 605 | ps := settled[k] | 608 | ps := settled[k] | ||
| 606 | // Only the focused output has an activated window. | 609 | // Only the focused output has an activated window. | ||
| 607 | focused := p.Focused && i == c.focusScreen | 610 | focused := p.Focused && i == c.focusScreen | ||
| 608 | - | 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, Dim: p.Dim} | 611 | + | 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, Dim: max(0, p.Dim)} |
| 609 | - | if p.Zoom > 0 { | 612 | + | if p.Zoom > 0 && p.Zoom < 1 { |
| 610 | // A scale motion: the content follows the drawn size. | 613 | // A scale motion: the content follows the drawn size. | ||
| 611 | - | sw.Preview = p.Zoom | 614 | + | sw.Zoom = p.Zoom |
| 612 | - | if sw.Preview >= 1 { | |||
| 613 | - | sw.Preview = 0 | |||
| 614 | - | } | |||
| 615 | } | 615 | } | ||
| 616 | if p.Peek { | 616 | if p.Peek { | ||
| 617 | sw.Dim = min(sw.Dim+c.cfg.Stash.Dim, 1) | 617 | sw.Dim = min(sw.Dim+c.cfg.Stash.Dim, 1) | ||
M internal/core/layout.go +5 -5
| @@ -128,11 +128,11 @@ type Placement struct { | |||
| 128 | 128 | Leaving bool | |
| 129 | 129 | // Fade, Dim and Zoom are set on shown layouts only (refreshShown), | |
| 130 | 130 | // animated: the window's fade (0 opaque, 1 invisible), a veil opacity | |
| 131 | - | // added to the one Peek draws, and the content zoom as drawn when a | |
| 132 | - | // scale motion runs (Preview times the drawn/settled width, 1 when the | |
| 133 | - | // content is drawn at its size; 0 without one). Preview itself stays | |
| 134 | - | // the settled one: input and popups read it to tell a card from a | |
| 135 | - | // window. | |
| 131 | + | // added to the one Peek draws (0..1), and the content zoom as drawn | |
| 132 | + | // when a scale motion runs (Preview times the drawn/settled width, 1 | |
| 133 | + | // when the content is drawn at its size; 0 without one). Preview | |
| 134 | + | // itself stays the settled one: input, popups and the renderer's paint | |
| 135 | + | // order read it to tell a card from a window. | |
| 136 | 136 | Fade, Dim, Zoom float64 | |
| 137 | 137 | } | |
| 138 | 138 | ||
| @@ -128,11 +128,11 @@ type Placement struct { | |||||
| 128 | Leaving bool | 128 | Leaving bool | ||
| 129 | // Fade, Dim and Zoom are set on shown layouts only (refreshShown), | 129 | // Fade, Dim and Zoom are set on shown layouts only (refreshShown), | ||
| 130 | // animated: the window's fade (0 opaque, 1 invisible), a veil opacity | 130 | // animated: the window's fade (0 opaque, 1 invisible), a veil opacity | ||
| 131 | - | // added to the one Peek draws, and the content zoom as drawn when a | 131 | + | // added to the one Peek draws (0..1), and the content zoom as drawn |
| 132 | - | // scale motion runs (Preview times the drawn/settled width, 1 when the | 132 | + | // when a scale motion runs (Preview times the drawn/settled width, 1 |
| 133 | - | // content is drawn at its size; 0 without one). Preview itself stays | 133 | + | // when the content is drawn at its size; 0 without one). Preview |
| 134 | - | // the settled one: input and popups read it to tell a card from a | 134 | + | // itself stays the settled one: input, popups and the renderer's paint |
| 135 | - | // window. | 135 | + | // order read it to tell a card from a window. |
| 136 | Fade, Dim, Zoom float64 | 136 | Fade, Dim, Zoom float64 | ||
| 137 | } | 137 | } | ||
| 138 | 138 | ||||
M internal/core/publish_alloc_test.go +59 -0
| @@ -14,8 +14,11 @@ import ( | |||
| 14 | 14 | // and the layouts are rebuilt for every publish, so the floor is not 0. | |
| 15 | 15 | // Measured: 43 before presizing the scene windows and separators, 36 after; | |
| 16 | 16 | // 70 with the overview open (its rows and card layouts are rebuilt too). | |
| 17 | + | // Leaving entries (a closed window still fading) add none: the settled | |
| 18 | + | // layout with them goes into a reused buffer. | |
| 17 | 19 | const ( | |
| 18 | 20 | publishAllocBudget = 36 | |
| 21 | + | publishLeavingAllocBudget = publishAllocBudget | |
| 19 | 22 | publishOverviewAllocBudget = 70 | |
| 20 | 23 | ) | |
| 21 | 24 | ||
| @@ -132,3 +135,59 @@ func TestPublishOverviewAllocations(t *testing.T) { | |||
| 132 | 135 | t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget) | |
| 133 | 136 | } | |
| 134 | 137 | } | |
| 138 | + | ||
| 139 | + | // TestPublishLeavingAllocations pins the cost of a frame with leaving | |
| 140 | + | // entries on every screen (two of the six windows closed, fading out, the | |
| 141 | + | // other four with rect motions): not above a frame without them. | |
| 142 | + | func TestPublishLeavingAllocations(t *testing.T) { | |
| 143 | + | c := publishRig(t, 2) | |
| 144 | + | t0 := time.Now() | |
| 145 | + | for _, sc := range c.screens { | |
| 146 | + | if sc.name() == "" { | |
| 147 | + | continue | |
| 148 | + | } | |
| 149 | + | c.refreshShown() | |
| 150 | + | layout := sc.settledLayout | |
| 151 | + | sc.rects = make(map[WindowID]rectMotion, 6) | |
| 152 | + | sp := viewSpring(100, 0) | |
| 153 | + | for i, p := range layout { | |
| 154 | + | if i < 2 { | |
| 155 | + | sc.mon.RemoveWindow(p.ID) | |
| 156 | + | c.leave(sc, p, t0) | |
| 157 | + | continue | |
| 158 | + | } | |
| 159 | + | sc.rects[p.ID] = rectMotion{ | |
| 160 | + | x: c.spring(sp, t0), y: c.spring(sp, t0), | |
| 161 | + | w: c.spring(sp, t0), h: c.spring(sp, t0), | |
| 162 | + | dx: 100, dy: 100, dw: 100, dh: 100, | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | sc.rectsWS = sc.mon.Current() | |
| 166 | + | } | |
| 167 | + | step := publishStep(t, c) | |
| 168 | + | step() | |
| 169 | + | for _, sc := range c.screens { | |
| 170 | + | if sc.name() == "" { | |
| 171 | + | continue | |
| 172 | + | } | |
| 173 | + | leaving := 0 | |
| 174 | + | for _, p := range sc.shown { | |
| 175 | + | if p.Leaving { | |
| 176 | + | leaving++ | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | if len(sc.rects) != 6 || leaving != 2 || len(sc.shown) != 6 { | |
| 180 | + | t.Fatalf("setup: rects %d leaving %d shown %d", len(sc.rects), leaving, len(sc.shown)) | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | n := testing.AllocsPerRun(50, step) | |
| 184 | + | t.Logf("leaving allocs per step = %v", n) | |
| 185 | + | for _, sc := range c.screens { | |
| 186 | + | if sc.name() != "" && len(sc.rects) != 6 { | |
| 187 | + | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | |
| 188 | + | } | |
| 189 | + | } | |
| 190 | + | if n > publishLeavingAllocBudget { | |
| 191 | + | t.Errorf("publish allocs per frame with leaving entries = %v, budget %d", n, publishLeavingAllocBudget) | |
| 192 | + | } | |
| 193 | + | } | |
| @@ -14,8 +14,11 @@ import ( | |||||
| 14 | // and the layouts are rebuilt for every publish, so the floor is not 0. | 14 | // and the layouts are rebuilt for every publish, so the floor is not 0. | ||
| 15 | // Measured: 43 before presizing the scene windows and separators, 36 after; | 15 | // Measured: 43 before presizing the scene windows and separators, 36 after; | ||
| 16 | // 70 with the overview open (its rows and card layouts are rebuilt too). | 16 | // 70 with the overview open (its rows and card layouts are rebuilt too). | ||
| 17 | + | // Leaving entries (a closed window still fading) add none: the settled | |||
| 18 | + | // layout with them goes into a reused buffer. | |||
| 17 | const ( | 19 | const ( | ||
| 18 | publishAllocBudget = 36 | 20 | publishAllocBudget = 36 | ||
| 21 | + | publishLeavingAllocBudget = publishAllocBudget | |||
| 19 | publishOverviewAllocBudget = 70 | 22 | publishOverviewAllocBudget = 70 | ||
| 20 | ) | 23 | ) | ||
| 21 | 24 | ||||
| @@ -132,3 +135,59 @@ func TestPublishOverviewAllocations(t *testing.T) { | |||||
| 132 | t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget) | 135 | t.Errorf("overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget) | ||
| 133 | } | 136 | } | ||
| 134 | } | 137 | } | ||
| 138 | + | ||||
| 139 | + | // TestPublishLeavingAllocations pins the cost of a frame with leaving | |||
| 140 | + | // entries on every screen (two of the six windows closed, fading out, the | |||
| 141 | + | // other four with rect motions): not above a frame without them. | |||
| 142 | + | func TestPublishLeavingAllocations(t *testing.T) { | |||
| 143 | + | c := publishRig(t, 2) | |||
| 144 | + | t0 := time.Now() | |||
| 145 | + | for _, sc := range c.screens { | |||
| 146 | + | if sc.name() == "" { | |||
| 147 | + | continue | |||
| 148 | + | } | |||
| 149 | + | c.refreshShown() | |||
| 150 | + | layout := sc.settledLayout | |||
| 151 | + | sc.rects = make(map[WindowID]rectMotion, 6) | |||
| 152 | + | sp := viewSpring(100, 0) | |||
| 153 | + | for i, p := range layout { | |||
| 154 | + | if i < 2 { | |||
| 155 | + | sc.mon.RemoveWindow(p.ID) | |||
| 156 | + | c.leave(sc, p, t0) | |||
| 157 | + | continue | |||
| 158 | + | } | |||
| 159 | + | sc.rects[p.ID] = rectMotion{ | |||
| 160 | + | x: c.spring(sp, t0), y: c.spring(sp, t0), | |||
| 161 | + | w: c.spring(sp, t0), h: c.spring(sp, t0), | |||
| 162 | + | dx: 100, dy: 100, dw: 100, dh: 100, | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | sc.rectsWS = sc.mon.Current() | |||
| 166 | + | } | |||
| 167 | + | step := publishStep(t, c) | |||
| 168 | + | step() | |||
| 169 | + | for _, sc := range c.screens { | |||
| 170 | + | if sc.name() == "" { | |||
| 171 | + | continue | |||
| 172 | + | } | |||
| 173 | + | leaving := 0 | |||
| 174 | + | for _, p := range sc.shown { | |||
| 175 | + | if p.Leaving { | |||
| 176 | + | leaving++ | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | if len(sc.rects) != 6 || leaving != 2 || len(sc.shown) != 6 { | |||
| 180 | + | t.Fatalf("setup: rects %d leaving %d shown %d", len(sc.rects), leaving, len(sc.shown)) | |||
| 181 | + | } | |||
| 182 | + | } | |||
| 183 | + | n := testing.AllocsPerRun(50, step) | |||
| 184 | + | t.Logf("leaving allocs per step = %v", n) | |||
| 185 | + | for _, sc := range c.screens { | |||
| 186 | + | if sc.name() != "" && len(sc.rects) != 6 { | |||
| 187 | + | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | |||
| 188 | + | } | |||
| 189 | + | } | |||
| 190 | + | if n > publishLeavingAllocBudget { | |||
| 191 | + | t.Errorf("publish allocs per frame with leaving entries = %v, budget %d", n, publishLeavingAllocBudget) | |||
| 192 | + | } | |||
| 193 | + | } | |||
M internal/core/screens.go +3 -1
| @@ -37,7 +37,9 @@ type screen struct { | |||
| 37 | 37 | // runs. Both are built once per publish (refreshShown); shown is nil | |
| 38 | 38 | // before the first publish, then callers measure the monitor's layout. | |
| 39 | 39 | settledLayout, shown []Placement | |
| 40 | - | shownBuf []Placement | |
| 40 | + | // shownBuf and settledBuf back shown and settledLayout while rect | |
| 41 | + | // motions run (refreshShown), reused across publishes. | |
| 42 | + | shownBuf, settledBuf []Placement | |
| 41 | 43 | // rects are the running per-window rect motions (transition.go) of | |
| 42 | 44 | // rectsWS, the workspace they began on; created lazily. | |
| 43 | 45 | rects map[WindowID]rectMotion | |
| @@ -37,7 +37,9 @@ type screen struct { | |||||
| 37 | // runs. Both are built once per publish (refreshShown); shown is nil | 37 | // runs. Both are built once per publish (refreshShown); shown is nil | ||
| 38 | // before the first publish, then callers measure the monitor's layout. | 38 | // before the first publish, then callers measure the monitor's layout. | ||
| 39 | settledLayout, shown []Placement | 39 | settledLayout, shown []Placement | ||
| 40 | - | shownBuf []Placement | 40 | + | // shownBuf and settledBuf back shown and settledLayout while rect |
| 41 | + | // motions run (refreshShown), reused across publishes. | |||
| 42 | + | shownBuf, settledBuf []Placement | |||
| 41 | // rects are the running per-window rect motions (transition.go) of | 43 | // rects are the running per-window rect motions (transition.go) of | ||
| 42 | // rectsWS, the workspace they began on; created lazily. | 44 | // rectsWS, the workspace they began on; created lazily. | ||
| 43 | rects map[WindowID]rectMotion | 45 | rects map[WindowID]rectMotion | ||
M internal/core/transition.go +37 -13
| @@ -147,11 +147,15 @@ func (m *rectMotion) show(p *Placement) { | |||
| 147 | 147 | settled := p.Rect | |
| 148 | 148 | p.Rect = m.apply(settled) | |
| 149 | 149 | p.Fade = max(0, min(1, m.settledFade()+m.df)) | |
| 150 | - | p.Dim = m.ddim | |
| 150 | + | p.Dim = max(0, min(1, p.Dim+m.ddim)) | |
| 151 | 151 | if m.scale && settled.W > 0 { | |
| 152 | 152 | zoom := p.Preview | |
| 153 | 153 | if zoom == 0 { | |
| 154 | 154 | zoom = 1 | |
| 155 | + | if m.leaving { | |
| 156 | + | // A leaving window settles shrunk, its content with it. | |
| 157 | + | zoom = appearScale | |
| 158 | + | } | |
| 155 | 159 | } | |
| 156 | 160 | zoom *= float64(p.Rect.W) / float64(settled.W) | |
| 157 | 161 | if zoom >= 0.999 { | |
| @@ -208,7 +212,7 @@ func (c *Core) refreshShown() { | |||
| 208 | 212 | sc.shown = sc.settledLayout | |
| 209 | 213 | continue | |
| 210 | 214 | } | |
| 211 | - | sc.settledLayout = sc.appendLeaving(sc.settledLayout) | |
| 215 | + | sc.settledLayout = sc.withLeaving(sc.settledLayout) | |
| 212 | 216 | buf := append(sc.shownBuf[:0], sc.settledLayout...) | |
| 213 | 217 | for i := range buf { | |
| 214 | 218 | p := &buf[i] | |
| @@ -223,24 +227,41 @@ func (c *Core) refreshShown() { | |||
| 223 | 227 | } | |
| 224 | 228 | } | |
| 225 | 229 | ||
| 226 | - | // appendLeaving appends the leaving windows' last placements to layout, | |
| 227 | - | // Hidden and Leaving, smallest ID first (a stable order keeps the scenes | |
| 228 | - | // comparable frame to frame). | |
| 229 | - | func (s *screen) appendLeaving(layout []Placement) []Placement { | |
| 230 | - | n := len(layout) | |
| 230 | + | // withLeaving is layout with the leaving windows' last placements added, | |
| 231 | + | // Hidden and Leaving, into the screen's reused buffer (the layout itself | |
| 232 | + | // is left alone). A leaving tile goes where the tiles are painted: before | |
| 233 | + | // the first window that opens the floats (a float above the tiles, as the | |
| 234 | + | // renderer orders them), so it never flashes over a float; a leaving float | |
| 235 | + | // goes last, over the tiles. Within a group smallest ID first: a stable | |
| 236 | + | // order keeps the scenes comparable frame to frame. | |
| 237 | + | func (s *screen) withLeaving(layout []Placement) []Placement { | |
| 238 | + | out := append(s.settledBuf[:0], layout...) | |
| 239 | + | floats := len(out) | |
| 240 | + | for i, p := range out { | |
| 241 | + | if p.Floating && !p.Below && p.Preview == 0 && !p.Hidden { | |
| 242 | + | floats = i | |
| 243 | + | break | |
| 244 | + | } | |
| 245 | + | } | |
| 231 | 246 | for _, rm := range s.rects { | |
| 232 | 247 | if !rm.leaving { | |
| 233 | 248 | continue | |
| 234 | 249 | } | |
| 235 | 250 | p := rm.left | |
| 236 | 251 | p.Hidden, p.Leaving, p.Focused, p.Fade, p.Dim, p.Zoom = true, true, false, 0, 0, 0 | |
| 237 | - | i := n | |
| 238 | - | for i < len(layout) && layout[i].ID < p.ID { | |
| 252 | + | lo, hi := floats, len(out) | |
| 253 | + | if !p.Floating { | |
| 254 | + | lo, hi = 0, floats | |
| 255 | + | floats++ | |
| 256 | + | } | |
| 257 | + | i := lo | |
| 258 | + | for i < hi && (!out[i].Leaving || out[i].ID < p.ID) { | |
| 239 | 259 | i++ | |
| 240 | 260 | } | |
| 241 | - | layout = slices.Insert(layout, i, p) | |
| 261 | + | out = slices.Insert(out, i, p) | |
| 242 | 262 | } | |
| 243 | - | return layout | |
| 263 | + | s.settledBuf = out | |
| 264 | + | return out | |
| 244 | 265 | } | |
| 245 | 266 | ||
| 246 | 267 | // appear starts the entrance of a window the action made visible on sc: | |
| @@ -288,9 +309,8 @@ func (c *Core) leave(sc *screen, p Placement, now time.Time) { | |||
| 288 | 309 | rm := rectMotion{scale: true, leaving: true, left: p} | |
| 289 | 310 | rm.left.Hidden, rm.left.Leaving, rm.left.Focused = true, true, false | |
| 290 | 311 | // The entry settles at 90 % of the rect it left, its content zoomed | |
| 291 | - | // to match (Preview): the offsets run from the full rect to that. | |
| 312 | + | // to match (show): the offsets run from the full rect to that. | |
| 292 | 313 | end := scaledRect(p.Rect, appearScale) | |
| 293 | - | rm.left.Preview = appearScale | |
| 294 | 314 | c.retargetComponent(&rm.x, &rm.dx, float64(p.Rect.X-end.X), 0, now) | |
| 295 | 315 | c.retargetComponent(&rm.y, &rm.dy, float64(p.Rect.Y-end.Y), 0, now) | |
| 296 | 316 | c.retargetComponent(&rm.w, &rm.dw, float64(p.Rect.W-end.W), 0, now) | |
| @@ -467,6 +487,10 @@ func (c *Core) transitionRects(b *viewShot, now time.Time) { | |||
| 467 | 487 | } | |
| 468 | 488 | // Where the window was drawn, unrounded, relative to where it goes. | |
| 469 | 489 | rm := sc.rects[p.ID] | |
| 490 | + | // A re-flow may change the aspect: the content zoom (appear) stops | |
| 491 | + | // following the frame and snaps to the settled size; the fade goes | |
| 492 | + | // on. | |
| 493 | + | rm.scale = false | |
| 470 | 494 | c.retargetComponent(&rm.x, &rm.dx, float64(old.rect.X-p.Rect.X)+old.off.x, old.vel.x, now) | |
| 471 | 495 | c.retargetComponent(&rm.y, &rm.dy, float64(old.rect.Y-p.Rect.Y)+old.off.y, old.vel.y, now) | |
| 472 | 496 | c.retargetComponent(&rm.w, &rm.dw, float64(old.rect.W-p.Rect.W)+old.off.w, old.vel.w, now) | |
| @@ -147,11 +147,15 @@ func (m *rectMotion) show(p *Placement) { | |||||
| 147 | settled := p.Rect | 147 | settled := p.Rect | ||
| 148 | p.Rect = m.apply(settled) | 148 | p.Rect = m.apply(settled) | ||
| 149 | p.Fade = max(0, min(1, m.settledFade()+m.df)) | 149 | p.Fade = max(0, min(1, m.settledFade()+m.df)) | ||
| 150 | - | p.Dim = m.ddim | 150 | + | p.Dim = max(0, min(1, p.Dim+m.ddim)) |
| 151 | if m.scale && settled.W > 0 { | 151 | if m.scale && settled.W > 0 { | ||
| 152 | zoom := p.Preview | 152 | zoom := p.Preview | ||
| 153 | if zoom == 0 { | 153 | if zoom == 0 { | ||
| 154 | zoom = 1 | 154 | zoom = 1 | ||
| 155 | + | if m.leaving { | |||
| 156 | + | // A leaving window settles shrunk, its content with it. | |||
| 157 | + | zoom = appearScale | |||
| 158 | + | } | |||
| 155 | } | 159 | } | ||
| 156 | zoom *= float64(p.Rect.W) / float64(settled.W) | 160 | zoom *= float64(p.Rect.W) / float64(settled.W) | ||
| 157 | if zoom >= 0.999 { | 161 | if zoom >= 0.999 { | ||
| @@ -208,7 +212,7 @@ func (c *Core) refreshShown() { | |||||
| 208 | sc.shown = sc.settledLayout | 212 | sc.shown = sc.settledLayout | ||
| 209 | continue | 213 | continue | ||
| 210 | } | 214 | } | ||
| 211 | - | sc.settledLayout = sc.appendLeaving(sc.settledLayout) | 215 | + | sc.settledLayout = sc.withLeaving(sc.settledLayout) |
| 212 | buf := append(sc.shownBuf[:0], sc.settledLayout...) | 216 | buf := append(sc.shownBuf[:0], sc.settledLayout...) | ||
| 213 | for i := range buf { | 217 | for i := range buf { | ||
| 214 | p := &buf[i] | 218 | p := &buf[i] | ||
| @@ -223,24 +227,41 @@ func (c *Core) refreshShown() { | |||||
| 223 | } | 227 | } | ||
| 224 | } | 228 | } | ||
| 225 | 229 | ||||
| 226 | - | // appendLeaving appends the leaving windows' last placements to layout, | 230 | + | // withLeaving is layout with the leaving windows' last placements added, |
| 227 | - | // Hidden and Leaving, smallest ID first (a stable order keeps the scenes | 231 | + | // Hidden and Leaving, into the screen's reused buffer (the layout itself |
| 228 | - | // comparable frame to frame). | 232 | + | // is left alone). A leaving tile goes where the tiles are painted: before |
| 229 | - | func (s *screen) appendLeaving(layout []Placement) []Placement { | 233 | + | // the first window that opens the floats (a float above the tiles, as the |
| 230 | - | n := len(layout) | 234 | + | // renderer orders them), so it never flashes over a float; a leaving float |
| 235 | + | // goes last, over the tiles. Within a group smallest ID first: a stable | |||
| 236 | + | // order keeps the scenes comparable frame to frame. | |||
| 237 | + | func (s *screen) withLeaving(layout []Placement) []Placement { | |||
| 238 | + | out := append(s.settledBuf[:0], layout...) | |||
| 239 | + | floats := len(out) | |||
| 240 | + | for i, p := range out { | |||
| 241 | + | if p.Floating && !p.Below && p.Preview == 0 && !p.Hidden { | |||
| 242 | + | floats = i | |||
| 243 | + | break | |||
| 244 | + | } | |||
| 245 | + | } | |||
| 231 | for _, rm := range s.rects { | 246 | for _, rm := range s.rects { | ||
| 232 | if !rm.leaving { | 247 | if !rm.leaving { | ||
| 233 | continue | 248 | continue | ||
| 234 | } | 249 | } | ||
| 235 | p := rm.left | 250 | p := rm.left | ||
| 236 | p.Hidden, p.Leaving, p.Focused, p.Fade, p.Dim, p.Zoom = true, true, false, 0, 0, 0 | 251 | p.Hidden, p.Leaving, p.Focused, p.Fade, p.Dim, p.Zoom = true, true, false, 0, 0, 0 | ||
| 237 | - | i := n | 252 | + | lo, hi := floats, len(out) |
| 238 | - | for i < len(layout) && layout[i].ID < p.ID { | 253 | + | if !p.Floating { |
| 254 | + | lo, hi = 0, floats | |||
| 255 | + | floats++ | |||
| 256 | + | } | |||
| 257 | + | i := lo | |||
| 258 | + | for i < hi && (!out[i].Leaving || out[i].ID < p.ID) { | |||
| 239 | i++ | 259 | i++ | ||
| 240 | } | 260 | } | ||
| 241 | - | layout = slices.Insert(layout, i, p) | 261 | + | out = slices.Insert(out, i, p) |
| 242 | } | 262 | } | ||
| 243 | - | return layout | 263 | + | s.settledBuf = out |
| 264 | + | return out | |||
| 244 | } | 265 | } | ||
| 245 | 266 | ||||
| 246 | // appear starts the entrance of a window the action made visible on sc: | 267 | // appear starts the entrance of a window the action made visible on sc: | ||
| @@ -288,9 +309,8 @@ func (c *Core) leave(sc *screen, p Placement, now time.Time) { | |||||
| 288 | rm := rectMotion{scale: true, leaving: true, left: p} | 309 | rm := rectMotion{scale: true, leaving: true, left: p} | ||
| 289 | rm.left.Hidden, rm.left.Leaving, rm.left.Focused = true, true, false | 310 | rm.left.Hidden, rm.left.Leaving, rm.left.Focused = true, true, false | ||
| 290 | // The entry settles at 90 % of the rect it left, its content zoomed | 311 | // The entry settles at 90 % of the rect it left, its content zoomed | ||
| 291 | - | // to match (Preview): the offsets run from the full rect to that. | 312 | + | // to match (show): the offsets run from the full rect to that. |
| 292 | end := scaledRect(p.Rect, appearScale) | 313 | end := scaledRect(p.Rect, appearScale) | ||
| 293 | - | rm.left.Preview = appearScale | |||
| 294 | c.retargetComponent(&rm.x, &rm.dx, float64(p.Rect.X-end.X), 0, now) | 314 | c.retargetComponent(&rm.x, &rm.dx, float64(p.Rect.X-end.X), 0, now) | ||
| 295 | c.retargetComponent(&rm.y, &rm.dy, float64(p.Rect.Y-end.Y), 0, now) | 315 | c.retargetComponent(&rm.y, &rm.dy, float64(p.Rect.Y-end.Y), 0, now) | ||
| 296 | c.retargetComponent(&rm.w, &rm.dw, float64(p.Rect.W-end.W), 0, now) | 316 | c.retargetComponent(&rm.w, &rm.dw, float64(p.Rect.W-end.W), 0, now) | ||
| @@ -467,6 +487,10 @@ func (c *Core) transitionRects(b *viewShot, now time.Time) { | |||||
| 467 | } | 487 | } | ||
| 468 | // Where the window was drawn, unrounded, relative to where it goes. | 488 | // Where the window was drawn, unrounded, relative to where it goes. | ||
| 469 | rm := sc.rects[p.ID] | 489 | rm := sc.rects[p.ID] | ||
| 490 | + | // A re-flow may change the aspect: the content zoom (appear) stops | |||
| 491 | + | // following the frame and snaps to the settled size; the fade goes | |||
| 492 | + | // on. | |||
| 493 | + | rm.scale = false | |||
| 470 | c.retargetComponent(&rm.x, &rm.dx, float64(old.rect.X-p.Rect.X)+old.off.x, old.vel.x, now) | 494 | c.retargetComponent(&rm.x, &rm.dx, float64(old.rect.X-p.Rect.X)+old.off.x, old.vel.x, now) | ||
| 471 | c.retargetComponent(&rm.y, &rm.dy, float64(old.rect.Y-p.Rect.Y)+old.off.y, old.vel.y, now) | 495 | c.retargetComponent(&rm.y, &rm.dy, float64(old.rect.Y-p.Rect.Y)+old.off.y, old.vel.y, now) | ||
| 472 | c.retargetComponent(&rm.w, &rm.dw, float64(old.rect.W-p.Rect.W)+old.off.w, old.vel.w, now) | 496 | c.retargetComponent(&rm.w, &rm.dw, float64(old.rect.W-p.Rect.W)+old.off.w, old.vel.w, now) | ||
M internal/core/transition_fade_internal_test.go +126 -12
| @@ -44,8 +44,9 @@ func sceneWindow(t *testing.T, s ports.Scene, id WindowID) ports.SceneWindow { | |||
| 44 | 44 | ||
| 45 | 45 | // appear fades a window in from invisible and grows it from 90 % of its | |
| 46 | 46 | // settled rect, its content zoomed to the drawn size; the shown placement | |
| 47 | - | // carries the derived Fade and Preview, the settled one neither. | |
| 48 | - | func TestAppearDerivesFadeAndPreview(t *testing.T) { | |
| 47 | + | // carries the derived Fade and Zoom, the settled one neither; Preview | |
| 48 | + | // stays 0 (not a card). | |
| 49 | + | func TestAppearDerivesFadeAndZoom(t *testing.T) { | |
| 49 | 50 | c, ic, cmds := fadeCore(t) | |
| 50 | 51 | c.cfg.Animations.On = true | |
| 51 | 52 | sc := c.cur() | |
| @@ -65,8 +66,8 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||
| 65 | 66 | if w.Rect != want { | |
| 66 | 67 | t.Fatalf("first rect %+v, want 90 %% of %+v = %+v", w.Rect, settled.Rect, want) | |
| 67 | 68 | } | |
| 68 | - | if math.Abs(w.Preview-0.9) > 0.02 { | |
| 69 | - | t.Fatalf("preview %v, want ~0.9 (content follows the drawn size)", w.Preview) | |
| 69 | + | if math.Abs(w.Zoom-0.9) > 0.02 || w.Preview != 0 { | |
| 70 | + | t.Fatalf("zoom %v preview %v, want ~0.9 and 0 (content follows the drawn size, not a card)", w.Zoom, w.Preview) | |
| 70 | 71 | } | |
| 71 | 72 | if ps := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]; ps != settled { | |
| 72 | 73 | t.Fatalf("settled placement changed: %+v vs %+v", ps, settled) | |
| @@ -87,8 +88,8 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||
| 87 | 88 | c.animate(ic.now, nil) | |
| 88 | 89 | s = indicatorScene(t, c) | |
| 89 | 90 | w = sceneWindow(t, s, 2) | |
| 90 | - | if !(w.Fade > 0 && w.Fade < 1) || !(w.Preview > 0.9 && w.Preview < 1) { | |
| 91 | - | t.Fatalf("mid-way fade %v preview %v, want strictly between", w.Fade, w.Preview) | |
| 91 | + | if !(w.Fade > 0 && w.Fade < 1) || !(w.Zoom > 0.9 && w.Zoom < 1) { | |
| 92 | + | t.Fatalf("mid-way fade %v zoom %v, want strictly between", w.Fade, w.Zoom) | |
| 92 | 93 | } | |
| 93 | 94 | if !(w.Rect.W > want.W && w.Rect.W < settled.Rect.W) { | |
| 94 | 95 | t.Fatalf("mid-way width %d, want between %d and %d", w.Rect.W, want.W, settled.Rect.W) | |
| @@ -98,7 +99,7 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||
| 98 | 99 | c.animate(ic.now, nil) | |
| 99 | 100 | s = indicatorScene(t, c) | |
| 100 | 101 | w = sceneWindow(t, s, 2) | |
| 101 | - | if w.Fade != 0 || w.Preview != 0 || w.Rect != settled.Rect || len(sc.rects) != 0 { | |
| 102 | + | if w.Fade != 0 || w.Zoom != 0 || w.Rect != settled.Rect || len(sc.rects) != 0 { | |
| 102 | 103 | t.Fatalf("settled: %+v, rects %d", w, len(sc.rects)) | |
| 103 | 104 | } | |
| 104 | 105 | if c.animating() { | |
| @@ -116,7 +117,7 @@ func TestAppearOffIsInstant(t *testing.T) { | |||
| 116 | 117 | if len(sc.rects) != 0 || c.animating() { | |
| 117 | 118 | t.Fatal("appear started a motion with animations off") | |
| 118 | 119 | } | |
| 119 | - | if w := sceneWindow(t, indicatorScene(t, c), 1); w.Fade != 0 || w.Preview != 0 { | |
| 120 | + | if w := sceneWindow(t, indicatorScene(t, c), 1); w.Fade != 0 || w.Zoom != 0 { | |
| 120 | 121 | t.Fatalf("faded with animations off: %+v", w) | |
| 121 | 122 | } | |
| 122 | 123 | } | |
| @@ -151,7 +152,7 @@ func TestLeavingEntryLifecycle(t *testing.T) { | |||
| 151 | 152 | t.Fatalf("leaving entry: shown %+v settled %+v", sc.shown[k], sc.settledLayout[k]) | |
| 152 | 153 | } | |
| 153 | 154 | w := sceneWindow(t, s, 2) | |
| 154 | - | if w.Hidden || w.Fade != 0 || w.Rect != left.Rect || w.Preview != 0 { | |
| 155 | + | if w.Hidden || w.Fade != 0 || w.Rect != left.Rect || w.Zoom != 0 || w.Preview != 0 { | |
| 155 | 156 | t.Fatalf("first leaving frame %+v, want drawn opaque at %+v", w, left.Rect) | |
| 156 | 157 | } | |
| 157 | 158 | if ids := drainConfigures(cmds); slices.Contains(ids, 2) { | |
| @@ -176,7 +177,7 @@ func TestLeavingEntryLifecycle(t *testing.T) { | |||
| 176 | 177 | c.animate(ic.now, nil) | |
| 177 | 178 | s = indicatorScene(t, c) | |
| 178 | 179 | w = sceneWindow(t, s, 2) | |
| 179 | - | if !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left.Rect.W) || !(w.Preview > 0.9 && w.Preview < 1) { | |
| 180 | + | if !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left.Rect.W) || !(w.Zoom > 0.9 && w.Zoom < 1) { | |
| 180 | 181 | t.Fatalf("mid-way %+v", w) | |
| 181 | 182 | } | |
| 182 | 183 | ||
| @@ -212,7 +213,7 @@ func TestLeavingEntryDroppedWhenShownAgain(t *testing.T) { | |||
| 212 | 213 | for _, w := range s.Windows { | |
| 213 | 214 | if w.ID == 2 { | |
| 214 | 215 | n++ | |
| 215 | - | if w.Fade != 0 || w.Preview != 0 { | |
| 216 | + | if w.Fade != 0 || w.Zoom != 0 { | |
| 216 | 217 | t.Fatalf("the re-added window keeps the leaving fade: %+v", w) | |
| 217 | 218 | } | |
| 218 | 219 | } | |
| @@ -295,7 +296,7 @@ func TestScaleMotionMultipliesSettledPreview(t *testing.T) { | |||
| 295 | 296 | if p.Rect.W != 150 || math.Abs(p.Zoom-0.3) > 1e-9 || p.Preview != 0.2 { | |
| 296 | 297 | t.Fatalf("shown %+v, want width 150, zoom 0.3 and the settled preview", p) | |
| 297 | 298 | } | |
| 298 | - | // A plain window drawn at its size: Zoom 1 (publish emits Preview 0). | |
| 299 | + | // A plain window drawn at its size: Zoom 1 (publish emits Zoom 0). | |
| 299 | 300 | rm.dw = 0.01 | |
| 300 | 301 | p = Placement{ID: 1, Rect: Rect{W: 100, H: 100}} | |
| 301 | 302 | rm.show(&p) | |
| @@ -322,3 +323,116 @@ func TestSnapshotSkipsLeavingEntries(t *testing.T) { | |||
| 322 | 323 | t.Fatalf("snapshot rects %+v, want window 1 only", shots[0].rects) | |
| 323 | 324 | } | |
| 324 | 325 | } | |
| 326 | + | ||
| 327 | + | // A leaving tile is listed where the tiles are painted, before the first | |
| 328 | + | // float that opens them; a leaving float goes last. Settled and shown stay | |
| 329 | + | // aligned. The veil keeps following the leaving float, fading with it. | |
| 330 | + | func TestLeavingPaintOrder(t *testing.T) { | |
| 331 | + | c, ic := indicatorCore(t) | |
| 332 | + | c.cfg.Animations.On = true | |
| 333 | + | c.cfg.Floating.Dim = 0.4 | |
| 334 | + | sc := c.cur() | |
| 335 | + | ws := sc.mon.Current() | |
| 336 | + | sc.mon.AddWindow(1) | |
| 337 | + | sc.mon.AddWindow(2) | |
| 338 | + | ws.AddFloating(3, 80, 60) | |
| 339 | + | settleShown(t, c) | |
| 340 | + | tile := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })] | |
| 341 | + | float := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 3 })] | |
| 342 | + | if !float.Floating || tile.Floating { | |
| 343 | + | t.Fatalf("setup: tile %+v float %+v", tile, float) | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // Close the tile: its entry sits before the float. | |
| 347 | + | sc.mon.RemoveWindow(2) | |
| 348 | + | c.leave(sc, tile, ic.now) | |
| 349 | + | s := indicatorScene(t, c) | |
| 350 | + | order := func(s ports.Scene) []WindowID { | |
| 351 | + | ids := make([]WindowID, 0, len(s.Windows)) | |
| 352 | + | for _, w := range s.Windows { | |
| 353 | + | ids = append(ids, w.ID) | |
| 354 | + | } | |
| 355 | + | return ids | |
| 356 | + | } | |
| 357 | + | if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) { | |
| 358 | + | t.Fatalf("order with a leaving tile %v, want [1 2 3] (tile before the float)", got) | |
| 359 | + | } | |
| 360 | + | for k := range sc.shown { | |
| 361 | + | if sc.shown[k].ID != sc.settledLayout[k].ID { | |
| 362 | + | t.Fatalf("index %d: shown %d, settled %d", k, sc.shown[k].ID, sc.settledLayout[k].ID) | |
| 363 | + | } | |
| 364 | + | } | |
| 365 | + | if s.Dim != 0.4 { | |
| 366 | + | t.Fatalf("veil %v with the float still shown, want 0.4", s.Dim) | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // Close the float too: its entry goes last and keeps the veil, which | |
| 370 | + | // fades with it. | |
| 371 | + | sc.mon.RemoveWindow(3) | |
| 372 | + | c.leave(sc, float, ic.now) | |
| 373 | + | s = indicatorScene(t, c) | |
| 374 | + | if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) { | |
| 375 | + | t.Fatalf("order with a leaving float %v, want [1 2 3] (float last)", got) | |
| 376 | + | } | |
| 377 | + | if s.Dim != 0.4 { | |
| 378 | + | t.Fatalf("veil %v right after the float left, want 0.4", s.Dim) | |
| 379 | + | } | |
| 380 | + | ic.now = ic.now.Add(60 * time.Millisecond) | |
| 381 | + | c.animate(ic.now, nil) | |
| 382 | + | s = indicatorScene(t, c) | |
| 383 | + | f := sceneWindow(t, s, 3) | |
| 384 | + | if !(s.Dim > 0 && s.Dim < 0.4) || math.Abs(s.Dim-0.4*(1-f.Fade)) > 1e-9 { | |
| 385 | + | t.Fatalf("veil %v mid-fade (float fade %v), want 0.4*(1-fade)", s.Dim, f.Fade) | |
| 386 | + | } | |
| 387 | + | ic.now = ic.now.Add(5 * time.Second) | |
| 388 | + | c.animate(ic.now, nil) | |
| 389 | + | s = indicatorScene(t, c) | |
| 390 | + | if s.Dim != 0 || len(s.Windows) != 1 { | |
| 391 | + | t.Fatalf("after the settle: dim %v windows %+v", s.Dim, s.Windows) | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | // A re-flow that retargets an appearing window's rect motion stops its | |
| 396 | + | // content zoom (the aspect may change) and keeps its fade. | |
| 397 | + | func TestRetargetClearsScaleKeepsFade(t *testing.T) { | |
| 398 | + | c, ic := indicatorCore(t) | |
| 399 | + | c.cfg.Animations.On = true | |
| 400 | + | sc := c.cur() | |
| 401 | + | sc.mon.AddWindow(1) | |
| 402 | + | settleShown(t, c) | |
| 403 | + | c.appear(sc, 1, ic.now) | |
| 404 | + | settleShown(t, c) | |
| 405 | + | if rm := sc.rects[1]; !rm.scale || !rm.fade.on { | |
| 406 | + | t.Fatalf("setup: %+v", rm) | |
| 407 | + | } | |
| 408 | + | ic.now = ic.now.Add(20 * time.Millisecond) | |
| 409 | + | c.animate(ic.now, nil) | |
| 410 | + | settleShown(t, c) | |
| 411 | + | before := c.snapshot(ic.now) | |
| 412 | + | sc.mon.AddWindow(2) // re-flows window 1 | |
| 413 | + | c.transition(before, ic.now) | |
| 414 | + | rm, ok := sc.rects[1] | |
| 415 | + | if !ok || rm.scale || !rm.fade.on || !rm.on() { | |
| 416 | + | t.Fatalf("after the re-flow: %+v", rm) | |
| 417 | + | } | |
| 418 | + | w := sceneWindow(t, indicatorScene(t, c), 1) | |
| 419 | + | if w.Zoom != 0 || !(w.Fade > 0 && w.Fade < 1) { | |
| 420 | + | t.Fatalf("zoom %v fade %v, want no zoom and a running fade", w.Zoom, w.Fade) | |
| 421 | + | } | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | // Dim offsets are clamped into 0..1 on the shown placement. | |
| 425 | + | func TestDimOffsetClamped(t *testing.T) { | |
| 426 | + | var rm rectMotion | |
| 427 | + | rm.ddim = -0.3 | |
| 428 | + | p := Placement{ID: 1, Rect: Rect{W: 10, H: 10}} | |
| 429 | + | rm.show(&p) | |
| 430 | + | if p.Dim != 0 { | |
| 431 | + | t.Fatalf("dim %v, want 0", p.Dim) | |
| 432 | + | } | |
| 433 | + | rm.ddim = 1.5 | |
| 434 | + | rm.show(&p) | |
| 435 | + | if p.Dim != 1 { | |
| 436 | + | t.Fatalf("dim %v, want 1", p.Dim) | |
| 437 | + | } | |
| 438 | + | } | |
| @@ -44,8 +44,9 @@ func sceneWindow(t *testing.T, s ports.Scene, id WindowID) ports.SceneWindow { | |||||
| 44 | 44 | ||||
| 45 | // appear fades a window in from invisible and grows it from 90 % of its | 45 | // appear fades a window in from invisible and grows it from 90 % of its | ||
| 46 | // settled rect, its content zoomed to the drawn size; the shown placement | 46 | // settled rect, its content zoomed to the drawn size; the shown placement | ||
| 47 | - | // carries the derived Fade and Preview, the settled one neither. | 47 | + | // carries the derived Fade and Zoom, the settled one neither; Preview |
| 48 | - | func TestAppearDerivesFadeAndPreview(t *testing.T) { | 48 | + | // stays 0 (not a card). |
| 49 | + | func TestAppearDerivesFadeAndZoom(t *testing.T) { | |||
| 49 | c, ic, cmds := fadeCore(t) | 50 | c, ic, cmds := fadeCore(t) | ||
| 50 | c.cfg.Animations.On = true | 51 | c.cfg.Animations.On = true | ||
| 51 | sc := c.cur() | 52 | sc := c.cur() | ||
| @@ -65,8 +66,8 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||||
| 65 | if w.Rect != want { | 66 | if w.Rect != want { | ||
| 66 | t.Fatalf("first rect %+v, want 90 %% of %+v = %+v", w.Rect, settled.Rect, want) | 67 | t.Fatalf("first rect %+v, want 90 %% of %+v = %+v", w.Rect, settled.Rect, want) | ||
| 67 | } | 68 | } | ||
| 68 | - | if math.Abs(w.Preview-0.9) > 0.02 { | 69 | + | if math.Abs(w.Zoom-0.9) > 0.02 || w.Preview != 0 { |
| 69 | - | t.Fatalf("preview %v, want ~0.9 (content follows the drawn size)", w.Preview) | 70 | + | t.Fatalf("zoom %v preview %v, want ~0.9 and 0 (content follows the drawn size, not a card)", w.Zoom, w.Preview) |
| 70 | } | 71 | } | ||
| 71 | if ps := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]; ps != settled { | 72 | if ps := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]; ps != settled { | ||
| 72 | t.Fatalf("settled placement changed: %+v vs %+v", ps, settled) | 73 | t.Fatalf("settled placement changed: %+v vs %+v", ps, settled) | ||
| @@ -87,8 +88,8 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||||
| 87 | c.animate(ic.now, nil) | 88 | c.animate(ic.now, nil) | ||
| 88 | s = indicatorScene(t, c) | 89 | s = indicatorScene(t, c) | ||
| 89 | w = sceneWindow(t, s, 2) | 90 | w = sceneWindow(t, s, 2) | ||
| 90 | - | if !(w.Fade > 0 && w.Fade < 1) || !(w.Preview > 0.9 && w.Preview < 1) { | 91 | + | if !(w.Fade > 0 && w.Fade < 1) || !(w.Zoom > 0.9 && w.Zoom < 1) { |
| 91 | - | t.Fatalf("mid-way fade %v preview %v, want strictly between", w.Fade, w.Preview) | 92 | + | t.Fatalf("mid-way fade %v zoom %v, want strictly between", w.Fade, w.Zoom) |
| 92 | } | 93 | } | ||
| 93 | if !(w.Rect.W > want.W && w.Rect.W < settled.Rect.W) { | 94 | if !(w.Rect.W > want.W && w.Rect.W < settled.Rect.W) { | ||
| 94 | t.Fatalf("mid-way width %d, want between %d and %d", w.Rect.W, want.W, settled.Rect.W) | 95 | t.Fatalf("mid-way width %d, want between %d and %d", w.Rect.W, want.W, settled.Rect.W) | ||
| @@ -98,7 +99,7 @@ func TestAppearDerivesFadeAndPreview(t *testing.T) { | |||||
| 98 | c.animate(ic.now, nil) | 99 | c.animate(ic.now, nil) | ||
| 99 | s = indicatorScene(t, c) | 100 | s = indicatorScene(t, c) | ||
| 100 | w = sceneWindow(t, s, 2) | 101 | w = sceneWindow(t, s, 2) | ||
| 101 | - | if w.Fade != 0 || w.Preview != 0 || w.Rect != settled.Rect || len(sc.rects) != 0 { | 102 | + | if w.Fade != 0 || w.Zoom != 0 || w.Rect != settled.Rect || len(sc.rects) != 0 { |
| 102 | t.Fatalf("settled: %+v, rects %d", w, len(sc.rects)) | 103 | t.Fatalf("settled: %+v, rects %d", w, len(sc.rects)) | ||
| 103 | } | 104 | } | ||
| 104 | if c.animating() { | 105 | if c.animating() { | ||
| @@ -116,7 +117,7 @@ func TestAppearOffIsInstant(t *testing.T) { | |||||
| 116 | if len(sc.rects) != 0 || c.animating() { | 117 | if len(sc.rects) != 0 || c.animating() { | ||
| 117 | t.Fatal("appear started a motion with animations off") | 118 | t.Fatal("appear started a motion with animations off") | ||
| 118 | } | 119 | } | ||
| 119 | - | if w := sceneWindow(t, indicatorScene(t, c), 1); w.Fade != 0 || w.Preview != 0 { | 120 | + | if w := sceneWindow(t, indicatorScene(t, c), 1); w.Fade != 0 || w.Zoom != 0 { |
| 120 | t.Fatalf("faded with animations off: %+v", w) | 121 | t.Fatalf("faded with animations off: %+v", w) | ||
| 121 | } | 122 | } | ||
| 122 | } | 123 | } | ||
| @@ -151,7 +152,7 @@ func TestLeavingEntryLifecycle(t *testing.T) { | |||||
| 151 | t.Fatalf("leaving entry: shown %+v settled %+v", sc.shown[k], sc.settledLayout[k]) | 152 | t.Fatalf("leaving entry: shown %+v settled %+v", sc.shown[k], sc.settledLayout[k]) | ||
| 152 | } | 153 | } | ||
| 153 | w := sceneWindow(t, s, 2) | 154 | w := sceneWindow(t, s, 2) | ||
| 154 | - | if w.Hidden || w.Fade != 0 || w.Rect != left.Rect || w.Preview != 0 { | 155 | + | if w.Hidden || w.Fade != 0 || w.Rect != left.Rect || w.Zoom != 0 || w.Preview != 0 { |
| 155 | t.Fatalf("first leaving frame %+v, want drawn opaque at %+v", w, left.Rect) | 156 | t.Fatalf("first leaving frame %+v, want drawn opaque at %+v", w, left.Rect) | ||
| 156 | } | 157 | } | ||
| 157 | if ids := drainConfigures(cmds); slices.Contains(ids, 2) { | 158 | if ids := drainConfigures(cmds); slices.Contains(ids, 2) { | ||
| @@ -176,7 +177,7 @@ func TestLeavingEntryLifecycle(t *testing.T) { | |||||
| 176 | c.animate(ic.now, nil) | 177 | c.animate(ic.now, nil) | ||
| 177 | s = indicatorScene(t, c) | 178 | s = indicatorScene(t, c) | ||
| 178 | w = sceneWindow(t, s, 2) | 179 | w = sceneWindow(t, s, 2) | ||
| 179 | - | if !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left.Rect.W) || !(w.Preview > 0.9 && w.Preview < 1) { | 180 | + | if !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left.Rect.W) || !(w.Zoom > 0.9 && w.Zoom < 1) { |
| 180 | t.Fatalf("mid-way %+v", w) | 181 | t.Fatalf("mid-way %+v", w) | ||
| 181 | } | 182 | } | ||
| 182 | 183 | ||||
| @@ -212,7 +213,7 @@ func TestLeavingEntryDroppedWhenShownAgain(t *testing.T) { | |||||
| 212 | for _, w := range s.Windows { | 213 | for _, w := range s.Windows { | ||
| 213 | if w.ID == 2 { | 214 | if w.ID == 2 { | ||
| 214 | n++ | 215 | n++ | ||
| 215 | - | if w.Fade != 0 || w.Preview != 0 { | 216 | + | if w.Fade != 0 || w.Zoom != 0 { |
| 216 | t.Fatalf("the re-added window keeps the leaving fade: %+v", w) | 217 | t.Fatalf("the re-added window keeps the leaving fade: %+v", w) | ||
| 217 | } | 218 | } | ||
| 218 | } | 219 | } | ||
| @@ -295,7 +296,7 @@ func TestScaleMotionMultipliesSettledPreview(t *testing.T) { | |||||
| 295 | if p.Rect.W != 150 || math.Abs(p.Zoom-0.3) > 1e-9 || p.Preview != 0.2 { | 296 | if p.Rect.W != 150 || math.Abs(p.Zoom-0.3) > 1e-9 || p.Preview != 0.2 { | ||
| 296 | t.Fatalf("shown %+v, want width 150, zoom 0.3 and the settled preview", p) | 297 | t.Fatalf("shown %+v, want width 150, zoom 0.3 and the settled preview", p) | ||
| 297 | } | 298 | } | ||
| 298 | - | // A plain window drawn at its size: Zoom 1 (publish emits Preview 0). | 299 | + | // A plain window drawn at its size: Zoom 1 (publish emits Zoom 0). |
| 299 | rm.dw = 0.01 | 300 | rm.dw = 0.01 | ||
| 300 | p = Placement{ID: 1, Rect: Rect{W: 100, H: 100}} | 301 | p = Placement{ID: 1, Rect: Rect{W: 100, H: 100}} | ||
| 301 | rm.show(&p) | 302 | rm.show(&p) | ||
| @@ -322,3 +323,116 @@ func TestSnapshotSkipsLeavingEntries(t *testing.T) { | |||||
| 322 | t.Fatalf("snapshot rects %+v, want window 1 only", shots[0].rects) | 323 | t.Fatalf("snapshot rects %+v, want window 1 only", shots[0].rects) | ||
| 323 | } | 324 | } | ||
| 324 | } | 325 | } | ||
| 326 | + | ||||
| 327 | + | // A leaving tile is listed where the tiles are painted, before the first | |||
| 328 | + | // float that opens them; a leaving float goes last. Settled and shown stay | |||
| 329 | + | // aligned. The veil keeps following the leaving float, fading with it. | |||
| 330 | + | func TestLeavingPaintOrder(t *testing.T) { | |||
| 331 | + | c, ic := indicatorCore(t) | |||
| 332 | + | c.cfg.Animations.On = true | |||
| 333 | + | c.cfg.Floating.Dim = 0.4 | |||
| 334 | + | sc := c.cur() | |||
| 335 | + | ws := sc.mon.Current() | |||
| 336 | + | sc.mon.AddWindow(1) | |||
| 337 | + | sc.mon.AddWindow(2) | |||
| 338 | + | ws.AddFloating(3, 80, 60) | |||
| 339 | + | settleShown(t, c) | |||
| 340 | + | tile := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })] | |||
| 341 | + | float := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 3 })] | |||
| 342 | + | if !float.Floating || tile.Floating { | |||
| 343 | + | t.Fatalf("setup: tile %+v float %+v", tile, float) | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | // Close the tile: its entry sits before the float. | |||
| 347 | + | sc.mon.RemoveWindow(2) | |||
| 348 | + | c.leave(sc, tile, ic.now) | |||
| 349 | + | s := indicatorScene(t, c) | |||
| 350 | + | order := func(s ports.Scene) []WindowID { | |||
| 351 | + | ids := make([]WindowID, 0, len(s.Windows)) | |||
| 352 | + | for _, w := range s.Windows { | |||
| 353 | + | ids = append(ids, w.ID) | |||
| 354 | + | } | |||
| 355 | + | return ids | |||
| 356 | + | } | |||
| 357 | + | if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) { | |||
| 358 | + | t.Fatalf("order with a leaving tile %v, want [1 2 3] (tile before the float)", got) | |||
| 359 | + | } | |||
| 360 | + | for k := range sc.shown { | |||
| 361 | + | if sc.shown[k].ID != sc.settledLayout[k].ID { | |||
| 362 | + | t.Fatalf("index %d: shown %d, settled %d", k, sc.shown[k].ID, sc.settledLayout[k].ID) | |||
| 363 | + | } | |||
| 364 | + | } | |||
| 365 | + | if s.Dim != 0.4 { | |||
| 366 | + | t.Fatalf("veil %v with the float still shown, want 0.4", s.Dim) | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | // Close the float too: its entry goes last and keeps the veil, which | |||
| 370 | + | // fades with it. | |||
| 371 | + | sc.mon.RemoveWindow(3) | |||
| 372 | + | c.leave(sc, float, ic.now) | |||
| 373 | + | s = indicatorScene(t, c) | |||
| 374 | + | if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) { | |||
| 375 | + | t.Fatalf("order with a leaving float %v, want [1 2 3] (float last)", got) | |||
| 376 | + | } | |||
| 377 | + | if s.Dim != 0.4 { | |||
| 378 | + | t.Fatalf("veil %v right after the float left, want 0.4", s.Dim) | |||
| 379 | + | } | |||
| 380 | + | ic.now = ic.now.Add(60 * time.Millisecond) | |||
| 381 | + | c.animate(ic.now, nil) | |||
| 382 | + | s = indicatorScene(t, c) | |||
| 383 | + | f := sceneWindow(t, s, 3) | |||
| 384 | + | if !(s.Dim > 0 && s.Dim < 0.4) || math.Abs(s.Dim-0.4*(1-f.Fade)) > 1e-9 { | |||
| 385 | + | t.Fatalf("veil %v mid-fade (float fade %v), want 0.4*(1-fade)", s.Dim, f.Fade) | |||
| 386 | + | } | |||
| 387 | + | ic.now = ic.now.Add(5 * time.Second) | |||
| 388 | + | c.animate(ic.now, nil) | |||
| 389 | + | s = indicatorScene(t, c) | |||
| 390 | + | if s.Dim != 0 || len(s.Windows) != 1 { | |||
| 391 | + | t.Fatalf("after the settle: dim %v windows %+v", s.Dim, s.Windows) | |||
| 392 | + | } | |||
| 393 | + | } | |||
| 394 | + | ||||
| 395 | + | // A re-flow that retargets an appearing window's rect motion stops its | |||
| 396 | + | // content zoom (the aspect may change) and keeps its fade. | |||
| 397 | + | func TestRetargetClearsScaleKeepsFade(t *testing.T) { | |||
| 398 | + | c, ic := indicatorCore(t) | |||
| 399 | + | c.cfg.Animations.On = true | |||
| 400 | + | sc := c.cur() | |||
| 401 | + | sc.mon.AddWindow(1) | |||
| 402 | + | settleShown(t, c) | |||
| 403 | + | c.appear(sc, 1, ic.now) | |||
| 404 | + | settleShown(t, c) | |||
| 405 | + | if rm := sc.rects[1]; !rm.scale || !rm.fade.on { | |||
| 406 | + | t.Fatalf("setup: %+v", rm) | |||
| 407 | + | } | |||
| 408 | + | ic.now = ic.now.Add(20 * time.Millisecond) | |||
| 409 | + | c.animate(ic.now, nil) | |||
| 410 | + | settleShown(t, c) | |||
| 411 | + | before := c.snapshot(ic.now) | |||
| 412 | + | sc.mon.AddWindow(2) // re-flows window 1 | |||
| 413 | + | c.transition(before, ic.now) | |||
| 414 | + | rm, ok := sc.rects[1] | |||
| 415 | + | if !ok || rm.scale || !rm.fade.on || !rm.on() { | |||
| 416 | + | t.Fatalf("after the re-flow: %+v", rm) | |||
| 417 | + | } | |||
| 418 | + | w := sceneWindow(t, indicatorScene(t, c), 1) | |||
| 419 | + | if w.Zoom != 0 || !(w.Fade > 0 && w.Fade < 1) { | |||
| 420 | + | t.Fatalf("zoom %v fade %v, want no zoom and a running fade", w.Zoom, w.Fade) | |||
| 421 | + | } | |||
| 422 | + | } | |||
| 423 | + | ||||
| 424 | + | // Dim offsets are clamped into 0..1 on the shown placement. | |||
| 425 | + | func TestDimOffsetClamped(t *testing.T) { | |||
| 426 | + | var rm rectMotion | |||
| 427 | + | rm.ddim = -0.3 | |||
| 428 | + | p := Placement{ID: 1, Rect: Rect{W: 10, H: 10}} | |||
| 429 | + | rm.show(&p) | |||
| 430 | + | if p.Dim != 0 { | |||
| 431 | + | t.Fatalf("dim %v, want 0", p.Dim) | |||
| 432 | + | } | |||
| 433 | + | rm.ddim = 1.5 | |||
| 434 | + | rm.show(&p) | |||
| 435 | + | if p.Dim != 1 { | |||
| 436 | + | t.Fatalf("dim %v, want 1", p.Dim) | |||
| 437 | + | } | |||
| 438 | + | } | |||