diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index 5b09af233..e7ceec890 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -3,8 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; - - namespace Terminal.Gui { /// @@ -123,12 +121,30 @@ namespace Terminal.Gui { _lines.Add (new StraightLine (start, length, orientation, style, attribute)); } - private void AddLine (StraightLine line) + /// + /// Adds a new line to the canvas + /// + /// + public void AddLine (StraightLine line) { _cachedBounds = Rect.Empty; _lines.Add (line); } + /// + /// Removes the last line added to the canvas + /// + /// + public StraightLine RemoveLastLine() + { + var l = _lines.LastOrDefault (); + if(l != null) { + _lines.Remove(l); + } + + return l; + } + /// /// Clears all lines from the LineCanvas. /// @@ -138,6 +154,15 @@ namespace Terminal.Gui { _lines.Clear (); } + /// + /// Clears any cached states from the canvas + /// Call this method if you make changes to lines + /// that have already been added. + /// + public void ClearCache () + { + _cachedBounds = Rect.Empty; + } private Rect _cachedBounds; /// @@ -703,256 +728,93 @@ namespace Terminal.Gui { AddLine (line); } } - - internal class IntersectionDefinition { - /// - /// The point at which the intersection happens - /// - internal Point Point { get; } - - /// - /// Defines how position relates - /// to . - /// - internal IntersectionType Type { get; } - - /// - /// The line that intersects - /// - internal StraightLine Line { get; } - - internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line) - { - Point = point; - Type = type; - Line = line; - } - } + } + internal class IntersectionDefinition { + /// + /// The point at which the intersection happens + /// + internal Point Point { get; } /// - /// The type of Rune that we will use before considering - /// double width, curved borders etc + /// Defines how position relates + /// to . /// - internal enum IntersectionRuneType { - None, - Dot, - ULCorner, - URCorner, - LLCorner, - LRCorner, - TopTee, - BottomTee, - RightTee, - LeftTee, - Cross, - HLine, - VLine, - } + internal IntersectionType Type { get; } - internal enum IntersectionType { - /// - /// There is no intersection - /// - None, + /// + /// The line that intersects + /// + internal StraightLine Line { get; } - /// - /// A line passes directly over this point traveling along - /// the horizontal axis - /// - PassOverHorizontal, - - /// - /// A line passes directly over this point traveling along - /// the vertical axis - /// - PassOverVertical, - - /// - /// A line starts at this point and is traveling up - /// - StartUp, - - /// - /// A line starts at this point and is traveling right - /// - StartRight, - - /// - /// A line starts at this point and is traveling down - /// - StartDown, - - /// - /// A line starts at this point and is traveling left - /// - StartLeft, - - /// - /// A line exists at this point who has 0 length - /// - Dot - } - - // TODO: Add events that notify when StraightLine changes to enable dynamic layout - internal class StraightLine { - public Point Start { get; } - public int Length { get; } - public Orientation Orientation { get; } - public LineStyle Style { get; } - public Attribute? Attribute { get; set; } - - internal StraightLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default) - { - this.Start = start; - this.Length = length; - this.Orientation = orientation; - this.Style = style; - this.Attribute = attribute; - } - - internal IntersectionDefinition Intersects (int x, int y) - { - switch (Orientation) { - case Orientation.Horizontal: return IntersectsHorizontally (x, y); - case Orientation.Vertical: return IntersectsVertically (x, y); - default: throw new ArgumentOutOfRangeException (nameof (Orientation)); - } - - } - - private IntersectionDefinition IntersectsHorizontally (int x, int y) - { - if (Start.Y != y) { - return null; - } else { - if (StartsAt (x, y)) { - - return new IntersectionDefinition ( - Start, - GetTypeByLength (IntersectionType.StartLeft, IntersectionType.PassOverHorizontal, IntersectionType.StartRight), - this - ); - - } - - if (EndsAt (x, y)) { - - return new IntersectionDefinition ( - Start, - Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft, - this - ); - - } else { - var xmin = Math.Min (Start.X, Start.X + Length); - var xmax = Math.Max (Start.X, Start.X + Length); - - if (xmin < x && xmax > x) { - return new IntersectionDefinition ( - new Point (x, y), - IntersectionType.PassOverHorizontal, - this - ); - } - } - - return null; - } - } - - private IntersectionDefinition IntersectsVertically (int x, int y) - { - if (Start.X != x) { - return null; - } else { - if (StartsAt (x, y)) { - - return new IntersectionDefinition ( - Start, - GetTypeByLength (IntersectionType.StartUp, IntersectionType.PassOverVertical, IntersectionType.StartDown), - this - ); - - } - - if (EndsAt (x, y)) { - - return new IntersectionDefinition ( - Start, - Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp, - this - ); - - } else { - var ymin = Math.Min (Start.Y, Start.Y + Length); - var ymax = Math.Max (Start.Y, Start.Y + Length); - - if (ymin < y && ymax > y) { - return new IntersectionDefinition ( - new Point (x, y), - IntersectionType.PassOverVertical, - this - ); - } - } - - return null; - } - } - - private IntersectionType GetTypeByLength (IntersectionType typeWhenNegative, IntersectionType typeWhenZero, IntersectionType typeWhenPositive) - { - if (Length == 0) { - return typeWhenZero; - } - - return Length < 0 ? typeWhenNegative : typeWhenPositive; - } - - private bool EndsAt (int x, int y) - { - var sub = (Length == 0) ? 0 : (Length > 0) ? 1 : -1; - if (Orientation == Orientation.Horizontal) { - return Start.X + Length - sub == x && Start.Y == y; - } - - return Start.X == x && Start.Y + Length - sub == y; - } - - private bool StartsAt (int x, int y) - { - return Start.X == x && Start.Y == y; - } - - /// - /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the - /// line that is furthest left/top and Size is defined by the line that extends the furthest - /// right/bottom. - /// - internal Rect Bounds { - get { - - // 0 and 1/-1 Length means a size (width or height) of 1 - var size = Math.Max (1, Math.Abs (Length)); - - // How much to offset x or y to get the start of the line - var offset = Math.Abs (Length < 0 ? Length + 1 : 0); - var x = Start.X - (Orientation == Orientation.Horizontal ? offset : 0); - var y = Start.Y - (Orientation == Orientation.Vertical ? offset : 0); - var width = Orientation == Orientation.Horizontal ? size : 1; - var height = Orientation == Orientation.Vertical ? size : 1; - - return new Rect (x, y, width, height); - } - } - - /// - /// Formats the Line as a string in (Start.X,Start.Y,Length,Orientation) notation. - /// - public override string ToString () - { - return $"({Start.X},{Start.Y},{Length},{Orientation})"; - } + internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line) + { + Point = point; + Type = type; + Line = line; } } + + /// + /// The type of Rune that we will use before considering + /// double width, curved borders etc + /// + internal enum IntersectionRuneType { + None, + Dot, + ULCorner, + URCorner, + LLCorner, + LRCorner, + TopTee, + BottomTee, + RightTee, + LeftTee, + Cross, + HLine, + VLine, + } + + internal enum IntersectionType { + /// + /// There is no intersection + /// + None, + + /// + /// A line passes directly over this point traveling along + /// the horizontal axis + /// + PassOverHorizontal, + + /// + /// A line passes directly over this point traveling along + /// the vertical axis + /// + PassOverVertical, + + /// + /// A line starts at this point and is traveling up + /// + StartUp, + + /// + /// A line starts at this point and is traveling right + /// + StartRight, + + /// + /// A line starts at this point and is traveling down + /// + StartDown, + + /// + /// A line starts at this point and is traveling left + /// + StartLeft, + + /// + /// A line exists at this point who has 0 length + /// + Dot + } } diff --git a/Terminal.Gui/Drawing/StraightLine.cs b/Terminal.Gui/Drawing/StraightLine.cs new file mode 100644 index 000000000..7d3dc0fb9 --- /dev/null +++ b/Terminal.Gui/Drawing/StraightLine.cs @@ -0,0 +1,196 @@ +using System; +namespace Terminal.Gui { + // TODO: Add events that notify when StraightLine changes to enable dynamic layout + /// + /// A line between two points on a horizontal or vertical + /// and a given style/color. + /// + public class StraightLine { + + /// + /// Gets or sets where the line begins. + /// + public Point Start { get; set; } + + /// + /// Gets or sets the length of the line. + /// + public int Length { get; set; } + + /// + /// Gets or sets the orientation (horizontal or vertical) of the line. + /// + public Orientation Orientation { get; set; } + + /// + /// Gets or sets the line style of the line (e.g. dotted, double). + /// + public LineStyle Style { get; set; } + + /// + /// Gets or sets the color of the line. + /// + public Attribute? Attribute { get; set; } + + /// + /// Creates a new instance of the class. + /// + /// + /// + /// + /// + /// + public StraightLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default) + { + this.Start = start; + this.Length = length; + this.Orientation = orientation; + this.Style = style; + this.Attribute = attribute; + } + + internal IntersectionDefinition Intersects (int x, int y) + { + switch (Orientation) { + case Orientation.Horizontal: return IntersectsHorizontally (x, y); + case Orientation.Vertical: return IntersectsVertically (x, y); + default: throw new ArgumentOutOfRangeException (nameof (Orientation)); + } + + } + + private IntersectionDefinition IntersectsHorizontally (int x, int y) + { + if (Start.Y != y) { + return null; + } else { + if (StartsAt (x, y)) { + + return new IntersectionDefinition ( + Start, + GetTypeByLength (IntersectionType.StartLeft, IntersectionType.PassOverHorizontal, IntersectionType.StartRight), + this + ); + + } + + if (EndsAt (x, y)) { + + return new IntersectionDefinition ( + Start, + Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft, + this + ); + + } else { + var xmin = Math.Min (Start.X, Start.X + Length); + var xmax = Math.Max (Start.X, Start.X + Length); + + if (xmin < x && xmax > x) { + return new IntersectionDefinition ( + new Point (x, y), + IntersectionType.PassOverHorizontal, + this + ); + } + } + + return null; + } + } + + private IntersectionDefinition IntersectsVertically (int x, int y) + { + if (Start.X != x) { + return null; + } else { + if (StartsAt (x, y)) { + + return new IntersectionDefinition ( + Start, + GetTypeByLength (IntersectionType.StartUp, IntersectionType.PassOverVertical, IntersectionType.StartDown), + this + ); + + } + + if (EndsAt (x, y)) { + + return new IntersectionDefinition ( + Start, + Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp, + this + ); + + } else { + var ymin = Math.Min (Start.Y, Start.Y + Length); + var ymax = Math.Max (Start.Y, Start.Y + Length); + + if (ymin < y && ymax > y) { + return new IntersectionDefinition ( + new Point (x, y), + IntersectionType.PassOverVertical, + this + ); + } + } + + return null; + } + } + + private IntersectionType GetTypeByLength (IntersectionType typeWhenNegative, IntersectionType typeWhenZero, IntersectionType typeWhenPositive) + { + if (Length == 0) { + return typeWhenZero; + } + + return Length < 0 ? typeWhenNegative : typeWhenPositive; + } + + private bool EndsAt (int x, int y) + { + var sub = (Length == 0) ? 0 : (Length > 0) ? 1 : -1; + if (Orientation == Orientation.Horizontal) { + return Start.X + Length - sub == x && Start.Y == y; + } + + return Start.X == x && Start.Y + Length - sub == y; + } + + private bool StartsAt (int x, int y) + { + return Start.X == x && Start.Y == y; + } + + /// + /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the + /// line that is furthest left/top and Size is defined by the line that extends the furthest + /// right/bottom. + /// + internal Rect Bounds { + get { + + // 0 and 1/-1 Length means a size (width or height) of 1 + var size = Math.Max (1, Math.Abs (Length)); + + // How much to offset x or y to get the start of the line + var offset = Math.Abs (Length < 0 ? Length + 1 : 0); + var x = Start.X - (Orientation == Orientation.Horizontal ? offset : 0); + var y = Start.Y - (Orientation == Orientation.Vertical ? offset : 0); + var width = Orientation == Orientation.Horizontal ? size : 1; + var height = Orientation == Orientation.Vertical ? size : 1; + + return new Rect (x, y, width, height); + } + } + + /// + /// Formats the Line as a string in (Start.X,Start.Y,Length,Orientation) notation. + /// + public override string ToString () + { + return $"({Start.X},{Start.Y},{Length},{Orientation})"; + } + } +} diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs index 1b8d9e37d..d905db261 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection.Metadata.Ecma335; -using System.Text; using Terminal.Gui; using Attribute = Terminal.Gui.Attribute; @@ -24,7 +22,7 @@ namespace UICatalog.Scenarios { var tools = new ToolsView () { Title = "Tools", - X = Pos.Right(canvas) - 20, + X = Pos.Right (canvas) - 20, Y = 2 }; @@ -34,6 +32,8 @@ namespace UICatalog.Scenarios { Win.Add (canvas); Win.Add (tools); + + Win.KeyPress += (s,e) => { e.Handled = canvas.ProcessKey (e.KeyEvent); }; } class ToolsView : Window { @@ -55,7 +55,7 @@ namespace UICatalog.Scenarios { private void ToolsView_Initialized (object sender, EventArgs e) { LayoutSubviews (); - Width = Math.Max (_colorPicker.Frame.Width, _stylePicker.Frame.Width) + GetFramesThickness().Horizontal; + Width = Math.Max (_colorPicker.Frame.Width, _stylePicker.Frame.Width) + GetFramesThickness ().Horizontal; Height = _colorPicker.Frame.Height + _stylePicker.Frame.Height + _addLayerBtn.Frame.Height + GetFramesThickness ().Vertical; SuperView.LayoutSubviews (); } @@ -97,7 +97,7 @@ namespace UICatalog.Scenarios { List _layers = new List (); LineCanvas _currentLayer; Color _currentColor = Color.White; - Point? _currentLineStart = null; + StraightLine? _currentLine = null; public LineStyle LineStyle { get; set; } @@ -106,18 +106,41 @@ namespace UICatalog.Scenarios { AddLayer (); } + Stack undoHistory = new (); + + public override bool ProcessKey (KeyEvent e) + { + if (e.Key == (Key.Z | Key.CtrlMask)) { + var pop = _currentLayer.RemoveLastLine (); + if(pop != null) { + undoHistory.Push (pop); + SetNeedsDisplay (); + return true; + } + } + + if (e.Key == (Key.Y | Key.CtrlMask)) { + if (undoHistory.Any()) { + var pop = undoHistory.Pop (); + _currentLayer.AddLine(pop); + SetNeedsDisplay (); + return true; + } + } + + return base.ProcessKey (e); + } internal void AddLayer () { _currentLayer = new LineCanvas (); _layers.Add (_currentLayer); } - public override void OnDrawContent (Rect contentArea) + public override void OnDrawContentComplete (Rect contentArea) { - base.OnDrawContent (contentArea); - + base.OnDrawContentComplete (contentArea); foreach (var canvas in _layers) { - + foreach (var c in canvas.GetCellMap ()) { Driver.SetAttribute (c.Value.Attribute?.Value ?? ColorScheme.Normal); this.AddRune (c.Key.X, c.Key.Y, c.Value.Rune.Value); @@ -128,14 +151,15 @@ namespace UICatalog.Scenarios { public override bool OnMouseEvent (MouseEvent mouseEvent) { if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { - if (_currentLineStart == null) { - _currentLineStart = new Point (mouseEvent.X - GetBoundsOffset().X, mouseEvent.Y - GetBoundsOffset ().X); - } - } else { - if (_currentLineStart != null) { + if (_currentLine == null) { - var start = _currentLineStart.Value; - var end = new Point (mouseEvent.X - GetBoundsOffset ().X, mouseEvent.Y - GetBoundsOffset ().X); + _currentLine = new StraightLine ( + new Point (mouseEvent.X - GetBoundsOffset ().X, mouseEvent.Y - GetBoundsOffset ().X), + 0, Orientation.Vertical, LineStyle, new Attribute (_currentColor, GetNormalColor ().Background)); + _currentLayer.AddLine (_currentLine); + } else { + var start = _currentLine.Start; + var end = new Point (mouseEvent.X - GetBoundsOffset ().X, mouseEvent.Y - GetBoundsOffset ().Y); var orientation = Orientation.Vertical; var length = end.Y - start.Y; @@ -150,15 +174,15 @@ namespace UICatalog.Scenarios { } else { length--; } - - _currentLayer.AddLine ( - start, - length, - orientation, - LineStyle, - new Attribute (_currentColor, GetNormalColor().Background)); - - _currentLineStart = null; + _currentLine.Length = length; + _currentLine.Orientation = orientation; + _currentLayer.ClearCache (); + SetNeedsDisplay (); + } + } else { + if (_currentLine != null) { + _currentLine = null; + undoHistory.Clear (); SetNeedsDisplay (); } } diff --git a/UnitTests/Drawing/StraightLineTests.cs b/UnitTests/Drawing/StraightLineTests.cs index c67c30fbc..3f7adbae4 100644 --- a/UnitTests/Drawing/StraightLineTests.cs +++ b/UnitTests/Drawing/StraightLineTests.cs @@ -81,7 +81,7 @@ namespace Terminal.Gui.DrawingTests { [Theory, SetupFakeDriver] public void Bounds (Orientation orientation, int x, int y, int length, int expectedX, int expectedY, int expectedWidth, int expectedHeight) { - var sl = new LineCanvas.StraightLine (new Point (x, y), length, orientation, LineStyle.Single); + var sl = new StraightLine (new Point (x, y), length, orientation, LineStyle.Single); Assert.Equal (new Rect (expectedX, expectedY, expectedWidth, expectedHeight), sl.Bounds); }