Skip to content

Commit 7b70d80

Browse files
Make pixel shifting smarter (#87)
1 parent f2f25eb commit 7b70d80

File tree

3 files changed

+203
-234
lines changed

3 files changed

+203
-234
lines changed
Lines changed: 41 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,255 +1,95 @@
11
using System;
2-
using System.Collections.Generic;
32
using DesktopClock.Utilities;
43

54
namespace DesktopClock.Tests;
65

76
public class PixelShifterTests
87
{
98
[Theory]
10-
[InlineData(5, 10)] // Evenly divisible.
11-
[InlineData(3, 10)] // Not evenly divisible.
12-
[InlineData(10, 5)] // Amount is larger than total.
13-
public void ShiftX_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift)
9+
[InlineData(50, 0.1, 100, 5)]
10+
[InlineData(20, 0.5, 4, 4)]
11+
public void GetEffectiveMaxOffset_ShouldUseWindowSizeRatio(double windowSize, double ratio, int maxOffset, double expected)
1412
{
1513
var shifter = new PixelShifter
1614
{
17-
PixelsPerShift = shiftAmount,
18-
MaxPixelOffset = maxTotalShift,
15+
MaxPixelOffsetRatio = ratio,
16+
MaxPixelOffset = maxOffset,
1917
};
2018

21-
double totalShiftX = 0;
22-
23-
// Test 100 times because it's random.
24-
for (var i = 0; i < 100; i++)
25-
{
26-
var shift = shifter.ShiftX();
27-
totalShiftX += shift;
19+
var effective = shifter.GetEffectiveMaxOffset(windowSize);
2820

29-
Assert.InRange(Math.Abs(totalShiftX), 0, maxTotalShift);
30-
}
21+
Assert.Equal(expected, effective);
3122
}
3223

33-
[Theory]
34-
[InlineData(5, 10)] // Evenly divisible.
35-
[InlineData(3, 10)] // Not evenly divisible.
36-
[InlineData(10, 5)] // Amount is larger than total.
37-
public void ShiftY_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift)
24+
[Fact]
25+
public void ShiftX_ShouldBounceDeterministicallyWithinBounds()
3826
{
3927
var shifter = new PixelShifter
4028
{
41-
PixelsPerShift = shiftAmount,
42-
MaxPixelOffset = maxTotalShift,
29+
PixelsPerShift = 2,
30+
MaxPixelOffset = 100,
31+
MaxPixelOffsetRatio = 0.1,
4332
};
4433

45-
double totalShiftY = 0;
34+
const double windowSize = 50;
35+
Assert.Equal(5d, shifter.GetEffectiveMaxOffset(windowSize));
4636

47-
// Test 100 times because it's random.
48-
for (var i = 0; i < 100; i++)
37+
var expectedShifts = new double[] { 2, 2, 1, -2, -2, -2, -2, -2, 2, 2 };
38+
foreach (var expected in expectedShifts)
4939
{
50-
var shift = shifter.ShiftY();
51-
totalShiftY += shift;
52-
53-
Assert.InRange(Math.Abs(totalShiftY), 0, maxTotalShift);
40+
var shift = shifter.ShiftX(windowSize);
41+
Assert.Equal(expected, shift);
42+
Assert.InRange(Math.Abs(shifter.TotalShiftX), 0, 5);
5443
}
5544
}
5645

5746
[Fact]
58-
public void ShiftX_WithZeroPixelsPerShift_ShouldReturnZero()
47+
public void ShiftY_ShouldBounceDeterministicallyWithinBounds()
5948
{
60-
// Arrange
6149
var shifter = new PixelShifter
6250
{
63-
PixelsPerShift = 0,
64-
MaxPixelOffset = 10,
51+
PixelsPerShift = 2,
52+
MaxPixelOffset = 100,
53+
MaxPixelOffsetRatio = 0.1,
6554
};
6655

67-
// Act
68-
var shift = shifter.ShiftX();
69-
70-
// Assert
71-
Assert.Equal(0, shift);
72-
}
56+
const double windowSize = 50;
57+
Assert.Equal(5d, shifter.GetEffectiveMaxOffset(windowSize));
7358

