namespace Terminal.Gui; /// The Border for a . /// /// /// Renders a border around the view with the . A border using /// will be drawn on the sides of that are greater than zero. /// /// /// The of will be drawn based on the value of /// : /// /// /// If 1: /// /// ┌┤1234├──┐ /// │ │ /// └────────┘ /// /// /// /// If 2: /// /// ┌────┐ /// ┌┤1234├──┐ /// │ │ /// └────────┘ /// /// /// /// If 3: /// /// ┌────┐ /// ┌┤1234├──┐ /// │└────┘ │ /// │ │ /// └────────┘ /// /// /// /// See the class. /// public class Border : Adornment { private LineStyle? _lineStyle; /// public Border () { /* Do nothing; A parameter-less constructor is required to support all views unit tests. */ } /// public Border (View parent) : base (parent) { /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */ Parent = parent; Application.GrabbingMouse += Application_GrabbingMouse; Application.UnGrabbingMouse += Application_UnGrabbingMouse; } #if SUBVIEW_BASED_BORDER private Line _left; /// /// The close button for the border. Set to , to to enable. /// public Button CloseButton { get; internal set; } #endif /// public override void BeginInit () { base.BeginInit (); #if SUBVIEW_BASED_BORDER if (Parent is { }) { // Left _left = new () { Orientation = Orientation.Vertical, }; Add (_left); CloseButton = new Button () { Text = "X", CanFocus = true, Visible = false, }; CloseButton.Accept += (s, e) => { e.Cancel = Parent.InvokeCommand (Command.QuitToplevel) == true; }; Add (CloseButton); LayoutStarted += OnLayoutStarted; } #endif } #if SUBVIEW_BASED_BORDER private void OnLayoutStarted (object sender, LayoutEventArgs e) { _left.Border.LineStyle = LineStyle; _left.X = Thickness.Left - 1; _left.Y = Thickness.Top - 1; _left.Width = 1; _left.Height = Height; CloseButton.X = Pos.AnchorEnd (Thickness.Right / 2 + 1) - (Pos.Right (CloseButton) - Pos.Left (CloseButton)); CloseButton.Y = 0; } #endif /// /// The color scheme for the Border. If set to , gets the /// scheme. color scheme. /// public override ColorScheme ColorScheme { get { if (base.ColorScheme is { }) { return base.ColorScheme; } return Parent?.ColorScheme; } set { base.ColorScheme = value; Parent?.SetNeedsDisplay (); } } Rectangle GetBorderBounds (Rectangle screenBounds) { return new ( 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) ) ) ); } /// /// Sets the style of the border by changing the . This is a helper API for setting the /// to (1,1,1,1) and setting the line style of the views that comprise the border. If /// set to no border will be drawn. /// public LineStyle LineStyle { get { if (_lineStyle.HasValue) { return _lineStyle.Value; } // TODO: Make Border.LineStyle inherit from the SuperView hierarchy // TODO: Right now, Window and FrameView use CM to set BorderStyle, which negates // TODO: all this. return Parent.SuperView?.BorderStyle ?? LineStyle.None; } set => _lineStyle = value; } #region Mouse Support private Point? _dragPosition; private Point _startGrabPoint; /// protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { if (base.OnMouseEvent (mouseEvent)) { return true; } if (!Parent.CanFocus) { return false; } if (!Parent.Arrangement.HasFlag (ViewArrangement.Movable)) { return false; } // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3312 if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { Parent.SetFocus (); Application.BringOverlappedTopToFront (); // Only start grabbing if the user clicks in the Thickness area // Adornment.Contains takes Parent SuperView=relative coords. if (Contains (mouseEvent.X + Parent.Frame.X + Frame.X, mouseEvent.Y + Parent.Frame.Y + Frame.Y)) { // Set the start grab point to the Frame coords _startGrabPoint = new (mouseEvent.X + Frame.X, mouseEvent.Y + Frame.Y); _dragPosition = new (mouseEvent.X, mouseEvent.Y); Application.GrabMouse (this); } return true; } if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { if (Application.MouseGrabView == this && _dragPosition.HasValue) { if (Parent.SuperView is null) { // Redraw the entire app window. Application.Top.SetNeedsDisplay (); } else { Parent.SuperView.SetNeedsDisplay (); } _dragPosition = new Point (mouseEvent.X, mouseEvent.Y); Point parentLoc = Parent.SuperView?.ScreenToBounds (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y) ?? mouseEvent.ScreenPosition; GetLocationEnsuringFullVisibility ( Parent, parentLoc.X - _startGrabPoint.X, parentLoc.Y - _startGrabPoint.Y, out int nx, out int ny, out _ ); Parent.X = nx; Parent.Y = ny; return true; } } if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue) { _dragPosition = null; Application.UngrabMouse (); return true; } return false; } /// protected override void Dispose (bool disposing) { Application.GrabbingMouse -= Application_GrabbingMouse; Application.UnGrabbingMouse -= Application_UnGrabbingMouse; _dragPosition = null; base.Dispose (disposing); } private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e) { if (Application.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } } private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e) { if (Application.MouseGrabView == this && _dragPosition.HasValue) { e.Cancel = true; } } #endregion Mouse Support /// public override void OnDrawContent (Rectangle contentArea) { base.OnDrawContent (contentArea); if (Thickness == Thickness.Empty) { return; } //Driver.SetAttribute (Colors.ColorSchemes ["Error"].Normal); Rectangle screenBounds = BoundsToScreen (contentArea); //OnDrawSubviews (bounds); // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title) // The border adornment (and title) are drawn at the outermost edge of border; // For Border // ...thickness extends outward (border/title is always as far in as possible) // PERF: How about a call to Rectangle.Offset? var borderBounds = GetBorderBounds (screenBounds); int topTitleLineY = borderBounds.Y; int titleY = borderBounds.Y; var titleBarsLength = 0; // the little vertical thingies int maxTitleWidth = Math.Max ( 0, Math.Min ( Parent.TitleTextFormatter.FormatAndGetSize ().Width, Math.Min (screenBounds.Width - 4, borderBounds.Width - 4) ) ); Parent.TitleTextFormatter.Size = new (maxTitleWidth, 1); int sideLineLength = borderBounds.Height; bool canDrawBorder = borderBounds is { Width: > 0, 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 (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) { Parent.TitleTextFormatter.Draw ( new (borderBounds.X + 2, titleY, maxTitleWidth, 1), Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (), Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetHotNormalColor ()); } if (canDrawBorder && LineStyle != LineStyle.None) { LineCanvas lc = Parent?.LineCanvas; bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1; bool drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0); bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1; bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0); Attribute prevAttr = Driver.GetAttribute (); if (ColorScheme is { }) { Driver.SetAttribute (GetNormalColor ()); } else { Driver.SetAttribute (Parent.GetNormalColor ()); } if (drawTop) { // ╔╡Title╞═════╗ // ╔╡╞═════╗ if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title)) { // ╔╡╞╗ should be ╔══╗ lc.AddLine ( new (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); } else { // ┌────┐ //┌┘View└ //│ if (Thickness.Top == 2) { lc.AddLine ( new (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); } // ┌────┐ //┌┘View└ //│ if (borderBounds.Width >= 4 && Thickness.Top > 2) { lc.AddLine ( new (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); lc.AddLine ( new (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); } // ╔╡Title╞═════╗ // Add a short horiz line for ╔╡ lc.AddLine ( new (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); // Add a vert line for ╔╡ lc.AddLine ( new (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute () ); // Add a vert line for ╞ lc.AddLine ( new ( 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 ( borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, titleY ), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); } } #if !SUBVIEW_BASED_BORDER if (drawLeft) { lc.AddLine ( new (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, LineStyle, Driver.GetAttribute () ); } #endif if (drawBottom) { lc.AddLine ( new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, LineStyle, Driver.GetAttribute () ); } if (drawRight) { lc.AddLine ( new (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, LineStyle, Driver.GetAttribute () ); } Driver.SetAttribute (prevAttr); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler)) { // Top var hruler = new Ruler { Length = screenBounds.Width, Orientation = Orientation.Horizontal }; if (drawTop) { hruler.Draw (new (screenBounds.X, screenBounds.Y)); } // Redraw title if (drawTop && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) { Parent.TitleTextFormatter.Draw ( new (borderBounds.X + 2, titleY, maxTitleWidth, 1), Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (), Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor ()); } //Left var vruler = new Ruler { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical }; if (drawLeft) { vruler.Draw (new (screenBounds.X, screenBounds.Y + 1), 1); } // Bottom if (drawBottom) { hruler.Draw (new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); } // Right if (drawRight) { vruler.Draw (new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1); } } } //base.OnDrawContent (contentArea); } }