Pos.Combine unit tests

This commit is contained in:
Tig Kindel
2023-02-24 12:52:43 -08:00
parent a0c44b7cdb
commit 7fbece9dc4
3 changed files with 310 additions and 328 deletions

View File

@@ -2349,6 +2349,107 @@ namespace Terminal.Gui {
LayoutComplete?.Invoke (args);
}
internal void CollectPos (Pos pos, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
switch (pos) {
case Pos.PosView pv:
if (pv.Target != this) {
nEdges.Add ((pv.Target, from));
}
foreach (var v in from.InternalSubviews) {
CollectAll (v, ref nNodes, ref nEdges);
}
return;
case Pos.PosCombine pc:
foreach (var v in from.InternalSubviews) {
CollectPos (pc.left, from, ref nNodes, ref nEdges);
CollectPos (pc.right, from, ref nNodes, ref nEdges);
}
break;
}
}
internal void CollectDim (Dim dim, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
switch (dim) {
case Dim.DimView dv:
if (dv.Target != this) {
nEdges.Add ((dv.Target, from));
}
foreach (var v in from.InternalSubviews) {
CollectAll (v, ref nNodes, ref nEdges);
}
return;
case Dim.DimCombine dc:
foreach (var v in from.InternalSubviews) {
CollectDim (dc.left, from, ref nNodes, ref nEdges);
CollectDim (dc.right, from, ref nNodes, ref nEdges);
}
break;
}
}
internal void CollectAll (View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
foreach (var v in from.InternalSubviews) {
nNodes.Add (v);
if (v.layoutStyle != LayoutStyle.Computed) {
continue;
}
CollectPos (v.X, v, ref nNodes, ref nEdges);
CollectPos (v.Y, v, ref nNodes, ref nEdges);
CollectDim (v.Width, v, ref nNodes, ref nEdges);
CollectDim (v.Height, v, ref nNodes, ref nEdges);
}
}
// https://en.wikipedia.org/wiki/Topological_sorting
internal static List<View> TopologicalSort (View superView, IEnumerable<View> nodes, ICollection<(View From, View To)> edges)
{
var result = new List<View> ();
// Set of all nodes with no incoming edges
var noEdgeNodes = new HashSet<View> (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
while (noEdgeNodes.Any ()) {
// remove a node n from S
var n = noEdgeNodes.First ();
noEdgeNodes.Remove (n);
// add n to tail of L
if (n != superView)
result.Add (n);
// for each node m with an edge e from n to m do
foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) {
var m = e.To;
// remove edge e from the graph
edges.Remove (e);
// if m has no other incoming edges then
if (edges.All (me => !me.To.Equals (m)) && m != superView) {
// insert m into S
noEdgeNodes.Add (m);
}
}
}
if (edges.Any ()) {
(var from, var to) = edges.First ();
if (from != Application.Top) {
if (!ReferenceEquals (from, to)) {
throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {from} linked with {to}. Did you forget to add it to {superView}?");
} else {
throw new InvalidOperationException ("TopologicalSort encountered a recursive cycle in the relative Pos/Dim in the views of " + superView);
}
}
}
// return L (a topologically sorted order)
return result;
} // TopologicalSort
/// <summary>
/// Invoked when a view starts executing or when the dimensions of the view have changed, for example in
/// response to the container view or terminal resizing.
@@ -2370,111 +2471,8 @@ namespace Terminal.Gui {
// Sort out the dependencies of the X, Y, Width, Height properties
var nodes = new HashSet<View> ();
var edges = new HashSet<(View, View)> ();
void CollectPos (Pos pos, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
switch (pos) {
case Pos.PosView pv:
if (pv.Target != this) {
nEdges.Add ((pv.Target, from));
}
foreach (var v in from.InternalSubviews) {
CollectAll (v, ref nNodes, ref nEdges);
}
return;
case Pos.PosCombine pc:
foreach (var v in from.InternalSubviews) {
CollectPos (pc.left, from, ref nNodes, ref nEdges);
CollectPos (pc.right, from, ref nNodes, ref nEdges);
}
break;
}
}
void CollectDim (Dim dim, View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
switch (dim) {
case Dim.DimView dv:
if (dv.Target != this) {
nEdges.Add ((dv.Target, from));
}
foreach (var v in from.InternalSubviews) {
CollectAll (v, ref nNodes, ref nEdges);
}
return;
case Dim.DimCombine dc:
foreach (var v in from.InternalSubviews) {
CollectDim (dc.left, from, ref nNodes, ref nEdges);
CollectDim (dc.right, from, ref nNodes, ref nEdges);
}
break;
}
}
void CollectAll (View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
{
foreach (var v in from.InternalSubviews) {
nNodes.Add (v);
if (v.layoutStyle != LayoutStyle.Computed) {
continue;
}
CollectPos (v.X, v, ref nNodes, ref nEdges);
CollectPos (v.Y, v, ref nNodes, ref nEdges);
CollectDim (v.Width, v, ref nNodes, ref nEdges);
CollectDim (v.Height, v, ref nNodes, ref nEdges);
}
}
CollectAll (this, ref nodes, ref edges);
// https://en.wikipedia.org/wiki/Topological_sorting
List<View> TopologicalSort (IEnumerable<View> nodes, ICollection<(View From, View To)> edges)
{
var result = new List<View> ();
// Set of all nodes with no incoming edges
var noEdgeNodes = new HashSet<View> (nodes.Where (n => edges.All (e => !e.To.Equals (n))));
while (noEdgeNodes.Any ()) {
// remove a node n from S
var n = noEdgeNodes.First ();
noEdgeNodes.Remove (n);
// add n to tail of L
if (n != this?.SuperView)
result.Add (n);
// for each node m with an edge e from n to m do
foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) {
var m = e.To;
// remove edge e from the graph
edges.Remove (e);
// if m has no other incoming edges then
if (edges.All (me => !me.To.Equals (m)) && m != this?.SuperView) {
// insert m into S
noEdgeNodes.Add (m);
}
}
}
if (edges.Any ()) {
(var from, var to) = edges.First ();
if (from != Application.Top) {
if (!ReferenceEquals (from, to)) {
throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {from} linked with {to}. Did you forget to add it to {this}?");
} else {
throw new InvalidOperationException ("TopologicalSort encountered a recursive cycle in the relative Pos/Dim in the views of " + this);
}
}
}
// return L (a topologically sorted order)
return result;
} // TopologicalSort
var ordered = TopologicalSort (nodes, edges);
var ordered = View.TopologicalSort (SuperView, nodes, edges);
foreach (var v in ordered) {
if (v.LayoutStyle == LayoutStyle.Computed) {
v.SetRelativeLayout (Frame);

View File

@@ -1,6 +1,7 @@
using NStack;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Terminal.Gui.Graphs;
using Xunit;
using Xunit.Abstractions;
@@ -19,127 +20,31 @@ namespace Terminal.Gui.CoreTests {
}
[Fact]
public void View_With_No_Difference_Between_An_Object_Initializer_And_A_Constructor ()
public void TopologicalSort_Missing_Add ()
{
// Object Initializer
var view = new View () {
X = 1,
Y = 2,
Width = 3,
Height = 4
};
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
var root = new View ();
var sub1 = new View ();
root.Add (sub1);
var sub2 = new View ();
sub1.Width = Dim.Width (sub2);
view.LayoutSubviews ();
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.False (view.Bounds.IsEmpty);
sub2.Width = Dim.Width (sub1);
// Default Constructor
view = new View ();
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.True (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Constructor
view = new View (1, 2, "");
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Default Constructor and post assignment equivalent to Object Initializer
view = new View ();
view.X = 1;
view.Y = 2;
view.Width = 3;
view.Height = 4;
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
}
[Fact]
public void FocusNearestView_Ensure_Focus_Ordered ()
public void TopologicalSort_Recursive_Ref ()
{
var top = new Toplevel ();
var win = new Window ();
var winSubview = new View ("WindowSubview") {
CanFocus = true
};
win.Add (winSubview);
top.Add (win);
var frm = new FrameView ();
var frmSubview = new View ("FrameSubview") {
CanFocus = true
};
frm.Add (frmSubview);
top.Add (frm);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
}
[Fact]
public void KeyPress_Handled_To_True_Prevents_Changes ()
{
Application.Init (new FakeDriver ());
Console.MockKeyPresses.Push (new ConsoleKeyInfo ('N', ConsoleKey.N, false, false, false));
var top = Application.Top;
var text = new TextField ("");
text.KeyPress += (e) => {
e.Handled = true;
Assert.True (e.Handled);
Assert.Equal (Key.N, e.KeyEvent.Key);
};
top.Add (text);
Application.Iteration += () => {
Console.MockKeyPresses.Push (new ConsoleKeyInfo ('N', ConsoleKey.N, false, false, false));
Assert.Equal ("", text.Text);
Application.RequestStop ();
};
Application.Run ();
// Shutdown must be called to safely clean up Application if Init has been called
Application.Shutdown ();
var root = new View ();
var sub1 = new View ();
root.Add (sub1);
var sub2 = new View ();
root.Add (sub2);
sub2.Width = Dim.Width (sub2);
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
}
[Fact, AutoInitShutdown]
@@ -1409,5 +1314,111 @@ Y
Assert.Equal ("Fill(0)", view2.Width.ToString ());
Assert.Equal ("Fill(0)", view2.Height.ToString ());
}
[Fact]
public void SetRelativeLayout_PosCombine_Center_Plus_Absolute ()
{
var superView = new View () {
AutoSize = false,
Width = 10,
Height = 10
};
var testView = new View () {
AutoSize = false,
X = Pos.Center (),
Y = Pos.Center (),
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (4, testView.Frame.X);
Assert.Equal (4, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = Pos.Center () + 1,
Y = Pos.Center () + 1,
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (5, testView.Frame.X);
Assert.Equal (5, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = 1 + Pos.Center (),
Y = 1 + Pos.Center (),
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (5, testView.Frame.X);
Assert.Equal (5, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = 1 + Pos.Percent (50),
Y = Pos.Percent (50) + 1,
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (6, testView.Frame.X);
Assert.Equal (6, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = Pos.Percent (10) + Pos.Percent (40),
Y = Pos.Percent (10) + Pos.Percent (40),
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (5, testView.Frame.X);
Assert.Equal (5, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = 1 + Pos.Percent (10) + Pos.Percent (40) - 1,
Y = 5 + Pos.Percent (10) + Pos.Percent (40) - 5,
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (5, testView.Frame.X);
Assert.Equal (5, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = Pos.Left(testView),
Y = Pos.Left (testView),
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (5, testView.Frame.X);
Assert.Equal (5, testView.Frame.Y);
testView = new View () {
AutoSize = false,
X = 1 + Pos.Left (testView),
Y = Pos.Top (testView) + 1,
Width = 1,
Height = 1
};
superView.Add (testView);
testView.SetRelativeLayout (superView.Frame);
Assert.Equal (6, testView.Frame.X);
Assert.Equal (6, testView.Frame.Y);
}
}
}

View File

@@ -137,32 +137,99 @@ namespace Terminal.Gui.CoreTests {
// TODO: Add more
}
[Fact]
public void TopologicalSort_Missing_Add ()
public void View_With_No_Difference_Between_An_Object_Initializer_And_A_Constructor ()
{
var root = new View ();
var sub1 = new View ();
root.Add (sub1);
var sub2 = new View ();
sub1.Width = Dim.Width (sub2);
// Object Initializer
var view = new View () {
X = 1,
Y = 2,
Width = 3,
Height = 4
};
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
view.LayoutSubviews ();
sub2.Width = Dim.Width (sub1);
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.False (view.Bounds.IsEmpty);
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
// Default Constructor
view = new View ();
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.True (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Constructor
view = new View (1, 2, "");
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Default Constructor and post assignment equivalent to Object Initializer
view = new View ();
view.X = 1;
view.Y = 2;
view.Width = 3;
view.Height = 4;
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
}
[Fact]
public void TopologicalSort_Recursive_Ref ()
public void FocusNearestView_Ensure_Focus_Ordered ()
{
var root = new View ();
var sub1 = new View ();
root.Add (sub1);
var sub2 = new View ();
root.Add (sub2);
sub2.Width = Dim.Width (sub2);
Assert.Throws<InvalidOperationException> (() => root.LayoutSubviews ());
var top = new Toplevel ();
var win = new Window ();
var winSubview = new View ("WindowSubview") {
CanFocus = true
};
win.Add (winSubview);
top.Add (win);
var frm = new FrameView ();
var frmSubview = new View ("FrameSubview") {
CanFocus = true
};
frm.Add (frmSubview);
top.Add (frm);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
}
[Fact]
@@ -1053,100 +1120,6 @@ namespace Terminal.Gui.CoreTests {
Application.Shutdown ();
}
[Fact]
public void View_With_No_Difference_Between_An_Object_Initializer_And_A_Constructor ()
{
// Object Initializer
var view = new View () {
X = 1,
Y = 2,
Width = 3,
Height = 4
};
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
view.LayoutSubviews ();
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.False (view.Bounds.IsEmpty);
// Default Constructor
view = new View ();
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.True (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Constructor
view = new View (1, 2, "");
Assert.Null (view.X);
Assert.Null (view.Y);
Assert.Null (view.Width);
Assert.Null (view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
// Default Constructor and post assignment equivalent to Object Initializer
view = new View ();
view.X = 1;
view.Y = 2;
view.Width = 3;
view.Height = 4;
Assert.Equal (1, view.X);
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
}
[Fact]
public void FocusNearestView_Ensure_Focus_Ordered ()
{
var top = new Toplevel ();
var win = new Window ();
var winSubview = new View ("WindowSubview") {
CanFocus = true
};
win.Add (winSubview);
top.Add (win);
var frm = new FrameView ();
var frmSubview = new View ("FrameSubview") {
CanFocus = true
};
frm.Add (frmSubview);
top.Add (frm);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ("FrameSubview", top.MostFocused.Text);
top.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
Assert.Equal ($"WindowSubview", top.MostFocused.Text);
}
[Fact]
public void KeyPress_Handled_To_True_Prevents_Changes ()
{