74-
[Fact]
75-
public void ShiftY_WithZeroPixelsPerShift_ShouldReturnZero()
76-
{
77-
// Arrange
78-
var shifter = new PixelShifter
79-
{
80-
PixelsPerShift = 0,
81-
MaxPixelOffset = 10,
82-
};
83-
84-
// Act
85-
var shift = shifter.ShiftY();
86-
87-
// Assert
88-
Assert.Equal(0, shift);
89-
}
90-
91-
[Fact]
92-
public void ShiftX_WithZeroMaxOffset_ShouldReturnZero()
93-
{
94-
// Arrange
95-
var shifter = new PixelShifter
59+
var expectedShifts = new double[] { 2, 2, 1, -2, -2, -2, -2, -2, 2, 2 };
60+
foreach (var expected in expectedShifts)
9661
{
97-
PixelsPerShift = 5,
98-
MaxPixelOffset = 0,
99-
};
100-
101-
// Act
102-
var shift = shifter.ShiftX();
103-
104-
// Assert
105-
Assert.Equal(0, shift);
62+
var shift = shifter.ShiftY(windowSize);
63+
Assert.Equal(expected, shift);
64+
Assert.InRange(Math.Abs(shifter.TotalShiftY), 0, 5);
65+
}
10666
}
10767

108-
[Fact]
109-
public void ShiftY_WithZeroMaxOffset_ShouldReturnZero()
68+
[Theory]
69+
[InlineData(0, 50)]
70+
[InlineData(2, 0)]
71+
public void ShiftX_WhenDisabled_ReturnsZero(int pixelsPerShift, double windowSize)
11072
{
111-
// Arrange
11273
var shifter = new PixelShifter
11374
{
114-
PixelsPerShift = 5,
115-
MaxPixelOffset = 0,
75+
PixelsPerShift = pixelsPerShift,
76+
MaxPixelOffset = 10,
77+
MaxPixelOffsetRatio = 0.1,
11678
};
11779

118-
// Act
119-
var shift = shifter.ShiftY();
80+
var shift = shifter.ShiftX(windowSize);
12081

121-
// Assert
12282
Assert.Equal(0, shift);
83+
Assert.Equal(0, shifter.TotalShiftX);
12384
}
12485

12586
[Fact]
12687
public void DefaultValues_ShouldBeExpected()
12788
{
128-
// Arrange
12989
var shifter = new PixelShifter();
13090

131-
// Assert
13291
Assert.Equal(1, shifter.PixelsPerShift);
13392
Assert.Equal(4, shifter.MaxPixelOffset);
134-
}
135-
136-
[Fact]
137-
public void ShiftX_ShouldReverseDirectionAtBoundary()
138-
{
139-
// Arrange - set up to hit boundary quickly
140-
var shifter = new PixelShifter
141-
{
142-
PixelsPerShift = 10,
143-
MaxPixelOffset = 10,
144-
};
145-
146-
// Act - call multiple times to force direction reversal
147-
double total = 0;
148-
var shifts = new List<double>();
149-
for (int i = 0; i < 10; i++)
150-
{
151-
var shift = shifter.ShiftX();
152-
shifts.Add(shift);
153-
total += shift;
154-
}
155-
156-
// Assert - total should stay within bounds
157-
Assert.InRange(Math.Abs(total), 0, 10);
158-
159-
// There should be both positive and negative shifts (direction reversal)
160-
// OR the total stayed within bounds
161-
Assert.True(Math.Abs(total) <= 10);
162-
}
163-
164-
[Fact]
165-
public void ShiftY_ShouldReverseDirectionAtBoundary()
166-
{
167-
// Arrange - set up to hit boundary quickly
168-
var shifter = new PixelShifter
169-
{
170-
PixelsPerShift = 10,
171-
MaxPixelOffset = 10,
172-
};
173-
174-
// Act - call multiple times to force direction reversal
175-
double total = 0;
176-
var shifts = new List<double>();
177-
for (int i = 0; i < 10; i++)
178-
{
179-
var shift = shifter.ShiftY();
180-
shifts.Add(shift);
181-
total += shift;
182-
}
183-
184-
// Assert - total should stay within bounds
185-
Assert.InRange(Math.Abs(total), 0, 10);
186-
}
187-
188-
[Fact]
189-
public void ShiftX_And_ShiftY_ShouldBeIndependent()
190-
{
191-
// Arrange
192-
var shifter = new PixelShifter
193-
{
194-
PixelsPerShift = 5,
195-
MaxPixelOffset = 20,
196-
};
197-
198-
// Act
199-
double totalX = 0, totalY = 0;
200-
for (int i = 0; i < 50; i++)
201-
{
202-
totalX += shifter.ShiftX();
203-
totalY += shifter.ShiftY();
204-
}
205-
206-
// Assert - both should be within bounds independently
207-
Assert.InRange(Math.Abs(totalX), 0, 20);
208-
Assert.InRange(Math.Abs(totalY), 0, 20);
209-
}
210-
211-
[Fact]
212-
public void ShiftX_MultipleShiftersWithSameConfig_ShouldBeIndependent()
213-
{
214-
// Arrange
215-
var shifter1 = new PixelShifter { PixelsPerShift = 5, MaxPixelOffset = 10 };
216-
var shifter2 = new PixelShifter { PixelsPerShift = 5, MaxPixelOffset = 10 };
217-
218-
// Act
219-
double total1 = 0, total2 = 0;
220-
for (int i = 0; i < 20; i++)
221-
{
222-
total1 += shifter1.ShiftX();
223-
total2 += shifter2.ShiftX();
224-
}
225-
226-
// Assert - both should be within their own bounds
227-
Assert.InRange(Math.Abs(total1), 0, 10);
228-
Assert.InRange(Math.Abs(total2), 0, 10);
229-
}
230-
231-
[Theory]
232-
[InlineData(1, 5)]
233-
[InlineData(2, 8)]
234-
[InlineData(3, 15)]
235-
public void Shift_ShouldReturnValueWithinPixelsPerShiftRange(int pixelsPerShift, int maxOffset)
236-
{
237-
// Arrange
238-
var shifter = new PixelShifter
239-
{
240-
PixelsPerShift = pixelsPerShift,
241-
MaxPixelOffset = maxOffset,
242-
};
243-
244-
// Act & Assert
245-
for (int i = 0; i < 50; i++)
246-
{
247-
var shiftX = shifter.ShiftX();
248-
var shiftY = shifter.ShiftY();
249-
250-
// Shift should be within the range [-pixelsPerShift, +pixelsPerShift]
251-
Assert.InRange(shiftX, -pixelsPerShift, pixelsPerShift);
252-
Assert.InRange(shiftY, -pixelsPerShift, pixelsPerShift);
253-
}
93+
Assert.Equal(0.1, shifter.MaxPixelOffsetRatio, 5);
25494
}
25595
}

