Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 41 additions & 201 deletions DesktopClock.Tests/PixelShifterTests.cs
Original file line number Diff line number Diff line change
@@ -1,255 +1,95 @@
using System;
using System.Collections.Generic;
using DesktopClock.Utilities;

namespace DesktopClock.Tests;

public class PixelShifterTests
{
[Theory]
[InlineData(5, 10)] // Evenly divisible.
[InlineData(3, 10)] // Not evenly divisible.
[InlineData(10, 5)] // Amount is larger than total.
public void ShiftX_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift)
[InlineData(50, 0.1, 100, 5)]
[InlineData(20, 0.5, 4, 4)]
public void GetEffectiveMaxOffset_ShouldUseWindowSizeRatio(double windowSize, double ratio, int maxOffset, double expected)
{
var shifter = new PixelShifter
{
PixelsPerShift = shiftAmount,
MaxPixelOffset = maxTotalShift,
MaxPixelOffsetRatio = ratio,
MaxPixelOffset = maxOffset,
};

double totalShiftX = 0;

// Test 100 times because it's random.
for (var i = 0; i < 100; i++)
{
var shift = shifter.ShiftX();
totalShiftX += shift;
var effective = shifter.GetEffectiveMaxOffset(windowSize);

Assert.InRange(Math.Abs(totalShiftX), 0, maxTotalShift);
}
Assert.Equal(expected, effective);
}

[Theory]
[InlineData(5, 10)] // Evenly divisible.
[InlineData(3, 10)] // Not evenly divisible.
[InlineData(10, 5)] // Amount is larger than total.
public void ShiftY_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift)
[Fact]
public void ShiftX_ShouldBounceDeterministicallyWithinBounds()
{
var shifter = new PixelShifter
{
PixelsPerShift = shiftAmount,
MaxPixelOffset = maxTotalShift,
PixelsPerShift = 2,
MaxPixelOffset = 100,
MaxPixelOffsetRatio = 0.1,
};

double totalShiftY = 0;
const double windowSize = 50;
Assert.Equal(5d, shifter.GetEffectiveMaxOffset(windowSize));

// Test 100 times because it's random.
for (var i = 0; i < 100; i++)
var expectedShifts = new double[] { 2, 2, 1, -2, -2, -2, -2, -2, 2, 2 };
foreach (var expected in expectedShifts)
{
var shift = shifter.ShiftY();
totalShiftY += shift;

Assert.InRange(Math.Abs(totalShiftY), 0, maxTotalShift);
var shift = shifter.ShiftX(windowSize);
Assert.Equal(expected, shift);
Assert.InRange(Math.Abs(shifter.TotalShiftX), 0, 5);
}
}

[Fact]
public void ShiftX_WithZeroPixelsPerShift_ShouldReturnZero()
public void ShiftY_ShouldBounceDeterministicallyWithinBounds()
{
// Arrange
var shifter = new PixelShifter
{
PixelsPerShift = 0,
MaxPixelOffset = 10,
PixelsPerShift = 2,
MaxPixelOffset = 100,
MaxPixelOffsetRatio = 0.1,
};

// Act
var shift = shifter.ShiftX();

// Assert
Assert.Equal(0, shift);
}
const double windowSize = 50;
Assert.Equal(5d, shifter.GetEffectiveMaxOffset(windowSize));

[Fact]
public void ShiftY_WithZeroPixelsPerShift_ShouldReturnZero()
{
// Arrange
var shifter = new PixelShifter
{
PixelsPerShift = 0,
MaxPixelOffset = 10,
};

// Act
var shift = shifter.ShiftY();

// Assert
Assert.Equal(0, shift);
}

[Fact]
public void ShiftX_WithZeroMaxOffset_ShouldReturnZero()
{
// Arrange
var shifter = new PixelShifter
var expectedShifts = new double[] { 2, 2, 1, -2, -2, -2, -2, -2, 2, 2 };
foreach (var expected in expectedShifts)
{
PixelsPerShift = 5,
MaxPixelOffset = 0,
};

// Act
var shift = shifter.ShiftX();

// Assert
Assert.Equal(0, shift);
var shift = shifter.ShiftY(windowSize);
Assert.Equal(expected, shift);
Assert.InRange(Math.Abs(shifter.TotalShiftY), 0, 5);
}
}

[Fact]
public void ShiftY_WithZeroMaxOffset_ShouldReturnZero()
[Theory]
[InlineData(0, 50)]
[InlineData(2, 0)]
public void ShiftX_WhenDisabled_ReturnsZero(int pixelsPerShift, double windowSize)
{
// Arrange
var shifter = new PixelShifter
{
PixelsPerShift = 5,
MaxPixelOffset = 0,
PixelsPerShift = pixelsPerShift,
MaxPixelOffset = 10,
MaxPixelOffsetRatio = 0.1,
};

// Act
var shift = shifter.ShiftY();
var shift = shifter.ShiftX(windowSize);

// Assert
Assert.Equal(0, shift);
Assert.Equal(0, shifter.TotalShiftX);
}

