using System; using System.Linq; using NStack; namespace Terminal.Gui { public partial class View { ColorScheme _colorScheme; /// /// The color scheme for this view, if it is not defined, it returns the 's /// color scheme. /// public virtual ColorScheme ColorScheme { get { if (_colorScheme == null) { return SuperView?.ColorScheme; } return _colorScheme; } set { if (_colorScheme != value) { _colorScheme = value; SetNeedsDisplay (); } } } /// /// Determines the current based on the value. /// /// if is /// or if is . /// If it's overridden can return other values. public virtual Attribute GetNormalColor () { return Enabled ? ColorScheme.Normal : ColorScheme.Disabled; } /// /// Determines the current based on the value. /// /// if is /// or if is . /// If it's overridden can return other values. public virtual Attribute GetFocusColor () { return Enabled ? ColorScheme.Focus : ColorScheme.Disabled; } /// /// Determines the current based on the value. /// /// if is /// or if is . /// If it's overridden can return other values. public virtual Attribute GetHotNormalColor () { return Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled; } /// /// Displays the specified character in the specified column and row of the View. /// /// Column (view-relative). /// Row (view-relative). /// Ch. public void AddRune (int col, int row, Rune ch) { if (row < 0 || col < 0) return; if (row > _frame.Height - 1 || col > _frame.Width - 1) return; Move (col, row); Driver.AddRune (ch); } /// /// Removes the and the setting on this view. /// protected void ClearNeedsDisplay () { _needsDisplay = Rect.Empty; _childNeedsDisplay = false; } // The view-relative region that needs to be redrawn internal Rect _needsDisplay { get; private set; } = Rect.Empty; /// /// Sets a flag indicating this view needs to be redisplayed because its state has changed. /// public void SetNeedsDisplay () { if (!IsInitialized) return; SetNeedsDisplay (Bounds); } /// /// Flags the view-relative region on this View as needing to be redrawn. /// /// The view-relative region that needs to be redrawn. public void SetNeedsDisplay (Rect region) { if (_needsDisplay.IsEmpty) _needsDisplay = region; else { var x = Math.Min (_needsDisplay.X, region.X); var y = Math.Min (_needsDisplay.Y, region.Y); var w = Math.Max (_needsDisplay.Width, region.Width); var h = Math.Max (_needsDisplay.Height, region.Height); _needsDisplay = new Rect (x, y, w, h); } _superView?.SetSubViewNeedsDisplay (); if (_subviews == null) return; foreach (var view in _subviews) if (view.Frame.IntersectsWith (region)) { var childRegion = Rect.Intersect (view.Frame, region); childRegion.X -= view.Frame.X; childRegion.Y -= view.Frame.Y; view.SetNeedsDisplay (childRegion); } } internal bool _childNeedsDisplay { get; private set; } /// /// Indicates that any Subviews (in the list) need to be repainted. /// public void SetSubViewNeedsDisplay () { if (_childNeedsDisplay) { return; } _childNeedsDisplay = true; if (_superView != null && !_superView._childNeedsDisplay) _superView.SetSubViewNeedsDisplay (); } /// /// Clears the view region with the current color. /// /// /// /// This clears the entire region used by this view. /// /// public void Clear () { var h = Frame.Height; var w = Frame.Width; for (var line = 0; line < h; line++) { Move (0, line); for (var col = 0; col < w; col++) Driver.AddRune (' '); } } // BUGBUG: Stupid that this takes screen-relative. We should have a tenet that says // "View APIs only deal with View-relative coords". /// /// Clears the specified region with the current color. /// /// /// /// The screen-relative region to clear. public void Clear (Rect regionScreen) { var h = regionScreen.Height; var w = regionScreen.Width; for (var line = regionScreen.Y; line < regionScreen.Y + h; line++) { Driver.Move (regionScreen.X, line); for (var col = 0; col < w; col++) Driver.AddRune (' '); } } // Clips a rectangle in screen coordinates to the dimensions currently available on the screen internal Rect ScreenClip (Rect regionScreen) { var x = regionScreen.X < 0 ? 0 : regionScreen.X; var y = regionScreen.Y < 0 ? 0 : regionScreen.Y; var w = regionScreen.X + regionScreen.Width >= Driver.Cols ? Driver.Cols - regionScreen.X : regionScreen.Width; var h = regionScreen.Y + regionScreen.Height >= Driver.Rows ? Driver.Rows - regionScreen.Y : regionScreen.Height; return new Rect (x, y, w, h); } /// /// Sets the 's clip region to . /// /// The current screen-relative clip region, which can be then re-applied by setting . /// /// /// is View-relative. /// /// /// If and do not intersect, the clip region will be set to . /// /// public Rect ClipToBounds () { var clip = Bounds; return SetClip (clip); } // BUGBUG: v2 - SetClip should return VIEW-relative so that it can be used to reset it; using Driver.Clip directly should not be necessary. /// /// Sets the clip region to the specified view-relative region. /// /// The current screen-relative clip region, which can be then re-applied by setting . /// View-relative clip region. /// /// If and do not intersect, the clip region will be set to . /// public Rect SetClip (Rect region) { var previous = Driver.Clip; Driver.Clip = Rect.Intersect (previous, ViewToScreen (region)); return previous; } /// /// Utility function to draw strings that contain a hotkey. /// /// String to display, the hotkey specifier before a letter flags the next letter as the hotkey. /// Hot color. /// Normal color. /// /// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by default. /// The hotkey specifier can be changed via /// public void DrawHotString (ustring text, Attribute hotColor, Attribute normalColor) { var hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; Application.Driver.SetAttribute (normalColor); foreach (var rune in text) { if (rune == hotkeySpec) { Application.Driver.SetAttribute (hotColor); continue; } Application.Driver.AddRune (rune); Application.Driver.SetAttribute (normalColor); } } /// /// Utility function to draw strings that contains a hotkey using a and the "focused" state. /// /// String to display, the underscore before a letter flags the next letter as the hotkey. /// If set to this uses the focused colors from the color scheme, otherwise the regular ones. /// The color scheme to use. public void DrawHotString (ustring text, bool focused, ColorScheme scheme) { if (focused) DrawHotString (text, scheme.HotFocus, scheme.Focus); else DrawHotString (text, Enabled ? scheme.HotNormal : scheme.Disabled, Enabled ? scheme.Normal : scheme.Disabled); } /// /// This moves the cursor to the specified column and row in the view. /// /// The move. /// The column to move to, in view-relative coordinates. /// the row to move to, in view-relative coordinates. /// Whether to clip the result of the ViewToScreen method, /// If , the and values are clamped to the screen (terminal) dimensions (0..TerminalDim-1). public void Move (int col, int row, bool clipped = true) { if (Driver.Rows == 0) { return; } ViewToScreen (col, row, out var rCol, out var rRow, clipped); Driver.Move (rCol, rRow); } /// /// The canvas that any line drawing that is to be shared by subviews of this view should add lines to. /// /// adds border lines to this LineCanvas. public virtual LineCanvas LineCanvas { get; set; } = new LineCanvas (); /// /// Gets or sets whether this View will use it's SuperView's for /// rendering any border lines. If the rendering of any borders drawn /// by this Frame will be done by it's parent's SuperView. If (the default) /// this View's method will be called to render the borders. /// public virtual bool SuperViewRendersLineCanvas { get; set; } = false; // TODO: Make this cancelable /// /// /// /// public virtual bool OnDrawFrames () { if (!IsInitialized) { return false; } var prevClip = Driver.Clip; Driver.Clip = ViewToScreen (Frame); // TODO: Figure out what we should do if we have no superview //if (SuperView != null) { // TODO: Clipping is disabled for now to ensure we see errors Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows);// screenBounds;// SuperView.ClipToBounds (); //} // Each of these renders lines to either this View's LineCanvas // Those lines will be finally rendered in OnRenderLineCanvas Margin?.Redraw (Margin.Frame); Border?.Redraw (Border.Frame); Padding?.Redraw (Padding.Frame); Driver.Clip = prevClip; return true; } /// /// Redraws this view and its subviews; only redraws the views that have been flagged for a re-display. /// /// The bounds (view-relative region) to redraw. /// /// /// Always use (view-relative) when calling , NOT (superview-relative). /// /// /// Views should set the color that they want to use on entry, as otherwise this will inherit /// the last color that was set globally on the driver. /// /// /// Overrides of must ensure they do not set Driver.Clip to a clip region /// larger than the parameter, as this will cause the driver to clip the entire region. /// /// public virtual void Redraw (Rect bounds) { if (!CanBeVisible (this)) { return; } OnDrawFrames (); var prevClip = ClipToBounds (); if (ColorScheme != null) { //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); Driver.SetAttribute (GetNormalColor ()); } if (SuperView != null) { Clear (ViewToScreen (bounds)); } // Invoke DrawContentEvent OnDrawContent (bounds); // Draw subviews // TODO: Implement OnDrawSubviews (cancelable); if (_subviews != null) { foreach (var view in _subviews) { if (view.Visible) { //!view._needsDisplay.IsEmpty || view._childNeedsDisplay || view.LayoutNeeded) { if (true) { //view.Frame.IntersectsWith (bounds)) { // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) { if (view.LayoutNeeded) { view.LayoutSubviews (); } // Draw the subview // Use the view's bounds (view-relative; Location will always be (0,0) //if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) { view.Redraw (view.Bounds); //} } view.ClearNeedsDisplay (); } } } Driver.Clip = prevClip; OnRenderLineCanvas (); // Invoke DrawContentCompleteEvent OnDrawContentComplete (bounds); // BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details. ClearLayoutNeeded (); ClearNeedsDisplay (); } internal void OnRenderLineCanvas () { //Driver.SetAttribute (new Attribute(Color.White, Color.Black)); // If we have a SuperView, it'll render our frames. if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rect.Empty) { foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal); Driver.Move (p.Key.X, p.Key.Y); Driver.AddRune (p.Value.Rune.Value); } LineCanvas.Clear (); } if (Subviews.Select (s => s.SuperViewRendersLineCanvas).Count () > 0) { foreach (var subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) { // Combine the LineCavas' LineCanvas.Merge (subview.LineCanvas); subview.LineCanvas.Clear (); } foreach (var p in LineCanvas.GetCellMap ()) { // Get the entire map Driver.SetAttribute (p.Value.Attribute?.Value ?? ColorScheme.Normal); Driver.Move (p.Key.X, p.Key.Y); Driver.AddRune (p.Value.Rune.Value); } LineCanvas.Clear (); } } /// /// Event invoked when the content area of the View is to be drawn. /// /// /// /// Will be invoked before any subviews added with have been drawn. /// /// /// Rect provides the view-relative rectangle describing the currently visible viewport into the . /// /// public event EventHandler DrawContent; /// /// Enables overrides to draw infinitely scrolled content and/or a background behind added controls. /// /// The view-relative rectangle describing the currently visible viewport into the /// /// This method will be called before any subviews added with have been drawn. /// public virtual void OnDrawContent (Rect contentArea) { // TODO: Make DrawContent a cancelable event // if (!DrawContent?.Invoke(this, new DrawEventArgs (viewport)) { DrawContent?.Invoke (this, new DrawEventArgs (contentArea)); if (!ustring.IsNullOrEmpty (TextFormatter.Text)) { if (TextFormatter != null) { TextFormatter.NeedsFormat = true; } // This should NOT clear TextFormatter?.Draw (ViewToScreen (contentArea), HasFocus ? GetFocusColor () : GetNormalColor (), HasFocus ? ColorScheme.HotFocus : GetHotNormalColor (), Rect.Empty, false); SetSubViewNeedsDisplay (); } } /// /// Event invoked when the content area of the View is completed drawing. /// /// /// /// Will be invoked after any subviews removed with have been completed drawing. /// /// /// Rect provides the view-relative rectangle describing the currently visible viewport into the . /// /// public event EventHandler DrawContentComplete; /// /// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls. /// /// The view-relative rectangle describing the currently visible viewport into the /// /// This method will be called after any subviews removed with have been completed drawing. /// public virtual void OnDrawContentComplete (Rect viewport) { DrawContentComplete?.Invoke (this, new DrawEventArgs (viewport)); } } }