DesktopClock/MainWindow.xaml.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.ComponentModel;
3-
using System.Diagnostics;
43
using System.Globalization;
54
using System.IO;
65
using System.Media;
@@ -36,12 +35,6 @@ public partial class MainWindow : Window
3635
[ObservableProperty]
3736
private string _currentTimeOrCountdownString;
3837

39-
/// <summary>
40-
/// The amount of margin applied in order to shift the clock's pixels and help prevent burn-in.
41-
/// </summary>
42-
[ObservableProperty]
43-
private Thickness _pixelShift;
44-
4538
public MainWindow()
4639
{
4740
InitializeComponent();
@@ -111,6 +104,8 @@ public void Exit()
111104
Application.Current.Shutdown();
112105
}
113106

107+
public PixelShifter PixelShifter => Settings.Default.BurnInMitigation ? (_pixelShifter ??= new()) : null;
108+
114109
protected override void OnClosed(EventArgs e)
115110
{
116111
Settings.Default.PropertyChanged -= _settingsPropertyChanged;
@@ -242,9 +237,7 @@ private void TryShiftPixels()
242237
if (!IsVisible || WindowState == WindowState.Minimized)
243238
return;
244239

245-
_pixelShifter ??= new();
246-
Left += _pixelShifter.ShiftX();
247-
Top += _pixelShifter.ShiftY();
240+
PixelShifter?.ApplyShift(this);
248241
});
249242
}
250243

@@ -270,9 +263,11 @@ private void Window_MouseDown(object sender, MouseButtonEventArgs e)
270263
if (e.ChangedButton == MouseButton.Left && Settings.Default.DragToMove)
271264
{
272265
// Pause time updates to maintain placement.
266+
PixelShifter?.ClearShift(this);
273267
_systemClockTimer.Stop();
274268

275269
DragMove();
270+
PixelShifter?.UpdateBasePosition(this);
276271
UpdateTimeString();
277272

278273
_systemClockTimer.Start();
@@ -298,6 +293,7 @@ private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
298293
private void Window_SourceInitialized(object sender, EventArgs e)
299294
{
300295
this.SetPlacement(Settings.Default.Placement);
296+
PixelShifter?.UpdateBasePosition(this);
301297

302298
// Apply click-through setting.
303299
this.SetClickThrough(Settings.Default.ClickThrough);
@@ -337,6 +333,7 @@ private void Window_ContentRendered(object sender, EventArgs e)
337333
private void Window_Closing(object sender, CancelEventArgs e)
338334
{
339335
// Save the last text and the placement to preserve dimensions and position of the clock.
336+
PixelShifter?.RestoreBasePosition(this);
340337
Settings.Default.LastDisplay = CurrentTimeOrCountdownString;
341338
Settings.Default.Placement = this.GetPlacement();
342339

@@ -358,6 +355,7 @@ private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
358355
{
359356
var widthChange = e.NewSize.Width - e.PreviousSize.Width;
360357
Left -= widthChange;
358+
PixelShifter?.AdjustForRightAlignedWidthChange(widthChange);
361359
}
362360
}
363361

0 commit comments

Comments
 (0)