[Fact]
public void DefaultValues_ShouldBeExpected()
{
// Arrange
var shifter = new PixelShifter();

// Assert
Assert.Equal(1, shifter.PixelsPerShift);
Assert.Equal(4, shifter.MaxPixelOffset);
}

[Fact]
public void ShiftX_ShouldReverseDirectionAtBoundary()
{
// Arrange - set up to hit boundary quickly
var shifter = new PixelShifter
{
PixelsPerShift = 10,
MaxPixelOffset = 10,
};

// Act - call multiple times to force direction reversal
double total = 0;
var shifts = new List<double>();
for (int i = 0; i < 10; i++)
{
var shift = shifter.ShiftX();
shifts.Add(shift);
total += shift;
}

// Assert - total should stay within bounds
Assert.InRange(Math.Abs(total), 0, 10);

// There should be both positive and negative shifts (direction reversal)
// OR the total stayed within bounds
Assert.True(Math.Abs(total) <= 10);
}

[Fact]
public void ShiftY_ShouldReverseDirectionAtBoundary()
{
// Arrange - set up to hit boundary quickly
var shifter = new PixelShifter
{
PixelsPerShift = 10,
MaxPixelOffset = 10,
};

// Act - call multiple times to force direction reversal
double total = 0;
var shifts = new List<double>();
for (int i = 0; i < 10; i++)
{
var shift = shifter.ShiftY();
shifts.Add(shift);
total += shift;
}

// Assert - total should stay within bounds
Assert.InRange(Math.Abs(total), 0, 10);
}

[Fact]
public void ShiftX_And_ShiftY_ShouldBeIndependent()
{
// Arrange
var shifter = new PixelShifter
{
PixelsPerShift = 5,
MaxPixelOffset = 20,
};

// Act
double totalX = 0, totalY = 0;
for (int i = 0; i < 50; i++)
{
totalX += shifter.ShiftX();
totalY += shifter.ShiftY();
}

// Assert - both should be within bounds independently
Assert.InRange(Math.Abs(totalX), 0, 20);
Assert.InRange(Math.Abs(totalY), 0, 20);
}

[Fact]
public void ShiftX_MultipleShiftersWithSameConfig_ShouldBeIndependent()
{
// Arrange
var shifter1 = new PixelShifter { PixelsPerShift = 5, MaxPixelOffset = 10 };
var shifter2 = new PixelShifter { PixelsPerShift = 5, MaxPixelOffset = 10 };

// Act
double total1 = 0, total2 = 0;
for (int i = 0; i < 20; i++)
{
total1 += shifter1.ShiftX();
total2 += shifter2.ShiftX();
}

// Assert - both should be within their own bounds
Assert.InRange(Math.Abs(total1), 0, 10);
Assert.InRange(Math.Abs(total2), 0, 10);
}

[Theory]
[InlineData(1, 5)]
[InlineData(2, 8)]
[InlineData(3, 15)]
public void Shift_ShouldReturnValueWithinPixelsPerShiftRange(int pixelsPerShift, int maxOffset)
{
// Arrange
var shifter = new PixelShifter
{
PixelsPerShift = pixelsPerShift,
MaxPixelOffset = maxOffset,
};

// Act & Assert
for (int i = 0; i < 50; i++)
{
var shiftX = shifter.ShiftX();
var shiftY = shifter.ShiftY();

// Shift should be within the range [-pixelsPerShift, +pixelsPerShift]
Assert.InRange(shiftX, -pixelsPerShift, pixelsPerShift);
Assert.InRange(shiftY, -pixelsPerShift, pixelsPerShift);
}
Assert.Equal(0.1, shifter.MaxPixelOffsetRatio, 5);
}
}
18 changes: 8 additions & 10 deletions DesktopClock/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Media;
Expand Down Expand Up @@ -36,12 +35,6 @@ public partial class MainWindow : Window
[ObservableProperty]
private string _currentTimeOrCountdownString;

/// <summary>
/// The amount of margin applied in order to shift the clock's pixels and help prevent burn-in.
/// </summary>
[ObservableProperty]
private Thickness _pixelShift;

public MainWindow()
{
InitializeComponent();
Expand Down Expand Up @@ -111,6 +104,8 @@ public void Exit()
Application.Current.Shutdown();
}

public PixelShifter PixelShifter => Settings.Default.BurnInMitigation ? (_pixelShifter ??= new()) : null;

protected override void OnClosed(EventArgs e)
{
Settings.Default.PropertyChanged -= _settingsPropertyChanged;
Expand Down Expand Up @@ -242,9 +237,7 @@ private void TryShiftPixels()
if (!IsVisible || WindowState == WindowState.Minimized)
return;

_pixelShifter ??= new();
Left += _pixelShifter.ShiftX();
Top += _pixelShifter.ShiftY();
PixelShifter?.ApplyShift(this);
});
}

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

DragMove();
PixelShifter?.UpdateBasePosition(this);
UpdateTimeString();

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

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

Expand All @@ -358,6 +355,7 @@ private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
var widthChange = e.NewSize.Width - e.PreviousSize.Width;
Left -= widthChange;
PixelShifter?.AdjustForRightAlignedWidthChange(widthChange);
}
}

Expand Down
Loading