using System.Text; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Xml.Linq; using static Terminal.Gui.TileView; namespace Terminal.Gui { // TODO: v2 - Missing 3D effect - 3D effects will be drawn by a mechanism separate from Frames // TODO: v2 - If a Frame has focus, navigation keys (e.g Command.NextView) should cycle through SubViews of the Frame // QUESTION: How does a user navigate out of a Frame to another Frame, or back into the Parent's SubViews? /// /// Frames are a special form of that act as adornments; they appear outside of the /// enabling borders, menus, etc... /// public class Frame : View { private Thickness _thickness = Thickness.Empty; internal override void CreateFrames () { /* Do nothing - Frames do not have Frames */ } internal override void LayoutFrames () { /* Do nothing - Frames do not have Frames */ } /// /// The Parent of this Frame (the View this Frame surrounds). /// public View Parent { get; set; } /// /// Frames cannot be used as sub-views, so this method always throws an . /// TODO: Are we sure? /// public override View SuperView { get { return null; } set { throw new NotImplementedException (); } } /// public override void ViewToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true) { // Frames are *Children* of a View, not SubViews. Thus View.ViewToScreen will not work. // To get the screen-relative coordinates of a Frame, we need to know who // the Parent is var parentFrame = Parent?.Frame ?? Frame; rrow = row + parentFrame.Y; rcol = col + parentFrame.X; // We now have rcol/rrow in coordinates relative to our View's SuperView. If our View's SuperView has // a SuperView, keep going... Parent?.SuperView?.ViewToScreen (rcol, rrow, out rcol, out rrow, clipped); } /// /// Frames only render to their Parent or Parent's SuperView's LineCanvas, /// so this always throws an . /// public override LineCanvas LineCanvas { get { throw new NotImplementedException (); } set { throw new NotImplementedException (); } } /// /// Does nothing for Frame /// /// public override bool OnDrawFrames () => false; /// /// Does nothing for Frame /// /// public override bool OnRenderLineCanvas () => false; /// /// Frames only render to their Parent or Parent's SuperView's LineCanvas, /// so this always throws an . /// public override bool SuperViewRendersLineCanvas { get { return false;// throw new NotImplementedException (); } set { throw new NotImplementedException (); } } /// /// /// /// public virtual void OnDrawSubViews (Rect clipRect) { // TODO: Enable subviews of Frames (adornments). // if (Subviews == null) { // return; // } // foreach (var view in Subviews) { // // BUGBUG: v2 - shouldn't this be !view.LayoutNeeded? Why draw if layout is going to happen and we'll just draw again? // if (view.LayoutNeeded) { // view.LayoutSubviews (); // } // if ((view.Visible && !view.NeedDisplay.IsEmpty && view.Frame.Width > 0 && view.Frame.Height > 0) || view.ChildNeedsDisplay) { // view.Redraw (view.Bounds); // view.NeedDisplay = Rect.Empty; // // BUGBUG - v2 why does this need to be set to false? // // Shouldn't it be set when the subviews draw? // view.ChildNeedsDisplay = false; // } // } } /// /// Redraws the Frames that comprise the . /// public override void OnDrawContent (Rect contentArea) { if (Thickness == Thickness.Empty) { return; } if (ColorScheme != null) { Driver.SetAttribute (GetNormalColor ()); } else { if (Id == "Padding") { Driver.SetAttribute (new Attribute (Parent.ColorScheme.HotNormal.Background, Parent.ColorScheme.HotNormal.Foreground)); } else { Driver.SetAttribute (Parent.GetNormalColor ()); } } //Driver.SetAttribute (Colors.Error.Normal); var screenBounds = ViewToScreen (Frame); // This just draws/clears the thickness, not the insides. Thickness.Draw (screenBounds, (string)(Data != null ? Data : string.Empty)); //OnDrawSubviews (bounds); // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title) // The border frame (and title) are drawn at the outermost edge of border; // For Border // ...thickness extends outward (border/title is always as far in as possible) var borderBounds = new Rect ( screenBounds.X + Math.Max (0, Thickness.Left - 1), screenBounds.Y + Math.Max (0, Thickness.Top - 1), Math.Max (0, screenBounds.Width - Math.Max (0, Math.Max (0, Thickness.Left - 1) + Math.Max (0, Thickness.Right - 1))), Math.Max (0, screenBounds.Height - Math.Max (0, Math.Max (0, Thickness.Top - 1) + Math.Max (0, Thickness.Bottom - 1)))); var topTitleLineY = borderBounds.Y; var titleY = borderBounds.Y; var titleBarsLength = 0; // the little vertical thingies var maxTitleWidth = Math.Min (Parent.Title.GetColumns (), Math.Min (screenBounds.Width - 4, borderBounds.Width - 4)); var sideLineLength = borderBounds.Height; var canDrawBorder = borderBounds.Width > 0 && borderBounds.Height > 0; if (!string.IsNullOrEmpty (Parent?.Title)) { if (Thickness.Top == 2) { topTitleLineY = borderBounds.Y - 1; titleY = topTitleLineY + 1; titleBarsLength = 2; } // ┌────┐ //┌┘View└ //│ if (Thickness.Top == 3) { topTitleLineY = borderBounds.Y - (Thickness.Top - 1); titleY = topTitleLineY + 1; titleBarsLength = 3; sideLineLength++; } // ┌────┐ //┌┘View└ //│ if (Thickness.Top > 3) { topTitleLineY = borderBounds.Y - 2; titleY = topTitleLineY + 1; titleBarsLength = 3; sideLineLength++; } } if (Id == "Border" && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) { var prevAttr = Driver.GetAttribute (); if (ColorScheme != null) { Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ()); } else { Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ()); } DrawTitle (new Rect (borderBounds.X, titleY, maxTitleWidth, 1), Parent?.Title); Driver.SetAttribute (prevAttr); } if (Id == "Border" && canDrawBorder && BorderStyle != LineStyle.None) { LineCanvas lc = Parent?.LineCanvas; var drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1; var drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0); var drawBottom = Thickness.Bottom > 0 && Frame.Width > 1; var drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0); var prevAttr = Driver.GetAttribute (); if (ColorScheme != null) { Driver.SetAttribute (GetNormalColor ()); } else { Driver.SetAttribute (Parent.GetNormalColor ()); } if (drawTop) { // ╔╡Title╞═════╗ // ╔╡╞═════╗ if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title)) { // ╔╡╞╗ should be ╔══╗ lc.AddLine (new Point (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); } else { // ┌────┐ //┌┘View└ //│ if (Thickness.Top == 2) { lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); } // ┌────┐ //┌┘View└ //│ if (borderBounds.Width >= 4 && Thickness.Top > 2) { lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); } // ╔╡Title╞═════╗ // Add a short horiz line for ╔╡ lc.AddLine (new Point (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); // Add a vert line for ╔╡ lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ()); // Add a vert line for ╞ lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ()); // Add the right hand line for ╞═════╗ lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, titleY), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); } } if (drawLeft) { lc.AddLine (new Point (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ()); } if (drawBottom) { lc.AddLine (new Point (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ()); } if (drawRight) { lc.AddLine (new Point (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ()); } Driver.SetAttribute (prevAttr); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) { // Top var hruler = new Ruler () { Length = screenBounds.Width, Orientation = Orientation.Horizontal }; if (drawTop) { hruler.Draw (new Point (screenBounds.X, screenBounds.Y)); } // Redraw title if (drawTop && Id == "Border" && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) { prevAttr = Driver.GetAttribute (); if (ColorScheme != null) { Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ()); } else { Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ()); } DrawTitle (new Rect (borderBounds.X, titleY, Parent.Title.GetColumns (), 1), Parent?.Title); Driver.SetAttribute (prevAttr); } //Left var vruler = new Ruler () { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical }; if (drawLeft) { vruler.Draw (new Point (screenBounds.X, screenBounds.Y + 1), 1); } // Bottom if (drawBottom) { hruler.Draw (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); } // Right if (drawRight) { vruler.Draw (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1); } } } ClearNeedsDisplay (); } // TODO: v2 - Frame.BorderStyle is temporary - Eventually the border will be drawn by a "BorderView" that is a subview of the Frame. /// /// /// public new LineStyle BorderStyle { get; set; } = LineStyle.None; /// /// Defines the rectangle that the will use to draw its content. /// public Thickness Thickness { get { return _thickness; } set { var prev = _thickness; _thickness = value; if (prev != _thickness) { Parent?.LayoutFrames (); OnThicknessChanged (prev); } } } /// /// Called whenever the property changes. /// public virtual void OnThicknessChanged (Thickness previousThickness) { ThicknessChanged?.Invoke (this, new ThicknessEventArgs () { Thickness = Thickness, PreviousThickness = previousThickness }); } /// /// Fired whenever the property changes. /// public event EventHandler ThicknessChanged; /// /// Gets the rectangle that describes the inner area of the frame. The Location is always (0,0). /// public override Rect Bounds { get { return Thickness?.GetInside (new Rect (Point.Empty, Frame.Size)) ?? new Rect (Point.Empty, Frame.Size); } set { throw new InvalidOperationException ("It makes no sense to set Bounds of a Thickness."); } } /// /// Draws the title for a Window-style view. /// /// Screen relative region where the title will be drawn. /// The title. public void DrawTitle (Rect region, string title) { var width = region.Width; if (!string.IsNullOrEmpty (title)) { Driver.Move (region.X + 2, region.Y); //Driver.AddRune (' '); var str = title.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 1)) >= width ? TextFormatter.Format (title, width, false, false) [0] : title; Driver.AddStr (str); } } /// /// Draws a frame in the current view, clipped by the boundary of this view /// /// View-relative region for the frame to be drawn. /// If set to it clear the region. [ObsoleteAttribute ("This method is obsolete in v2. Use use LineCanvas or Frame instead.", false)] public void DrawFrame (Rect region, bool clear) { var savedClip = ClipToBounds (); var screenBounds = ViewToScreen (region); if (clear) { Driver.FillRect (region); } var lc = new LineCanvas (); var drawTop = region.Width > 1 && region.Height > 1; var drawLeft = region.Width > 1 && region.Height > 1; var drawBottom = region.Width > 1 && region.Height > 1; var drawRight = region.Width > 1 && region.Height > 1; if (drawTop) { lc.AddLine (screenBounds.Location, screenBounds.Width, Orientation.Horizontal, BorderStyle); } if (drawLeft) { lc.AddLine (screenBounds.Location, screenBounds.Height, Orientation.Vertical, BorderStyle); } if (drawBottom) { lc.AddLine (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1), screenBounds.Width, Orientation.Horizontal, BorderStyle); } if (drawRight) { lc.AddLine (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y), screenBounds.Height, Orientation.Vertical, BorderStyle); } foreach (var p in lc.GetMap ()) { Driver.Move (p.Key.X, p.Key.Y); Driver.AddRune (p.Value); } lc.Clear (); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) { // Top var hruler = new Ruler () { Length = screenBounds.Width, Orientation = Orientation.Horizontal }; if (drawTop) { hruler.Draw (new Point (screenBounds.X, screenBounds.Y)); } //Left var vruler = new Ruler () { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical }; if (drawLeft) { vruler.Draw (new Point (screenBounds.X, screenBounds.Y + 1), 1); } // Bottom if (drawBottom) { hruler.Draw (new Point (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); } // Right if (drawRight) { vruler.Draw (new Point (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1); } } Driver.Clip = savedClip; } } }