Files

732 lines
28 KiB
C#

using AtomUI.Controls.Primitives;
using AtomUI.Desktop.Controls.DesignTokens;
using AtomUI.Theme.Resources;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Shouldly;
using Xunit;
using AtomUI.Controls;
using AtomUINumericUpDown = AtomUI.Desktop.Controls.NumericUpDown;
using AtomUIScrollViewer = AtomUI.Desktop.Controls.ScrollViewer;
using AvaloniaWindow = Avalonia.Controls.Window;
namespace AtomUI.Desktop.Controls.Tests.NumericUpDown;
public class NumericUpDownHandleTests
{
static NumericUpDownHandleTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Fact]
public void Disabled_NumericUpDown_Hides_Floatable_Spinner_Handle()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsEnabled = false,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<ContentPresenter>()
.Single(item => item.Name == "PART_SpinnerHandle");
spinnerHandle.Opacity.ShouldBe(0.0);
});
}
[Fact]
public void Disabled_NumericUpDown_Uses_Disabled_Text_Foreground()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsEnabled = false,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var scrollViewer = numericUpDown.GetVisualDescendants()
.OfType<AtomUIScrollViewer>()
.Single(item => item.Name == "ScrollViewer");
BrushShouldHaveSameColor(
scrollViewer.Foreground,
GetThemeResource<IBrush>(SharedTokenKind.ColorTextDisabled));
});
}
[Fact]
public void Filled_NumericUpDown_Uses_Filled_Spinner_Handle_Background()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
StyleVariant = InputControlStyleVariant.Filled,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Single(item => item.GetType().Name == "ButtonSpinnerHandle");
BrushShouldHaveSameColor(
spinnerHandle.Background,
GetThemeResource<IBrush>(ButtonSpinnerTokenKind.FilledHandleBg));
});
}
[Fact]
public void Default_Input_Mode_Uses_Floating_Handle_Template()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
numericUpDown.Mode.ShouldBe(NumericUpDownMode.Input);
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Single(item => item.GetType().Name == "ButtonSpinnerHandle");
var spinnerButtons = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Where(item => item.Name is "PART_DecreaseButton" or "PART_IncreaseButton")
.ToList();
spinnerButtons.Count.ShouldBe(2);
spinnerButtons.All(item => item.GetVisualAncestors().Contains(spinnerHandle)).ShouldBeTrue();
});
}
[Fact]
public void Default_Input_Mode_Uses_Outer_Content_Padding_Only()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var textBox = numericUpDown.GetVisualDescendants()
.OfType<TextBox>()
.Single(item => item.Name == "PART_TextBox");
var contentFrame = numericUpDown.GetVisualDescendants()
.OfType<global::AtomUI.Desktop.Controls.AddOnDecoratedBoxContentFrame>()
.Single(item => item.Name == "PART_ContentFrame");
contentFrame.Padding.Left.ShouldBeGreaterThan(0);
textBox.ShouldBeAssignableTo<EmbeddedTextBox>();
textBox.SizeType.ShouldBe(CustomizableSizeType.Custom);
textBox.Padding.ShouldBe(new Thickness(0));
});
}
[Fact]
public void Spinner_Mode_Uses_Outer_Content_Padding_Only()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var textBox = numericUpDown.GetVisualDescendants()
.OfType<TextBox>()
.Single(item => item.Name == "PART_TextBox");
var contentFrame = numericUpDown.GetVisualDescendants()
.OfType<global::AtomUI.Desktop.Controls.AddOnDecoratedBoxContentFrame>()
.Single(item => item.Name == "PART_ContentFrame");
contentFrame.Padding.ShouldBe(new Thickness(0));
textBox.ShouldBeAssignableTo<EmbeddedTextBox>();
textBox.SizeType.ShouldBe(CustomizableSizeType.Custom);
textBox.Padding.ShouldBe(new Thickness(0));
});
}
[Fact]
public void Input_Mode_TextBox_Uses_Custom_SizeType_Instead_Of_LocalValue_Binding_Or_Custom_Padding_State()
{
var source = ReadRepoFile("src/AtomUI.Desktop.Controls/NumericUpDown/Themes/NumericUpDownTheme.axaml");
source.ShouldContain("SizeType=\"Custom\"");
source.ShouldContain("Padding=\"0\"");
source.ShouldNotContain("IsCustomPadding");
source.ShouldNotContain("Padding=\"{Binding Source=0");
}
[Fact]
public void Input_Mode_Updates_ButtonSpinner_Visibility_From_ShowButtonSpinner()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
ShowButtonSpinner = false,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var spinner = numericUpDown.GetVisualDescendants()
.OfType<global::AtomUI.Desktop.Controls.ButtonSpinner>()
.Single(item => item.Name == "PART_Spinner");
spinner.IsButtonSpinnerVisible.ShouldBeFalse();
numericUpDown.ShowButtonSpinner = true;
Dispatcher.UIThread.RunJobs();
spinner.IsButtonSpinnerVisible.ShouldBeTrue();
});
}
[Fact]
public void Spinner_Mode_Uses_Inline_Stepper_Template()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Count(item => item.GetType().Name == "ButtonSpinnerHandle")
.ShouldBe(0);
numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton")
.ShouldNotBeNull();
numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton")
.ShouldNotBeNull();
});
}
[Fact]
public void Spinner_Mode_With_Hidden_Handle_Uses_Input_Template()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
Mode = NumericUpDownMode.Spinner,
ShowButtonSpinner = false,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Single(item => item.GetType().Name == "ButtonSpinnerHandle");
var textBox = numericUpDown.GetVisualDescendants()
.OfType<TextBox>()
.Single(item => item.Name == "PART_TextBox");
var spinnerButtons = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Where(item => item.Name is "PART_DecreaseButton" or "PART_IncreaseButton")
.ToList();
textBox.TextAlignment.ShouldBe(TextAlignment.Start);
spinnerButtons.Count.ShouldBe(2);
spinnerButtons.All(item => item.GetVisualAncestors().Contains(spinnerHandle)).ShouldBeTrue();
numericUpDown.ShowButtonSpinner = true;
Dispatcher.UIThread.RunJobs();
numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Count(item => item.GetType().Name == "ButtonSpinnerHandle")
.ShouldBe(0);
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
decreaseButton.GetVisualParent().ShouldBeAssignableTo<PixelAlignedBorder>();
increaseButton.GetVisualParent().ShouldBeAssignableTo<PixelAlignedBorder>();
});
}
[Fact]
public void Spinner_Mode_Buttons_Do_Not_Draw_Own_Rounded_Frame()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
decreaseButton.CornerRadius.ShouldBe(new CornerRadius(0));
increaseButton.CornerRadius.ShouldBe(new CornerRadius(0));
decreaseButton.BorderThickness.ShouldBe(new Thickness(0));
increaseButton.BorderThickness.ShouldBe(new Thickness(0));
var decreaseSegment = decreaseButton.GetVisualParent();
var increaseSegment = increaseButton.GetVisualParent();
decreaseSegment.ShouldBeAssignableTo<PixelAlignedBorder>();
increaseSegment.ShouldBeAssignableTo<PixelAlignedBorder>();
((PixelAlignedBorder)decreaseSegment!).BorderThickness.ShouldBe(new Thickness(0, 0, 1, 0));
((PixelAlignedBorder)increaseSegment!).BorderThickness.ShouldBe(new Thickness(1, 0, 0, 0));
});
}
[Fact]
public void Spinner_Mode_Action_Segments_Use_Input_Padding_And_Normal_Icon_Size()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 150,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var actionPadding = GetThemeResource<Thickness>(SharedTokenKind.InputPadding);
var iconSize = GetThemeResource<double>(SharedTokenKind.IconSize);
var lineWidth = GetThemeResource<double>(SharedTokenKind.LineWidth);
var expectedWidth = actionPadding.Left + iconSize + actionPadding.Right + lineWidth;
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
decreaseButton.Padding.ShouldBe(actionPadding);
increaseButton.Padding.ShouldBe(actionPadding);
decreaseButton.IconWidth.ShouldBe(iconSize);
increaseButton.IconWidth.ShouldBe(iconSize);
decreaseButton.IconHeight.ShouldBe(iconSize);
increaseButton.IconHeight.ShouldBe(iconSize);
decreaseButton.GetVisualParent()!.Bounds.Width.ShouldBe(expectedWidth, 0.5);
increaseButton.GetVisualParent()!.Bounds.Width.ShouldBe(expectedWidth, 0.5);
});
}
[Fact]
public void Spinner_Mode_Action_Buttons_Use_Handle_State_Visuals()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 150,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var hoverBrush = GetThemeResource<IBrush>(ButtonSpinnerTokenKind.HandleHoverColor);
var activeBackground = GetThemeResource<IBrush>(ButtonSpinnerTokenKind.HandleActiveBg);
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
increaseButton.IsMotionEnabled = false;
((IPseudoClasses)increaseButton.Classes).Set(":pointerover", true);
Dispatcher.UIThread.RunJobs();
BrushShouldHaveSameColor(increaseButton.IconBrush, hoverBrush);
((IPseudoClasses)increaseButton.Classes).Set(":pressed", true);
Dispatcher.UIThread.RunJobs();
BrushShouldHaveSameColor(increaseButton.Background, activeBackground);
});
}
[Fact]
public void Spinner_Mode_Uses_Input_Control_Height_And_Centers_Action_Icons()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 150,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var controlHeight = GetThemeResource<double>(SharedTokenKind.ControlHeight);
var actionPadding = GetThemeResource<Thickness>(SharedTokenKind.InputPadding);
var iconSize = GetThemeResource<double>(SharedTokenKind.IconSize);
var lineWidth = GetThemeResource<double>(SharedTokenKind.LineWidth);
var expectedWidth = actionPadding.Left + iconSize + actionPadding.Right;
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
var contentFrame = numericUpDown.GetVisualDescendants()
.OfType<global::AtomUI.Desktop.Controls.AddOnDecoratedBoxContentFrame>()
.Single(item => item.Name == "PART_ContentFrame");
var iconPresenter = increaseButton.GetVisualDescendants()
.OfType<IconPresenter>()
.Single(item => item.Name == "IconPresenter");
numericUpDown.Bounds.Height.ShouldBe(controlHeight, 0.5);
contentFrame.Padding.ShouldBe(new Thickness(0));
increaseButton.Bounds.Height.ShouldBe(controlHeight - lineWidth * 2, 0.5);
increaseButton.Bounds.Width.ShouldBe(expectedWidth, 0.5);
var buttonCenter = increaseButton.TranslatePoint(
new Point(increaseButton.Bounds.Width / 2, increaseButton.Bounds.Height / 2),
numericUpDown);
var iconCenter = iconPresenter.TranslatePoint(
new Point(iconPresenter.Bounds.Width / 2, iconPresenter.Bounds.Height / 2),
numericUpDown);
buttonCenter.ShouldNotBeNull();
iconCenter.ShouldNotBeNull();
iconCenter.Value.X.ShouldBe(buttonCenter.Value.X, 0.5);
iconCenter.Value.Y.ShouldBe(buttonCenter.Value.Y, 0.5);
iconCenter.Value.Y.ShouldBe(controlHeight / 2, 0.5);
});
}
[Fact]
public void Spinner_Mode_Updates_Button_Enabled_State_From_Min_Max()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Minimum = 0,
Maximum = 3,
Value = 0,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
decreaseButton.IsEnabled.ShouldBeFalse();
increaseButton.IsEnabled.ShouldBeTrue();
numericUpDown.Value = 3;
Dispatcher.UIThread.RunJobs();
decreaseButton.IsEnabled.ShouldBeTrue();
increaseButton.IsEnabled.ShouldBeFalse();
});
}
[Fact]
public void Spinner_Mode_Button_Clicks_Change_Value()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Minimum = 0,
Maximum = 10,
Increment = 1,
Value = 3,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
increaseButton.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent, increaseButton));
Dispatcher.UIThread.RunJobs();
numericUpDown.Value.ShouldBe(4);
decreaseButton.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent, decreaseButton));
Dispatcher.UIThread.RunJobs();
numericUpDown.Value.ShouldBe(3);
});
}
[Fact]
public void Spinner_Mode_Respects_AllowSpin()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Minimum = 0,
Maximum = 10,
Value = 3,
AllowSpin = false,
Mode = NumericUpDownMode.Spinner,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
var decreaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton");
var increaseButton = numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_IncreaseButton");
decreaseButton.IsEnabled.ShouldBeFalse();
increaseButton.IsEnabled.ShouldBeFalse();
});
}
[Fact]
public void Mode_Runtime_Switch_Rebuilds_Template_Parts()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, () =>
{
numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Count(item => item.GetType().Name == "ButtonSpinnerHandle")
.ShouldBe(1);
numericUpDown.Mode = NumericUpDownMode.Spinner;
Dispatcher.UIThread.RunJobs();
numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Count(item => item.GetType().Name == "ButtonSpinnerHandle")
.ShouldBe(0);
numericUpDown.GetVisualDescendants()
.OfType<IconButton>()
.Single(item => item.Name == "PART_DecreaseButton")
.ShouldNotBeNull();
});
}
[Fact]
public void Floating_Handle_Render_Uses_Render_Scale_Aware_BorderThickness()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, window =>
{
window.SetRenderScaling(1.5);
Dispatcher.UIThread.RunJobs();
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Single(item => item.GetType().Name == "ButtonSpinnerHandle");
var drawingGroup = RenderToDrawingGroup(spinnerHandle);
var penThicknesses = EnumerateGeometryDrawings(drawingGroup)
.Select(drawing => drawing.Pen?.Thickness)
.Where(thickness => thickness is not null)
.Select(thickness => thickness!.Value)
.ToArray();
penThicknesses.ShouldContain(thickness => Math.Abs(thickness - 2d / 3d) < 0.0001);
});
}
[Fact]
public void Floating_Handle_Render_Aligns_Left_Separator_To_Physical_Pixel()
{
var numericUpDown = new AtomUINumericUpDown
{
Width = 160,
Value = 3,
IsMotionEnabled = false
};
ShowInWindow(numericUpDown, window =>
{
window.SetRenderScaling(1.5);
Dispatcher.UIThread.RunJobs();
var spinnerHandle = numericUpDown.GetVisualDescendants()
.OfType<TemplatedControl>()
.Single(item => item.GetType().Name == "ButtonSpinnerHandle");
var drawingGroup = RenderToDrawingGroup(spinnerHandle);
var verticalLine = EnumerateStrokeDrawings(drawingGroup)
.Single(drawing => drawing.Geometry?.Bounds is { Width: < 0.0001, Height: > 0 });
verticalLine.Pen.ShouldNotBeNull();
verticalLine.Pen!.Thickness.ShouldBe(2d / 3d, 0.0001);
verticalLine.Geometry!.Bounds.X.ShouldBe(1d / 3d, 0.0001);
});
}
private static T GetThemeResource<T>(object key)
{
var application = Application.Current;
application.ShouldNotBeNull();
application!.TryGetResource(key, application.ActualThemeVariant, out var value).ShouldBeTrue();
value.ShouldBeAssignableTo<T>();
return (T)value!;
}
private static void BrushShouldHaveSameColor(IBrush? actual, IBrush? expected)
{
GetSolidBrushColor(actual).ShouldBe(GetSolidBrushColor(expected));
}
private static Color GetSolidBrushColor(IBrush? brush)
{
brush.ShouldNotBeNull();
brush.ShouldBeAssignableTo<ISolidColorBrush>();
return ((ISolidColorBrush)brush!).Color;
}
private static string ReadRepoFile(string relativePath)
{
var path = GetRepoFile(relativePath);
File.Exists(path).ShouldBeTrue($"Expected repository file to exist: {relativePath}");
return File.ReadAllText(path);
}
private static string GetRepoFile(string relativePath)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
{
return candidate;
}
directory = directory.Parent;
}
return Path.Combine(AppContext.BaseDirectory, relativePath);
}
private static void ShowInWindow(Control content, Action assertion)
{
ShowInWindow(content, _ => assertion());
}
private static void ShowInWindow(Control content, Action<AvaloniaWindow> assertion)
{
var window = new AvaloniaWindow
{
Width = 240,
Height = 120,
Content = content
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
assertion(window);
}
finally
{
window.Close();
}
}
private static DrawingGroup RenderToDrawingGroup(Control control)
{
var drawingGroup = new DrawingGroup();
using (var context = drawingGroup.Open())
{
control.Render(context);
}
return drawingGroup;
}
private static IEnumerable<GeometryDrawing> EnumerateGeometryDrawings(Drawing drawing)
{
if (drawing is GeometryDrawing geometryDrawing)
{
yield return geometryDrawing;
}
else if (drawing is DrawingGroup drawingGroup)
{
foreach (var child in drawingGroup.Children.SelectMany(EnumerateGeometryDrawings))
{
yield return child;
}
}
}
private static IEnumerable<GeometryDrawing> EnumerateStrokeDrawings(Drawing drawing)
{
return EnumerateGeometryDrawings(drawing)
.Where(drawing => drawing.Pen is not null);
}
}