refactor(core): remove focus pulse and add focus flash contracts
Remove the focus pulse from core, ports, vulkan shaders and config. Add ports.FocusFlash, Scene.Flash and the focus.flash* config keys; no flash behaviour yet.
- commit
- ade9e829e53a1afc20f4550e577852ca32e23349
- author
- brice · · 2026-10-19 04:27 UTC
- parent
- dc5dff6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
38 files · +140 -828
M README.md +4 -3
| @@ -225,9 +225,10 @@ cursor.hide-after = 5s | |||
| 225 | 225 | focus.follow-move = off | |
| 226 | 226 | animations = off | |
| 227 | 227 | animations.speed = normal | |
| 228 | - | focus.animation = pulse | |
| 229 | - | focus.effect = screen | |
| 230 | - | focus.strength = 0.04 | |
| 228 | + | focus.flash = on | |
| 229 | + | focus.flash-width = 2 | |
| 230 | + | focus.flash-opacity = 0.6 | |
| 231 | + | focus.flash-duration = 200ms | |
| 231 | 232 | ||
| 232 | 233 | # Rendering and logging | |
| 233 | 234 | render.direct-scanout = on | |
| @@ -225,9 +225,10 @@ cursor.hide-after = 5s | |||||
| 225 | focus.follow-move = off | 225 | focus.follow-move = off | ||
| 226 | animations = off | 226 | animations = off | ||
| 227 | animations.speed = normal | 227 | animations.speed = normal | ||
| 228 | - | focus.animation = pulse | 228 | + | focus.flash = on |
| 229 | - | focus.effect = screen | 229 | + | focus.flash-width = 2 |
| 230 | - | focus.strength = 0.04 | 230 | + | focus.flash-opacity = 0.6 |
| 231 | + | focus.flash-duration = 200ms | |||
| 231 | 232 | ||||
| 232 | # Rendering and logging | 233 | # Rendering and logging | ||
| 233 | render.direct-scanout = on | 234 | render.direct-scanout = on | ||
M docs/config.md +5 -2
| @@ -50,8 +50,11 @@ See [examples/config](../examples/config) for every key with its default. | |||
| 50 | 50 | | `animations` | `off` | **Experimental.** Animate layout changes and make swipes follow the fingers | | |
| 51 | 51 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | |
| 52 | 52 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | |
| 53 | - | | `focus.animation` | `pulse` | Brief brightening of a newly focused window, or `off`. Needs `animations = on` | | |
| 54 | - | | `focus.strength` | `0.04` | Strength of the focus pulse, `0.01` to `0.2` | | |
| 53 | + | | `focus.flash` | `on` | Briefly draw an inner border on the window, column or output an action changed | | |
| 54 | + | | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` | | |
| 55 | + | | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` | | |
| 56 | + | | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` | | |
| 57 | + | | `focus.flash-color` | `border.active` | Flash color `#rrggbb`; empty uses `border.active` | | |
| 55 | 58 | | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace | | |
| 56 | 59 | | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` | | |
| 57 | 60 | | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout | | |
| @@ -50,8 +50,11 @@ See [examples/config](../examples/config) for every key with its default. | |||||
| 50 | | `animations` | `off` | **Experimental.** Animate layout changes and make swipes follow the fingers | | 50 | | `animations` | `off` | **Experimental.** Animate layout changes and make swipes follow the fingers | | ||
| 51 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | 51 | | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` | | ||
| 52 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | 52 | | `focus.follow-move` | `off` | Follow a column moved to another workspace | | ||
| 53 | - | | `focus.animation` | `pulse` | Brief brightening of a newly focused window, or `off`. Needs `animations = on` | | 53 | + | | `focus.flash` | `on` | Briefly draw an inner border on the window, column or output an action changed | |
| 54 | - | | `focus.strength` | `0.04` | Strength of the focus pulse, `0.01` to `0.2` | | 54 | + | | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` | |
| 55 | + | | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` | | |||
| 56 | + | | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` | | |||
| 57 | + | | `focus.flash-color` | `border.active` | Flash color `#rrggbb`; empty uses `border.active` | | |||
| 55 | | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace | | 58 | | `workspace.<name>.monitor` | focused output | Home monitor of a named workspace | | ||
| 56 | | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` | | 59 | | `workspace.<name>.size` | `inherit` | [Smaller viewport](#workspace-size), e.g. `1920x1080` | | ||
| 57 | | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout | | 60 | | `workspace.<name>.max-columns`, `.overflow` | output values | Per-workspace layout | | ||
M examples/config +9 -6
| @@ -85,12 +85,15 @@ focus.follow-move = off | |||
| 85 | 85 | # (10x faster). | |
| 86 | 86 | animations = off | |
| 87 | 87 | animations.speed = normal | |
| 88 | - | # Focus indicator on the newly focused window: an animation (pulse or off) | |
| 89 | - | # of an effect (screen: a slight brightening), peaking at strength | |
| 90 | - | # (0.01 to 0.2; 0.04: black reaches about 10/255). | |
| 91 | - | focus.animation = pulse | |
| 92 | - | focus.effect = screen | |
| 93 | - | focus.strength = 0.04 | |
| 88 | + | # Focus flash: a brief inner border on the window, column or output an | |
| 89 | + | # action just changed. Width in logical pixels (1 to 8), starting opacity | |
| 90 | + | # (0.1 to 1), fade duration (50ms to 1s, not scaled by animations.speed), | |
| 91 | + | # color #rrggbb (empty: border.active). | |
| 92 | + | focus.flash = on | |
| 93 | + | focus.flash-width = 2 | |
| 94 | + | focus.flash-opacity = 0.6 | |
| 95 | + | focus.flash-duration = 200ms | |
| 96 | + | focus.flash-color = | |
| 94 | 97 | ||
| 95 | 98 | # Named workspaces are outside the numbered list and require a bind. The bind | |
| 96 | 99 | # shows one; pressing it again returns to the previous workspace. | |
| @@ -85,12 +85,15 @@ focus.follow-move = off | |||||
| 85 | # (10x faster). | 85 | # (10x faster). | ||
| 86 | animations = off | 86 | animations = off | ||
| 87 | animations.speed = normal | 87 | animations.speed = normal | ||
| 88 | - | # Focus indicator on the newly focused window: an animation (pulse or off) | 88 | + | # Focus flash: a brief inner border on the window, column or output an |
| 89 | - | # of an effect (screen: a slight brightening), peaking at strength | 89 | + | # action just changed. Width in logical pixels (1 to 8), starting opacity |
| 90 | - | # (0.01 to 0.2; 0.04: black reaches about 10/255). | 90 | + | # (0.1 to 1), fade duration (50ms to 1s, not scaled by animations.speed), |
| 91 | - | focus.animation = pulse | 91 | + | # color #rrggbb (empty: border.active). |
| 92 | - | focus.effect = screen | 92 | + | focus.flash = on |
| 93 | - | focus.strength = 0.04 | 93 | + | focus.flash-width = 2 |
| 94 | + | focus.flash-opacity = 0.6 | |||
| 95 | + | focus.flash-duration = 200ms | |||
| 96 | + | focus.flash-color = | |||
| 94 | 97 | ||||
| 95 | # Named workspaces are outside the numbered list and require a bind. The bind | 98 | # Named workspaces are outside the numbered list and require a bind. The bind | ||
| 96 | # shows one; pressing it again returns to the previous workspace. | 99 | # shows one; pressing it again returns to the previous workspace. | ||
M internal/adapters/config/config.go +31 -18
| @@ -184,9 +184,10 @@ func Defaults() ports.Config { | |||
| 184 | 184 | // Animations are experimental: off unless the config turns them on. | |
| 185 | 185 | c.Animations.On = false | |
| 186 | 186 | c.Animations.Slowdown = normalSlowdown | |
| 187 | - | c.Focus.Animation = ports.FocusAnimationPulse | |
| 188 | - | c.Focus.Effect = ports.FocusEffectScreen | |
| 189 | - | c.Focus.Strength = 0.04 | |
| 187 | + | c.Focus.Flash = true | |
| 188 | + | c.Focus.FlashWidth = 2 | |
| 189 | + | c.Focus.FlashOpacity = 0.6 | |
| 190 | + | c.Focus.FlashDuration = 200 * time.Millisecond | |
| 190 | 191 | c.Binds = map[string]string{} | |
| 191 | 192 | for _, b := range defaultBinds { | |
| 192 | 193 | combos := []string{b.combo} | |
| @@ -840,23 +841,35 @@ func set(c *ports.Config, key, v string) error { | |||
| 840 | 841 | return err | |
| 841 | 842 | } | |
| 842 | 843 | c.Focus.FollowMove = b | |
| 843 | - | case "focus.animation": | |
| 844 | - | if v != ports.FocusAnimationPulse && v != ports.FocusAnimationOff { | |
| 845 | - | return fmt.Errorf("must be pulse or off") | |
| 846 | - | } | |
| 847 | - | c.Focus.Animation = v | |
| 848 | - | case "focus.effect": | |
| 849 | - | if v != ports.FocusEffectScreen { | |
| 850 | - | return fmt.Errorf("must be screen") | |
| 851 | - | } | |
| 852 | - | c.Focus.Effect = v | |
| 853 | - | case "focus.strength": | |
| 854 | - | // Capped low: the focus indicator must never flash. | |
| 844 | + | case "focus.flash": | |
| 845 | + | b, err := onOff(v) | |
| 846 | + | if err != nil { | |
| 847 | + | return err | |
| 848 | + | } | |
| 849 | + | c.Focus.Flash = b | |
| 850 | + | case "focus.flash-width": | |
| 851 | + | n, err := strconv.Atoi(v) | |
| 852 | + | if err != nil || n < 1 || n > 8 { | |
| 853 | + | return fmt.Errorf("must be an integer between 1 and 8") | |
| 854 | + | } | |
| 855 | + | c.Focus.FlashWidth = n | |
| 856 | + | case "focus.flash-opacity": | |
| 855 | 857 | f, err := strconv.ParseFloat(v, 64) | |
| 856 | - | if err != nil || !(f >= 0.01 && f <= 0.2) { | |
| 857 | - | return fmt.Errorf("must be between 0.01 and 0.2") | |
| 858 | + | if err != nil || !(f >= 0.1 && f <= 1) { | |
| 859 | + | return fmt.Errorf("must be between 0.1 and 1") | |
| 860 | + | } | |
| 861 | + | c.Focus.FlashOpacity = f | |
| 862 | + | case "focus.flash-duration": | |
| 863 | + | d, err := time.ParseDuration(v) | |
| 864 | + | if err != nil || d < 50*time.Millisecond || d > time.Second { | |
| 865 | + | return fmt.Errorf("must be a duration between 50ms and 1s, e.g. 200ms") | |
| 866 | + | } | |
| 867 | + | c.Focus.FlashDuration = d | |
| 868 | + | case "focus.flash-color": | |
| 869 | + | if v != "" && !color.MatchString(v) { | |
| 870 | + | return fmt.Errorf("must be #rrggbb or empty (border.active)") | |
| 858 | 871 | } | |
| 859 | - | c.Focus.Strength = f | |
| 872 | + | c.Focus.FlashColor = v | |
| 860 | 873 | case "render.direct-scanout": | |
| 861 | 874 | b, err := onOff(v) | |
| 862 | 875 | if err != nil { | |
| @@ -184,9 +184,10 @@ func Defaults() ports.Config { | |||||
| 184 | // Animations are experimental: off unless the config turns them on. | 184 | // Animations are experimental: off unless the config turns them on. | ||
| 185 | c.Animations.On = false | 185 | c.Animations.On = false | ||
| 186 | c.Animations.Slowdown = normalSlowdown | 186 | c.Animations.Slowdown = normalSlowdown | ||
| 187 | - | c.Focus.Animation = ports.FocusAnimationPulse | 187 | + | c.Focus.Flash = true |
| 188 | - | c.Focus.Effect = ports.FocusEffectScreen | 188 | + | c.Focus.FlashWidth = 2 |
| 189 | - | c.Focus.Strength = 0.04 | 189 | + | c.Focus.FlashOpacity = 0.6 |
| 190 | + | c.Focus.FlashDuration = 200 * time.Millisecond | |||
| 190 | c.Binds = map[string]string{} | 191 | c.Binds = map[string]string{} | ||
| 191 | for _, b := range defaultBinds { | 192 | for _, b := range defaultBinds { | ||
| 192 | combos := []string{b.combo} | 193 | combos := []string{b.combo} | ||
| @@ -840,23 +841,35 @@ func set(c *ports.Config, key, v string) error { | |||||
| 840 | return err | 841 | return err | ||
| 841 | } | 842 | } | ||
| 842 | c.Focus.FollowMove = b | 843 | c.Focus.FollowMove = b | ||
| 843 | - | case "focus.animation": | 844 | + | case "focus.flash": |
| 844 | - | if v != ports.FocusAnimationPulse && v != ports.FocusAnimationOff { | 845 | + | b, err := onOff(v) |
| 845 | - | return fmt.Errorf("must be pulse or off") | 846 | + | if err != nil { |
| 846 | - | } | 847 | + | return err |
| 847 | - | c.Focus.Animation = v | 848 | + | } |
| 848 | - | case "focus.effect": | 849 | + | c.Focus.Flash = b |
| 849 | - | if v != ports.FocusEffectScreen { | 850 | + | case "focus.flash-width": |
| 850 | - | return fmt.Errorf("must be screen") | 851 | + | n, err := strconv.Atoi(v) |
| 851 | - | } | 852 | + | if err != nil || n < 1 || n > 8 { |
| 852 | - | c.Focus.Effect = v | 853 | + | return fmt.Errorf("must be an integer between 1 and 8") |
| 853 | - | case "focus.strength": | 854 | + | } |
| 854 | - | // Capped low: the focus indicator must never flash. | 855 | + | c.Focus.FlashWidth = n |
| 856 | + | case "focus.flash-opacity": | |||
| 855 | f, err := strconv.ParseFloat(v, 64) | 857 | f, err := strconv.ParseFloat(v, 64) | ||
| 856 | - | if err != nil || !(f >= 0.01 && f <= 0.2) { | 858 | + | if err != nil || !(f >= 0.1 && f <= 1) { |
| 857 | - | return fmt.Errorf("must be between 0.01 and 0.2") | 859 | + | return fmt.Errorf("must be between 0.1 and 1") |
| 860 | + | } | |||
| 861 | + | c.Focus.FlashOpacity = f | |||
| 862 | + | case "focus.flash-duration": | |||
| 863 | + | d, err := time.ParseDuration(v) | |||
| 864 | + | if err != nil || d < 50*time.Millisecond || d > time.Second { | |||
| 865 | + | return fmt.Errorf("must be a duration between 50ms and 1s, e.g. 200ms") | |||
| 866 | + | } | |||
| 867 | + | c.Focus.FlashDuration = d | |||
| 868 | + | case "focus.flash-color": | |||
| 869 | + | if v != "" && !color.MatchString(v) { | |||
| 870 | + | return fmt.Errorf("must be #rrggbb or empty (border.active)") | |||
| 858 | } | 871 | } | ||
| 859 | - | c.Focus.Strength = f | 872 | + | c.Focus.FlashColor = v |
| 860 | case "render.direct-scanout": | 873 | case "render.direct-scanout": | ||
| 861 | b, err := onOff(v) | 874 | b, err := onOff(v) | ||
| 862 | if err != nil { | 875 | if err != nil { | ||
M internal/adapters/config/config_test.go +26 -7
| @@ -110,21 +110,40 @@ func TestAnimations(t *testing.T) { | |||
| 110 | 110 | } | |
| 111 | 111 | } | |
| 112 | 112 | ||
| 113 | - | func TestFocusIndicator(t *testing.T) { | |
| 113 | + | func TestFocusFlash(t *testing.T) { | |
| 114 | 114 | d := Defaults().Focus | |
| 115 | - | if d.Animation != ports.FocusAnimationPulse || d.Effect != ports.FocusEffectScreen || d.Strength != 0.04 { | |
| 116 | - | t.Fatal("focus indicator defaults", d) | |
| 115 | + | if !d.Flash || d.FlashWidth != 2 || d.FlashOpacity != 0.6 || d.FlashDuration != 200*time.Millisecond || d.FlashColor != "" { | |
| 116 | + | t.Fatal("focus flash defaults", d) | |
| 117 | 117 | } | |
| 118 | - | c, w := parseString(t, "focus.animation = off\nfocus.effect = screen\nfocus.strength = 0.08\n") | |
| 119 | - | if c.Focus.Animation != ports.FocusAnimationOff || c.Focus.Effect != ports.FocusEffectScreen || c.Focus.Strength != 0.08 || len(w) != 0 { | |
| 118 | + | c, w := parseString(t, "focus.flash = off\nfocus.flash-width = 8\nfocus.flash-opacity = 1\nfocus.flash-duration = 1s\nfocus.flash-color = #112233\n") | |
| 119 | + | want := d | |
| 120 | + | want.Flash, want.FlashWidth, want.FlashOpacity, want.FlashDuration, want.FlashColor = false, 8, 1, time.Second, "#112233" | |
| 121 | + | if c.Focus != want || len(w) != 0 { | |
| 120 | 122 | t.Fatal(c.Focus, w) | |
| 121 | 123 | } | |
| 122 | - | // Each rejected value warns and keeps its default. | |
| 123 | - | for _, line := range []string{"focus.animation = blink", "focus.effect = glow", "focus.strength = 0", "focus.strength = 0.5", "focus.strength = x"} { | |
| 124 | + | c, w = parseString(t, "focus.flash = on\nfocus.flash-width = 1\nfocus.flash-opacity = 0.1\nfocus.flash-duration = 50ms\nfocus.flash-color =\n") | |
| 125 | + | want.Flash, want.FlashWidth, want.FlashOpacity, want.FlashDuration, want.FlashColor = true, 1, 0.1, 50*time.Millisecond, "" | |
| 126 | + | if c.Focus != want || len(w) != 0 { | |
| 127 | + | t.Fatal(c.Focus, w) | |
| 128 | + | } | |
| 129 | + | // Each rejected value warns and keeps its default; removed keys are unknown. | |
| 130 | + | for _, line := range []string{ | |
| 131 | + | "focus.flash = maybe", | |
| 132 | + | "focus.flash-width = 0", "focus.flash-width = 9", "focus.flash-width = x", "focus.flash-width = 1.5", | |
| 133 | + | "focus.flash-opacity = 0.05", "focus.flash-opacity = 1.1", "focus.flash-opacity = x", | |
| 134 | + | "focus.flash-duration = 10ms", "focus.flash-duration = 2s", "focus.flash-duration = x", "focus.flash-duration = 200", | |
| 135 | + | "focus.flash-color = red", "focus.flash-color = #12345", | |
| 136 | + | "focus.animation = pulse", "focus.effect = screen", "focus.strength = 0.04", | |
| 137 | + | } { | |
| 124 | 138 | c, w = parseString(t, line+"\n") | |
| 125 | 139 | if c.Focus != d || len(w) != 1 { | |
| 126 | 140 | t.Fatal(line, c.Focus, w) | |
| 127 | 141 | } | |
| 142 | + | if strings.HasPrefix(line, "focus.animation") || strings.HasPrefix(line, "focus.effect") || strings.HasPrefix(line, "focus.strength") { | |
| 143 | + | if !strings.Contains(w[0].Msg, "unknown key") { | |
| 144 | + | t.Fatal(line, w) | |
| 145 | + | } | |
| 146 | + | } | |
| 128 | 147 | } | |
| 129 | 148 | } | |
| 130 | 149 | ||
| @@ -110,21 +110,40 @@ func TestAnimations(t *testing.T) { | |||||
| 110 | } | 110 | } | ||
| 111 | } | 111 | } | ||
| 112 | 112 | ||||
| 113 | - | func TestFocusIndicator(t *testing.T) { | 113 | + | func TestFocusFlash(t *testing.T) { |
| 114 | d := Defaults().Focus | 114 | d := Defaults().Focus | ||
| 115 | - | if d.Animation != ports.FocusAnimationPulse || d.Effect != ports.FocusEffectScreen || d.Strength != 0.04 { | 115 | + | if !d.Flash || d.FlashWidth != 2 || d.FlashOpacity != 0.6 || d.FlashDuration != 200*time.Millisecond || d.FlashColor != "" { |
| 116 | - | t.Fatal("focus indicator defaults", d) | 116 | + | t.Fatal("focus flash defaults", d) |
| 117 | } | 117 | } | ||
| 118 | - | c, w := parseString(t, "focus.animation = off\nfocus.effect = screen\nfocus.strength = 0.08\n") | 118 | + | c, w := parseString(t, "focus.flash = off\nfocus.flash-width = 8\nfocus.flash-opacity = 1\nfocus.flash-duration = 1s\nfocus.flash-color = #112233\n") |
| 119 | - | if c.Focus.Animation != ports.FocusAnimationOff || c.Focus.Effect != ports.FocusEffectScreen || c.Focus.Strength != 0.08 || len(w) != 0 { | 119 | + | want := d |
| 120 | + | want.Flash, want.FlashWidth, want.FlashOpacity, want.FlashDuration, want.FlashColor = false, 8, 1, time.Second, "#112233" | |||
| 121 | + | if c.Focus != want || len(w) != 0 { | |||
| 120 | t.Fatal(c.Focus, w) | 122 | t.Fatal(c.Focus, w) | ||
| 121 | } | 123 | } | ||
| 122 | - | // Each rejected value warns and keeps its default. | 124 | + | c, w = parseString(t, "focus.flash = on\nfocus.flash-width = 1\nfocus.flash-opacity = 0.1\nfocus.flash-duration = 50ms\nfocus.flash-color =\n") |
| 123 | - | for _, line := range []string{"focus.animation = blink", "focus.effect = glow", "focus.strength = 0", "focus.strength = 0.5", "focus.strength = x"} { | 125 | + | want.Flash, want.FlashWidth, want.FlashOpacity, want.FlashDuration, want.FlashColor = true, 1, 0.1, 50*time.Millisecond, "" |
| 126 | + | if c.Focus != want || len(w) != 0 { | |||
| 127 | + | t.Fatal(c.Focus, w) | |||
| 128 | + | } | |||
| 129 | + | // Each rejected value warns and keeps its default; removed keys are unknown. | |||
| 130 | + | for _, line := range []string{ | |||
| 131 | + | "focus.flash = maybe", | |||
| 132 | + | "focus.flash-width = 0", "focus.flash-width = 9", "focus.flash-width = x", "focus.flash-width = 1.5", | |||
| 133 | + | "focus.flash-opacity = 0.05", "focus.flash-opacity = 1.1", "focus.flash-opacity = x", | |||
| 134 | + | "focus.flash-duration = 10ms", "focus.flash-duration = 2s", "focus.flash-duration = x", "focus.flash-duration = 200", | |||
| 135 | + | "focus.flash-color = red", "focus.flash-color = #12345", | |||
| 136 | + | "focus.animation = pulse", "focus.effect = screen", "focus.strength = 0.04", | |||
| 137 | + | } { | |||
| 124 | c, w = parseString(t, line+"\n") | 138 | c, w = parseString(t, line+"\n") | ||
| 125 | if c.Focus != d || len(w) != 1 { | 139 | if c.Focus != d || len(w) != 1 { | ||
| 126 | t.Fatal(line, c.Focus, w) | 140 | t.Fatal(line, c.Focus, w) | ||
| 127 | } | 141 | } | ||
| 142 | + | if strings.HasPrefix(line, "focus.animation") || strings.HasPrefix(line, "focus.effect") || strings.HasPrefix(line, "focus.strength") { | |||
| 143 | + | if !strings.Contains(w[0].Msg, "unknown key") { | |||
| 144 | + | t.Fatal(line, w) | |||
| 145 | + | } | |||
| 146 | + | } | |||
| 128 | } | 147 | } | ||
| 129 | } | 148 | } | ||
| 130 | 149 | ||||
M internal/adapters/drm/overlay.go +2 -2
| @@ -93,9 +93,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 93 | 93 | // buffer, and so is a colour the plane cannot show (raw PQ values | |
| 94 | 94 | // on an SDR output, SDR on HDR without a pipeline). A dimmed | |
| 95 | 95 | // window (a peeking stashed one) needs the veil drawn over it, a | |
| 96 | - | // fading one its translucency, and one with the focus effect its | |
| 96 | + | // fading one its translucency. | |
| 97 | 97 | // lift; an overview preview is drawn smaller than its buffer. | |
| 98 | - | if w.Dim <= 0 && w.Fade <= 0 && w.Zoom <= 0 && w.FocusEffect <= 0 && w.Preview <= 0 && c.DMABuf != nil && !isYUVFormat(c.DMABuf.Format) && c.Opaque && len(c.Children) == 0 && c.Transform == 0 && !cropped(c) && (kept == nil || !kept(w.ID)) { | |
| 98 | + | if w.Dim <= 0 && w.Fade <= 0 && w.Zoom <= 0 && w.Preview <= 0 && c.DMABuf != nil && !isYUVFormat(c.DMABuf.Format) && c.Opaque && len(c.Children) == 0 && c.Transform == 0 && !cropped(c) && (kept == nil || !kept(w.ID)) { | |
| 99 | 99 | if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" { | |
| 100 | 100 | pick, pickAt, mode = w, i, m | |
| 101 | 101 | continue | |
| @@ -93,9 +93,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 93 | // buffer, and so is a colour the plane cannot show (raw PQ values | 93 | // buffer, and so is a colour the plane cannot show (raw PQ values | ||
| 94 | // on an SDR output, SDR on HDR without a pipeline). A dimmed | 94 | // on an SDR output, SDR on HDR without a pipeline). A dimmed | ||
| 95 | // window (a peeking stashed one) needs the veil drawn over it, a | 95 | // window (a peeking stashed one) needs the veil drawn over it, a | ||
| 96 | - | // fading one its translucency, and one with the focus effect its | 96 | + | // fading one its translucency. |
| 97 | // lift; an overview preview is drawn smaller than its buffer. | 97 | // lift; an overview preview is drawn smaller than its buffer. | ||
| 98 | - | if w.Dim <= 0 && w.Fade <= 0 && w.Zoom <= 0 && w.FocusEffect <= 0 && w.Preview <= 0 && c.DMABuf != nil && !isYUVFormat(c.DMABuf.Format) && c.Opaque && len(c.Children) == 0 && c.Transform == 0 && !cropped(c) && (kept == nil || !kept(w.ID)) { | 98 | + | if w.Dim <= 0 && w.Fade <= 0 && w.Zoom <= 0 && w.Preview <= 0 && c.DMABuf != nil && !isYUVFormat(c.DMABuf.Format) && c.Opaque && len(c.Children) == 0 && c.Transform == 0 && !cropped(c) && (kept == nil || !kept(w.ID)) { |
| 99 | if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" { | 99 | if m, why := planeColor(c.Color, c.DMABuf.Format, c.Opaque, hdrOn, pipeline); why == "" { | ||
| 100 | pick, pickAt, mode = w, i, m | 100 | pick, pickAt, mode = w, i, m | ||
| 101 | continue | 101 | continue | ||
M internal/adapters/drm/overlay_test.go +0 -6
| @@ -156,12 +156,6 @@ func TestOverlayCandidate(t *testing.T) { | |||
| 156 | 156 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" { | |
| 157 | 157 | t.Fatalf("zoomed window reason %q", reason) | |
| 158 | 158 | } | |
| 159 | - | // A window with the focus effect needs it composed. | |
| 160 | - | s, _ = overlayScene() | |
| 161 | - | s.Windows[len(s.Windows)-1].FocusEffect = 0.05 | |
| 162 | - | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" { | |
| 163 | - | t.Fatalf("pulsing window reason %q", reason) | |
| 164 | - | } | |
| 165 | 159 | // An overview preview is drawn smaller than its buffer: composed. | |
| 166 | 160 | s, _ = overlayScene() | |
| 167 | 161 | s.Windows[len(s.Windows)-1].Preview = 0.5 | |
| @@ -156,12 +156,6 @@ func TestOverlayCandidate(t *testing.T) { | |||||
| 156 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" { | 156 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" { | ||
| 157 | t.Fatalf("zoomed window reason %q", reason) | 157 | t.Fatalf("zoomed window reason %q", reason) | ||
| 158 | } | 158 | } | ||
| 159 | - | // A window with the focus effect needs it composed. | |||
| 160 | - | s, _ = overlayScene() | |||
| 161 | - | s.Windows[len(s.Windows)-1].FocusEffect = 0.05 | |||
| 162 | - | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" { | |||
| 163 | - | t.Fatalf("pulsing window reason %q", reason) | |||
| 164 | - | } | |||
| 165 | // An overview preview is drawn smaller than its buffer: composed. | 159 | // An overview preview is drawn smaller than its buffer: composed. | ||
| 166 | s, _ = overlayScene() | 160 | s, _ = overlayScene() | ||
| 167 | s.Windows[len(s.Windows)-1].Preview = 0.5 | 161 | s.Windows[len(s.Windows)-1].Preview = 0.5 | ||
M internal/adapters/vulkan/damage.go +1 -0
| @@ -276,6 +276,7 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||
| 276 | 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | |
| 277 | 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | |
| 278 | 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | |
| 279 | + | (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && *old.Flash != *cur.Flash) || | |
| 279 | 280 | len(old.Windows) != len(cur.Windows) { | |
| 280 | 281 | return image.Rectangle{}, false | |
| 281 | 282 | } | |
| @@ -276,6 +276,7 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim | |||||
| 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | 276 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | ||
| 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | 277 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | ||
| 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | 278 | !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || | ||
| 279 | + | (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && *old.Flash != *cur.Flash) || | |||
| 279 | len(old.Windows) != len(cur.Windows) { | 280 | len(old.Windows) != len(cur.Windows) { | ||
| 280 | return image.Rectangle{}, false | 281 | return image.Rectangle{}, false | ||
| 281 | } | 282 | } | ||
M internal/adapters/vulkan/damage_delta_test.go +4 -4
| @@ -17,9 +17,9 @@ import ( | |||
| 17 | 17 | // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta | |
| 18 | 18 | // lists the fields that force a full redraw. | |
| 19 | 19 | func TestSceneFieldCount(t *testing.T) { | |
| 20 | - | // 22 with Animating, which sceneDelta intentionally ignores: it only | |
| 20 | + | // 23 with Animating, which sceneDelta intentionally ignores: it only | |
| 21 | 21 | // schedules the next frame, it draws nothing. | |
| 22 | - | const fields = 22 | |
| 22 | + | const fields = 23 | |
| 23 | 23 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))), | |
| 24 | 24 | "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count") | |
| 25 | 25 | } | |
| @@ -28,7 +28,7 @@ func TestSceneFieldCount(t *testing.T) { | |||
| 28 | 28 | // sceneDelta compares windows by struct equality, so a new field is covered | |
| 29 | 29 | // as long as it only changes where or how that window draws (inside Rect). | |
| 30 | 30 | func TestSceneWindowFieldCount(t *testing.T) { | |
| 31 | - | const fields = 15 | |
| 31 | + | const fields = 14 | |
| 32 | 32 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.SceneWindow{}))), | |
| 33 | 33 | "SceneWindow fields changed: check sceneDelta (a field that draws outside Rect needs its own rule), then this count") | |
| 34 | 34 | } | |
| @@ -75,7 +75,7 @@ func TestDamageDeltaAtScale(t *testing.T) { | |||
| 75 | 75 | func TestDamageDeltaWindowFieldChange(t *testing.T) { | |
| 76 | 76 | old := deltaScene(1, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | |
| 77 | 77 | cur := deltaScene(2, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | |
| 78 | - | cur.Windows[0].FocusEffect = 0.3 | |
| 78 | + | cur.Windows[0].Dim = 0.3 | |
| 79 | 79 | d := newDamage(held(old), cur, image.Rect(0, 0, 1000, 500)) | |
| 80 | 80 | require.False(t, d.all()) | |
| 81 | 81 | assert.Equal(t, image.Rect(0, 0, 100, 100), d.area) | |
| @@ -17,9 +17,9 @@ import ( | |||||
| 17 | // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta | 17 | // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta | ||
| 18 | // lists the fields that force a full redraw. | 18 | // lists the fields that force a full redraw. | ||
| 19 | func TestSceneFieldCount(t *testing.T) { | 19 | func TestSceneFieldCount(t *testing.T) { | ||
| 20 | - | // 22 with Animating, which sceneDelta intentionally ignores: it only | 20 | + | // 23 with Animating, which sceneDelta intentionally ignores: it only |
| 21 | // schedules the next frame, it draws nothing. | 21 | // schedules the next frame, it draws nothing. | ||
| 22 | - | const fields = 22 | 22 | + | const fields = 23 |
| 23 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))), | 23 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))), | ||
| 24 | "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count") | 24 | "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count") | ||
| 25 | } | 25 | } | ||
| @@ -28,7 +28,7 @@ func TestSceneFieldCount(t *testing.T) { | |||||
| 28 | // sceneDelta compares windows by struct equality, so a new field is covered | 28 | // sceneDelta compares windows by struct equality, so a new field is covered | ||
| 29 | // as long as it only changes where or how that window draws (inside Rect). | 29 | // as long as it only changes where or how that window draws (inside Rect). | ||
| 30 | func TestSceneWindowFieldCount(t *testing.T) { | 30 | func TestSceneWindowFieldCount(t *testing.T) { | ||
| 31 | - | const fields = 15 | 31 | + | const fields = 14 |
| 32 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.SceneWindow{}))), | 32 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.SceneWindow{}))), | ||
| 33 | "SceneWindow fields changed: check sceneDelta (a field that draws outside Rect needs its own rule), then this count") | 33 | "SceneWindow fields changed: check sceneDelta (a field that draws outside Rect needs its own rule), then this count") | ||
| 34 | } | 34 | } | ||
| @@ -75,7 +75,7 @@ func TestDamageDeltaAtScale(t *testing.T) { | |||||
| 75 | func TestDamageDeltaWindowFieldChange(t *testing.T) { | 75 | func TestDamageDeltaWindowFieldChange(t *testing.T) { | ||
| 76 | old := deltaScene(1, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | 76 | old := deltaScene(1, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | ||
| 77 | cur := deltaScene(2, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | 77 | cur := deltaScene(2, win(1, 0, 0, 100, 100), win(2, 100, 0, 100, 100)) | ||
| 78 | - | cur.Windows[0].FocusEffect = 0.3 | 78 | + | cur.Windows[0].Dim = 0.3 |
| 79 | d := newDamage(held(old), cur, image.Rect(0, 0, 1000, 500)) | 79 | d := newDamage(held(old), cur, image.Rect(0, 0, 1000, 500)) | ||
| 80 | require.False(t, d.all()) | 80 | require.False(t, d.all()) | ||
| 81 | assert.Equal(t, image.Rect(0, 0, 100, 100), d.area) | 81 | assert.Equal(t, image.Rect(0, 0, 100, 100), d.area) | ||
M internal/adapters/vulkan/hdr_composition_test.go +0 -19
| @@ -259,25 +259,6 @@ func TestHDRLinearBlend(t *testing.T) { | |||
| 259 | 259 | } | |
| 260 | 260 | } | |
| 261 | 261 | ||
| 262 | - | // The focus pulse on an HDR output applies the SDR lift to sRGB content | |
| 263 | - | // before decoding: sRGB 64 at 0.1 shows as sRGB 83.1 would. A dark input | |
| 264 | - | // separates this from a lift in linear light or none at all. | |
| 265 | - | func TestHDRPulse(t *testing.T) { | |
| 266 | - | r := hdrTestRenderer(t) | |
| 267 | - | c := shmContent(t, 64, 16, 64*4, fill(64, 16, [4]byte{64, 64, 64, 255})) | |
| 268 | - | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, FocusEffect: 0.1}}} | |
| 269 | - | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil { | |
| 270 | - | t.Fatal(err) | |
| 271 | - | } | |
| 272 | - | got := hdrTargetAt(t, r, 5, 5) | |
| 273 | - | want := pqEncode(srgbToLinear((64+0.1*(255-64))/255) * 203) | |
| 274 | - | for i, v := range got { | |
| 275 | - | if math.Abs(v-want) > .006 { | |
| 276 | - | t.Errorf("channel %d: got %.4f want %.4f", i, v, want) | |
| 277 | - | } | |
| 278 | - | } | |
| 279 | - | } | |
| 280 | - | ||
| 281 | 262 | func TestHDRSolidColorsMatchPartialRedraw(t *testing.T) { | |
| 282 | 263 | r := hdrTestRenderer(t) | |
| 283 | 264 | const background = "#804020" | |
| @@ -259,25 +259,6 @@ func TestHDRLinearBlend(t *testing.T) { | |||||
| 259 | } | 259 | } | ||
| 260 | } | 260 | } | ||
| 261 | 261 | ||||
| 262 | - | // The focus pulse on an HDR output applies the SDR lift to sRGB content | |||
| 263 | - | // before decoding: sRGB 64 at 0.1 shows as sRGB 83.1 would. A dark input | |||
| 264 | - | // separates this from a lift in linear light or none at all. | |||
| 265 | - | func TestHDRPulse(t *testing.T) { | |||
| 266 | - | r := hdrTestRenderer(t) | |||
| 267 | - | c := shmContent(t, 64, 16, 64*4, fill(64, 16, [4]byte{64, 64, 64, 255})) | |||
| 268 | - | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}, FocusEffect: 0.1}}} | |||
| 269 | - | if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil { | |||
| 270 | - | t.Fatal(err) | |||
| 271 | - | } | |||
| 272 | - | got := hdrTargetAt(t, r, 5, 5) | |||
| 273 | - | want := pqEncode(srgbToLinear((64+0.1*(255-64))/255) * 203) | |||
| 274 | - | for i, v := range got { | |||
| 275 | - | if math.Abs(v-want) > .006 { | |||
| 276 | - | t.Errorf("channel %d: got %.4f want %.4f", i, v, want) | |||
| 277 | - | } | |||
| 278 | - | } | |||
| 279 | - | } | |||
| 280 | - | ||||
| 281 | func TestHDRSolidColorsMatchPartialRedraw(t *testing.T) { | 262 | func TestHDRSolidColorsMatchPartialRedraw(t *testing.T) { | ||
| 282 | r := hdrTestRenderer(t) | 263 | r := hdrTestRenderer(t) | ||
| 283 | const background = "#804020" | 264 | const background = "#804020" | ||
M internal/adapters/vulkan/renderer_test.go +0 -24
| @@ -111,30 +111,6 @@ func TestRendererFade(t *testing.T) { | |||
| 111 | 111 | } | |
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | - | // A pulsing window lifts toward white: black the most, white not at all. | |
| 115 | - | func TestRendererPulse(t *testing.T) { | |
| 116 | - | r, err := New(64, 48) | |
| 117 | - | if err != nil { | |
| 118 | - | t.Skipf("Vulkan unavailable: %v", err) | |
| 119 | - | } | |
| 120 | - | defer r.Close() | |
| 121 | - | for i, tc := range []struct { | |
| 122 | - | in, want uint8 | |
| 123 | - | }{{0, 26}, {64, 83}, {200, 206}, {255, 255}} { | |
| 124 | - | // One window per case: the shm copy is cached per window. | |
| 125 | - | id := ports.WindowID(i + 1) | |
| 126 | - | c := solidContent(t, 64, 48, color.RGBA{tc.in, tc.in, tc.in, 255}) | |
| 127 | - | s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: id, Rect: ports.Rect{W: 64, H: 48}, FocusEffect: 0.1}}} | |
| 128 | - | if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{id: c}); err != nil { | |
| 129 | - | t.Fatal(err) | |
| 130 | - | } | |
| 131 | - | got := readPixels(t, r).At(10, 10).(color.RGBA) | |
| 132 | - | if d := int(got.R) - int(tc.want); d < -1 || d > 1 || got.R != got.G || got.G != got.B { | |
| 133 | - | t.Errorf("pulse of %d = %v, want %d", tc.in, got, tc.want) | |
| 134 | - | } | |
| 135 | - | } | |
| 136 | - | } | |
| 137 | - | ||
| 138 | 114 | func TestRendererContents(t *testing.T) { | |
| 139 | 115 | r, err := New(64, 48) | |
| 140 | 116 | if err != nil { | |
| @@ -111,30 +111,6 @@ func TestRendererFade(t *testing.T) { | |||||
| 111 | } | 111 | } | ||
| 112 | } | 112 | } | ||
| 113 | 113 | ||||
| 114 | - | // A pulsing window lifts toward white: black the most, white not at all. | |||
| 115 | - | func TestRendererPulse(t *testing.T) { | |||
| 116 | - | r, err := New(64, 48) | |||
| 117 | - | if err != nil { | |||
| 118 | - | t.Skipf("Vulkan unavailable: %v", err) | |||
| 119 | - | } | |||
| 120 | - | defer r.Close() | |||
| 121 | - | for i, tc := range []struct { | |||
| 122 | - | in, want uint8 | |||
| 123 | - | }{{0, 26}, {64, 83}, {200, 206}, {255, 255}} { | |||
| 124 | - | // One window per case: the shm copy is cached per window. | |||
| 125 | - | id := ports.WindowID(i + 1) | |||
| 126 | - | c := solidContent(t, 64, 48, color.RGBA{tc.in, tc.in, tc.in, 255}) | |||
| 127 | - | s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: id, Rect: ports.Rect{W: 64, H: 48}, FocusEffect: 0.1}}} | |||
| 128 | - | if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{id: c}); err != nil { | |||
| 129 | - | t.Fatal(err) | |||
| 130 | - | } | |||
| 131 | - | got := readPixels(t, r).At(10, 10).(color.RGBA) | |||
| 132 | - | if d := int(got.R) - int(tc.want); d < -1 || d > 1 || got.R != got.G || got.G != got.B { | |||
| 133 | - | t.Errorf("pulse of %d = %v, want %d", tc.in, got, tc.want) | |||
| 134 | - | } | |||
| 135 | - | } | |||
| 136 | - | } | |||
| 137 | - | ||||
| 138 | func TestRendererContents(t *testing.T) { | 114 | func TestRendererContents(t *testing.T) { | ||
| 139 | r, err := New(64, 48) | 115 | r, err := New(64, 48) | ||
| 140 | if err != nil { | 116 | if err != nil { | ||
M internal/adapters/vulkan/scene.go +1 -7
| @@ -20,9 +20,6 @@ type sceneWalk struct { | |||
| 20 | 20 | // zoom shrinks the surfaces of the window being placed: an overview | |
| 21 | 21 | // preview (ports.SceneWindow.Preview); 1 otherwise. | |
| 22 | 22 | zoom float64 | |
| 23 | - | // pulse is the focus effect of the window being placed | |
| 24 | - | // (ports.SceneWindow.FocusEffect); 0 otherwise. | |
| 25 | - | pulse float32 | |
| 26 | 23 | // alpha is the opacity of the window being drawn, 1 - its Fade | |
| 27 | 24 | // (ports.SceneWindow.Fade): its fills, lines and surfaces are scaled by | |
| 28 | 25 | // it (premultiplied); 1 otherwise. | |
| @@ -251,9 +248,8 @@ func (w *sceneWalk) windows() { | |||
| 251 | 248 | } else if win.Preview > 0 { | |
| 252 | 249 | w.zoom = win.Preview | |
| 253 | 250 | } | |
| 254 | - | w.pulse = float32(max(0, min(win.FocusEffect, 1))) | |
| 255 | 251 | w.place(win.ID, &content, c.X, c.Y, c.W, c.H) | |
| 256 | - | w.zoom, w.pulse = 1, 0 | |
| 252 | + | w.zoom = 1 | |
| 257 | 253 | } | |
| 258 | 254 | if win.Floating { | |
| 259 | 255 | w.separators(win.ID) | |
| @@ -543,7 +539,6 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 543 | 539 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque) | |
| 544 | 540 | r.setContentColor(&dr, content) | |
| 545 | 541 | dr.pc.color[3] *= w.alpha | |
| 546 | - | dr.pc.mapy[2] = w.pulse | |
| 547 | 542 | if im.yuv { | |
| 548 | 543 | dr.pc.misc[1] |= flagYUV | |
| 549 | 544 | if content.DMABuf.Format == fourcc('P', '0', '1', '0') { | |
| @@ -590,7 +585,6 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||
| 590 | 585 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | |
| 591 | 586 | r.setContentColor(&dr, content) | |
| 592 | 587 | dr.pc.color[3] *= w.alpha | |
| 593 | - | dr.pc.mapy[2] = w.pulse | |
| 594 | 588 | dr.shm = c | |
| 595 | 589 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | |
| 596 | 590 | w.draws = append(w.draws, dr) | |
| @@ -20,9 +20,6 @@ type sceneWalk struct { | |||||
| 20 | // zoom shrinks the surfaces of the window being placed: an overview | 20 | // zoom shrinks the surfaces of the window being placed: an overview | ||
| 21 | // preview (ports.SceneWindow.Preview); 1 otherwise. | 21 | // preview (ports.SceneWindow.Preview); 1 otherwise. | ||
| 22 | zoom float64 | 22 | zoom float64 | ||
| 23 | - | // pulse is the focus effect of the window being placed | |||
| 24 | - | // (ports.SceneWindow.FocusEffect); 0 otherwise. | |||
| 25 | - | pulse float32 | |||
| 26 | // alpha is the opacity of the window being drawn, 1 - its Fade | 23 | // alpha is the opacity of the window being drawn, 1 - its Fade | ||
| 27 | // (ports.SceneWindow.Fade): its fills, lines and surfaces are scaled by | 24 | // (ports.SceneWindow.Fade): its fills, lines and surfaces are scaled by | ||
| 28 | // it (premultiplied); 1 otherwise. | 25 | // it (premultiplied); 1 otherwise. | ||
| @@ -251,9 +248,8 @@ func (w *sceneWalk) windows() { | |||||
| 251 | } else if win.Preview > 0 { | 248 | } else if win.Preview > 0 { | ||
| 252 | w.zoom = win.Preview | 249 | w.zoom = win.Preview | ||
| 253 | } | 250 | } | ||
| 254 | - | w.pulse = float32(max(0, min(win.FocusEffect, 1))) | |||
| 255 | w.place(win.ID, &content, c.X, c.Y, c.W, c.H) | 251 | w.place(win.ID, &content, c.X, c.Y, c.W, c.H) | ||
| 256 | - | w.zoom, w.pulse = 1, 0 | 252 | + | w.zoom = 1 |
| 257 | } | 253 | } | ||
| 258 | if win.Floating { | 254 | if win.Floating { | ||
| 259 | w.separators(win.ID) | 255 | w.separators(win.ID) | ||
| @@ -543,7 +539,6 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 543 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque) | 539 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeImage, content.Opaque) | ||
| 544 | r.setContentColor(&dr, content) | 540 | r.setContentColor(&dr, content) | ||
| 545 | dr.pc.color[3] *= w.alpha | 541 | dr.pc.color[3] *= w.alpha | ||
| 546 | - | dr.pc.mapy[2] = w.pulse | |||
| 547 | if im.yuv { | 542 | if im.yuv { | ||
| 548 | dr.pc.misc[1] |= flagYUV | 543 | dr.pc.misc[1] |= flagYUV | ||
| 549 | if content.DMABuf.Format == fourcc('P', '0', '1', '0') { | 544 | if content.DMABuf.Format == fourcc('P', '0', '1', '0') { | ||
| @@ -590,7 +585,6 @@ func (w *sceneWalk) content(dst, full image.Rectangle, content *ports.SurfaceCon | |||||
| 590 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | 585 | dr := r.contentDraw(rect, full, content.Width, content.Height, content.Source, content.Transform, modeBuffer, content.Opaque) | ||
| 591 | r.setContentColor(&dr, content) | 586 | r.setContentColor(&dr, content) | ||
| 592 | dr.pc.color[3] *= w.alpha | 587 | dr.pc.color[3] *= w.alpha | ||
| 593 | - | dr.pc.mapy[2] = w.pulse | |||
| 594 | dr.shm = c | 588 | dr.shm = c | ||
| 595 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | 589 | dr.pc.buf = [4]uint32{0, uint32(content.Width), uint32(content.Height), 0} | ||
| 596 | w.draws = append(w.draws, dr) | 590 | w.draws = append(w.draws, dr) | ||
M internal/adapters/vulkan/shaders/compose.frag +0 -6
| @@ -146,12 +146,6 @@ void main() { | |||
| 146 | 146 | c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a; | |
| 147 | 147 | } | |
| 148 | 148 | } | |
| 149 | - | if (d.mapy.z > 0.0) { | |
| 150 | - | // Focus pulse: a screen-like lift toward white, on premultiplied | |
| 151 | - | // values (rgb/a + k * (1 - rgb/a)) * a. Black shows it most; white | |
| 152 | - | // never saturates. | |
| 153 | - | c.rgb += d.mapy.z * (c.a - c.rgb); | |
| 154 | - | } | |
| 155 | 149 | if (d.misc.x != modeSolid) { | |
| 156 | 150 | // The surface's alpha modifier; premultiplied, it scales all four. | |
| 157 | 151 | c *= d.color.a; | |
| @@ -146,12 +146,6 @@ void main() { | |||||
| 146 | c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a; | 146 | c.rgb = mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308))) * c.a; | ||
| 147 | } | 147 | } | ||
| 148 | } | 148 | } | ||
| 149 | - | if (d.mapy.z > 0.0) { | |||
| 150 | - | // Focus pulse: a screen-like lift toward white, on premultiplied | |||
| 151 | - | // values (rgb/a + k * (1 - rgb/a)) * a. Black shows it most; white | |||
| 152 | - | // never saturates. | |||
| 153 | - | c.rgb += d.mapy.z * (c.a - c.rgb); | |||
| 154 | - | } | |||
| 155 | if (d.misc.x != modeSolid) { | 149 | if (d.misc.x != modeSolid) { | ||
| 156 | // The surface's alpha modifier; premultiplied, it scales all four. | 150 | // The surface's alpha modifier; premultiplied, it scales all four. | ||
| 157 | c *= d.color.a; | 151 | c *= d.color.a; | ||
M internal/adapters/vulkan/shaders/compose.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shaders/compose.vert +1 -1
| @@ -5,7 +5,7 @@ | |||
| 5 | 5 | struct Draw { | |
| 6 | 6 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | |
| 7 | 7 | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | |
| 8 | - | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z is the focus pulse | |
| 8 | + | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z and w are unused | |
| 9 | 9 | vec4 color; // solid premultiplied RGBA; content opacity in alpha | |
| 10 | 10 | vec4 crop; // source crop bounds in buffer pixels | |
| 11 | 11 | uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w | |
| @@ -5,7 +5,7 @@ | |||||
| 5 | struct Draw { | 5 | struct Draw { | ||
| 6 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | 6 | ivec4 rect; // visible x0, y0, x1, y1 in target pixels | ||
| 7 | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | 7 | vec4 map; // source texel = map.xy + fragment centre.x * map.zw | ||
| 8 | - | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z is the focus pulse | 8 | + | vec4 mapy; // + fragment centre.y * mapy.xy; mapy.z and w are unused |
| 9 | vec4 color; // solid premultiplied RGBA; content opacity in alpha | 9 | vec4 color; // solid premultiplied RGBA; content opacity in alpha | ||
| 10 | vec4 crop; // source crop bounds in buffer pixels | 10 | vec4 crop; // source crop bounds in buffer pixels | ||
| 11 | uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w | 11 | uvec4 buf; // pixel-buffer first word, width, height; YUV metadata in w | ||
M internal/adapters/vulkan/shaders/compose_hdr.frag +0 -6
| @@ -141,12 +141,6 @@ void main() { | |||
| 141 | 141 | // ext_linear has the protocol's default 80 cd/m² reference white. | |
| 142 | 142 | // Keep negative and above-white values in fp16 until the HDR pass. | |
| 143 | 143 | vec3 v = c.rgb / c.a; | |
| 144 | - | bool srgb = (d.misc.y & (flagPQ | flagExtendedLinear)) == 0u; | |
| 145 | - | if (srgb && d.mapy.z > 0.0) { | |
| 146 | - | // Focus pulse: the SDR lift, on sRGB-encoded values before | |
| 147 | - | // decoding, so it looks the same as on an SDR output. | |
| 148 | - | v += d.mapy.z * (1.0 - v); | |
| 149 | - | } | |
| 150 | 144 | vec3 linearRGB = (d.misc.y & flagPQ) != 0u ? pqToLinear709(v, d.color.x) : | |
| 151 | 145 | (d.misc.y & flagExtendedLinear) != 0u ? v * (80.0 / d.color.x) : decodeSRGB(v); | |
| 152 | 146 | c.rgb = linearRGB * c.a; | |
| @@ -141,12 +141,6 @@ void main() { | |||||
| 141 | // ext_linear has the protocol's default 80 cd/m² reference white. | 141 | // ext_linear has the protocol's default 80 cd/m² reference white. | ||
| 142 | // Keep negative and above-white values in fp16 until the HDR pass. | 142 | // Keep negative and above-white values in fp16 until the HDR pass. | ||
| 143 | vec3 v = c.rgb / c.a; | 143 | vec3 v = c.rgb / c.a; | ||
| 144 | - | bool srgb = (d.misc.y & (flagPQ | flagExtendedLinear)) == 0u; | |||
| 145 | - | if (srgb && d.mapy.z > 0.0) { | |||
| 146 | - | // Focus pulse: the SDR lift, on sRGB-encoded values before | |||
| 147 | - | // decoding, so it looks the same as on an SDR output. | |||
| 148 | - | v += d.mapy.z * (1.0 - v); | |||
| 149 | - | } | |||
| 150 | vec3 linearRGB = (d.misc.y & flagPQ) != 0u ? pqToLinear709(v, d.color.x) : | 144 | vec3 linearRGB = (d.misc.y & flagPQ) != 0u ? pqToLinear709(v, d.color.x) : | ||
| 151 | (d.misc.y & flagExtendedLinear) != 0u ? v * (80.0 / d.color.x) : decodeSRGB(v); | 145 | (d.misc.y & flagExtendedLinear) != 0u ? v * (80.0 / d.color.x) : decodeSRGB(v); | ||
| 152 | c.rgb = linearRGB * c.a; | 146 | c.rgb = linearRGB * c.a; | ||
M internal/adapters/vulkan/shaders/compose_hdr.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shaders/compose_hdr_single.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/adapters/vulkan/shaders/compose_single.frag.spv +0 -0 binary file: no text diff
binary file: no text diff
M internal/app/app.go +0 -4
| @@ -176,10 +176,6 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||
| 176 | 176 | rects := make([]string, 0, len(s.Windows)) | |
| 177 | 177 | for _, w := range s.Windows { | |
| 178 | 178 | rects = append(rects, fmt.Sprintf("%d:%d,%d %dx%d", w.ID, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) | |
| 179 | - | if w.FocusEffect > 0 { | |
| 180 | - | // The focus indicator frame by frame: window and effect. | |
| 181 | - | ev = ev.Uint64("focus_effect_window", uint64(w.ID)).Float64("focus_effect", w.FocusEffect) | |
| 182 | - | } | |
| 183 | 179 | } | |
| 184 | 180 | ev.Strs("windows", rects).Msg("scene") | |
| 185 | 181 | } | |
| @@ -176,10 +176,6 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||||
| 176 | rects := make([]string, 0, len(s.Windows)) | 176 | rects := make([]string, 0, len(s.Windows)) | ||
| 177 | for _, w := range s.Windows { | 177 | for _, w := range s.Windows { | ||
| 178 | rects = append(rects, fmt.Sprintf("%d:%d,%d %dx%d", w.ID, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) | 178 | rects = append(rects, fmt.Sprintf("%d:%d,%d %dx%d", w.ID, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) | ||
| 179 | - | if w.FocusEffect > 0 { | |||
| 180 | - | // The focus indicator frame by frame: window and effect. | |||
| 181 | - | ev = ev.Uint64("focus_effect_window", uint64(w.ID)).Float64("focus_effect", w.FocusEffect) | |||
| 182 | - | } | |||
| 183 | } | 179 | } | ||
| 184 | ev.Strs("windows", rects).Msg("scene") | 180 | ev.Strs("windows", rects).Msg("scene") | ||
| 185 | } | 181 | } | ||
M internal/app/app_test.go +0 -2
| @@ -243,8 +243,6 @@ func headlessPointerClickFocus(t *testing.T, animated bool) { | |||
| 243 | 243 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 244 | 244 | cfg := config.Defaults() | |
| 245 | 245 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | |
| 246 | - | // Pulse frames would fill the scene tap before the click. | |
| 247 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |
| 248 | 246 | cfg.Animations.On = animated | |
| 249 | 247 | // The tap drops a scene it has no room for: a collector keeps up with | |
| 250 | 248 | // the animation frames. | |
| @@ -243,8 +243,6 @@ func headlessPointerClickFocus(t *testing.T, animated bool) { | |||||
| 243 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | 243 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | ||
| 244 | cfg := config.Defaults() | 244 | cfg := config.Defaults() | ||
| 245 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | 245 | cfg.Terminal.Command = []string{"foot", "-c", "/dev/null", "sh"} | ||
| 246 | - | // Pulse frames would fill the scene tap before the click. | |||
| 247 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |||
| 248 | cfg.Animations.On = animated | 246 | cfg.Animations.On = animated | ||
| 249 | // The tap drops a scene it has no room for: a collector keeps up with | 247 | // The tap drops a scene it has no room for: a collector keeps up with | ||
| 250 | // the animation frames. | 248 | // the animation frames. | ||
M internal/app/scenelog_test.go +2 -2
| @@ -13,7 +13,7 @@ func logTestScenes() []ports.Scene { | |||
| 13 | 13 | Output: "A", Seq: 7, OutputWidth: 1920, OutputHeight: 1080, Scale: 1, | |
| 14 | 14 | Windows: []ports.SceneWindow{ | |
| 15 | 15 | {ID: 1, Rect: ports.Rect{X: 10, Y: 20, W: 300, H: 200}}, | |
| 16 | - | {ID: 2, Rect: ports.Rect{X: 5, Y: 6, W: 7, H: 8}, FocusEffect: 0.5}, | |
| 16 | + | {ID: 2, Rect: ports.Rect{X: 5, Y: 6, W: 7, H: 8}}, | |
| 17 | 17 | }, | |
| 18 | 18 | }, {Output: "B"}} | |
| 19 | 19 | } | |
| @@ -40,7 +40,7 @@ func TestLogScenesDebug(t *testing.T) { | |||
| 40 | 40 | for _, want := range []string{ | |
| 41 | 41 | `"output":"A"`, `"seq":7`, `"out_w":1920`, `"out_h":1080`, `"scale":1`, | |
| 42 | 42 | `"windows":["1:10,20 300x200","2:5,6 7x8"]`, | |
| 43 | - | `"focus_effect_window":2`, `"focus_effect":0.5`, `"message":"scene"`, | |
| 43 | + | `"message":"scene"`, | |
| 44 | 44 | } { | |
| 45 | 45 | if !strings.Contains(got, want) { | |
| 46 | 46 | t.Fatalf("debug log lacks %s: %s", want, got) | |
| @@ -13,7 +13,7 @@ func logTestScenes() []ports.Scene { | |||||
| 13 | Output: "A", Seq: 7, OutputWidth: 1920, OutputHeight: 1080, Scale: 1, | 13 | Output: "A", Seq: 7, OutputWidth: 1920, OutputHeight: 1080, Scale: 1, | ||
| 14 | Windows: []ports.SceneWindow{ | 14 | Windows: []ports.SceneWindow{ | ||
| 15 | {ID: 1, Rect: ports.Rect{X: 10, Y: 20, W: 300, H: 200}}, | 15 | {ID: 1, Rect: ports.Rect{X: 10, Y: 20, W: 300, H: 200}}, | ||
| 16 | - | {ID: 2, Rect: ports.Rect{X: 5, Y: 6, W: 7, H: 8}, FocusEffect: 0.5}, | 16 | + | {ID: 2, Rect: ports.Rect{X: 5, Y: 6, W: 7, H: 8}}, |
| 17 | }, | 17 | }, | ||
| 18 | }, {Output: "B"}} | 18 | }, {Output: "B"}} | ||
| 19 | } | 19 | } | ||
| @@ -40,7 +40,7 @@ func TestLogScenesDebug(t *testing.T) { | |||||
| 40 | for _, want := range []string{ | 40 | for _, want := range []string{ | ||
| 41 | `"output":"A"`, `"seq":7`, `"out_w":1920`, `"out_h":1080`, `"scale":1`, | 41 | `"output":"A"`, `"seq":7`, `"out_w":1920`, `"out_h":1080`, `"scale":1`, | ||
| 42 | `"windows":["1:10,20 300x200","2:5,6 7x8"]`, | 42 | `"windows":["1:10,20 300x200","2:5,6 7x8"]`, | ||
| 43 | - | `"focus_effect_window":2`, `"focus_effect":0.5`, `"message":"scene"`, | 43 | + | `"message":"scene"`, |
| 44 | } { | 44 | } { | ||
| 45 | if !strings.Contains(got, want) { | 45 | if !strings.Contains(got, want) { | ||
| 46 | t.Fatalf("debug log lacks %s: %s", want, got) | 46 | t.Fatalf("debug log lacks %s: %s", want, got) | ||
M internal/core/core.go +5 -52
| @@ -156,8 +156,6 @@ type Core struct { | |||
| 156 | 156 | pointerAt [2]float64 | |
| 157 | 157 | // capt is the capture state (capture.go, capindicator.go). | |
| 158 | 158 | capt captureState | |
| 159 | - | // pulse marks a window that just got the focus (pulse.go). | |
| 160 | - | pulse focusPulse | |
| 161 | 159 | // switcher is the column switcher in progress: the screen it is on and | |
| 162 | 160 | // the timer that shows its cards (switcher.go). | |
| 163 | 161 | switcher struct { | |
| @@ -532,14 +530,6 @@ func (v viewport) area(p Placement) Rect { | |||
| 532 | 530 | return v.usable | |
| 533 | 531 | } | |
| 534 | 532 | ||
| 535 | - | // hasRoom reports whether p's area can show anything: panels can leave | |
| 536 | - | // tiles no room at all. Unlike shows, it ignores where p is, so a window | |
| 537 | - | // still sliding in counts. | |
| 538 | - | func hasRoom(v viewport, p Placement) bool { | |
| 539 | - | a := v.area(p) | |
| 540 | - | return a.W > 0 && a.H > 0 | |
| 541 | - | } | |
| 542 | - | ||
| 543 | 533 | // shows reports whether the user sees p: not hidden, and overlapping its | |
| 544 | 534 | // area. | |
| 545 | 535 | func (v viewport) shows(p Placement) bool { | |
| @@ -636,9 +626,8 @@ func (c *Core) setLayers(all []ports.LayerSurface) { | |||
| 636 | 626 | func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) } | |
| 637 | 627 | ||
| 638 | 628 | // publishFrame publishes the scenes. only is the screen whose page flip | |
| 639 | - | // moved the springs (nil: any, or none moved): the pulse advances with the | |
| 640 | - | // focused screen's flip, and only a flip's own screen gets a fresh Seq for | |
| 641 | - | // its animation, so another output keeps its Seq while it draws the same. | |
| 629 | + | // moved the springs (nil: any, or none moved): only a flip's own screen gets | |
| 630 | + | // a fresh Seq for its animation, so another output keeps its Seq while it draws the same. | |
| 642 | 631 | func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |
| 643 | 632 | if c.inputEpochChanged() { | |
| 644 | 633 | return errSecurityChanged | |
| @@ -685,44 +674,17 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 685 | 674 | return err | |
| 686 | 675 | } | |
| 687 | 676 | focus := c.keyboardFocus() | |
| 688 | - | window, _ := c.cur().mon.Focused() | |
| 689 | - | c.pulseFocus(window, focus == window) | |
| 690 | - | // The pulse runs on the focused output: a flip of another output reuses | |
| 691 | - | // its value so the focused scene keeps its Seq. | |
| 692 | - | if c.pulse.id == 0 { | |
| 693 | - | c.pulse.value = 0 | |
| 694 | - | } else if only == nil || only == c.cur() { | |
| 695 | - | c.pulse.value = c.advancePulse(c.now()) | |
| 696 | - | } | |
| 697 | - | pulse := c.pulse.value | |
| 698 | - | // drawable: the scene can show a pulse on the focused window. | |
| 699 | - | drawable := false | |
| 700 | 677 | scenes := make([]ports.Scene, 0, len(c.screens)) | |
| 701 | 678 | // Before the first output (and after the last is unplugged) the | |
| 702 | 679 | // placeholder's scene has no output name: no renderer draws it. | |
| 703 | 680 | for i, sc := range c.screens { | |
| 704 | - | scene, shown, err := c.sceneFor(ctx, i, sc, only, capture, pulse) | |
| 681 | + | scene, err := c.sceneFor(ctx, i, sc, only, capture) | |
| 705 | 682 | if err != nil { | |
| 706 | 683 | return err | |
| 707 | 684 | } | |
| 708 | - | drawable = drawable || shown | |
| 709 | 685 | scenes = append(scenes, scene) | |
| 710 | 686 | } | |
| 711 | 687 | c.configures.prune() | |
| 712 | - | // A pulse nothing shows (fullscreen, overview) asks for no frames. | |
| 713 | - | if c.pulse.drawable = drawable; !drawable && c.pulse.id != 0 { | |
| 714 | - | c.pulse.id = 0 | |
| 715 | - | // The focused scene was built as animating: publish it settled, | |
| 716 | - | // or its flips would keep waking core. A scene that kept its Seq | |
| 717 | - | // needs a fresh one, or outputs keep the animating one they show. | |
| 718 | - | if i := slices.Index(c.screens, c.cur()); i >= 0 && scenes[i].Animating && !c.cur().springing() { | |
| 719 | - | scenes[i].Animating = false | |
| 720 | - | if scenes[i].Seq == c.cur().last.Seq { | |
| 721 | - | c.seq++ | |
| 722 | - | scenes[i].Seq = c.seq | |
| 723 | - | } | |
| 724 | - | } | |
| 725 | - | } | |
| 726 | 688 | for i, sc := range c.screens { | |
| 727 | 689 | sc.last = scenes[i] | |
| 728 | 690 | } | |
| @@ -776,11 +738,11 @@ func (c *Core) step(ctx context.Context, only *screen) error { | |||
| 776 | 738 | return c.slidFrame(ctx, false, only) | |
| 777 | 739 | } | |
| 778 | 740 | ||
| 779 | - | // animatingOther reports whether a spring or the pulse runs on a screen | |
| 741 | + | // animatingOther reports whether a spring runs on a screen | |
| 780 | 742 | // other than only. | |
| 781 | 743 | func (c *Core) animatingOther(only *screen) bool { | |
| 782 | 744 | for _, sc := range c.screens { | |
| 783 | - | if sc != only && (sc.springing() || c.pulsing(sc)) { | |
| 745 | + | if sc != only && sc.springing() { | |
| 784 | 746 | return true | |
| 785 | 747 | } | |
| 786 | 748 | } | |
| @@ -1128,7 +1090,6 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1128 | 1090 | c.syncSecurity() | |
| 1129 | 1091 | defer c.stopFrame() | |
| 1130 | 1092 | defer c.stopCaptureTimer() | |
| 1131 | - | defer c.stopPulseTimer() | |
| 1132 | 1093 | defer c.cancelSwitcher() | |
| 1133 | 1094 | defer c.stopRepeat() | |
| 1134 | 1095 | // Startup commands run once per session. Full launcher queues retain a | |
| @@ -1198,14 +1159,6 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 1198 | 1159 | return nil | |
| 1199 | 1160 | } | |
| 1200 | 1161 | continue | |
| 1201 | - | case <-c.pulse.timerC: | |
| 1202 | - | if c.securityCheckpoint(ctx) != nil { | |
| 1203 | - | return nil | |
| 1204 | - | } | |
| 1205 | - | if c.pulseTick() && c.publish(ctx) != nil { | |
| 1206 | - | return nil | |
| 1207 | - | } | |
| 1208 | - | continue | |
| 1209 | 1162 | case <-c.switcher.timerC: | |
| 1210 | 1163 | if c.securityCheckpoint(ctx) != nil { | |
| 1211 | 1164 | return nil | |
| @@ -156,8 +156,6 @@ type Core struct { | |||||
| 156 | pointerAt [2]float64 | 156 | pointerAt [2]float64 | ||
| 157 | // capt is the capture state (capture.go, capindicator.go). | 157 | // capt is the capture state (capture.go, capindicator.go). | ||
| 158 | capt captureState | 158 | capt captureState | ||
| 159 | - | // pulse marks a window that just got the focus (pulse.go). | |||
| 160 | - | pulse focusPulse | |||
| 161 | // switcher is the column switcher in progress: the screen it is on and | 159 | // switcher is the column switcher in progress: the screen it is on and | ||
| 162 | // the timer that shows its cards (switcher.go). | 160 | // the timer that shows its cards (switcher.go). | ||
| 163 | switcher struct { | 161 | switcher struct { | ||
| @@ -532,14 +530,6 @@ func (v viewport) area(p Placement) Rect { | |||||
| 532 | return v.usable | 530 | return v.usable | ||
| 533 | } | 531 | } | ||
| 534 | 532 | ||||
| 535 | - | // hasRoom reports whether p's area can show anything: panels can leave | |||
| 536 | - | // tiles no room at all. Unlike shows, it ignores where p is, so a window | |||
| 537 | - | // still sliding in counts. | |||
| 538 | - | func hasRoom(v viewport, p Placement) bool { | |||
| 539 | - | a := v.area(p) | |||
| 540 | - | return a.W > 0 && a.H > 0 | |||
| 541 | - | } | |||
| 542 | - | ||||
| 543 | // shows reports whether the user sees p: not hidden, and overlapping its | 533 | // shows reports whether the user sees p: not hidden, and overlapping its | ||
| 544 | // area. | 534 | // area. | ||
| 545 | func (v viewport) shows(p Placement) bool { | 535 | func (v viewport) shows(p Placement) bool { | ||
| @@ -636,9 +626,8 @@ func (c *Core) setLayers(all []ports.LayerSurface) { | |||||
| 636 | func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) } | 626 | func (c *Core) publish(ctx context.Context) error { return c.publishFrame(ctx, nil) } | ||
| 637 | 627 | ||||
| 638 | // publishFrame publishes the scenes. only is the screen whose page flip | 628 | // publishFrame publishes the scenes. only is the screen whose page flip | ||
| 639 | - | // moved the springs (nil: any, or none moved): the pulse advances with the | 629 | + | // moved the springs (nil: any, or none moved): only a flip's own screen gets |
| 640 | - | // focused screen's flip, and only a flip's own screen gets a fresh Seq for | 630 | + | // a fresh Seq for its animation, so another output keeps its Seq while it draws the same. |
| 641 | - | // its animation, so another output keeps its Seq while it draws the same. | |||
| 642 | func (c *Core) publishFrame(ctx context.Context, only *screen) error { | 631 | func (c *Core) publishFrame(ctx context.Context, only *screen) error { | ||
| 643 | if c.inputEpochChanged() { | 632 | if c.inputEpochChanged() { | ||
| 644 | return errSecurityChanged | 633 | return errSecurityChanged | ||
| @@ -685,44 +674,17 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 685 | return err | 674 | return err | ||
| 686 | } | 675 | } | ||
| 687 | focus := c.keyboardFocus() | 676 | focus := c.keyboardFocus() | ||
| 688 | - | window, _ := c.cur().mon.Focused() | |||
| 689 | - | c.pulseFocus(window, focus == window) | |||
| 690 | - | // The pulse runs on the focused output: a flip of another output reuses | |||
| 691 | - | // its value so the focused scene keeps its Seq. | |||
| 692 | - | if c.pulse.id == 0 { | |||
| 693 | - | c.pulse.value = 0 | |||
| 694 | - | } else if only == nil || only == c.cur() { | |||
| 695 | - | c.pulse.value = c.advancePulse(c.now()) | |||
| 696 | - | } | |||
| 697 | - | pulse := c.pulse.value | |||
| 698 | - | // drawable: the scene can show a pulse on the focused window. | |||
| 699 | - | drawable := false | |||
| 700 | scenes := make([]ports.Scene, 0, len(c.screens)) | 677 | scenes := make([]ports.Scene, 0, len(c.screens)) | ||
| 701 | // Before the first output (and after the last is unplugged) the | 678 | // Before the first output (and after the last is unplugged) the | ||
| 702 | // placeholder's scene has no output name: no renderer draws it. | 679 | // placeholder's scene has no output name: no renderer draws it. | ||
| 703 | for i, sc := range c.screens { | 680 | for i, sc := range c.screens { | ||
| 704 | - | scene, shown, err := c.sceneFor(ctx, i, sc, only, capture, pulse) | 681 | + | scene, err := c.sceneFor(ctx, i, sc, only, capture) |
| 705 | if err != nil { | 682 | if err != nil { | ||
| 706 | return err | 683 | return err | ||
| 707 | } | 684 | } | ||
| 708 | - | drawable = drawable || shown | |||
| 709 | scenes = append(scenes, scene) | 685 | scenes = append(scenes, scene) | ||
| 710 | } | 686 | } | ||
| 711 | c.configures.prune() | 687 | c.configures.prune() | ||
| 712 | - | // A pulse nothing shows (fullscreen, overview) asks for no frames. | |||
| 713 | - | if c.pulse.drawable = drawable; !drawable && c.pulse.id != 0 { | |||
| 714 | - | c.pulse.id = 0 | |||
| 715 | - | // The focused scene was built as animating: publish it settled, | |||
| 716 | - | // or its flips would keep waking core. A scene that kept its Seq | |||
| 717 | - | // needs a fresh one, or outputs keep the animating one they show. | |||
| 718 | - | if i := slices.Index(c.screens, c.cur()); i >= 0 && scenes[i].Animating && !c.cur().springing() { | |||
| 719 | - | scenes[i].Animating = false | |||
| 720 | - | if scenes[i].Seq == c.cur().last.Seq { | |||
| 721 | - | c.seq++ | |||
| 722 | - | scenes[i].Seq = c.seq | |||
| 723 | - | } | |||
| 724 | - | } | |||
| 725 | - | } | |||
| 726 | for i, sc := range c.screens { | 688 | for i, sc := range c.screens { | ||
| 727 | sc.last = scenes[i] | 689 | sc.last = scenes[i] | ||
| 728 | } | 690 | } | ||
| @@ -776,11 +738,11 @@ func (c *Core) step(ctx context.Context, only *screen) error { | |||||
| 776 | return c.slidFrame(ctx, false, only) | 738 | return c.slidFrame(ctx, false, only) | ||
| 777 | } | 739 | } | ||
| 778 | 740 | ||||
| 779 | - | // animatingOther reports whether a spring or the pulse runs on a screen | 741 | + | // animatingOther reports whether a spring runs on a screen |
| 780 | // other than only. | 742 | // other than only. | ||
| 781 | func (c *Core) animatingOther(only *screen) bool { | 743 | func (c *Core) animatingOther(only *screen) bool { | ||
| 782 | for _, sc := range c.screens { | 744 | for _, sc := range c.screens { | ||
| 783 | - | if sc != only && (sc.springing() || c.pulsing(sc)) { | 745 | + | if sc != only && sc.springing() { |
| 784 | return true | 746 | return true | ||
| 785 | } | 747 | } | ||
| 786 | } | 748 | } | ||
| @@ -1128,7 +1090,6 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1128 | c.syncSecurity() | 1090 | c.syncSecurity() | ||
| 1129 | defer c.stopFrame() | 1091 | defer c.stopFrame() | ||
| 1130 | defer c.stopCaptureTimer() | 1092 | defer c.stopCaptureTimer() | ||
| 1131 | - | defer c.stopPulseTimer() | |||
| 1132 | defer c.cancelSwitcher() | 1093 | defer c.cancelSwitcher() | ||
| 1133 | defer c.stopRepeat() | 1094 | defer c.stopRepeat() | ||
| 1134 | // Startup commands run once per session. Full launcher queues retain a | 1095 | // Startup commands run once per session. Full launcher queues retain a | ||
| @@ -1198,14 +1159,6 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 1198 | return nil | 1159 | return nil | ||
| 1199 | } | 1160 | } | ||
| 1200 | continue | 1161 | continue | ||
| 1201 | - | case <-c.pulse.timerC: | |||
| 1202 | - | if c.securityCheckpoint(ctx) != nil { | |||
| 1203 | - | return nil | |||
| 1204 | - | } | |||
| 1205 | - | if c.pulseTick() && c.publish(ctx) != nil { | |||
| 1206 | - | return nil | |||
| 1207 | - | } | |||
| 1208 | - | continue | |||
| 1209 | case <-c.switcher.timerC: | 1162 | case <-c.switcher.timerC: | ||
| 1210 | if c.securityCheckpoint(ctx) != nil { | 1163 | if c.securityCheckpoint(ctx) != nil { | ||
| 1211 | return nil | 1164 | return nil | ||
M internal/core/core_test.go +0 -2
| @@ -973,7 +973,6 @@ func TestFullscreenAtMapIgnored(t *testing.T) { both(t, fullscreenAtMapIgnored) | |||
| 973 | 973 | func fullscreenAtMapIgnored(t *testing.T, animated bool) { | |
| 974 | 974 | cfg := scrollDefaults() | |
| 975 | 975 | cfg.Layout.Overflow = "fixed" | |
| 976 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |
| 977 | 976 | client := make(chan ports.ClientEvent, 8) | |
| 978 | 977 | output := make(chan ports.OutputEvent, 8) | |
| 979 | 978 | commands := make(chan ports.ClientCommand, 64) | |
| @@ -1063,7 +1062,6 @@ func TestExternalFullscreenAtMapApplies(t *testing.T) { both(t, externalFullscre | |||
| 1063 | 1062 | ||
| 1064 | 1063 | func externalFullscreenAtMapApplies(t *testing.T, animated bool) { | |
| 1065 | 1064 | cfg := scrollDefaults() | |
| 1066 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |
| 1067 | 1065 | client := make(chan ports.ClientEvent, 8) | |
| 1068 | 1066 | output := make(chan ports.OutputEvent, 8) | |
| 1069 | 1067 | commands := make(chan ports.ClientCommand, 64) | |
| @@ -973,7 +973,6 @@ func TestFullscreenAtMapIgnored(t *testing.T) { both(t, fullscreenAtMapIgnored) | |||||
| 973 | func fullscreenAtMapIgnored(t *testing.T, animated bool) { | 973 | func fullscreenAtMapIgnored(t *testing.T, animated bool) { | ||
| 974 | cfg := scrollDefaults() | 974 | cfg := scrollDefaults() | ||
| 975 | cfg.Layout.Overflow = "fixed" | 975 | cfg.Layout.Overflow = "fixed" | ||
| 976 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |||
| 977 | client := make(chan ports.ClientEvent, 8) | 976 | client := make(chan ports.ClientEvent, 8) | ||
| 978 | output := make(chan ports.OutputEvent, 8) | 977 | output := make(chan ports.OutputEvent, 8) | ||
| 979 | commands := make(chan ports.ClientCommand, 64) | 978 | commands := make(chan ports.ClientCommand, 64) | ||
| @@ -1063,7 +1062,6 @@ func TestExternalFullscreenAtMapApplies(t *testing.T) { both(t, externalFullscre | |||||
| 1063 | 1062 | ||||
| 1064 | func externalFullscreenAtMapApplies(t *testing.T, animated bool) { | 1063 | func externalFullscreenAtMapApplies(t *testing.T, animated bool) { | ||
| 1065 | cfg := scrollDefaults() | 1064 | cfg := scrollDefaults() | ||
| 1066 | - | cfg.Focus.Animation = ports.FocusAnimationOff | |||
| 1067 | client := make(chan ports.ClientEvent, 8) | 1065 | client := make(chan ports.ClientEvent, 8) | ||
| 1068 | output := make(chan ports.OutputEvent, 8) | 1066 | output := make(chan ports.OutputEvent, 8) | ||
| 1069 | commands := make(chan ports.ClientCommand, 64) | 1067 | commands := make(chan ports.ClientCommand, 64) | ||
D internal/core/frame_pulse_test.go +0 -69
| @@ -1,69 +0,0 @@ | |||
| 1 | - | package core | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "context" | |
| 5 | - | "testing" | |
| 6 | - | "time" | |
| 7 | - | ||
| 8 | - | "github.com/bnema/neferwl/internal/ports" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | // stepScenes steps only the screen and returns the scene of each output. | |
| 12 | - | func stepScenes(t *testing.T, c *Core, only *screen) map[string]ports.Scene { | |
| 13 | - | t.Helper() | |
| 14 | - | if err := c.step(context.Background(), only); err != nil { | |
| 15 | - | t.Fatal(err) | |
| 16 | - | } | |
| 17 | - | out := map[string]ports.Scene{} | |
| 18 | - | for _, s := range <-c.ch.Scenes { | |
| 19 | - | out[s.Output] = s | |
| 20 | - | } | |
| 21 | - | return out | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | // While the focus pulse runs on the focused output, a flip of another output | |
| 25 | - | // keeps the focused output's Seq; its own flip samples the pulse. | |
| 26 | - | func TestFlipOfOtherOutputKeepsPulseSeq(t *testing.T) { | |
| 27 | - | c, ic := pulseCore(t) | |
| 28 | - | a := c.cur() | |
| 29 | - | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 30 | - | b := c.screens[1] | |
| 31 | - | for id := WindowID(3); id <= 5; id++ { | |
| 32 | - | b.mon.AddWindow(id) | |
| 33 | - | } | |
| 34 | - | indicatorScene(t, c) | |
| 35 | - | ||
| 36 | - | // A pulse on window 1 of A. | |
| 37 | - | a.mon.Current().FocusID(1) | |
| 38 | - | indicatorScene(t, c) | |
| 39 | - | ic.now = ic.now.Add(pulseSettle) | |
| 40 | - | if !c.pulseTick() { | |
| 41 | - | t.Fatal("no pulse after settle") | |
| 42 | - | } | |
| 43 | - | // A camera transition runs on B. | |
| 44 | - | w := b.mon.Current() | |
| 45 | - | w.view.off = 100 | |
| 46 | - | w.view.motion = newMotion(viewSpring(w.view.off, 0), ic.now, 1) | |
| 47 | - | ||
| 48 | - | ic.now = ic.now.Add(pulseRise / 2) | |
| 49 | - | first := stepScenes(t, c, a) | |
| 50 | - | if pulseOf(first["A"], 1) <= 0 { | |
| 51 | - | t.Fatalf("no pulse on A: %v", pulseOf(first["A"], 1)) | |
| 52 | - | } | |
| 53 | - | ic.now = ic.now.Add(10 * time.Millisecond) | |
| 54 | - | second := stepScenes(t, c, b) | |
| 55 | - | if second["A"].Seq != first["A"].Seq || !second["A"].SameAs(first["A"]) { | |
| 56 | - | t.Fatalf("A changed on a flip of B: Seq %d, was %d", second["A"].Seq, first["A"].Seq) | |
| 57 | - | } | |
| 58 | - | if second["B"].SameAs(first["B"]) { | |
| 59 | - | t.Fatal("B did not move on its own flip") | |
| 60 | - | } | |
| 61 | - | ic.now = ic.now.Add(10 * time.Millisecond) | |
| 62 | - | third := stepScenes(t, c, a) | |
| 63 | - | if third["A"].Seq <= second["A"].Seq || pulseOf(third["A"], 1) == pulseOf(second["A"], 1) { | |
| 64 | - | t.Fatalf("A did not sample the pulse on its own flip: Seq %d, was %d", third["A"].Seq, second["A"].Seq) | |
| 65 | - | } | |
| 66 | - | if third["B"].Seq != second["B"].Seq { | |
| 67 | - | t.Fatalf("B changed on a flip of A: Seq %d, was %d", third["B"].Seq, second["B"].Seq) | |
| 68 | - | } | |
| 69 | - | } | |
| @@ -1,69 +0,0 @@ | |||||
| 1 | - | package core | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "context" | |||
| 5 | - | "testing" | |||
| 6 | - | "time" | |||
| 7 | - | ||||
| 8 | - | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | // stepScenes steps only the screen and returns the scene of each output. | |||
| 12 | - | func stepScenes(t *testing.T, c *Core, only *screen) map[string]ports.Scene { | |||
| 13 | - | t.Helper() | |||
| 14 | - | if err := c.step(context.Background(), only); err != nil { | |||
| 15 | - | t.Fatal(err) | |||
| 16 | - | } | |||
| 17 | - | out := map[string]ports.Scene{} | |||
| 18 | - | for _, s := range <-c.ch.Scenes { | |||
| 19 | - | out[s.Output] = s | |||
| 20 | - | } | |||
| 21 | - | return out | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | // While the focus pulse runs on the focused output, a flip of another output | |||
| 25 | - | // keeps the focused output's Seq; its own flip samples the pulse. | |||
| 26 | - | func TestFlipOfOtherOutputKeepsPulseSeq(t *testing.T) { | |||
| 27 | - | c, ic := pulseCore(t) | |||
| 28 | - | a := c.cur() | |||
| 29 | - | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |||
| 30 | - | b := c.screens[1] | |||
| 31 | - | for id := WindowID(3); id <= 5; id++ { | |||
| 32 | - | b.mon.AddWindow(id) | |||
| 33 | - | } | |||
| 34 | - | indicatorScene(t, c) | |||
| 35 | - | ||||
| 36 | - | // A pulse on window 1 of A. | |||
| 37 | - | a.mon.Current().FocusID(1) | |||
| 38 | - | indicatorScene(t, c) | |||
| 39 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 40 | - | if !c.pulseTick() { | |||
| 41 | - | t.Fatal("no pulse after settle") | |||
| 42 | - | } | |||
| 43 | - | // A camera transition runs on B. | |||
| 44 | - | w := b.mon.Current() | |||
| 45 | - | w.view.off = 100 | |||
| 46 | - | w.view.motion = newMotion(viewSpring(w.view.off, 0), ic.now, 1) | |||
| 47 | - | ||||
| 48 | - | ic.now = ic.now.Add(pulseRise / 2) | |||
| 49 | - | first := stepScenes(t, c, a) | |||
| 50 | - | if pulseOf(first["A"], 1) <= 0 { | |||
| 51 | - | t.Fatalf("no pulse on A: %v", pulseOf(first["A"], 1)) | |||
| 52 | - | } | |||
| 53 | - | ic.now = ic.now.Add(10 * time.Millisecond) | |||
| 54 | - | second := stepScenes(t, c, b) | |||
| 55 | - | if second["A"].Seq != first["A"].Seq || !second["A"].SameAs(first["A"]) { | |||
| 56 | - | t.Fatalf("A changed on a flip of B: Seq %d, was %d", second["A"].Seq, first["A"].Seq) | |||
| 57 | - | } | |||
| 58 | - | if second["B"].SameAs(first["B"]) { | |||
| 59 | - | t.Fatal("B did not move on its own flip") | |||
| 60 | - | } | |||
| 61 | - | ic.now = ic.now.Add(10 * time.Millisecond) | |||
| 62 | - | third := stepScenes(t, c, a) | |||
| 63 | - | if third["A"].Seq <= second["A"].Seq || pulseOf(third["A"], 1) == pulseOf(second["A"], 1) { | |||
| 64 | - | t.Fatalf("A did not sample the pulse on its own flip: Seq %d, was %d", third["A"].Seq, second["A"].Seq) | |||
| 65 | - | } | |||
| 66 | - | if third["B"].Seq != second["B"].Seq { | |||
| 67 | - | t.Fatalf("B changed on a flip of A: Seq %d, was %d", third["B"].Seq, second["B"].Seq) | |||
| 68 | - | } | |||
| 69 | - | } | |||
M internal/core/gesture.go +3 -6
| @@ -577,14 +577,11 @@ func (c *Core) animate(now time.Time, only *screen) { | |||
| 577 | 577 | // sliding reports whether a spring runs on the named output. | |
| 578 | 578 | func (c *Core) sliding(output string) bool { | |
| 579 | 579 | i := c.screenIndex(output) | |
| 580 | - | return i >= 0 && (c.screens[i].springing() || c.pulsing(c.screens[i])) | |
| 580 | + | return i >= 0 && c.screens[i].springing() | |
| 581 | 581 | } | |
| 582 | 582 | ||
| 583 | - | // animating reports whether a spring or the focus pulse runs on any output. | |
| 583 | + | // animating reports whether a spring runs on any output. | |
| 584 | 584 | func (c *Core) animating() bool { | |
| 585 | - | if c.pulse.id != 0 { | |
| 586 | - | return true | |
| 587 | - | } | |
| 588 | 585 | for _, sc := range c.screens { | |
| 589 | 586 | if sc.springing() { | |
| 590 | 587 | return true | |
| @@ -610,7 +607,7 @@ func (m *Monitor) springing() bool { | |||
| 610 | 607 | func (c *Core) frameFallback() time.Duration { | |
| 611 | 608 | refresh := 60000 | |
| 612 | 609 | for _, sc := range c.screens { | |
| 613 | - | if r := sc.info.RefreshMilli; r > 0 && (sc.springing() || c.pulsing(sc)) { | |
| 610 | + | if r := sc.info.RefreshMilli; r > 0 && sc.springing() { | |
| 614 | 611 | refresh = min(refresh, r) | |
| 615 | 612 | } | |
| 616 | 613 | } | |
| @@ -577,14 +577,11 @@ func (c *Core) animate(now time.Time, only *screen) { | |||||
| 577 | // sliding reports whether a spring runs on the named output. | 577 | // sliding reports whether a spring runs on the named output. | ||
| 578 | func (c *Core) sliding(output string) bool { | 578 | func (c *Core) sliding(output string) bool { | ||
| 579 | i := c.screenIndex(output) | 579 | i := c.screenIndex(output) | ||
| 580 | - | return i >= 0 && (c.screens[i].springing() || c.pulsing(c.screens[i])) | 580 | + | return i >= 0 && c.screens[i].springing() |
| 581 | } | 581 | } | ||
| 582 | 582 | ||||
| 583 | - | // animating reports whether a spring or the focus pulse runs on any output. | 583 | + | // animating reports whether a spring runs on any output. |
| 584 | func (c *Core) animating() bool { | 584 | func (c *Core) animating() bool { | ||
| 585 | - | if c.pulse.id != 0 { | |||
| 586 | - | return true | |||
| 587 | - | } | |||
| 588 | for _, sc := range c.screens { | 585 | for _, sc := range c.screens { | ||
| 589 | if sc.springing() { | 586 | if sc.springing() { | ||
| 590 | return true | 587 | return true | ||
| @@ -610,7 +607,7 @@ func (m *Monitor) springing() bool { | |||||
| 610 | func (c *Core) frameFallback() time.Duration { | 607 | func (c *Core) frameFallback() time.Duration { | ||
| 611 | refresh := 60000 | 608 | refresh := 60000 | ||
| 612 | for _, sc := range c.screens { | 609 | for _, sc := range c.screens { | ||
| 613 | - | if r := sc.info.RefreshMilli; r > 0 && (sc.springing() || c.pulsing(sc)) { | 610 | + | if r := sc.info.RefreshMilli; r > 0 && sc.springing() { |
| 614 | refresh = min(refresh, r) | 611 | refresh = min(refresh, r) | ||
| 615 | } | 612 | } | ||
| 616 | } | 613 | } | ||
M internal/core/persist_test.go +1 -1
| @@ -22,7 +22,7 @@ func steppingClock(t *testing.T) *portsmocks.MockClock { | |||
| 22 | 22 | now = now.Add(time.Second) | |
| 23 | 23 | return now | |
| 24 | 24 | }).Maybe() | |
| 25 | - | // Timers (the focus pulse) never fire: tests drive events only. | |
| 25 | + | // Timers never fire: tests drive events only. | |
| 26 | 26 | clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer { | |
| 27 | 27 | timer := portsmocks.NewMockTimer(t) | |
| 28 | 28 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| @@ -22,7 +22,7 @@ func steppingClock(t *testing.T) *portsmocks.MockClock { | |||||
| 22 | now = now.Add(time.Second) | 22 | now = now.Add(time.Second) | ||
| 23 | return now | 23 | return now | ||
| 24 | }).Maybe() | 24 | }).Maybe() | ||
| 25 | - | // Timers (the focus pulse) never fire: tests drive events only. | 25 | + | // Timers never fire: tests drive events only. |
| 26 | clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer { | 26 | clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer { | ||
| 27 | timer := portsmocks.NewMockTimer(t) | 27 | timer := portsmocks.NewMockTimer(t) | ||
| 28 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | 28 | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | ||
D internal/core/pulse.go +0 -183
| @@ -1,183 +0,0 @@ | |||
| 1 | - | package core | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "math" | |
| 5 | - | "time" | |
| 6 | - | ||
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // The focus pulse is the pulse animation of the focus indicator | |
| 11 | - | // (focus.animation): it briefly shows the indicator's effect (focus.effect, a | |
| 12 | - | // light screen blend visible on black backgrounds too) on a window that just | |
| 13 | - | // got the keyboard focus. Only the last focus counts: it must hold for | |
| 14 | - | // pulseSettle before the pulse starts, a focus change stops a running one, and | |
| 15 | - | // the window that pulsed last does not pulse again within pulseCooldown | |
| 16 | - | // (switching back after another window held the focus is a new change). | |
| 17 | - | // Fast switching therefore shows nothing. Fullscreen windows, a window alone | |
| 18 | - | // in the usable area (e.g. a maximized column), overview previews, a window focused as | |
| 19 | - | // it maps and a protected session never pulse. | |
| 20 | - | ||
| 21 | - | const ( | |
| 22 | - | pulseSettle = 150 * time.Millisecond | |
| 23 | - | pulseRise = 90 * time.Millisecond | |
| 24 | - | pulseFall = 230 * time.Millisecond | |
| 25 | - | pulseCooldown = time.Second | |
| 26 | - | // pulseRecheck is how often a pulse waiting for a slide to land checks again. | |
| 27 | - | pulseRecheck = pulseSettle / 3 | |
| 28 | - | ) | |
| 29 | - | ||
| 30 | - | // focusPulse is the pulse state. target is the window focus last seen and | |
| 31 | - | // since when; id is the window pulsing from start (0: none). last and lastAt | |
| 32 | - | // are the previous pulse, for the cooldown. drawable is whether the last | |
| 33 | - | // scene could show a pulse on target. | |
| 34 | - | type focusPulse struct { | |
| 35 | - | target WindowID | |
| 36 | - | since time.Time | |
| 37 | - | id WindowID | |
| 38 | - | start time.Time | |
| 39 | - | last WindowID | |
| 40 | - | lastAt time.Time | |
| 41 | - | drawable bool | |
| 42 | - | // value is the effect of the last sample, kept for scenes published | |
| 43 | - | // between the focused output's own flips. | |
| 44 | - | value float64 | |
| 45 | - | timerC <-chan time.Time | |
| 46 | - | timerStop func() bool | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | func (c *Core) pulseOn() bool { | |
| 50 | - | return c.animOn() && c.cfg.Focus.Animation == ports.FocusAnimationPulse | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | // pulseFocus follows the window focus. held is false while something else | |
| 54 | - | // (a popup grab, a layer, a lock surface) has the keyboard: the running pulse | |
| 55 | - | // stops, and the window getting it back is not a new focus. A new window | |
| 56 | - | // stops the running pulse and waits pulseSettle before one starts. With the | |
| 57 | - | // pulse off the focus is still followed, so turning it on pulses nothing. | |
| 58 | - | func (c *Core) pulseFocus(window WindowID, held bool) { | |
| 59 | - | p := &c.pulse | |
| 60 | - | if !held || !c.pulseOn() { | |
| 61 | - | p.id = 0 | |
| 62 | - | c.stopPulseTimer() | |
| 63 | - | if held { | |
| 64 | - | p.target = window | |
| 65 | - | } | |
| 66 | - | return | |
| 67 | - | } | |
| 68 | - | if window == p.target { | |
| 69 | - | return | |
| 70 | - | } | |
| 71 | - | p.target, p.id = window, 0 | |
| 72 | - | c.stopPulseTimer() | |
| 73 | - | if window == 0 { | |
| 74 | - | return | |
| 75 | - | } | |
| 76 | - | p.since = c.now() | |
| 77 | - | if c.justMapped(window) { | |
| 78 | - | return | |
| 79 | - | } | |
| 80 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle) | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | // justMapped reports whether the window got the focus as it mapped: opening | |
| 84 | - | // an app is not a focus change, and its first frames may still be on their | |
| 85 | - | // way. Its focus counts from the next change. | |
| 86 | - | func (c *Core) justMapped(id WindowID) bool { | |
| 87 | - | p := &c.pulse | |
| 88 | - | return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | // pulseTick runs when the settle timer fired. It reports whether a pulse | |
| 92 | - | // started. | |
| 93 | - | func (c *Core) pulseTick() bool { | |
| 94 | - | p := &c.pulse | |
| 95 | - | p.timerC, p.timerStop = nil, nil | |
| 96 | - | if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) { | |
| 97 | - | return false | |
| 98 | - | } | |
| 99 | - | now := c.now() | |
| 100 | - | if wait := pulseSettle - now.Sub(p.since); wait > 0 { | |
| 101 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, wait) | |
| 102 | - | return false | |
| 103 | - | } | |
| 104 | - | // A swipe or its landing slide still shows the neighbours: decide on | |
| 105 | - | // the settled layout, or a window left alone would start a pulse cut | |
| 106 | - | // short. | |
| 107 | - | if !c.cur().settled() { | |
| 108 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck) | |
| 109 | - | return false | |
| 110 | - | } | |
| 111 | - | if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown { | |
| 112 | - | return false | |
| 113 | - | } | |
| 114 | - | p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now | |
| 115 | - | return true | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | // advancePulse is the effect of the running pulse at now: a quick sine rise | |
| 119 | - | // to focus.strength, then a smooth fall. It ends the pulse once over. | |
| 120 | - | func (c *Core) advancePulse(now time.Time) float64 { | |
| 121 | - | p := &c.pulse | |
| 122 | - | if p.id == 0 { | |
| 123 | - | return 0 | |
| 124 | - | } | |
| 125 | - | peak := c.cfg.Focus.Strength | |
| 126 | - | t := now.Sub(p.start) | |
| 127 | - | switch { | |
| 128 | - | case t < 0: | |
| 129 | - | return 0 | |
| 130 | - | case t < pulseRise: | |
| 131 | - | return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2) | |
| 132 | - | case t < pulseRise+pulseFall: | |
| 133 | - | x := 1 - float64(t-pulseRise)/float64(pulseFall) | |
| 134 | - | return peak * x * x * (3 - 2*x) | |
| 135 | - | } | |
| 136 | - | p.id = 0 | |
| 137 | - | return 0 | |
| 138 | - | } | |
| 139 | - | ||
| 140 | - | // settled reports whether no swipe or slide moves the monitor's layout | |
| 141 | - | // (screen.settled adds the rect motions). | |
| 142 | - | func (m *Monitor) settled() bool { | |
| 143 | - | if m.switchView.busy() { | |
| 144 | - | return false | |
| 145 | - | } | |
| 146 | - | for w := range m.all() { | |
| 147 | - | if w.view.busy() || w.stashView.busy() { | |
| 148 | - | return false | |
| 149 | - | } | |
| 150 | - | } | |
| 151 | - | return true | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // visibleCount is the number of windows of layout seen in v. | |
| 155 | - | func visibleCount(layout []Placement, v viewport) int { | |
| 156 | - | n := 0 | |
| 157 | - | for _, p := range layout { | |
| 158 | - | if p.Preview == 0 && v.shows(p) { | |
| 159 | - | n++ | |
| 160 | - | } | |
| 161 | - | } | |
| 162 | - | return n | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | // pulsing reports whether a pulse runs on the screen. | |
| 166 | - | func (c *Core) pulsing(sc *screen) bool { | |
| 167 | - | return c.pulse.id != 0 && sc == c.cur() | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | func (c *Core) stopPulseTimer() { | |
| 171 | - | p := &c.pulse | |
| 172 | - | if p.timerStop != nil { | |
| 173 | - | p.timerStop() | |
| 174 | - | } | |
| 175 | - | p.timerC, p.timerStop = nil, nil | |
| 176 | - | } | |
| 177 | - | ||
| 178 | - | // stopPulse ends the running pulse and its timer (security change). The | |
| 179 | - | // focus is kept: the window focused again after an unlock does not pulse. | |
| 180 | - | func (c *Core) stopPulse() { | |
| 181 | - | c.stopPulseTimer() | |
| 182 | - | c.pulse.id, c.pulse.drawable = 0, false | |
| 183 | - | } | |
| @@ -1,183 +0,0 @@ | |||||
| 1 | - | package core | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "math" | |||
| 5 | - | "time" | |||
| 6 | - | ||||
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | // The focus pulse is the pulse animation of the focus indicator | |||
| 11 | - | // (focus.animation): it briefly shows the indicator's effect (focus.effect, a | |||
| 12 | - | // light screen blend visible on black backgrounds too) on a window that just | |||
| 13 | - | // got the keyboard focus. Only the last focus counts: it must hold for | |||
| 14 | - | // pulseSettle before the pulse starts, a focus change stops a running one, and | |||
| 15 | - | // the window that pulsed last does not pulse again within pulseCooldown | |||
| 16 | - | // (switching back after another window held the focus is a new change). | |||
| 17 | - | // Fast switching therefore shows nothing. Fullscreen windows, a window alone | |||
| 18 | - | // in the usable area (e.g. a maximized column), overview previews, a window focused as | |||
| 19 | - | // it maps and a protected session never pulse. | |||
| 20 | - | ||||
| 21 | - | const ( | |||
| 22 | - | pulseSettle = 150 * time.Millisecond | |||
| 23 | - | pulseRise = 90 * time.Millisecond | |||
| 24 | - | pulseFall = 230 * time.Millisecond | |||
| 25 | - | pulseCooldown = time.Second | |||
| 26 | - | // pulseRecheck is how often a pulse waiting for a slide to land checks again. | |||
| 27 | - | pulseRecheck = pulseSettle / 3 | |||
| 28 | - | ) | |||
| 29 | - | ||||
| 30 | - | // focusPulse is the pulse state. target is the window focus last seen and | |||
| 31 | - | // since when; id is the window pulsing from start (0: none). last and lastAt | |||
| 32 | - | // are the previous pulse, for the cooldown. drawable is whether the last | |||
| 33 | - | // scene could show a pulse on target. | |||
| 34 | - | type focusPulse struct { | |||
| 35 | - | target WindowID | |||
| 36 | - | since time.Time | |||
| 37 | - | id WindowID | |||
| 38 | - | start time.Time | |||
| 39 | - | last WindowID | |||
| 40 | - | lastAt time.Time | |||
| 41 | - | drawable bool | |||
| 42 | - | // value is the effect of the last sample, kept for scenes published | |||
| 43 | - | // between the focused output's own flips. | |||
| 44 | - | value float64 | |||
| 45 | - | timerC <-chan time.Time | |||
| 46 | - | timerStop func() bool | |||
| 47 | - | } | |||
| 48 | - | ||||
| 49 | - | func (c *Core) pulseOn() bool { | |||
| 50 | - | return c.animOn() && c.cfg.Focus.Animation == ports.FocusAnimationPulse | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | // pulseFocus follows the window focus. held is false while something else | |||
| 54 | - | // (a popup grab, a layer, a lock surface) has the keyboard: the running pulse | |||
| 55 | - | // stops, and the window getting it back is not a new focus. A new window | |||
| 56 | - | // stops the running pulse and waits pulseSettle before one starts. With the | |||
| 57 | - | // pulse off the focus is still followed, so turning it on pulses nothing. | |||
| 58 | - | func (c *Core) pulseFocus(window WindowID, held bool) { | |||
| 59 | - | p := &c.pulse | |||
| 60 | - | if !held || !c.pulseOn() { | |||
| 61 | - | p.id = 0 | |||
| 62 | - | c.stopPulseTimer() | |||
| 63 | - | if held { | |||
| 64 | - | p.target = window | |||
| 65 | - | } | |||
| 66 | - | return | |||
| 67 | - | } | |||
| 68 | - | if window == p.target { | |||
| 69 | - | return | |||
| 70 | - | } | |||
| 71 | - | p.target, p.id = window, 0 | |||
| 72 | - | c.stopPulseTimer() | |||
| 73 | - | if window == 0 { | |||
| 74 | - | return | |||
| 75 | - | } | |||
| 76 | - | p.since = c.now() | |||
| 77 | - | if c.justMapped(window) { | |||
| 78 | - | return | |||
| 79 | - | } | |||
| 80 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle) | |||
| 81 | - | } | |||
| 82 | - | ||||
| 83 | - | // justMapped reports whether the window got the focus as it mapped: opening | |||
| 84 | - | // an app is not a focus change, and its first frames may still be on their | |||
| 85 | - | // way. Its focus counts from the next change. | |||
| 86 | - | func (c *Core) justMapped(id WindowID) bool { | |||
| 87 | - | p := &c.pulse | |||
| 88 | - | return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | // pulseTick runs when the settle timer fired. It reports whether a pulse | |||
| 92 | - | // started. | |||
| 93 | - | func (c *Core) pulseTick() bool { | |||
| 94 | - | p := &c.pulse | |||
| 95 | - | p.timerC, p.timerStop = nil, nil | |||
| 96 | - | if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) { | |||
| 97 | - | return false | |||
| 98 | - | } | |||
| 99 | - | now := c.now() | |||
| 100 | - | if wait := pulseSettle - now.Sub(p.since); wait > 0 { | |||
| 101 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, wait) | |||
| 102 | - | return false | |||
| 103 | - | } | |||
| 104 | - | // A swipe or its landing slide still shows the neighbours: decide on | |||
| 105 | - | // the settled layout, or a window left alone would start a pulse cut | |||
| 106 | - | // short. | |||
| 107 | - | if !c.cur().settled() { | |||
| 108 | - | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck) | |||
| 109 | - | return false | |||
| 110 | - | } | |||
| 111 | - | if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown { | |||
| 112 | - | return false | |||
| 113 | - | } | |||
| 114 | - | p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now | |||
| 115 | - | return true | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | // advancePulse is the effect of the running pulse at now: a quick sine rise | |||
| 119 | - | // to focus.strength, then a smooth fall. It ends the pulse once over. | |||
| 120 | - | func (c *Core) advancePulse(now time.Time) float64 { | |||
| 121 | - | p := &c.pulse | |||
| 122 | - | if p.id == 0 { | |||
| 123 | - | return 0 | |||
| 124 | - | } | |||
| 125 | - | peak := c.cfg.Focus.Strength | |||
| 126 | - | t := now.Sub(p.start) | |||
| 127 | - | switch { | |||
| 128 | - | case t < 0: | |||
| 129 | - | return 0 | |||
| 130 | - | case t < pulseRise: | |||
| 131 | - | return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2) | |||
| 132 | - | case t < pulseRise+pulseFall: | |||
| 133 | - | x := 1 - float64(t-pulseRise)/float64(pulseFall) | |||
| 134 | - | return peak * x * x * (3 - 2*x) | |||
| 135 | - | } | |||
| 136 | - | p.id = 0 | |||
| 137 | - | return 0 | |||
| 138 | - | } | |||
| 139 | - | ||||
| 140 | - | // settled reports whether no swipe or slide moves the monitor's layout | |||
| 141 | - | // (screen.settled adds the rect motions). | |||
| 142 | - | func (m *Monitor) settled() bool { | |||
| 143 | - | if m.switchView.busy() { | |||
| 144 | - | return false | |||
| 145 | - | } | |||
| 146 | - | for w := range m.all() { | |||
| 147 | - | if w.view.busy() || w.stashView.busy() { | |||
| 148 | - | return false | |||
| 149 | - | } | |||
| 150 | - | } | |||
| 151 | - | return true | |||
| 152 | - | } | |||
| 153 | - | ||||
| 154 | - | // visibleCount is the number of windows of layout seen in v. | |||
| 155 | - | func visibleCount(layout []Placement, v viewport) int { | |||
| 156 | - | n := 0 | |||
| 157 | - | for _, p := range layout { | |||
| 158 | - | if p.Preview == 0 && v.shows(p) { | |||
| 159 | - | n++ | |||
| 160 | - | } | |||
| 161 | - | } | |||
| 162 | - | return n | |||
| 163 | - | } | |||
| 164 | - | ||||
| 165 | - | // pulsing reports whether a pulse runs on the screen. | |||
| 166 | - | func (c *Core) pulsing(sc *screen) bool { | |||
| 167 | - | return c.pulse.id != 0 && sc == c.cur() | |||
| 168 | - | } | |||
| 169 | - | ||||
| 170 | - | func (c *Core) stopPulseTimer() { | |||
| 171 | - | p := &c.pulse | |||
| 172 | - | if p.timerStop != nil { | |||
| 173 | - | p.timerStop() | |||
| 174 | - | } | |||
| 175 | - | p.timerC, p.timerStop = nil, nil | |||
| 176 | - | } | |||
| 177 | - | ||||
| 178 | - | // stopPulse ends the running pulse and its timer (security change). The | |||
| 179 | - | // focus is kept: the window focused again after an unlock does not pulse. | |||
| 180 | - | func (c *Core) stopPulse() { | |||
| 181 | - | c.stopPulseTimer() | |||
| 182 | - | c.pulse.id, c.pulse.drawable = 0, false | |||
| 183 | - | } | |||
D internal/core/pulse_test.go +0 -319
| @@ -1,319 +0,0 @@ | |||
| 1 | - | package core | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | "time" | |
| 6 | - | ||
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |
| 8 | - | ) | |
| 9 | - | ||
| 10 | - | // pulseCore is indicatorCore with the focus pulse on and two tiled windows, | |
| 11 | - | // 2 focused, its pulse settled and over. | |
| 12 | - | func pulseCore(t *testing.T) (*Core, *indicatorClock) { | |
| 13 | - | t.Helper() | |
| 14 | - | c, ic := indicatorCore(t) | |
| 15 | - | c.cfg.Animations.On = true | |
| 16 | - | c.cfg.Focus.Animation = ports.FocusAnimationPulse | |
| 17 | - | c.cfg.Focus.Strength = 0.05 | |
| 18 | - | c.cur().mon.AddWindow(1) | |
| 19 | - | c.cur().mon.AddWindow(2) | |
| 20 | - | indicatorScene(t, c) | |
| 21 | - | ic.now = ic.now.Add(pulseSettle) | |
| 22 | - | c.pulseTick() | |
| 23 | - | ic.now = ic.now.Add(pulseCooldown) | |
| 24 | - | indicatorScene(t, c) | |
| 25 | - | return c, ic | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | func pulseOf(s ports.Scene, id WindowID) float64 { | |
| 29 | - | for _, w := range s.Windows { | |
| 30 | - | if w.ID == id { | |
| 31 | - | return w.FocusEffect | |
| 32 | - | } | |
| 33 | - | } | |
| 34 | - | return -1 | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | func TestFocusPulseAfterSettle(t *testing.T) { | |
| 38 | - | c, ic := pulseCore(t) | |
| 39 | - | c.cur().mon.Current().FocusID(1) | |
| 40 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 { | |
| 41 | - | t.Fatalf("pulse before settle: %v", pulseOf(s, 1)) | |
| 42 | - | } | |
| 43 | - | ic.now = ic.now.Add(pulseSettle) | |
| 44 | - | if !c.pulseTick() { | |
| 45 | - | t.Fatal("no pulse after settle") | |
| 46 | - | } | |
| 47 | - | if !c.animating() { | |
| 48 | - | t.Fatal("a running pulse asks for no frames") | |
| 49 | - | } | |
| 50 | - | ic.now = ic.now.Add(pulseRise) | |
| 51 | - | s := indicatorScene(t, c) | |
| 52 | - | if p := pulseOf(s, 1); p < 0.05*0.99 || p > 0.05 { | |
| 53 | - | t.Fatalf("peak %v, want the configured 0.05", p) | |
| 54 | - | } | |
| 55 | - | if pulseOf(s, 2) != 0 { | |
| 56 | - | t.Fatal("unfocused window pulses") | |
| 57 | - | } | |
| 58 | - | ic.now = ic.now.Add(pulseFall) | |
| 59 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || c.animating() { | |
| 60 | - | t.Fatalf("pulse left after its end: %v", pulseOf(s, 1)) | |
| 61 | - | } | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | // Switching faster than pulseSettle never pulses: only the last focus counts. | |
| 65 | - | func TestFocusPulseDebounced(t *testing.T) { | |
| 66 | - | c, ic := pulseCore(t) | |
| 67 | - | for range 4 { | |
| 68 | - | c.cur().mon.Current().FocusID(1) | |
| 69 | - | indicatorScene(t, c) | |
| 70 | - | ic.now = ic.now.Add(pulseSettle / 3) | |
| 71 | - | c.cur().mon.Current().FocusID(2) | |
| 72 | - | indicatorScene(t, c) | |
| 73 | - | ic.now = ic.now.Add(pulseSettle / 3) | |
| 74 | - | } | |
| 75 | - | // A stale timer of an earlier focus fires: the focus is too recent. | |
| 76 | - | if c.pulseTick() { | |
| 77 | - | t.Fatal("pulse while switching") | |
| 78 | - | } | |
| 79 | - | if c.pulse.timerC == nil { | |
| 80 | - | t.Fatal("no timer for the last focus") | |
| 81 | - | } | |
| 82 | - | ic.now = ic.now.Add(pulseSettle) | |
| 83 | - | // Window 2 pulsed in pulseCore over a second ago: it may pulse again. | |
| 84 | - | if !c.pulseTick() || c.pulse.id != 2 { | |
| 85 | - | t.Fatalf("last focus did not pulse: %d", c.pulse.id) | |
| 86 | - | } | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | // A focus change stops the running pulse; coming back within the cooldown | |
| 90 | - | // does not pulse the same window again. | |
| 91 | - | func TestFocusPulseStopsAndCoolsDown(t *testing.T) { | |
| 92 | - | c, ic := pulseCore(t) | |
| 93 | - | c.cur().mon.Current().FocusID(1) | |
| 94 | - | indicatorScene(t, c) | |
| 95 | - | ic.now = ic.now.Add(pulseSettle) | |
| 96 | - | c.pulseTick() | |
| 97 | - | ic.now = ic.now.Add(pulseRise / 2) | |
| 98 | - | c.cur().mon.Current().FocusID(2) | |
| 99 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || pulseOf(s, 2) != 0 { | |
| 100 | - | t.Fatal("pulse survived a focus change") | |
| 101 | - | } | |
| 102 | - | c.cur().mon.Current().FocusID(1) | |
| 103 | - | indicatorScene(t, c) | |
| 104 | - | ic.now = ic.now.Add(pulseSettle) | |
| 105 | - | if c.pulseTick() { | |
| 106 | - | t.Fatal("same window pulsed within the cooldown") | |
| 107 | - | } | |
| 108 | - | } | |
| 109 | - | ||
| 110 | - | // A window focused as it maps does not pulse; focusing it again later does. | |
| 111 | - | func TestFocusPulseSkipsNewWindow(t *testing.T) { | |
| 112 | - | c, ic := pulseCore(t) | |
| 113 | - | c.windows.mapped(ports.WindowMapped{ID: 3}, ic.now) | |
| 114 | - | c.cur().mon.AddWindow(3) | |
| 115 | - | indicatorScene(t, c) | |
| 116 | - | if c.pulse.timerC != nil { | |
| 117 | - | t.Fatal("opening a window arms the pulse") | |
| 118 | - | } | |
| 119 | - | ic.now = ic.now.Add(pulseSettle) | |
| 120 | - | if c.pulseTick() || c.animating() { | |
| 121 | - | t.Fatal("a new window pulses") | |
| 122 | - | } | |
| 123 | - | c.cur().mon.Current().FocusID(1) | |
| 124 | - | indicatorScene(t, c) | |
| 125 | - | c.cur().mon.Current().FocusID(3) | |
| 126 | - | indicatorScene(t, c) | |
| 127 | - | ic.now = ic.now.Add(pulseSettle) | |
| 128 | - | if !c.pulseTick() || c.pulse.id != 3 { | |
| 129 | - | t.Fatal("an existing window did not pulse") | |
| 130 | - | } | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | // A window that mapped earlier without the focus pulses when focused. | |
| 134 | - | func TestFocusPulseWindowMappedEarlier(t *testing.T) { | |
| 135 | - | c, ic := pulseCore(t) | |
| 136 | - | c.windows.mapped(ports.WindowMapped{ID: 1}, ic.now.Add(-time.Minute)) | |
| 137 | - | c.cur().mon.Current().FocusID(1) | |
| 138 | - | indicatorScene(t, c) | |
| 139 | - | ic.now = ic.now.Add(pulseSettle) | |
| 140 | - | if !c.pulseTick() || c.pulse.id != 1 { | |
| 141 | - | t.Fatal("a window mapped earlier did not pulse") | |
| 142 | - | } | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | func TestFocusPulseSkipsFullscreen(t *testing.T) { | |
| 146 | - | c, ic := pulseCore(t) | |
| 147 | - | c.cur().mon.Current().FocusID(1) | |
| 148 | - | c.cur().mon.ToggleFullscreen() | |
| 149 | - | indicatorScene(t, c) | |
| 150 | - | ic.now = ic.now.Add(pulseSettle) | |
| 151 | - | if c.pulseTick() || c.animating() { | |
| 152 | - | t.Fatal("fullscreen window asks for pulse frames") | |
| 153 | - | } | |
| 154 | - | // Fullscreen entered mid-pulse ends it. | |
| 155 | - | c.cur().mon.ToggleFullscreen() | |
| 156 | - | c.cur().mon.Current().FocusID(2) | |
| 157 | - | indicatorScene(t, c) | |
| 158 | - | ic.now = ic.now.Add(pulseSettle) | |
| 159 | - | if !c.pulseTick() { | |
| 160 | - | t.Fatal("no pulse on the tiled window") | |
| 161 | - | } | |
| 162 | - | c.cur().mon.ToggleFullscreen() | |
| 163 | - | if s := indicatorScene(t, c); pulseOf(s, 2) != 0 || c.animating() { | |
| 164 | - | t.Fatal("pulse survived fullscreen") | |
| 165 | - | } | |
| 166 | - | } | |
| 167 | - | ||
| 168 | - | // A window alone on screen (a maximized column here) needs no pulse; once | |
| 169 | - | // another window shows beside it, focusing it pulses again. | |
| 170 | - | func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |
| 171 | - | c, ic := pulseCore(t) | |
| 172 | - | c.cur().mon.Current().FocusID(1) | |
| 173 | - | c.cur().mon.Current().ToggleFullWidth() | |
| 174 | - | indicatorScene(t, c) | |
| 175 | - | ic.now = ic.now.Add(pulseSettle) | |
| 176 | - | if c.pulseTick() || c.animating() { | |
| 177 | - | t.Fatal("a window alone on screen pulses") | |
| 178 | - | } | |
| 179 | - | c.cur().mon.Current().ToggleFullWidth() | |
| 180 | - | c.cur().mon.Current().FocusID(2) | |
| 181 | - | indicatorScene(t, c) | |
| 182 | - | ic.now = ic.now.Add(pulseSettle) | |
| 183 | - | if !c.pulseTick() || c.pulse.id != 2 { | |
| 184 | - | t.Fatal("a window beside another did not pulse") | |
| 185 | - | } | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | // A cascade window alone in its band above a bottom panel is alone on | |
| 189 | - | // screen: the next band, under the panel, does not count. | |
| 190 | - | func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) { | |
| 191 | - | c, ic := pulseCore(t) | |
| 192 | - | m := c.cur().mon | |
| 193 | - | m.SetOverflow(OverflowCascade) | |
| 194 | - | m.SetMaxColumns(1) | |
| 195 | - | m.SetUsable(Rect{W: 300, H: 180}) | |
| 196 | - | m.Current().FocusID(1) | |
| 197 | - | indicatorScene(t, c) | |
| 198 | - | ic.now = ic.now.Add(pulseSettle) | |
| 199 | - | if c.pulseTick() || c.animating() { | |
| 200 | - | t.Fatal("a window alone in its band above a panel pulses") | |
| 201 | - | } | |
| 202 | - | } | |
| 203 | - | ||
| 204 | - | // A swipe, its landing slide or a transition still shows the neighbours: the | |
| 205 | - | // pulse waits for the layout to settle, then decides on it. | |
| 206 | - | func TestFocusPulseWaitsForSlide(t *testing.T) { | |
| 207 | - | for _, tc := range []struct { | |
| 208 | - | name string | |
| 209 | - | slide func(*testing.T, *Core, time.Time) | |
| 210 | - | alone bool | |
| 211 | - | }{ | |
| 212 | - | {"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) { | |
| 213 | - | c.cur().mon.switchView.motion = newMotion(workspaceSpring(0.5, 0), now, 1) | |
| 214 | - | }, true}, | |
| 215 | - | {"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchView.off = 0.3 }, true}, | |
| 216 | - | {"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().view.off = 40 }, false}, | |
| 217 | - | {"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) { | |
| 218 | - | sc := c.cur() | |
| 219 | - | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}} | |
| 220 | - | sc.rectsWS = sc.mon.Current() | |
| 221 | - | }, false}, | |
| 222 | - | {"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) { | |
| 223 | - | m := c.cur().mon | |
| 224 | - | for id := WindowID(3); id <= 4; id++ { | |
| 225 | - | m.AddWindow(id) | |
| 226 | - | } | |
| 227 | - | indicatorScene(t, c) | |
| 228 | - | before := c.snapshot(now) | |
| 229 | - | for range 3 { | |
| 230 | - | c.applyAction(ActionFocusColumnLeft) | |
| 231 | - | } | |
| 232 | - | c.transition(before, now) | |
| 233 | - | if !m.Current().view.motion.on || m.settled() { | |
| 234 | - | t.Fatal("no camera transition after the focus moved the view") | |
| 235 | - | } | |
| 236 | - | }, false}, | |
| 237 | - | } { | |
| 238 | - | t.Run(tc.name, func(t *testing.T) { | |
| 239 | - | c, ic := pulseCore(t) | |
| 240 | - | m := c.cur().mon | |
| 241 | - | m.Current().FocusID(1) | |
| 242 | - | indicatorScene(t, c) | |
| 243 | - | tc.slide(t, c, ic.now) | |
| 244 | - | indicatorScene(t, c) | |
| 245 | - | ic.now = ic.now.Add(pulseSettle) | |
| 246 | - | if c.pulseTick() || c.pulse.timerC == nil { | |
| 247 | - | t.Fatal("pulse decided during a slide") | |
| 248 | - | } | |
| 249 | - | m.stopSwitch() | |
| 250 | - | m.Current().view.stop() | |
| 251 | - | c.cur().stopRects() | |
| 252 | - | if tc.alone { | |
| 253 | - | m.Current().ToggleFullWidth() | |
| 254 | - | } | |
| 255 | - | indicatorScene(t, c) | |
| 256 | - | ic.now = ic.now.Add(pulseRecheck) | |
| 257 | - | if got := c.pulseTick(); got == tc.alone { | |
| 258 | - | t.Fatalf("pulse started %v after landing, window alone %v", got, tc.alone) | |
| 259 | - | } | |
| 260 | - | }) | |
| 261 | - | } | |
| 262 | - | } | |
| 263 | - | ||
| 264 | - | // A popup grab or layer taking the keyboard stops the pulse; the window | |
| 265 | - | // getting it back is not a new focus and does not pulse again. | |
| 266 | - | func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) { | |
| 267 | - | c, ic := pulseCore(t) | |
| 268 | - | c.cur().mon.Current().FocusID(1) | |
| 269 | - | indicatorScene(t, c) | |
| 270 | - | ic.now = ic.now.Add(pulseSettle) | |
| 271 | - | c.pulseTick() | |
| 272 | - | // The keyboard goes elsewhere (held false), then comes back. | |
| 273 | - | c.pulseFocus(1, false) | |
| 274 | - | if c.animating() { | |
| 275 | - | t.Fatal("pulse kept while the keyboard is elsewhere") | |
| 276 | - | } | |
| 277 | - | ic.now = ic.now.Add(2 * pulseCooldown) | |
| 278 | - | c.pulseFocus(1, true) | |
| 279 | - | if c.pulse.timerC != nil { | |
| 280 | - | t.Fatal("returning keyboard counts as a new focus") | |
| 281 | - | } | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | func TestFocusPulseOff(t *testing.T) { | |
| 285 | - | c, ic := pulseCore(t) | |
| 286 | - | c.cfg.Focus.Animation = ports.FocusAnimationOff | |
| 287 | - | c.cur().mon.Current().FocusID(1) | |
| 288 | - | indicatorScene(t, c) | |
| 289 | - | if c.pulse.timerC != nil { | |
| 290 | - | t.Fatal("timer armed with the pulse off") | |
| 291 | - | } | |
| 292 | - | ic.now = ic.now.Add(pulseSettle) | |
| 293 | - | if c.pulseTick() || c.animating() { | |
| 294 | - | t.Fatal("pulse with focus.animation off") | |
| 295 | - | } | |
| 296 | - | // Turning it back on does not pulse the window already focused. | |
| 297 | - | c.cfg.Focus.Animation = ports.FocusAnimationPulse | |
| 298 | - | indicatorScene(t, c) | |
| 299 | - | if c.pulse.timerC != nil { | |
| 300 | - | t.Fatal("enabling the pulse pulses the focused window") | |
| 301 | - | } | |
| 302 | - | } | |
| 303 | - | ||
| 304 | - | func TestFocusPulseOffWithAnimationsOff(t *testing.T) { | |
| 305 | - | c, ic := pulseCore(t) | |
| 306 | - | c.cfg.Animations.On = false | |
| 307 | - | if c.pulseOn() { | |
| 308 | - | t.Fatal("pulse on with animations off") | |
| 309 | - | } | |
| 310 | - | c.cur().mon.Current().FocusID(1) | |
| 311 | - | indicatorScene(t, c) | |
| 312 | - | if c.pulse.timerC != nil || c.pulse.id != 0 { | |
| 313 | - | t.Fatal("pulse armed with animations off") | |
| 314 | - | } | |
| 315 | - | ic.now = ic.now.Add(pulseSettle) | |
| 316 | - | if c.pulseTick() || c.animating() { | |
| 317 | - | t.Fatal("pulse with animations off") | |
| 318 | - | } | |
| 319 | - | } | |
| @@ -1,319 +0,0 @@ | |||||
| 1 | - | package core | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | "time" | |||
| 6 | - | ||||
| 7 | - | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | - | ) | |||
| 9 | - | ||||
| 10 | - | // pulseCore is indicatorCore with the focus pulse on and two tiled windows, | |||
| 11 | - | // 2 focused, its pulse settled and over. | |||
| 12 | - | func pulseCore(t *testing.T) (*Core, *indicatorClock) { | |||
| 13 | - | t.Helper() | |||
| 14 | - | c, ic := indicatorCore(t) | |||
| 15 | - | c.cfg.Animations.On = true | |||
| 16 | - | c.cfg.Focus.Animation = ports.FocusAnimationPulse | |||
| 17 | - | c.cfg.Focus.Strength = 0.05 | |||
| 18 | - | c.cur().mon.AddWindow(1) | |||
| 19 | - | c.cur().mon.AddWindow(2) | |||
| 20 | - | indicatorScene(t, c) | |||
| 21 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 22 | - | c.pulseTick() | |||
| 23 | - | ic.now = ic.now.Add(pulseCooldown) | |||
| 24 | - | indicatorScene(t, c) | |||
| 25 | - | return c, ic | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | func pulseOf(s ports.Scene, id WindowID) float64 { | |||
| 29 | - | for _, w := range s.Windows { | |||
| 30 | - | if w.ID == id { | |||
| 31 | - | return w.FocusEffect | |||
| 32 | - | } | |||
| 33 | - | } | |||
| 34 | - | return -1 | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | func TestFocusPulseAfterSettle(t *testing.T) { | |||
| 38 | - | c, ic := pulseCore(t) | |||
| 39 | - | c.cur().mon.Current().FocusID(1) | |||
| 40 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 { | |||
| 41 | - | t.Fatalf("pulse before settle: %v", pulseOf(s, 1)) | |||
| 42 | - | } | |||
| 43 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 44 | - | if !c.pulseTick() { | |||
| 45 | - | t.Fatal("no pulse after settle") | |||
| 46 | - | } | |||
| 47 | - | if !c.animating() { | |||
| 48 | - | t.Fatal("a running pulse asks for no frames") | |||
| 49 | - | } | |||
| 50 | - | ic.now = ic.now.Add(pulseRise) | |||
| 51 | - | s := indicatorScene(t, c) | |||
| 52 | - | if p := pulseOf(s, 1); p < 0.05*0.99 || p > 0.05 { | |||
| 53 | - | t.Fatalf("peak %v, want the configured 0.05", p) | |||
| 54 | - | } | |||
| 55 | - | if pulseOf(s, 2) != 0 { | |||
| 56 | - | t.Fatal("unfocused window pulses") | |||
| 57 | - | } | |||
| 58 | - | ic.now = ic.now.Add(pulseFall) | |||
| 59 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || c.animating() { | |||
| 60 | - | t.Fatalf("pulse left after its end: %v", pulseOf(s, 1)) | |||
| 61 | - | } | |||
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | // Switching faster than pulseSettle never pulses: only the last focus counts. | |||
| 65 | - | func TestFocusPulseDebounced(t *testing.T) { | |||
| 66 | - | c, ic := pulseCore(t) | |||
| 67 | - | for range 4 { | |||
| 68 | - | c.cur().mon.Current().FocusID(1) | |||
| 69 | - | indicatorScene(t, c) | |||
| 70 | - | ic.now = ic.now.Add(pulseSettle / 3) | |||
| 71 | - | c.cur().mon.Current().FocusID(2) | |||
| 72 | - | indicatorScene(t, c) | |||
| 73 | - | ic.now = ic.now.Add(pulseSettle / 3) | |||
| 74 | - | } | |||
| 75 | - | // A stale timer of an earlier focus fires: the focus is too recent. | |||
| 76 | - | if c.pulseTick() { | |||
| 77 | - | t.Fatal("pulse while switching") | |||
| 78 | - | } | |||
| 79 | - | if c.pulse.timerC == nil { | |||
| 80 | - | t.Fatal("no timer for the last focus") | |||
| 81 | - | } | |||
| 82 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 83 | - | // Window 2 pulsed in pulseCore over a second ago: it may pulse again. | |||
| 84 | - | if !c.pulseTick() || c.pulse.id != 2 { | |||
| 85 | - | t.Fatalf("last focus did not pulse: %d", c.pulse.id) | |||
| 86 | - | } | |||
| 87 | - | } | |||
| 88 | - | ||||
| 89 | - | // A focus change stops the running pulse; coming back within the cooldown | |||
| 90 | - | // does not pulse the same window again. | |||
| 91 | - | func TestFocusPulseStopsAndCoolsDown(t *testing.T) { | |||
| 92 | - | c, ic := pulseCore(t) | |||
| 93 | - | c.cur().mon.Current().FocusID(1) | |||
| 94 | - | indicatorScene(t, c) | |||
| 95 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 96 | - | c.pulseTick() | |||
| 97 | - | ic.now = ic.now.Add(pulseRise / 2) | |||
| 98 | - | c.cur().mon.Current().FocusID(2) | |||
| 99 | - | if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || pulseOf(s, 2) != 0 { | |||
| 100 | - | t.Fatal("pulse survived a focus change") | |||
| 101 | - | } | |||
| 102 | - | c.cur().mon.Current().FocusID(1) | |||
| 103 | - | indicatorScene(t, c) | |||
| 104 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 105 | - | if c.pulseTick() { | |||
| 106 | - | t.Fatal("same window pulsed within the cooldown") | |||
| 107 | - | } | |||
| 108 | - | } | |||
| 109 | - | ||||
| 110 | - | // A window focused as it maps does not pulse; focusing it again later does. | |||
| 111 | - | func TestFocusPulseSkipsNewWindow(t *testing.T) { | |||
| 112 | - | c, ic := pulseCore(t) | |||
| 113 | - | c.windows.mapped(ports.WindowMapped{ID: 3}, ic.now) | |||
| 114 | - | c.cur().mon.AddWindow(3) | |||
| 115 | - | indicatorScene(t, c) | |||
| 116 | - | if c.pulse.timerC != nil { | |||
| 117 | - | t.Fatal("opening a window arms the pulse") | |||
| 118 | - | } | |||
| 119 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 120 | - | if c.pulseTick() || c.animating() { | |||
| 121 | - | t.Fatal("a new window pulses") | |||
| 122 | - | } | |||
| 123 | - | c.cur().mon.Current().FocusID(1) | |||
| 124 | - | indicatorScene(t, c) | |||
| 125 | - | c.cur().mon.Current().FocusID(3) | |||
| 126 | - | indicatorScene(t, c) | |||
| 127 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 128 | - | if !c.pulseTick() || c.pulse.id != 3 { | |||
| 129 | - | t.Fatal("an existing window did not pulse") | |||
| 130 | - | } | |||
| 131 | - | } | |||
| 132 | - | ||||
| 133 | - | // A window that mapped earlier without the focus pulses when focused. | |||
| 134 | - | func TestFocusPulseWindowMappedEarlier(t *testing.T) { | |||
| 135 | - | c, ic := pulseCore(t) | |||
| 136 | - | c.windows.mapped(ports.WindowMapped{ID: 1}, ic.now.Add(-time.Minute)) | |||
| 137 | - | c.cur().mon.Current().FocusID(1) | |||
| 138 | - | indicatorScene(t, c) | |||
| 139 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 140 | - | if !c.pulseTick() || c.pulse.id != 1 { | |||
| 141 | - | t.Fatal("a window mapped earlier did not pulse") | |||
| 142 | - | } | |||
| 143 | - | } | |||
| 144 | - | ||||
| 145 | - | func TestFocusPulseSkipsFullscreen(t *testing.T) { | |||
| 146 | - | c, ic := pulseCore(t) | |||
| 147 | - | c.cur().mon.Current().FocusID(1) | |||
| 148 | - | c.cur().mon.ToggleFullscreen() | |||
| 149 | - | indicatorScene(t, c) | |||
| 150 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 151 | - | if c.pulseTick() || c.animating() { | |||
| 152 | - | t.Fatal("fullscreen window asks for pulse frames") | |||
| 153 | - | } | |||
| 154 | - | // Fullscreen entered mid-pulse ends it. | |||
| 155 | - | c.cur().mon.ToggleFullscreen() | |||
| 156 | - | c.cur().mon.Current().FocusID(2) | |||
| 157 | - | indicatorScene(t, c) | |||
| 158 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 159 | - | if !c.pulseTick() { | |||
| 160 | - | t.Fatal("no pulse on the tiled window") | |||
| 161 | - | } | |||
| 162 | - | c.cur().mon.ToggleFullscreen() | |||
| 163 | - | if s := indicatorScene(t, c); pulseOf(s, 2) != 0 || c.animating() { | |||
| 164 | - | t.Fatal("pulse survived fullscreen") | |||
| 165 | - | } | |||
| 166 | - | } | |||
| 167 | - | ||||
| 168 | - | // A window alone on screen (a maximized column here) needs no pulse; once | |||
| 169 | - | // another window shows beside it, focusing it pulses again. | |||
| 170 | - | func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |||
| 171 | - | c, ic := pulseCore(t) | |||
| 172 | - | c.cur().mon.Current().FocusID(1) | |||
| 173 | - | c.cur().mon.Current().ToggleFullWidth() | |||
| 174 | - | indicatorScene(t, c) | |||
| 175 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 176 | - | if c.pulseTick() || c.animating() { | |||
| 177 | - | t.Fatal("a window alone on screen pulses") | |||
| 178 | - | } | |||
| 179 | - | c.cur().mon.Current().ToggleFullWidth() | |||
| 180 | - | c.cur().mon.Current().FocusID(2) | |||
| 181 | - | indicatorScene(t, c) | |||
| 182 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 183 | - | if !c.pulseTick() || c.pulse.id != 2 { | |||
| 184 | - | t.Fatal("a window beside another did not pulse") | |||
| 185 | - | } | |||
| 186 | - | } | |||
| 187 | - | ||||
| 188 | - | // A cascade window alone in its band above a bottom panel is alone on | |||
| 189 | - | // screen: the next band, under the panel, does not count. | |||
| 190 | - | func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) { | |||
| 191 | - | c, ic := pulseCore(t) | |||
| 192 | - | m := c.cur().mon | |||
| 193 | - | m.SetOverflow(OverflowCascade) | |||
| 194 | - | m.SetMaxColumns(1) | |||
| 195 | - | m.SetUsable(Rect{W: 300, H: 180}) | |||
| 196 | - | m.Current().FocusID(1) | |||
| 197 | - | indicatorScene(t, c) | |||
| 198 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 199 | - | if c.pulseTick() || c.animating() { | |||
| 200 | - | t.Fatal("a window alone in its band above a panel pulses") | |||
| 201 | - | } | |||
| 202 | - | } | |||
| 203 | - | ||||
| 204 | - | // A swipe, its landing slide or a transition still shows the neighbours: the | |||
| 205 | - | // pulse waits for the layout to settle, then decides on it. | |||
| 206 | - | func TestFocusPulseWaitsForSlide(t *testing.T) { | |||
| 207 | - | for _, tc := range []struct { | |||
| 208 | - | name string | |||
| 209 | - | slide func(*testing.T, *Core, time.Time) | |||
| 210 | - | alone bool | |||
| 211 | - | }{ | |||
| 212 | - | {"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) { | |||
| 213 | - | c.cur().mon.switchView.motion = newMotion(workspaceSpring(0.5, 0), now, 1) | |||
| 214 | - | }, true}, | |||
| 215 | - | {"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchView.off = 0.3 }, true}, | |||
| 216 | - | {"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().view.off = 40 }, false}, | |||
| 217 | - | {"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) { | |||
| 218 | - | sc := c.cur() | |||
| 219 | - | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}} | |||
| 220 | - | sc.rectsWS = sc.mon.Current() | |||
| 221 | - | }, false}, | |||
| 222 | - | {"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) { | |||
| 223 | - | m := c.cur().mon | |||
| 224 | - | for id := WindowID(3); id <= 4; id++ { | |||
| 225 | - | m.AddWindow(id) | |||
| 226 | - | } | |||
| 227 | - | indicatorScene(t, c) | |||
| 228 | - | before := c.snapshot(now) | |||
| 229 | - | for range 3 { | |||
| 230 | - | c.applyAction(ActionFocusColumnLeft) | |||
| 231 | - | } | |||
| 232 | - | c.transition(before, now) | |||
| 233 | - | if !m.Current().view.motion.on || m.settled() { | |||
| 234 | - | t.Fatal("no camera transition after the focus moved the view") | |||
| 235 | - | } | |||
| 236 | - | }, false}, | |||
| 237 | - | } { | |||
| 238 | - | t.Run(tc.name, func(t *testing.T) { | |||
| 239 | - | c, ic := pulseCore(t) | |||
| 240 | - | m := c.cur().mon | |||
| 241 | - | m.Current().FocusID(1) | |||
| 242 | - | indicatorScene(t, c) | |||
| 243 | - | tc.slide(t, c, ic.now) | |||
| 244 | - | indicatorScene(t, c) | |||
| 245 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 246 | - | if c.pulseTick() || c.pulse.timerC == nil { | |||
| 247 | - | t.Fatal("pulse decided during a slide") | |||
| 248 | - | } | |||
| 249 | - | m.stopSwitch() | |||
| 250 | - | m.Current().view.stop() | |||
| 251 | - | c.cur().stopRects() | |||
| 252 | - | if tc.alone { | |||
| 253 | - | m.Current().ToggleFullWidth() | |||
| 254 | - | } | |||
| 255 | - | indicatorScene(t, c) | |||
| 256 | - | ic.now = ic.now.Add(pulseRecheck) | |||
| 257 | - | if got := c.pulseTick(); got == tc.alone { | |||
| 258 | - | t.Fatalf("pulse started %v after landing, window alone %v", got, tc.alone) | |||
| 259 | - | } | |||
| 260 | - | }) | |||
| 261 | - | } | |||
| 262 | - | } | |||
| 263 | - | ||||
| 264 | - | // A popup grab or layer taking the keyboard stops the pulse; the window | |||
| 265 | - | // getting it back is not a new focus and does not pulse again. | |||
| 266 | - | func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) { | |||
| 267 | - | c, ic := pulseCore(t) | |||
| 268 | - | c.cur().mon.Current().FocusID(1) | |||
| 269 | - | indicatorScene(t, c) | |||
| 270 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 271 | - | c.pulseTick() | |||
| 272 | - | // The keyboard goes elsewhere (held false), then comes back. | |||
| 273 | - | c.pulseFocus(1, false) | |||
| 274 | - | if c.animating() { | |||
| 275 | - | t.Fatal("pulse kept while the keyboard is elsewhere") | |||
| 276 | - | } | |||
| 277 | - | ic.now = ic.now.Add(2 * pulseCooldown) | |||
| 278 | - | c.pulseFocus(1, true) | |||
| 279 | - | if c.pulse.timerC != nil { | |||
| 280 | - | t.Fatal("returning keyboard counts as a new focus") | |||
| 281 | - | } | |||
| 282 | - | } | |||
| 283 | - | ||||
| 284 | - | func TestFocusPulseOff(t *testing.T) { | |||
| 285 | - | c, ic := pulseCore(t) | |||
| 286 | - | c.cfg.Focus.Animation = ports.FocusAnimationOff | |||
| 287 | - | c.cur().mon.Current().FocusID(1) | |||
| 288 | - | indicatorScene(t, c) | |||
| 289 | - | if c.pulse.timerC != nil { | |||
| 290 | - | t.Fatal("timer armed with the pulse off") | |||
| 291 | - | } | |||
| 292 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 293 | - | if c.pulseTick() || c.animating() { | |||
| 294 | - | t.Fatal("pulse with focus.animation off") | |||
| 295 | - | } | |||
| 296 | - | // Turning it back on does not pulse the window already focused. | |||
| 297 | - | c.cfg.Focus.Animation = ports.FocusAnimationPulse | |||
| 298 | - | indicatorScene(t, c) | |||
| 299 | - | if c.pulse.timerC != nil { | |||
| 300 | - | t.Fatal("enabling the pulse pulses the focused window") | |||
| 301 | - | } | |||
| 302 | - | } | |||
| 303 | - | ||||
| 304 | - | func TestFocusPulseOffWithAnimationsOff(t *testing.T) { | |||
| 305 | - | c, ic := pulseCore(t) | |||
| 306 | - | c.cfg.Animations.On = false | |||
| 307 | - | if c.pulseOn() { | |||
| 308 | - | t.Fatal("pulse on with animations off") | |||
| 309 | - | } | |||
| 310 | - | c.cur().mon.Current().FocusID(1) | |||
| 311 | - | indicatorScene(t, c) | |||
| 312 | - | if c.pulse.timerC != nil || c.pulse.id != 0 { | |||
| 313 | - | t.Fatal("pulse armed with animations off") | |||
| 314 | - | } | |||
| 315 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 316 | - | if c.pulseTick() || c.animating() { | |||
| 317 | - | t.Fatal("pulse with animations off") | |||
| 318 | - | } | |||
| 319 | - | } | |||
M internal/core/scene.go +3 -12
| @@ -8,8 +8,7 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 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. | |
| 12 | - | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | |
| 11 | + | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView) (scene ports.Scene, err error) { | |
| 13 | 12 | o := sc.mon.Output() | |
| 14 | 13 | // view.frame is the viewport of the workspace on screen: the whole | |
| 15 | 14 | // output unless it has a size override (never in the overview). | |
| @@ -68,8 +67,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 68 | 67 | // Only the focused output lights the focused window's lines. | |
| 69 | 68 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | |
| 70 | 69 | } | |
| 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 | 70 | for k, p := range layout { | |
| 74 | 71 | ps := settled[k] | |
| 75 | 72 | // Only the focused output has an activated window. | |
| @@ -97,12 +94,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 97 | 94 | } | |
| 98 | 95 | continue | |
| 99 | 96 | } | |
| 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 | 97 | scene.Windows = append(scene.Windows, sw) | |
| 107 | 98 | t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID} | |
| 108 | 99 | if !p.Hidden && p.Preview == 0 { | |
| @@ -120,7 +111,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 120 | 111 | cp, t := c.captureConfigure(sc, ps, t) | |
| 121 | 112 | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | |
| 122 | 113 | if err := c.command(ctx, v); err != nil { | |
| 123 | - | return ports.Scene{}, false, err | |
| 114 | + | return ports.Scene{}, err | |
| 124 | 115 | } | |
| 125 | 116 | c.configures.mark(v) | |
| 126 | 117 | } | |
| @@ -160,5 +151,5 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 160 | 151 | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | |
| 161 | 152 | } | |
| 162 | 153 | } | |
| 163 | - | return scene, drawable, nil | |
| 154 | + | return scene, nil | |
| 164 | 155 | } | |
| @@ -8,8 +8,7 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | // sceneFor builds the scene of screen i and sends the configures it sizes. | 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. | 11 | + | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView) (scene ports.Scene, err error) { |
| 12 | - | func (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() | 12 | o := sc.mon.Output() | ||
| 14 | // view.frame is the viewport of the workspace on screen: the whole | 13 | // view.frame is the viewport of the workspace on screen: the whole | ||
| 15 | // output unless it has a size override (never in the overview). | 14 | // output unless it has a size override (never in the overview). | ||
| @@ -68,8 +67,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 68 | // Only the focused output lights the focused window's lines. | 67 | // 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) | 68 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | ||
| 70 | } | 69 | } | ||
| 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 { | 70 | for k, p := range layout { | ||
| 74 | ps := settled[k] | 71 | ps := settled[k] | ||
| 75 | // Only the focused output has an activated window. | 72 | // Only the focused output has an activated window. | ||
| @@ -97,12 +94,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 97 | } | 94 | } | ||
| 98 | continue | 95 | continue | ||
| 99 | } | 96 | } | ||
| 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) | 97 | scene.Windows = append(scene.Windows, sw) | ||
| 107 | t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID} | 98 | t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID} | ||
| 108 | if !p.Hidden && p.Preview == 0 { | 99 | if !p.Hidden && p.Preview == 0 { | ||
| @@ -120,7 +111,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 120 | cp, t := c.captureConfigure(sc, ps, t) | 111 | cp, t := c.captureConfigure(sc, ps, t) | ||
| 121 | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | 112 | if v, send := c.configures.nextWithCapture(ps, t, cp); send { | ||
| 122 | if err := c.command(ctx, v); err != nil { | 113 | if err := c.command(ctx, v); err != nil { | ||
| 123 | - | return ports.Scene{}, false, err | 114 | + | return ports.Scene{}, err |
| 124 | } | 115 | } | ||
| 125 | c.configures.mark(v) | 116 | c.configures.mark(v) | ||
| 126 | } | 117 | } | ||
| @@ -160,5 +151,5 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 160 | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | 151 | scene.CaptureScene = c.captureWindowScene(capture, scene.Seq) | ||
| 161 | } | 152 | } | ||
| 162 | } | 153 | } | ||
| 163 | - | return scene, drawable, nil | 154 | + | return scene, nil |
| 164 | } | 155 | } | ||
M internal/core/scene_animating_test.go +9 -25
| @@ -55,33 +55,17 @@ func TestSceneAnimatingFollowsSprings(t *testing.T) { | |||
| 55 | 55 | } | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | - | // A pulse cancelled because nothing can show it (fullscreen) publishes the | |
| 59 | - | // focused scene settled, with a fresh Seq: otherwise outputs keep the | |
| 60 | - | // animating scene and wake core on every flip. | |
| 61 | - | func TestCancelledPulseClearsAnimating(t *testing.T) { | |
| 62 | - | c, ic := pulseCore(t) | |
| 63 | - | c.cur().mon.Current().FocusID(1) | |
| 64 | - | indicatorScene(t, c) | |
| 65 | - | ic.now = ic.now.Add(pulseSettle) | |
| 66 | - | if !c.pulseTick() { | |
| 67 | - | t.Fatal("bad setup: no pulse") | |
| 68 | - | } | |
| 69 | - | running := indicatorScene(t, c) | |
| 70 | - | if !running.Animating { | |
| 71 | - | t.Fatal("bad setup: the pulsing scene is not animating") | |
| 72 | - | } | |
| 73 | - | c.cur().mon.ToggleFullscreen() | |
| 74 | - | cancelled := indicatorScene(t, c) | |
| 75 | - | if cancelled.Animating || c.animating() { | |
| 76 | - | t.Fatalf("cancelled pulse: scene animating=%v core animating=%v", cancelled.Animating, c.animating()) | |
| 77 | - | } | |
| 78 | - | if cancelled.Seq == running.Seq { | |
| 79 | - | t.Fatalf("cancelled pulse kept the animating scene's Seq %d", running.Seq) | |
| 58 | + | // stepScenes steps only the screen and returns the scene of each output. | |
| 59 | + | func stepScenes(t *testing.T, c *Core, only *screen) map[string]ports.Scene { | |
| 60 | + | t.Helper() | |
| 61 | + | if err := c.step(context.Background(), only); err != nil { | |
| 62 | + | t.Fatal(err) | |
| 80 | 63 | } | |
| 81 | - | idle := indicatorScene(t, c) | |
| 82 | - | if idle.Animating || idle.Seq != cancelled.Seq { | |
| 83 | - | t.Fatalf("idle publish after cancel: animating=%v Seq %d (was %d)", idle.Animating, idle.Seq, cancelled.Seq) | |
| 64 | + | out := map[string]ports.Scene{} | |
| 65 | + | for _, s := range <-c.ch.Scenes { | |
| 66 | + | out[s.Output] = s | |
| 84 | 67 | } | |
| 68 | + | return out | |
| 85 | 69 | } | |
| 86 | 70 | ||
| 87 | 71 | // Animating is per output: a spring on one does not mark the other. | |
| @@ -55,33 +55,17 @@ func TestSceneAnimatingFollowsSprings(t *testing.T) { | |||||
| 55 | } | 55 | } | ||
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | - | // A pulse cancelled because nothing can show it (fullscreen) publishes the | 58 | + | // stepScenes steps only the screen and returns the scene of each output. |
| 59 | - | // focused scene settled, with a fresh Seq: otherwise outputs keep the | 59 | + | func stepScenes(t *testing.T, c *Core, only *screen) map[string]ports.Scene { |
| 60 | - | // animating scene and wake core on every flip. | 60 | + | t.Helper() |
| 61 | - | func TestCancelledPulseClearsAnimating(t *testing.T) { | 61 | + | if err := c.step(context.Background(), only); err != nil { |
| 62 | - | c, ic := pulseCore(t) | 62 | + | t.Fatal(err) |
| 63 | - | c.cur().mon.Current().FocusID(1) | |||
| 64 | - | indicatorScene(t, c) | |||
| 65 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 66 | - | if !c.pulseTick() { | |||
| 67 | - | t.Fatal("bad setup: no pulse") | |||
| 68 | - | } | |||
| 69 | - | running := indicatorScene(t, c) | |||
| 70 | - | if !running.Animating { | |||
| 71 | - | t.Fatal("bad setup: the pulsing scene is not animating") | |||
| 72 | - | } | |||
| 73 | - | c.cur().mon.ToggleFullscreen() | |||
| 74 | - | cancelled := indicatorScene(t, c) | |||
| 75 | - | if cancelled.Animating || c.animating() { | |||
| 76 | - | t.Fatalf("cancelled pulse: scene animating=%v core animating=%v", cancelled.Animating, c.animating()) | |||
| 77 | - | } | |||
| 78 | - | if cancelled.Seq == running.Seq { | |||
| 79 | - | t.Fatalf("cancelled pulse kept the animating scene's Seq %d", running.Seq) | |||
| 80 | } | 63 | } | ||
| 81 | - | idle := indicatorScene(t, c) | 64 | + | out := map[string]ports.Scene{} |
| 82 | - | if idle.Animating || idle.Seq != cancelled.Seq { | 65 | + | for _, s := range <-c.ch.Scenes { |
| 83 | - | t.Fatalf("idle publish after cancel: animating=%v Seq %d (was %d)", idle.Animating, idle.Seq, cancelled.Seq) | 66 | + | out[s.Output] = s |
| 84 | } | 67 | } | ||
| 68 | + | return out | |||
| 85 | } | 69 | } | ||
| 86 | 70 | ||||
| 87 | // Animating is per output: a spring on one does not mark the other. | 71 | // Animating is per output: a spring on one does not mark the other. | ||
M internal/core/screens.go +0 -4
| @@ -56,10 +56,6 @@ type screen struct { | |||
| 56 | 56 | // springing reports whether a camera or rect spring runs on the screen. | |
| 57 | 57 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | |
| 58 | 58 | ||
| 59 | - | // settled reports whether nothing moves the screen's layout: no swipe, | |
| 60 | - | // no slide, no rect motion. | |
| 61 | - | func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 } | |
| 62 | - | ||
| 63 | 59 | // stopRects drops the rect motions: the windows are drawn where they are. | |
| 64 | 60 | func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil } | |
| 65 | 61 | ||
| @@ -56,10 +56,6 @@ type screen struct { | |||||
| 56 | // springing reports whether a camera or rect spring runs on the screen. | 56 | // springing reports whether a camera or rect spring runs on the screen. | ||
| 57 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | 57 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | ||
| 58 | 58 | ||||
| 59 | - | // settled reports whether nothing moves the screen's layout: no swipe, | |||
| 60 | - | // no slide, no rect motion. | |||
| 61 | - | func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 } | |||
| 62 | - | ||||
| 63 | // stopRects drops the rect motions: the windows are drawn where they are. | 59 | // stopRects drops the rect motions: the windows are drawn where they are. | ||
| 64 | func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil } | 60 | func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil } | ||
| 65 | 61 | ||||
M internal/core/security.go +0 -1
| @@ -47,7 +47,6 @@ func (c *Core) syncSecurity() bool { | |||
| 47 | 47 | // protected epoch clears that focus. | |
| 48 | 48 | c.gesture = clientGesture{} | |
| 49 | 49 | c.stopAnimations() | |
| 50 | - | c.stopPulse() | |
| 51 | 50 | c.cancelSwitcher() | |
| 52 | 51 | c.stopRepeat() | |
| 53 | 52 | if state.Protected { | |
| @@ -47,7 +47,6 @@ func (c *Core) syncSecurity() bool { | |||||
| 47 | // protected epoch clears that focus. | 47 | // protected epoch clears that focus. | ||
| 48 | c.gesture = clientGesture{} | 48 | c.gesture = clientGesture{} | ||
| 49 | c.stopAnimations() | 49 | c.stopAnimations() | ||
| 50 | - | c.stopPulse() | |||
| 51 | c.cancelSwitcher() | 50 | c.cancelSwitcher() | ||
| 52 | c.stopRepeat() | 51 | c.stopRepeat() | ||
| 53 | if state.Protected { | 52 | if state.Protected { | ||
M internal/core/transition.go +1 -2
| @@ -701,8 +701,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 701 | 701 | // while it shows on another) is animated on both screens: the destination's | |
| 702 | 702 | // windows slide in from where the source drew them (no workspace slide | |
| 703 | 703 | // there), the source slides to its next workspace. The focus-monitor-* | |
| 704 | - | // actions move nothing: no window changes place, so they are not animated | |
| 705 | - | // (the focus pulse marks the new focus). | |
| 704 | + | // actions move nothing: no window changes place, so they are not animated. | |
| 706 | 705 | func (c *Core) transition(before []viewShot, now time.Time) { | |
| 707 | 706 | if !c.animOn() { | |
| 708 | 707 | return | |
| @@ -701,8 +701,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 701 | // while it shows on another) is animated on both screens: the destination's | 701 | // while it shows on another) is animated on both screens: the destination's | ||
| 702 | // windows slide in from where the source drew them (no workspace slide | 702 | // windows slide in from where the source drew them (no workspace slide | ||
| 703 | // there), the source slides to its next workspace. The focus-monitor-* | 703 | // there), the source slides to its next workspace. The focus-monitor-* | ||
| 704 | - | // actions move nothing: no window changes place, so they are not animated | 704 | + | // actions move nothing: no window changes place, so they are not animated. |
| 705 | - | // (the focus pulse marks the new focus). | |||
| 706 | func (c *Core) transition(before []viewShot, now time.Time) { | 705 | func (c *Core) transition(before []viewShot, now time.Time) { | ||
| 707 | if !c.animOn() { | 706 | if !c.animOn() { | ||
| 708 | return | 707 | return | ||
M internal/core/viewport_test.go +4 -6
| @@ -184,20 +184,18 @@ func TestTileStopsAtPanel(t *testing.T) { | |||
| 184 | 184 | } | |
| 185 | 185 | } | |
| 186 | 186 | ||
| 187 | - | // Panels that leave no room draw no tile, a leaving one included, and the | |
| 188 | - | // focus pulse has nothing to show. | |
| 187 | + | // Panels that leave no room draw no tile, a leaving one included. | |
| 189 | 188 | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | |
| 190 | 189 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | |
| 191 | 190 | c.cur().mon.AddWindow(1) | |
| 192 | - | c.pulse.target = 1 | |
| 193 | 191 | scene := check(map[WindowID]bool{1: false}) | |
| 194 | - | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { | |
| 195 | - | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) | |
| 192 | + | if scene.Draws(scene.Windows[0]) { | |
| 193 | + | t.Fatalf("tile drawn with no usable area") | |
| 196 | 194 | } | |
| 197 | 195 | sc := c.cur() | |
| 198 | 196 | sc.shown = slices.Clone(sc.shownLayout()) | |
| 199 | 197 | sc.shown[0].Leaving = true | |
| 200 | - | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | |
| 198 | + | scene, err := c.sceneFor(context.Background(), 0, sc, nil, nil) | |
| 201 | 199 | if err != nil { | |
| 202 | 200 | t.Fatal(err) | |
| 203 | 201 | } | |
| @@ -184,20 +184,18 @@ func TestTileStopsAtPanel(t *testing.T) { | |||||
| 184 | } | 184 | } | ||
| 185 | } | 185 | } | ||
| 186 | 186 | ||||
| 187 | - | // Panels that leave no room draw no tile, a leaving one included, and the | 187 | + | // Panels that leave no room draw no tile, a leaving one included. |
| 188 | - | // focus pulse has nothing to show. | |||
| 189 | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | 188 | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | ||
| 190 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | 189 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | ||
| 191 | c.cur().mon.AddWindow(1) | 190 | c.cur().mon.AddWindow(1) | ||
| 192 | - | c.pulse.target = 1 | |||
| 193 | scene := check(map[WindowID]bool{1: false}) | 191 | scene := check(map[WindowID]bool{1: false}) | ||
| 194 | - | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { | 192 | + | if scene.Draws(scene.Windows[0]) { |
| 195 | - | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) | 193 | + | t.Fatalf("tile drawn with no usable area") |
| 196 | } | 194 | } | ||
| 197 | sc := c.cur() | 195 | sc := c.cur() | ||
| 198 | sc.shown = slices.Clone(sc.shownLayout()) | 196 | sc.shown = slices.Clone(sc.shownLayout()) | ||
| 199 | sc.shown[0].Leaving = true | 197 | sc.shown[0].Leaving = true | ||
| 200 | - | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | 198 | + | scene, err := c.sceneFor(context.Background(), 0, sc, nil, nil) |
| 201 | if err != nil { | 199 | if err != nil { | ||
| 202 | t.Fatal(err) | 200 | t.Fatal(err) | ||
| 203 | } | 201 | } | ||
M internal/ports/config.go +11 -14
| @@ -89,14 +89,6 @@ type OutputLayout struct { | |||
| 89 | 89 | LayoutRules | |
| 90 | 90 | } | |
| 91 | 91 | ||
| 92 | - | // Focus indicator: the animation (Config.Focus.Animation) shapes in time | |
| 93 | - | // the visual effect (Config.Focus.Effect) drawn on a newly focused window. | |
| 94 | - | const ( | |
| 95 | - | FocusAnimationOff = "off" | |
| 96 | - | FocusAnimationPulse = "pulse" | |
| 97 | - | FocusEffectScreen = "screen" | |
| 98 | - | ) | |
| 99 | - | ||
| 100 | 92 | // Config is the parsed compositor configuration (see the config adapter for keys). | |
| 101 | 93 | type Config struct { | |
| 102 | 94 | Keyboard struct { | |
| @@ -148,7 +140,7 @@ type Config struct { | |||
| 148 | 140 | // motion; the next motion shows it. 0 never hides it. | |
| 149 | 141 | Cursor struct{ HideAfter time.Duration } | |
| 150 | 142 | // Animations: On enables motion (off lands everything instantly); | |
| 151 | - | // Slowdown scales every spring's duration (not the focus pulse), 0.1 to | |
| 143 | + | // Slowdown scales every spring's duration, 0.1 to | |
| 152 | 144 | // 10 (the config's animations.speed maps to it). | |
| 153 | 145 | Animations struct { | |
| 154 | 146 | On bool | |
| @@ -158,11 +150,16 @@ type Config struct { | |||
| 158 | 150 | // FollowMove shows the target workspace after a column or window | |
| 159 | 151 | // moves to it. | |
| 160 | 152 | FollowMove bool | |
| 161 | - | // Animation and Effect mark a window that just got the focus: | |
| 162 | - | // FocusAnimation* (off disables it) and FocusEffect*. Strength is | |
| 163 | - | // the effect's peak, 0.01 to 0.2. | |
| 164 | - | Animation, Effect string | |
| 165 | - | Strength float64 | |
| 153 | + | // Flash briefly draws an inner border on the entity an action changed | |
| 154 | + | // (window, column, output). FlashWidth is in logical pixels (1 to 8), | |
| 155 | + | // FlashOpacity the starting opacity (0.1 to 1), FlashDuration the fade | |
| 156 | + | // (50ms to 1s, not scaled by Animations.Slowdown), FlashColor #rrggbb or | |
| 157 | + | // empty for Border.Active. | |
| 158 | + | Flash bool | |
| 159 | + | FlashWidth int | |
| 160 | + | FlashOpacity float64 | |
| 161 | + | FlashDuration time.Duration | |
| 162 | + | FlashColor string | |
| 166 | 163 | } | |
| 167 | 164 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | |
| 168 | 165 | Workspaces []WorkspaceConfig | |
| @@ -89,14 +89,6 @@ type OutputLayout struct { | |||||
| 89 | LayoutRules | 89 | LayoutRules | ||
| 90 | } | 90 | } | ||
| 91 | 91 | ||||
| 92 | - | // Focus indicator: the animation (Config.Focus.Animation) shapes in time | |||
| 93 | - | // the visual effect (Config.Focus.Effect) drawn on a newly focused window. | |||
| 94 | - | const ( | |||
| 95 | - | FocusAnimationOff = "off" | |||
| 96 | - | FocusAnimationPulse = "pulse" | |||
| 97 | - | FocusEffectScreen = "screen" | |||
| 98 | - | ) | |||
| 99 | - | ||||
| 100 | // Config is the parsed compositor configuration (see the config adapter for keys). | 92 | // Config is the parsed compositor configuration (see the config adapter for keys). | ||
| 101 | type Config struct { | 93 | type Config struct { | ||
| 102 | Keyboard struct { | 94 | Keyboard struct { | ||
| @@ -148,7 +140,7 @@ type Config struct { | |||||
| 148 | // motion; the next motion shows it. 0 never hides it. | 140 | // motion; the next motion shows it. 0 never hides it. | ||
| 149 | Cursor struct{ HideAfter time.Duration } | 141 | Cursor struct{ HideAfter time.Duration } | ||
| 150 | // Animations: On enables motion (off lands everything instantly); | 142 | // Animations: On enables motion (off lands everything instantly); | ||
| 151 | - | // Slowdown scales every spring's duration (not the focus pulse), 0.1 to | 143 | + | // Slowdown scales every spring's duration, 0.1 to |
| 152 | // 10 (the config's animations.speed maps to it). | 144 | // 10 (the config's animations.speed maps to it). | ||
| 153 | Animations struct { | 145 | Animations struct { | ||
| 154 | On bool | 146 | On bool | ||
| @@ -158,11 +150,16 @@ type Config struct { | |||||
| 158 | // FollowMove shows the target workspace after a column or window | 150 | // FollowMove shows the target workspace after a column or window | ||
| 159 | // moves to it. | 151 | // moves to it. | ||
| 160 | FollowMove bool | 152 | FollowMove bool | ||
| 161 | - | // Animation and Effect mark a window that just got the focus: | 153 | + | // Flash briefly draws an inner border on the entity an action changed |
| 162 | - | // FocusAnimation* (off disables it) and FocusEffect*. Strength is | 154 | + | // (window, column, output). FlashWidth is in logical pixels (1 to 8), |
| 163 | - | // the effect's peak, 0.01 to 0.2. | 155 | + | // FlashOpacity the starting opacity (0.1 to 1), FlashDuration the fade |
| 164 | - | Animation, Effect string | 156 | + | // (50ms to 1s, not scaled by Animations.Slowdown), FlashColor #rrggbb or |
| 165 | - | Strength float64 | 157 | + | // empty for Border.Active. |
| 158 | + | Flash bool | |||
| 159 | + | FlashWidth int | |||
| 160 | + | FlashOpacity float64 | |||
| 161 | + | FlashDuration time.Duration | |||
| 162 | + | FlashColor string | |||
| 166 | } | 163 | } | ||
| 167 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | 164 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | ||
| 168 | Workspaces []WorkspaceConfig | 165 | Workspaces []WorkspaceConfig | ||
M internal/ports/scene.go +15 -11
| @@ -69,7 +69,10 @@ type Scene struct { | |||
| 69 | 69 | // drawn over everything; nil when none. Captures never hold them: the | |
| 70 | 70 | // capture pipeline serves requests from a scene without them. | |
| 71 | 71 | CaptureIndicators []CaptureIndicator | |
| 72 | - | // Animating is set by core when a spring or the focus pulse runs on | |
| 72 | + | // Flash is the focus flash drawn over everything of this output; nil | |
| 73 | + | // draws nothing. | |
| 74 | + | Flash *FocusFlash | |
| 75 | + | // Animating is set by core when a spring or a focus flash runs on | |
| 73 | 76 | // this output after this frame was computed: the output reports the | |
| 74 | 77 | // flip of the scene to core (OutputFrame), which moves the animation to | |
| 75 | 78 | // the next frame. A scene of a settled output carries false, so a flip | |
| @@ -176,6 +179,17 @@ type SceneLayer struct { | |||
| 176 | 179 | Rect Rect | |
| 177 | 180 | } | |
| 178 | 181 | ||
| 182 | + | // FocusFlash is a brief inner border marking the entity an action just | |
| 183 | + | // changed: a window, a column or a whole output. Rect is in logical | |
| 184 | + | // pixels, Width the border width inside it, Color #rrggbb and Alpha its | |
| 185 | + | // opacity at this frame (0 to 1). | |
| 186 | + | type FocusFlash struct { | |
| 187 | + | Rect Rect | |
| 188 | + | Width int | |
| 189 | + | Color string | |
| 190 | + | Alpha float64 | |
| 191 | + | } | |
| 192 | + | ||
| 179 | 193 | // SceneWindow carries core → renderer window placement. | |
| 180 | 194 | type SceneWindow struct { | |
| 181 | 195 | ID WindowID | |
| @@ -195,16 +209,6 @@ type SceneWindow struct { | |||
| 195 | 209 | // or leaving animates it; a faded window is composed, never scanned | |
| 196 | 210 | // out. | |
| 197 | 211 | Fade float64 | |
| 198 | - | // FocusEffect, above 0, is the focus indicator's effect at this frame | |
| 199 | - | // (focus.animation, focus.effect): the window's surfaces are lifted | |
| 200 | - | // toward white by that fraction (screen blend). Renderers without the | |
| 201 | - | // effect ignore it. | |
| 202 | - | // | |
| 203 | - | // Only focus.effect = screen exists, so the effect kind is not carried: | |
| 204 | - | // core ignores Config.Focus.Effect and the shaders always apply screen | |
| 205 | - | // (compose.frag, compose_hdr.frag, push constant mapy.z). A second effect | |
| 206 | - | // needs its kind here and in the push constants (a misc flag). | |
| 207 | - | FocusEffect float64 | |
| 208 | 212 | // Preview, above 0, draws the window's surfaces that much smaller in | |
| 209 | 213 | // Rect (an overview thumbnail): the client keeps its size. A preview | |
| 210 | 214 | // is a card: it never opens the floats. | |
| @@ -69,7 +69,10 @@ type Scene struct { | |||||
| 69 | // drawn over everything; nil when none. Captures never hold them: the | 69 | // drawn over everything; nil when none. Captures never hold them: the | ||
| 70 | // capture pipeline serves requests from a scene without them. | 70 | // capture pipeline serves requests from a scene without them. | ||
| 71 | CaptureIndicators []CaptureIndicator | 71 | CaptureIndicators []CaptureIndicator | ||
| 72 | - | // Animating is set by core when a spring or the focus pulse runs on | 72 | + | // Flash is the focus flash drawn over everything of this output; nil |
| 73 | + | // draws nothing. | |||
| 74 | + | Flash *FocusFlash | |||
| 75 | + | // Animating is set by core when a spring or a focus flash runs on | |||
| 73 | // this output after this frame was computed: the output reports the | 76 | // this output after this frame was computed: the output reports the | ||
| 74 | // flip of the scene to core (OutputFrame), which moves the animation to | 77 | // flip of the scene to core (OutputFrame), which moves the animation to | ||
| 75 | // the next frame. A scene of a settled output carries false, so a flip | 78 | // the next frame. A scene of a settled output carries false, so a flip | ||
| @@ -176,6 +179,17 @@ type SceneLayer struct { | |||||
| 176 | Rect Rect | 179 | Rect Rect | ||
| 177 | } | 180 | } | ||
| 178 | 181 | ||||
| 182 | + | // FocusFlash is a brief inner border marking the entity an action just | |||
| 183 | + | // changed: a window, a column or a whole output. Rect is in logical | |||
| 184 | + | // pixels, Width the border width inside it, Color #rrggbb and Alpha its | |||
| 185 | + | // opacity at this frame (0 to 1). | |||
| 186 | + | type FocusFlash struct { | |||
| 187 | + | Rect Rect | |||
| 188 | + | Width int | |||
| 189 | + | Color string | |||
| 190 | + | Alpha float64 | |||
| 191 | + | } | |||
| 192 | + | ||||
| 179 | // SceneWindow carries core → renderer window placement. | 193 | // SceneWindow carries core → renderer window placement. | ||
| 180 | type SceneWindow struct { | 194 | type SceneWindow struct { | ||
| 181 | ID WindowID | 195 | ID WindowID | ||
| @@ -195,16 +209,6 @@ type SceneWindow struct { | |||||
| 195 | // or leaving animates it; a faded window is composed, never scanned | 209 | // or leaving animates it; a faded window is composed, never scanned | ||
| 196 | // out. | 210 | // out. | ||
| 197 | Fade float64 | 211 | Fade float64 | ||
| 198 | - | // FocusEffect, above 0, is the focus indicator's effect at this frame | |||
| 199 | - | // (focus.animation, focus.effect): the window's surfaces are lifted | |||
| 200 | - | // toward white by that fraction (screen blend). Renderers without the | |||
| 201 | - | // effect ignore it. | |||
| 202 | - | // | |||
| 203 | - | // Only focus.effect = screen exists, so the effect kind is not carried: | |||
| 204 | - | // core ignores Config.Focus.Effect and the shaders always apply screen | |||
| 205 | - | // (compose.frag, compose_hdr.frag, push constant mapy.z). A second effect | |||
| 206 | - | // needs its kind here and in the push constants (a misc flag). | |||
| 207 | - | FocusEffect float64 | |||
| 208 | // Preview, above 0, draws the window's surfaces that much smaller in | 212 | // Preview, above 0, draws the window's surfaces that much smaller in | ||
| 209 | // Rect (an overview thumbnail): the client keeps its size. A preview | 213 | // Rect (an overview thumbnail): the client keeps its size. A preview | ||
| 210 | // is a card: it never opens the floats. | 214 | // is a card: it never opens the floats. | ||
M internal/ports/scene_same_test.go +2 -2
| @@ -83,9 +83,9 @@ func TestSceneSameAs(t *testing.T) { | |||
| 83 | 83 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | |
| 84 | 84 | // update Scene.SameAs, then this count. | |
| 85 | 85 | func TestSceneSameAsCoversEveryField(t *testing.T) { | |
| 86 | - | // 22 with Animating, which SameAs intentionally does not compare: it | |
| 86 | + | // 23 with Animating, which SameAs intentionally does not compare: it | |
| 87 | 87 | // only schedules the next frame, it draws nothing. | |
| 88 | - | const fields = 22 | |
| 88 | + | const fields = 23 | |
| 89 | 89 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | |
| 90 | 90 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | |
| 91 | 91 | } | |
| @@ -83,9 +83,9 @@ func TestSceneSameAs(t *testing.T) { | |||||
| 83 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | 83 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | ||
| 84 | // update Scene.SameAs, then this count. | 84 | // update Scene.SameAs, then this count. | ||
| 85 | func TestSceneSameAsCoversEveryField(t *testing.T) { | 85 | func TestSceneSameAsCoversEveryField(t *testing.T) { | ||
| 86 | - | // 22 with Animating, which SameAs intentionally does not compare: it | 86 | + | // 23 with Animating, which SameAs intentionally does not compare: it |
| 87 | // only schedules the next frame, it draws nothing. | 87 | // only schedules the next frame, it draws nothing. | ||
| 88 | - | const fields = 22 | 88 | + | const fields = 23 |
| 89 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | 89 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | ||
| 90 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | 90 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | ||
| 91 | } | 91 | } | ||