mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-28 23:08:36 +08:00
461 lines
13 KiB
C#
461 lines
13 KiB
C#
using AtomUI.Controls;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.Grid;
|
|
|
|
public class GridResponsiveTests
|
|
{
|
|
static GridResponsiveTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Col_Exposes_Xxxl_Breakpoint_Property()
|
|
{
|
|
var col = new Col
|
|
{
|
|
Xxxl = new GridColSize { Span = 6 }
|
|
};
|
|
|
|
col.Xxxl!.Span.ShouldBe(6);
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Gutter_Uses_Mobile_First_Cascade_For_Partial_Map()
|
|
{
|
|
var host = new TestMediaBreakHost(MediaBreakPoint.Large)
|
|
{
|
|
Width = 240,
|
|
Height = 100
|
|
};
|
|
var row = new Row
|
|
{
|
|
Gutter = GridGutter.Parse("xs: 8, md: 16")
|
|
};
|
|
row.Children.Add(new Col { Span = 12, Content = new Border { Height = 10, Background = Brushes.White } });
|
|
row.Children.Add(new Col { Span = 12, Content = new Border { Height = 10, Background = Brushes.White } });
|
|
host.Children.Add(row);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 100, Content = host };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
row.Children[0].Bounds.X.ShouldBe(0, 0.5);
|
|
row.Children[0].Bounds.Width.ShouldBe(112, 0.5);
|
|
row.Children[1].Bounds.X.ShouldBe(128, 0.5);
|
|
row.Children[1].Bounds.Width.ShouldBe(112, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2, 12)]
|
|
[InlineData(3, 8)]
|
|
[InlineData(4, 6)]
|
|
public void Row_Does_Not_Wrap_Exact_Grid_Line_Due_To_Floating_Point_Error(int columnCount, int span)
|
|
{
|
|
const double rowWidth = 411.2;
|
|
var row = new Row
|
|
{
|
|
Gutter = GridGutter.Parse("16,16")
|
|
};
|
|
|
|
for (var i = 0; i < columnCount; i++)
|
|
{
|
|
row.Children.Add(new Col
|
|
{
|
|
Span = span,
|
|
Height = 10
|
|
});
|
|
}
|
|
|
|
row.Measure(new Size(rowWidth, double.PositiveInfinity));
|
|
row.Arrange(new Rect(0, 0, rowWidth, row.DesiredSize.Height));
|
|
|
|
row.DesiredSize.Height.ShouldBe(10);
|
|
row.Children.ShouldAllBe(child => child.Bounds.Y == 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Layout_Recovers_After_Splitter_Pane_Shrinks_And_Expands()
|
|
{
|
|
var firstCard = new Col
|
|
{
|
|
Span = 12,
|
|
Content = new Border { Height = 100, Background = Brushes.White }
|
|
};
|
|
var secondCard = new Col
|
|
{
|
|
Span = 12,
|
|
Content = new Border { Height = 100, Background = Brushes.White }
|
|
};
|
|
var cardRow = new Row
|
|
{
|
|
Gutter = GridGutter.Parse("16,16")
|
|
};
|
|
cardRow.Children.Add(firstCard);
|
|
cardRow.Children.Add(secondCard);
|
|
|
|
var nameInput = new AtomUI.Desktop.Controls.LineEdit();
|
|
var extensionInput = new AtomUI.Desktop.Controls.LineEdit { Width = 64 };
|
|
var phoneInput = new AtomUI.Desktop.Controls.LineEdit();
|
|
var namePanel = new DockPanel
|
|
{
|
|
HorizontalSpacing = 4,
|
|
Children =
|
|
{
|
|
new Avalonia.Controls.TextBlock { Text = "Name:" },
|
|
nameInput
|
|
}
|
|
};
|
|
var phonePanel = new DockPanel
|
|
{
|
|
HorizontalSpacing = 4,
|
|
LastChildFill = true,
|
|
Children =
|
|
{
|
|
new Avalonia.Controls.TextBlock { Text = "Phone:" },
|
|
extensionInput,
|
|
new Avalonia.Controls.TextBlock { Text = "-" },
|
|
phoneInput
|
|
}
|
|
};
|
|
DockPanel.SetDock(extensionInput, Dock.Right);
|
|
DockPanel.SetDock(phonePanel.Children[2], Dock.Right);
|
|
|
|
var formRow = new Row
|
|
{
|
|
Gutter = GridGutter.Parse("16,16")
|
|
};
|
|
var nameCol = new Col { Span = 10, Content = namePanel };
|
|
var spacerCol = new Col { Span = 2 };
|
|
var phoneCol = new Col { Span = 12, Content = phonePanel };
|
|
formRow.Children.Add(nameCol);
|
|
formRow.Children.Add(spacerCol);
|
|
formRow.Children.Add(phoneCol);
|
|
|
|
var leftPane = new Border { Background = Brushes.Yellow };
|
|
var rightPane = new StackPanel
|
|
{
|
|
Margin = new Thickness(20),
|
|
Spacing = 12,
|
|
Children =
|
|
{
|
|
cardRow,
|
|
formRow
|
|
}
|
|
};
|
|
var splitter = new AtomUI.Desktop.Controls.Splitter
|
|
{
|
|
Orientation = Orientation.Vertical
|
|
};
|
|
AtomUI.Desktop.Controls.Splitter.SetSize(leftPane, AtomUI.Dimension.Parse("50%"));
|
|
splitter.Children.Add(leftPane);
|
|
splitter.Children.Add(rightPane);
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 1000,
|
|
Height = 360,
|
|
Content = splitter
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
window.SetRenderScaling(1.25);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var initialRightPaneWidth = rightPane.Bounds.Width;
|
|
var initialNameInputWidth = nameInput.Bounds.Width;
|
|
var initialPhoneInputWidth = phoneInput.Bounds.Width;
|
|
initialNameInputWidth.ShouldBeGreaterThan(0);
|
|
initialPhoneInputWidth.ShouldBeGreaterThan(0);
|
|
|
|
foreach (var percent in new[] { 65, 35, 72, 28, 58, 42, 50 })
|
|
{
|
|
AtomUI.Desktop.Controls.Splitter.SetSize(
|
|
leftPane,
|
|
AtomUI.Dimension.Parse($"{percent}%"));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
secondCard.Bounds.Y.ShouldBe(0, 0.5,
|
|
$"two Span=12 columns must not wrap at {percent}% splitter size.");
|
|
phoneCol.Bounds.Y.ShouldBe(0, 0.5,
|
|
$"the 10+2+12 form row must not wrap at {percent}% splitter size.");
|
|
}
|
|
|
|
rightPane.Bounds.Width.ShouldBe(initialRightPaneWidth, 0.5);
|
|
nameInput.Bounds.Width.ShouldBe(initialNameInputWidth, 0.5);
|
|
phoneInput.Bounds.Width.ShouldBe(initialPhoneInputWidth, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Still_Wraps_When_Grid_Line_Exceeds_24_Columns()
|
|
{
|
|
const double rowWidth = 411.2;
|
|
var row = new Row
|
|
{
|
|
Gutter = GridGutter.Parse("16,16")
|
|
};
|
|
row.Children.Add(new Col { Span = 13, Height = 10 });
|
|
row.Children.Add(new Col { Span = 12, Height = 10 });
|
|
|
|
row.Measure(new Size(rowWidth, double.PositiveInfinity));
|
|
row.Arrange(new Rect(0, 0, rowWidth, row.DesiredSize.Height));
|
|
|
|
row.DesiredSize.Height.ShouldBe(36);
|
|
row.Children[0].Bounds.Y.ShouldBe(0);
|
|
row.Children[1].Bounds.Y.ShouldBe(26);
|
|
}
|
|
|
|
[Fact]
|
|
public void GridColSpanInfo_Uses_Mobile_First_Cascade_For_Partial_Map()
|
|
{
|
|
var span = GridColSpanInfo.Parse("xs: 24, md: 12");
|
|
|
|
span.GetValue(MediaBreakPoint.Large).ShouldBe(12);
|
|
}
|
|
|
|
[Fact]
|
|
public void Col_Span_Zero_Is_Hidden_From_Row_Layout()
|
|
{
|
|
var row = new Row
|
|
{
|
|
Width = 240,
|
|
Height = 40
|
|
};
|
|
var hidden = new Col
|
|
{
|
|
Span = 0,
|
|
Content = new Border { Width = 80, Height = 10, Background = Brushes.White }
|
|
};
|
|
var visible = new Col
|
|
{
|
|
Span = 24,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
row.Children.Add(hidden);
|
|
row.Children.Add(visible);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 80, Content = row };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
hidden.Bounds.Width.ShouldBe(0);
|
|
hidden.Bounds.Height.ShouldBe(0);
|
|
visible.Bounds.X.ShouldBe(0, 0.5);
|
|
visible.Bounds.Width.ShouldBe(240, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Col_Flex_Fills_Remaining_Row_Width()
|
|
{
|
|
var row = new Row
|
|
{
|
|
Width = 240,
|
|
Height = 40
|
|
};
|
|
var fixedCol = new Col
|
|
{
|
|
Span = 8,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
var flexCol = new Col
|
|
{
|
|
Flex = 1,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
row.Children.Add(fixedCol);
|
|
row.Children.Add(flexCol);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 80, Content = row };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
fixedCol.Bounds.Width.ShouldBe(80, 0.5);
|
|
flexCol.Bounds.X.ShouldBe(80, 0.5);
|
|
flexCol.Bounds.Width.ShouldBe(160, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Col_Responsive_Flex_Overrides_Base_Span_At_Active_Breakpoint()
|
|
{
|
|
var host = new TestMediaBreakHost(MediaBreakPoint.Large)
|
|
{
|
|
Width = 240,
|
|
Height = 80
|
|
};
|
|
var row = new Row();
|
|
var fixedCol = new Col
|
|
{
|
|
Span = 8,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
var responsiveFlexCol = new Col
|
|
{
|
|
Span = 8,
|
|
Lg = new GridColSize { Flex = 1 },
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
row.Children.Add(fixedCol);
|
|
row.Children.Add(responsiveFlexCol);
|
|
host.Children.Add(row);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 80, Content = host };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
fixedCol.Bounds.Width.ShouldBe(80, 0.5);
|
|
responsiveFlexCol.Bounds.X.ShouldBe(80, 0.5);
|
|
responsiveFlexCol.Bounds.Width.ShouldBe(160, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ColInfoExtension_Creates_Flex_Override()
|
|
{
|
|
var extension = new ColInfoExtension
|
|
{
|
|
Flex = 2
|
|
};
|
|
|
|
var size = extension.ProvideValue(null!) as GridColSize;
|
|
|
|
size.ShouldNotBeNull();
|
|
size!.Flex.ShouldBe(new GridColFlex(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Responsive_Justify_Uses_Active_Breakpoint_Value()
|
|
{
|
|
var host = new TestMediaBreakHost(MediaBreakPoint.Large)
|
|
{
|
|
Width = 240,
|
|
Height = 80
|
|
};
|
|
var row = new Row
|
|
{
|
|
JustifyInfo = GridRowJustifyInfo.Parse("xs: start, md: end")
|
|
};
|
|
var col = new Col
|
|
{
|
|
Span = 6,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
row.Children.Add(col);
|
|
host.Children.Add(row);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 80, Content = host };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
col.Bounds.X.ShouldBe(180, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Responsive_Align_Uses_Active_Breakpoint_Value()
|
|
{
|
|
var host = new TestMediaBreakHost(MediaBreakPoint.Large)
|
|
{
|
|
Width = 240,
|
|
Height = 80
|
|
};
|
|
var row = new Row
|
|
{
|
|
AlignInfo = GridRowAlignInfo.Parse("xs: top, md: bottom")
|
|
};
|
|
var tall = new Col
|
|
{
|
|
Span = 6,
|
|
Content = new Border { Height = 30, Background = Brushes.White }
|
|
};
|
|
var shortCol = new Col
|
|
{
|
|
Span = 6,
|
|
Content = new Border { Height = 10, Background = Brushes.White }
|
|
};
|
|
row.Children.Add(tall);
|
|
row.Children.Add(shortCol);
|
|
host.Children.Add(row);
|
|
|
|
var window = new AvaloniaWindow { Width = 240, Height = 80, Content = host };
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
tall.Bounds.Y.ShouldBe(0, 0.5);
|
|
shortCol.Bounds.Y.ShouldBe(20, 0.5);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private sealed class TestMediaBreakHost : Panel, IMediaBreakAwareControl
|
|
{
|
|
public TestMediaBreakHost(MediaBreakPoint breakPoint)
|
|
{
|
|
MediaBreakPoint = breakPoint;
|
|
}
|
|
|
|
public MediaBreakPoint MediaBreakPoint { get; private set; }
|
|
|
|
public event EventHandler<MediaBreakPointChangedEventArgs>? MediaBreakPointChanged;
|
|
|
|
public void SetMediaBreakPoint(MediaBreakPoint mediaBreakPoint)
|
|
{
|
|
MediaBreakPoint = mediaBreakPoint;
|
|
MediaBreakPointChanged?.Invoke(this, new MediaBreakPointChangedEventArgs(mediaBreakPoint));
|
|
}
|
|
}
|
|
}
|