using System.Diagnostics; namespace Terminal.Gui.ViewBase; /// The Border for a . Accessed via /// /// /// 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 Thickness.Top is 1: /// ┌┤1234├──┐ /// │ │ /// └────────┘ /// // If Thickness.Top is 2: /// ┌────┐ /// ┌┤1234├──┐ /// │ │ /// └────────┘ /// If Thickness.Top is 3: /// ┌────┐ /// ┌┤1234├──┐ /// │└────┘ │ /// │ │ /// └────────┘ /// /// /// /// The Border provides keyboard and mouse support for moving and resizing the View. See . /// /// public partial 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) { Parent = parent; CanFocus = false; TabStop = TabBehavior.TabGroup; ThicknessChanged += OnThicknessChanged; } // TODO: Move DrawIndicator out of Border and into View private void OnThicknessChanged (object? sender, EventArgs e) { if (IsInitialized) { ShowHideDrawIndicator (); } } private void ShowHideDrawIndicator () { if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.DrawIndicator) && Thickness != Thickness.Empty) { if (DrawIndicator is null) { DrawIndicator = new () { Id = "DrawIndicator", X = 1, Style = new SpinnerStyle.Dots2 (), SpinDelay = 0, Visible = false }; Add (DrawIndicator); } } else if (DrawIndicator is { }) { Remove (DrawIndicator); DrawIndicator!.Dispose (); DrawIndicator = null; } } internal void AdvanceDrawIndicator () { if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.DrawIndicator) && DrawIndicator is { }) { DrawIndicator.AdvanceAnimation (false); DrawIndicator.Render (); } } #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 (App is { }) { App.Mouse.GrabbingMouse += Application_GrabbingMouse; App.Mouse.UnGrabbingMouse += Application_UnGrabbingMouse; } if (Parent is null) { return; } ShowHideDrawIndicator (); HighlightStates |= (Parent.Arrangement != ViewArrangement.Fixed ? MouseState.Pressed : MouseState.None); #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.Handled = 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 internal Rectangle GetBorderRectangle () { Rectangle screenRect = ViewportToScreen (Viewport); return new ( screenRect.X + Math.Max (0, Thickness.Left - 1), screenRect.Y + Math.Max (0, Thickness.Top - 1), Math.Max ( 0, screenRect.Width - Math.Max ( 0, Math.Max (0, Thickness.Left - 1) + Math.Max (0, Thickness.Right - 1) ) ), Math.Max ( 0, screenRect.Height - Math.Max ( 0, Math.Max (0, Thickness.Top - 1) + Math.Max (0, Thickness.Bottom - 1) ) ) ); } // TODO: Make LineStyle nullable https://github.com/gui-cs/Terminal.Gui/issues/4021 /// /// 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; } private BorderSettings _settings = BorderSettings.Title; /// /// Gets or sets the settings for the border. /// public BorderSettings Settings { get => _settings; set { if (value == _settings) { return; } _settings = value; Parent?.SetNeedsDraw (); } } /// protected override bool OnDrawingContent () { if (Thickness == Thickness.Empty) { return true; } Rectangle screenBounds = ViewportToScreen (Viewport); // 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? Rectangle borderBounds = GetBorderRectangle (); 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 ?? 0, Math.Min (screenBounds.Width - 4, borderBounds.Width - 4) ) ); if (Parent is { }) { Parent.TitleTextFormatter.ConstrainToSize = new (maxTitleWidth, 1); } int sideLineLength = borderBounds.Height; bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 }; LineStyle lineStyle = LineStyle; if (Settings.FastHasFlags (BorderSettings.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 (Driver is { } && Parent is { } && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title) && !string.IsNullOrEmpty (Parent?.Title)) { Rectangle titleRect = new (borderBounds.X + 2, titleY, maxTitleWidth, 1); Parent.TitleTextFormatter.Draw (driver: Driver, screen: titleRect, normalColor: GetAttributeForRole (Parent.HasFocus ? VisualRole.Focus : VisualRole.Normal), hotColor: GetAttributeForRole (Parent.HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal)); Parent?.LineCanvas.Exclude (new (titleRect)); } 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 && Frame.Height > 1; bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0); //Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default; Attribute normalAttribute = GetAttributeForRole (VisualRole.Normal); if (MouseState.HasFlag (MouseState.Pressed)) { normalAttribute = GetAttributeForRole (VisualRole.Highlight); } SetAttribute (normalAttribute); if (drawTop) { // ╔╡Title╞═════╗ // ╔╡╞═════╗ if (borderBounds.Width < 4 || !Settings.FastHasFlags (BorderSettings.Title) || string.IsNullOrEmpty (Parent?.Title)) { // ╔╡╞╗ should be ╔══╗ lc?.AddLine ( new (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, lineStyle, normalAttribute ); } else { // ┌────┐ //┌┘View└ //│ if (Thickness.Top == 2) { lc?.AddLine ( new (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, normalAttribute ); } // ┌────┐ //┌┘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, normalAttribute ); lc?.AddLine ( new (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, normalAttribute ); } // ╔╡Title╞═════╗ // Add a short horiz line for ╔╡ lc?.AddLine ( new (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, lineStyle, normalAttribute ); // Add a vert line for ╔╡ lc?.AddLine ( new (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, normalAttribute ); // 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, normalAttribute ); // 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, normalAttribute ); } } #if !SUBVIEW_BASED_BORDER if (drawLeft) { lc?.AddLine ( new (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, lineStyle, normalAttribute ); } #endif if (drawBottom) { lc?.AddLine ( new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, lineStyle, normalAttribute ); } if (drawRight) { lc?.AddLine ( new (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, lineStyle, normalAttribute ); } // SetAttribute (prevAttr); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if (Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler)) { // Top var hruler = new Ruler { Length = screenBounds.Width, Orientation = Orientation.Horizontal }; if (drawTop) { hruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y)); } // Redraw title if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title)) { Parent!.TitleTextFormatter.Draw (driver: Driver, screen: new (borderBounds.X + 2, titleY, maxTitleWidth, 1), normalColor: Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal), hotColor: Parent.HasFocus ? Parent.GetAttributeForRole (VisualRole.Focus) : Parent.GetAttributeForRole (VisualRole.Normal)); } //Left var vruler = new Ruler { Length = screenBounds.Height - 2, Orientation = Orientation.Vertical }; if (drawLeft) { vruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + 1), start: 1); } // Bottom if (drawBottom) { hruler.Draw (driver: Driver, location: new (screenBounds.X, screenBounds.Y + screenBounds.Height - 1)); } // Right if (drawRight) { vruler.Draw (driver: Driver, location: new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), start: 1); } } // TODO: This should not be done on each draw? if (Settings.FastHasFlags (BorderSettings.Gradient)) { SetupGradientLineCanvas (lc!, screenBounds); } else { lc!.Fill = null; } } return true; ; } /// /// Gets the subview used to render . /// public SpinnerView? DrawIndicator { get; private set; } private void SetupGradientLineCanvas (LineCanvas lc, Rectangle rect) { GetAppealingGradientColors (out List stops, out List steps); var g = new Gradient (stops, steps); var fore = new GradientFill (rect, g, GradientDirection.Diagonal); var back = new SolidFill (GetAttributeForRole (VisualRole.Normal).Background); lc.Fill = new (fore, back); } private static void GetAppealingGradientColors (out List stops, out List steps) { // Define the colors of the gradient stops with more appealing colors stops = [ new (0, 128, 255), // Bright Blue new (0, 255, 128), // Bright Green new (255, 255), // Bright Yellow new (255, 128), // Bright Orange new (255, 0, 128) ]; // Define the number of steps between each color for smoother transitions // If we pass only a single value then it will assume equal steps between all pairs steps = [15]; } }