From 7fbece9dc4d83b078bcacb97b857d3aac208f13d Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Fri, 24 Feb 2023 12:52:43 -0800 Subject: [PATCH] Pos.Combine unit tests --- Terminal.Gui/Core/View.cs | 206 +++++++++++++++-------------- UnitTests/Core/LayoutTests.cs | 237 ++++++++++++++++++---------------- UnitTests/Core/ViewTests.cs | 195 ++++++++++++---------------- 3 files changed, 310 insertions(+), 328 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 29b552394..fa446f720 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -2349,6 +2349,107 @@ namespace Terminal.Gui { LayoutComplete?.Invoke (args); } + internal void CollectPos (Pos pos, View from, ref HashSet 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 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 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 TopologicalSort (View superView, IEnumerable nodes, ICollection<(View From, View To)> edges) + { + var result = new List (); + + // Set of all nodes with no incoming edges + var noEdgeNodes = new HashSet (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 + + /// /// 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 (); var edges = new HashSet<(View, View)> (); - - void CollectPos (Pos pos, View from, ref HashSet 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 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 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 TopologicalSort (IEnumerable nodes, ICollection<(View From, View To)> edges) - { - var result = new List (); - - // Set of all nodes with no incoming edges - var noEdgeNodes = new HashSet (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); diff --git a/UnitTests/Core/LayoutTests.cs b/UnitTests/Core/LayoutTests.cs index 0dec38e4e..cf6cffe6c 100644 --- a/UnitTests/Core/LayoutTests.cs +++ b/UnitTests/Core/LayoutTests.cs @@ -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 (() => 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 (() => 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 (() => 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); + } } } diff --git a/UnitTests/Core/ViewTests.cs b/UnitTests/Core/ViewTests.cs index e81c2c7ac..4ae3c5206 100644 --- a/UnitTests/Core/ViewTests.cs +++ b/UnitTests/Core/ViewTests.cs @@ -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 (() => 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 (() => 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 (() => 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 () {