diff --git a/CommunityToolkitExample/LoginView.cs b/CommunityToolkitExample/LoginView.cs index 8d66f29bbf..7f8fbe53d0 100644 --- a/CommunityToolkitExample/LoginView.cs +++ b/CommunityToolkitExample/LoginView.cs @@ -59,7 +59,7 @@ public void Receive (Message message) } } SetText(); - Application.Refresh (); + Application.LayoutAndDraw (); } private void SetText () diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 02351612f0..3d47aeb410 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -39,7 +39,6 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresDynamicCode ("AOT")] public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } - internal static bool IsInitialized { get; set; } internal static int MainThreadId { get; set; } = -1; // INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop. @@ -59,12 +58,12 @@ internal static void InternalInit ( bool calledViaRunT = false ) { - if (IsInitialized && driver is null) + if (Initialized && driver is null) { return; } - if (IsInitialized) + if (Initialized) { throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown."); } @@ -173,7 +172,7 @@ internal static void InternalInit ( SupportedCultures = GetSupportedCultures (); MainThreadId = Thread.CurrentThread.ManagedThreadId; - bool init = IsInitialized = true; + bool init = Initialized = true; InitializedChanged?.Invoke (null, new (init)); } @@ -215,17 +214,27 @@ public static void Shutdown () { // TODO: Throw an exception if Init hasn't been called. - bool wasInitialized = IsInitialized; + bool wasInitialized = Initialized; ResetState (); PrintJsonErrors (); if (wasInitialized) { - bool init = IsInitialized; + bool init = Initialized; InitializedChanged?.Invoke (null, new (in init)); } } + /// + /// Gets whether the application has been initialized with and not yet shutdown with . + /// + /// + /// + /// The event is raised after the and methods have been called. + /// + /// + public static bool Initialized { get; internal set; } + /// /// This event is raised after the and methods have been called. /// diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index c9fa96c631..a883dea8e5 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -120,7 +120,7 @@ public static bool RaiseKeyDownEvent (Key key) /// if the key was handled. public static bool RaiseKeyUpEvent (Key key) { - if (!IsInitialized) + if (!Initialized) { return true; } @@ -200,7 +200,7 @@ internal static void AddApplicationKeyBindings () Command.Refresh, static () => { - Refresh (); + LayoutAndDraw (); return true; } diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index c4477ac85d..a944c4c376 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -227,7 +227,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) { if (deepestViewUnderMouse is Adornment adornmentView) { - deepestViewUnderMouse = adornmentView.Parent!.SuperView; + deepestViewUnderMouse = adornmentView.Parent?.SuperView; } else { diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 966fb04c68..898fc935b0 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; namespace Terminal.Gui; @@ -80,27 +81,20 @@ public static RunState Begin (Toplevel toplevel) { ArgumentNullException.ThrowIfNull (toplevel); -//#if DEBUG_IDISPOSABLE -// Debug.Assert (!toplevel.WasDisposed); + //#if DEBUG_IDISPOSABLE + // Debug.Assert (!toplevel.WasDisposed); -// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) -// { -// Debug.Assert (_cachedRunStateToplevel.WasDisposed); -// } -//#endif + // if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel) + // { + // Debug.Assert (_cachedRunStateToplevel.WasDisposed); + // } + //#endif // Ensure the mouse is ungrabbed. MouseGrabView = null; var rs = new RunState (toplevel); - // View implements ISupportInitializeNotification which is derived from ISupportInitialize - if (!toplevel.IsInitialized) - { - toplevel.BeginInit (); - toplevel.EndInit (); - } - #if DEBUG_IDISPOSABLE if (Top is { } && toplevel != Top && !TopLevels.Contains (Top)) { @@ -176,16 +170,26 @@ public static RunState Begin (Toplevel toplevel) Top.HasFocus = false; } + // Force leave events for any entered views in the old Top + if (GetLastMousePosition () is { }) + { + RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new List ()); + } + Top?.OnDeactivate (toplevel); - Toplevel previousCurrent = Top!; + Toplevel previousTop = Top!; Top = toplevel; - Top.OnActivate (previousCurrent); + Top.OnActivate (previousTop); } } - toplevel.SetRelativeLayout (Driver!.Screen.Size); - toplevel.LayoutSubviews (); + // View implements ISupportInitializeNotification which is derived from ISupportInitialize + if (!toplevel.IsInitialized) + { + toplevel.BeginInit (); + toplevel.EndInit (); // Calls Layout + } // Try to set initial focus to any TabStop if (!toplevel.HasFocus) @@ -195,15 +199,16 @@ public static RunState Begin (Toplevel toplevel) toplevel.OnLoaded (); - Refresh (); - if (PositionCursor ()) { - Driver.UpdateCursor (); + Driver?.UpdateCursor (); } NotifyNewRunState?.Invoke (toplevel, new (rs)); + // Force an Idle event so that an Iteration (and Refresh) happen. + Application.Invoke (() => { }); + return rs; } @@ -225,11 +230,12 @@ internal static bool PositionCursor () // If the view is not visible or enabled, don't position the cursor if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled) { - Driver!.GetCursorVisibility (out CursorVisibility current); + CursorVisibility current = CursorVisibility.Invisible; + Driver?.GetCursorVisibility (out current); if (current != CursorVisibility.Invisible) { - Driver.SetCursorVisibility (CursorVisibility.Invisible); + Driver?.SetCursorVisibility (CursorVisibility.Invisible); } return false; @@ -326,7 +332,7 @@ internal static bool PositionCursor () public static T Run (Func? errorHandler = null, ConsoleDriver? driver = null) where T : Toplevel, new() { - if (!IsInitialized) + if (!Initialized) { // Init() has NOT been called. InternalInit (driver, null, true); @@ -381,7 +387,7 @@ public static void Run (Toplevel view, Func? errorHandler = nul { ArgumentNullException.ThrowIfNull (view); - if (IsInitialized) + if (Initialized) { if (Driver is null) { @@ -452,7 +458,10 @@ public static void Run (Toplevel view, Func? errorHandler = nul /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a /// token that can be used to stop the timeout by calling . /// - public static object AddTimeout (TimeSpan time, Func callback) { return MainLoop!.AddTimeout (time, callback); } + public static object? AddTimeout (TimeSpan time, Func callback) + { + return MainLoop?.AddTimeout (time, callback) ?? null; + } /// Removes a previously scheduled timeout /// The token parameter is the value returned by . @@ -486,20 +495,25 @@ public static void Invoke (Action action) /// Wakes up the running application that might be waiting on input. public static void Wakeup () { MainLoop?.Wakeup (); } - /// Triggers a refresh of the entire display. - public static void Refresh () + /// + /// Causes any Toplevels that need layout to be laid out. Then draws any Toplevels that need display. Only Views that need to be laid out (see ) will be laid out. + /// Only Views that need to be drawn (see ) will be drawn. + /// + /// If the entire View hierarchy will be redrawn. The default is and should only be overriden for testing. + public static void LayoutAndDraw (bool forceDraw = false) { - foreach (Toplevel tl in TopLevels.Reverse ()) - { - if (tl.LayoutNeeded) - { - tl.LayoutSubviews (); - } + bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size); - tl.Draw (); + if (forceDraw) + { + Driver?.ClearContents (); } - Driver!.Refresh (); + View.SetClipToScreen (); + View.Draw (TopLevels, neededLayout || forceDraw); + View.SetClipToScreen (); + + Driver?.Refresh (); } /// This event is raised on each iteration of the main loop. @@ -534,24 +548,25 @@ public static void RunLoop (RunState state) return; } - RunIteration (ref state, ref firstIteration); + firstIteration = RunIteration (ref state, firstIteration); } MainLoop!.Running = false; // Run one last iteration to consume any outstanding input events from Driver // This is important for remaining OnKeyUp events. - RunIteration (ref state, ref firstIteration); + RunIteration (ref state, firstIteration); } /// Run one application iteration. /// The state returned by . /// - /// Set to if this is the first run loop iteration. Upon return, it - /// will be set to if at least one iteration happened. + /// Set to if this is the first run loop iteration. /// - public static void RunIteration (ref RunState state, ref bool firstIteration) + /// if at least one iteration happened. + public static bool RunIteration (ref RunState state, bool firstIteration = false) { + // If the driver has events pending do an iteration of the driver MainLoop if (MainLoop!.Running && MainLoop.EventsPending ()) { // Notify Toplevel it's ready @@ -561,6 +576,7 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) } MainLoop.RunIteration (); + Iteration?.Invoke (null, new ()); } @@ -568,16 +584,17 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) if (Top is null) { - return; + return firstIteration; } - Refresh (); + LayoutAndDraw (); if (PositionCursor ()) { Driver!.UpdateCursor (); } + return firstIteration; } /// Stops the provided , causing or the if provided. @@ -652,7 +669,7 @@ public static void End (RunState runState) if (TopLevels.Count > 0) { Top = TopLevels.Peek (); - Top.SetNeedsDisplay (); + Top.SetNeedsDraw (); } if (runState.Toplevel is { HasFocus: true }) @@ -670,6 +687,6 @@ public static void End (RunState runState) runState.Toplevel = null; runState.Dispose (); - Refresh (); + LayoutAndDraw (); } } diff --git a/Terminal.Gui/Application/Application.Screen.cs b/Terminal.Gui/Application/Application.Screen.cs index 087021d1c5..c5bf6d6fd0 100644 --- a/Terminal.Gui/Application/Application.Screen.cs +++ b/Terminal.Gui/Application/Application.Screen.cs @@ -3,13 +3,35 @@ namespace Terminal.Gui; public static partial class Application // Screen related stuff { + private static Rectangle? _screen; + /// - /// Gets the size of the screen. This is the size of the screen as reported by the . + /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the . /// /// + /// /// If the has not been initialized, this will return a default size of 2048x2048; useful for unit tests. + /// /// - public static Rectangle Screen => Driver?.Screen ?? new (0, 0, 2048, 2048); + public static Rectangle Screen + { + get + { + if (_screen == null) + { + _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048)); + } + return _screen.Value; + } + set + { + if (value is {} && (value.X != 0 || value.Y != 0)) + { + throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported"); + } + _screen = value; + } + } /// Invoked when the terminal's size changed. The new size of the terminal is provided. /// @@ -33,14 +55,15 @@ public static bool OnSizeChanging (SizeChangedEventArgs args) return false; } + Screen = new (Point.Empty, args.Size.Value); + foreach (Toplevel t in TopLevels) { - t.SetRelativeLayout (args.Size.Value); - t.LayoutSubviews (); t.OnSizeChanging (new (args.Size)); + t.SetNeedsLayout (); } - Refresh (); + LayoutAndDraw (); return true; } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 2b7476fbcb..d9e6c68d92 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -185,6 +185,8 @@ internal static void ResetState (bool ignoreDisposed = false) Driver = null; } + _screen = null; + // Don't reset ForceDriver; it needs to be set before Init is called. //ForceDriver = string.Empty; //Force16Colors = false; @@ -194,7 +196,7 @@ internal static void ResetState (bool ignoreDisposed = false) NotifyNewRunState = null; NotifyStopRunState = null; MouseGrabView = null; - IsInitialized = false; + Initialized = false; // Mouse _lastMousePosition = null; diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs index 5835957a29..1fe6972eae 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -30,15 +30,6 @@ public ApplicationNavigation () public View? GetFocused () { return _focused; - - if (_focused is { CanFocus: true, HasFocus: true }) - { - return _focused; - } - - _focused = null; - - return null; } /// diff --git a/Terminal.Gui/Clipboard/Clipboard.cs b/Terminal.Gui/Clipboard/Clipboard.cs index 5dccea0a41..f8bf907c75 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -148,7 +148,7 @@ public static (int exitCode, string result) Process ( bool waitForOutput = true ) { - var output = string.Empty; + var output = string.Empty; using (var process = new Process { diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 7d6de3834f..e1e1730011 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -23,14 +23,14 @@ public abstract class ConsoleDriver /// Gets the location and size of the terminal screen. internal Rectangle Screen => new (0, 0, Cols, Rows); - private Rectangle _clip; + private Region? _clip = null; /// /// Gets or sets the clip rectangle that and are subject /// to. /// /// The rectangle describing the of region. - public Rectangle Clip + internal Region? Clip { get => _clip; set @@ -40,8 +40,13 @@ public Rectangle Clip return; } + _clip = value; + // Don't ever let Clip be bigger than Screen - _clip = Rectangle.Intersect (Screen, value); + if (_clip is { }) + { + _clip.Intersect (Screen); + } } } @@ -52,13 +57,13 @@ public Rectangle Clip /// Gets the column last set by . and are used by /// and to determine where to add content. /// - public int Col { get; internal set; } + internal int Col { get; private set; } /// The number of columns visible in the terminal. - public virtual int Cols + internal virtual int Cols { get => _cols; - internal set + set { _cols = value; ClearContents (); @@ -70,22 +75,22 @@ internal set /// is called. /// The format of the array is rows, columns. The first index is the row, the second index is the column. /// - public Cell [,]? Contents { get; internal set; } + internal Cell [,]? Contents { get; set; } /// The leftmost column in the terminal. - public virtual int Left { get; internal set; } = 0; + internal virtual int Left { get; set; } = 0; /// /// Gets the row last set by . and are used by /// and to determine where to add content. /// - public int Row { get; internal set; } + internal int Row { get; private set; } /// The number of rows visible in the terminal. - public virtual int Rows + internal virtual int Rows { get => _rows; - internal set + set { _rows = value; ClearContents (); @@ -93,7 +98,7 @@ internal set } /// The topmost row in the terminal. - public virtual int Top { get; internal set; } = 0; + internal virtual int Top { get; set; } = 0; /// /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. @@ -120,16 +125,18 @@ internal set /// /// /// Rune to add. - public void AddRune (Rune rune) + internal void AddRune (Rune rune) { int runeWidth = -1; - bool validLocation = IsValidLocation (Col, Row); + bool validLocation = IsValidLocation (rune, Col, Row); if (Contents is null) { return; } + Rectangle clipRect = Clip!.GetBounds (); + if (validLocation) { rune = rune.MakePrintable (); @@ -217,24 +224,29 @@ public void AddRune (Rune rune) { Contents [Row, Col].Rune = rune; - if (Col < Clip.Right - 1) + if (Col < clipRect.Right - 1) { Contents [Row, Col + 1].IsDirty = true; } } else if (runeWidth == 2) { - if (Col == Clip.Right - 1) + if (!Clip.Contains (Col + 1, Row)) { // We're at the right edge of the clip, so we can't display a wide character. // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col].Rune = Rune.ReplacementChar; } + else if (!Clip.Contains (Col, Row)) + { + // Our 1st column is outside the clip, so we can't display a wide character. + Contents [Row, Col+1].Rune = Rune.ReplacementChar; + } else { Contents [Row, Col].Rune = rune; - if (Col < Clip.Right - 1) + if (Col < clipRect.Right - 1) { // Invalidate cell to right so that it doesn't get drawn // TODO: Figure out if it is better to show a replacement character or ' ' @@ -264,7 +276,7 @@ public void AddRune (Rune rune) { Debug.Assert (runeWidth <= 2); - if (validLocation && Col < Clip.Right) + if (validLocation && Col < clipRect.Right) { lock (Contents!) { @@ -288,7 +300,7 @@ public void AddRune (Rune rune) /// convenience method that calls with the constructor. /// /// Character to add. - public void AddRune (char c) { AddRune (new Rune (c)); } + internal void AddRune (char c) { AddRune (new Rune (c)); } /// Adds the to the display at the cursor position. /// @@ -300,7 +312,7 @@ public void AddRune (Rune rune) /// If requires more columns than are available, the output will be clipped. /// /// String. - public void AddStr (string str) + internal void AddStr (string str) { List runes = str.EnumerateRunes ().ToList (); @@ -311,12 +323,14 @@ public void AddStr (string str) } /// Clears the of the driver. - public void ClearContents () + internal void ClearContents () { Contents = new Cell [Rows, Cols]; + //CONCURRENCY: Unsynchronized access to Clip isn't safe. // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere. - Clip = Screen; + Clip = new (Screen); + _dirtyLines = new bool [Rows]; lock (Contents) @@ -335,13 +349,20 @@ public void ClearContents () _dirtyLines [row] = true; } } + + ClearedContents?.Invoke (this, EventArgs.Empty); } + /// + /// Raised each time is called. For benchmarking. + /// + public event EventHandler? ClearedContents; + /// /// Sets as dirty for situations where views /// don't need layout and redrawing, but just refresh the screen. /// - public void SetContentsAsDirty () + internal void SetContentsAsDirty () { lock (Contents!) { @@ -366,15 +387,20 @@ public void SetContentsAsDirty () /// /// The Screen-relative rectangle. /// The Rune used to fill the rectangle - public void FillRect (Rectangle rect, Rune rune = default) + internal void FillRect (Rectangle rect, Rune rune = default) { - rect = Rectangle.Intersect (rect, Clip); + // BUGBUG: This should be a method on Region + rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); lock (Contents!) { for (int r = rect.Y; r < rect.Y + rect.Height; r++) { for (int c = rect.X; c < rect.X + rect.Width; c++) { + if (!IsValidLocation (rune, c, r)) + { + continue; + } Contents [r, c] = new Cell { Rune = (rune != default ? rune : (Rune)' '), @@ -392,7 +418,7 @@ public void FillRect (Rectangle rect, Rune rune = default) /// /// /// - public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } + internal void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } /// Gets the terminal cursor visibility. /// The current @@ -411,18 +437,28 @@ public void FillRect (Rectangle rect, Rune rune = default) /// public virtual bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } - /// Tests whether the specified coordinate are valid for drawing. + /// Tests whether the specified coordinate are valid for drawing the specified Rune. + /// Used to determine if one or two columns are required. /// The column. /// The row. /// /// if the coordinate is outside the screen bounds or outside of . /// otherwise. /// - public bool IsValidLocation (int col, int row) + internal bool IsValidLocation (Rune rune, int col, int row) { - return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); + if (rune.GetColumns () < 2) + { + return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); + } + else + { + + return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); + } } + // TODO: Make internal once Menu is upgraded /// /// Updates and to the specified column and row in . /// Used by and to determine where to add content. @@ -445,10 +481,21 @@ public virtual void Move (int col, int row) /// Called when the terminal size changes. Fires the event. /// - public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } + internal void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } /// Updates the screen to reflect all the changes that have been done to the display buffer - public abstract void Refresh (); + internal void Refresh () + { + bool updated = UpdateScreen (); + UpdateCursor (); + + Refreshed?.Invoke (this, new EventArgs (in updated)); + } + + /// + /// Raised each time is called. For benchmarking. + /// + public event EventHandler>? Refreshed; /// Sets the terminal cursor visibility. /// The wished @@ -466,7 +513,8 @@ public virtual void Move (int col, int row) public abstract void UpdateCursor (); /// Redraws the physical screen with the contents that have been queued up via any of the printing commands. - public abstract void UpdateScreen (); + /// if any updates to the screen were made. + public abstract bool UpdateScreen (); #region Setup & Teardown @@ -530,7 +578,7 @@ public Attribute CurrentAttribute /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. /// Implementations should call base.SetAttribute(c). /// C. - public Attribute SetAttribute (Attribute c) + internal Attribute SetAttribute (Attribute c) { Attribute prevAttribute = CurrentAttribute; CurrentAttribute = c; @@ -540,7 +588,7 @@ public Attribute SetAttribute (Attribute c) /// Gets the current . /// The current attribute. - public Attribute GetAttribute () { return CurrentAttribute; } + internal Attribute GetAttribute () { return CurrentAttribute; } // TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be // removed (and Attribute can lose the platformColor property). diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 88005861ad..446926fef1 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -19,20 +19,20 @@ internal class CursesDriver : ConsoleDriver private UnixMainLoop _mainLoopDriver; private object _processInputToken; - public override int Cols + internal override int Cols { get => Curses.Cols; - internal set + set { Curses.Cols = value; ClearContents (); } } - public override int Rows + internal override int Rows { get => Curses.Lines; - internal set + set { Curses.Lines = value; ClearContents (); @@ -93,7 +93,7 @@ public override void Move (int col, int row) return; } - if (IsValidLocation (col, row)) + if (IsValidLocation (default, col, row)) { Curses.move (row, col); } @@ -101,16 +101,12 @@ public override void Move (int col, int row) { // Not a valid location (outside screen or clip region) // Move within the clip region, then AddRune will actually move to Col, Row - Curses.move (Clip.Y, Clip.X); + Rectangle clipRect = Clip.GetBounds (); + Curses.move (clipRect.Y, clipRect.X); } } - public override void Refresh () - { - UpdateScreen (); - UpdateCursor (); - } - + public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control) { KeyCode key; @@ -228,8 +224,9 @@ public override void UpdateCursor () } } - public override void UpdateScreen () + public override bool UpdateScreen () { + bool updated = false; if (Force16Colors) { for (var row = 0; row < Rows; row++) @@ -297,7 +294,7 @@ public override void UpdateScreen () || Contents.Length != Rows * Cols || Rows != Console.WindowHeight) { - return; + return updated; } var top = 0; @@ -315,7 +312,7 @@ public override void UpdateScreen () { if (Console.WindowHeight < 1) { - return; + return updated; } if (!_dirtyLines [row]) @@ -325,7 +322,7 @@ public override void UpdateScreen () if (!SetCursorPosition (0, row)) { - return; + return updated; } _dirtyLines [row] = false; @@ -338,6 +335,7 @@ public override void UpdateScreen () for (; col < cols; col++) { + updated = true; if (!Contents [row, col].IsDirty) { if (output.Length > 0) @@ -440,6 +438,8 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out outputWidth = 0; } } + + return updated; } private bool SetCursorPosition (int col, int row) diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 73c12959f4..a5f297ee69 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -101,8 +101,10 @@ internal override MainLoop Init () return new MainLoop (_mainLoopDriver); } - public override void UpdateScreen () + public override bool UpdateScreen () { + bool updated = false; + int savedRow = FakeConsole.CursorTop; int savedCol = FakeConsole.CursorLeft; bool savedCursorVisible = FakeConsole.CursorVisible; @@ -122,6 +124,8 @@ public override void UpdateScreen () continue; } + updated = true; + FakeConsole.CursorTop = row; FakeConsole.CursorLeft = 0; @@ -218,13 +222,9 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out FakeConsole.CursorTop = savedRow; FakeConsole.CursorLeft = savedCol; FakeConsole.CursorVisible = savedCursorVisible; + return updated; } - public override void Refresh () - { - UpdateScreen (); - UpdateCursor (); - } #region Color Handling @@ -456,7 +456,7 @@ public virtual void ResizeScreen () } // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); } public override void UpdateCursor () diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index cab7d3e6bf..a5afbf2580 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -823,12 +823,6 @@ internal class NetDriver : ConsoleDriver public override bool SupportsTrueColor => Environment.OSVersion.Platform == PlatformID.Unix || (IsWinPlatform && Environment.OSVersion.Version.Build >= 14931); - public override void Refresh () - { - UpdateScreen (); - UpdateCursor (); - } - public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) { var input = new InputResult @@ -876,15 +870,16 @@ public override void Suspend () StartReportingMouseMoves (); } - public override void UpdateScreen () + public override bool UpdateScreen () { + bool updated = false; if (RunningUnitTests || _winSizeChanging || Console.WindowHeight < 1 || Contents.Length != Rows * Cols || Rows != Console.WindowHeight) { - return; + return updated; } var top = 0; @@ -902,7 +897,7 @@ public override void UpdateScreen () { if (Console.WindowHeight < 1) { - return; + return updated; } if (!_dirtyLines [row]) @@ -912,9 +907,10 @@ public override void UpdateScreen () if (!SetCursorPosition (0, row)) { - return; + return updated; } + updated = true; _dirtyLines [row] = false; output.Clear (); @@ -1043,6 +1039,8 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out lastCol += outputWidth; outputWidth = 0; } + + return updated; } internal override void End () @@ -1239,12 +1237,12 @@ public virtual void ResizeScreen () catch (IOException) { // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); } catch (ArgumentOutOfRangeException) { // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); } } else @@ -1253,7 +1251,7 @@ public virtual void ResizeScreen () } // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); } #endregion diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 0b15245427..6c8c14b8cf 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1100,13 +1100,6 @@ public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsol public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; } - public override void Refresh () - { - UpdateScreen (); - //WinConsole?.SetInitialCursorVisibility (); - UpdateCursor (); - } - public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) { var input = new WindowsConsole.InputRecord @@ -1299,13 +1292,14 @@ public override bool EnsureCursorVisibility () #endregion Cursor Handling - public override void UpdateScreen () + public override bool UpdateScreen () { + bool updated = false; Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (Cols, Rows); if (!windowSize.IsEmpty && (windowSize.Width != Cols || windowSize.Height != Rows)) { - return; + return updated; } var bufferCoords = new WindowsConsole.Coord @@ -1322,6 +1316,7 @@ public override void UpdateScreen () } _dirtyLines [row] = false; + updated = true; for (var col = 0; col < Cols; col++) { @@ -1380,6 +1375,8 @@ public override void UpdateScreen () } WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); + + return updated; } internal override void End () @@ -1419,6 +1416,7 @@ internal override MainLoop Init () Size winSize = WinConsole.GetConsoleOutputWindow (out Point pos); Cols = winSize.Width; Rows = winSize.Height; + OnSizeChanged (new SizeChangedEventArgs (new (Cols, Rows))); } WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); @@ -1441,7 +1439,7 @@ internal override MainLoop Init () _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); _damageRegion = new WindowsConsole.SmallRect { @@ -1861,7 +1859,7 @@ private void ResizeScreen () { _outputBuffer = new WindowsConsole.ExtendedCharInfo [Rows * Cols]; // CONCURRENCY: Unsynchronized access to Clip is not safe. - Clip = new (0, 0, Cols, Rows); + Clip = new (Screen); _damageRegion = new WindowsConsole.SmallRect { @@ -2210,15 +2208,18 @@ bool IMainLoopDriver.EventsPending () // Note: ManualResetEventSlim.Wait will wait indefinitely if the timeout is -1. The timeout is -1 when there // are no timers, but there IS an idle handler waiting. _eventReady.Wait (waitTimeout, _eventReadyTokenSource.Token); + // } + _eventReady.Reset (); } catch (OperationCanceledException) { + _eventReady.Reset (); return true; } finally { - _eventReady.Reset (); + //_eventReady.Reset (); } if (!_eventReadyTokenSource.IsCancellationRequested) @@ -2316,10 +2317,18 @@ private void WindowsInputHandler () if (_resultQueue?.Count == 0) { - _resultQueue.Enqueue (_winConsole.ReadConsoleInput ()); + var input = _winConsole.ReadConsoleInput (); + + //if (input [0].EventType != WindowsConsole.EventType.Focus) + { + _resultQueue.Enqueue (input); + } } - _eventReady.Set (); + if (_resultQueue?.Count > 0) + { + _eventReady.Set (); + } } } diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index 235d657d98..8261b156a6 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -1,62 +1,9 @@ #nullable enable namespace Terminal.Gui; -/// Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines. +/// Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines. public class LineCanvas : IDisposable { - /// - /// Optional which when present overrides the - /// (colors) of lines in the canvas. This can be used e.g. to apply a global - /// across all lines. - /// - public FillPair? Fill { get; set; } - - private readonly List _lines = []; - - private readonly Dictionary _runeResolvers = new () - { - { - IntersectionRuneType.ULCorner, - new ULIntersectionRuneResolver () - }, - { - IntersectionRuneType.URCorner, - new URIntersectionRuneResolver () - }, - { - IntersectionRuneType.LLCorner, - new LLIntersectionRuneResolver () - }, - { - IntersectionRuneType.LRCorner, - new LRIntersectionRuneResolver () - }, - { - IntersectionRuneType.TopTee, - new TopTeeIntersectionRuneResolver () - }, - { - IntersectionRuneType.LeftTee, - new LeftTeeIntersectionRuneResolver () - }, - { - IntersectionRuneType.RightTee, - new RightTeeIntersectionRuneResolver () - }, - { - IntersectionRuneType.BottomTee, - new BottomTeeIntersectionRuneResolver () - }, - { - IntersectionRuneType.Cross, - new CrossIntersectionRuneResolver () - } - - // TODO: Add other resolvers - }; - - private Rectangle _cachedViewport; - /// Creates a new instance. public LineCanvas () { @@ -66,54 +13,62 @@ public LineCanvas () Applied += ConfigurationManager_Applied; } + private readonly List _lines = []; + /// Creates a new instance with the given . /// Initial lines for the canvas. public LineCanvas (IEnumerable lines) : this () { _lines = lines.ToList (); } + /// + /// Optional which when present overrides the + /// (colors) of lines in the canvas. This can be used e.g. to apply a global + /// across all lines. + /// + public FillPair? Fill { get; set; } + + private Rectangle _cachedBounds; + /// /// 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. + /// the furthest left/top and Size is defined by the line that extends the furthest right/bottom. /// - public Rectangle Viewport + public Rectangle Bounds { get { - if (_cachedViewport.IsEmpty) + if (_cachedBounds.IsEmpty) { if (_lines.Count == 0) { - return _cachedViewport; + return _cachedBounds; } - Rectangle viewport = _lines [0].Viewport; + Rectangle bounds = _lines [0].Bounds; for (var i = 1; i < _lines.Count; i++) { - viewport = Rectangle.Union (viewport, _lines [i].Viewport); + bounds = Rectangle.Union (bounds, _lines [i].Bounds); } - if (viewport is { Width: 0 } or { Height: 0 }) + if (bounds is { Width: 0 } or { Height: 0 }) { - viewport = viewport with + bounds = bounds with { - Width = Math.Clamp (viewport.Width, 1, short.MaxValue), - Height = Math.Clamp (viewport.Height, 1, short.MaxValue) + Width = Math.Clamp (bounds.Width, 1, short.MaxValue), + Height = Math.Clamp (bounds.Height, 1, short.MaxValue) }; } - _cachedViewport = viewport; + _cachedBounds = bounds; } - return _cachedViewport; + return _cachedBounds; } } /// Gets the lines in the canvas. public IReadOnlyCollection Lines => _lines.AsReadOnly (); - /// - public void Dispose () { Applied -= ConfigurationManager_Applied; } - /// /// Adds a new long line to the canvas starting at . /// @@ -141,7 +96,7 @@ public void AddLine ( Attribute? attribute = null ) { - _cachedViewport = Rectangle.Empty; + _cachedBounds = Rectangle.Empty; _lines.Add (new (start, length, orientation, style, attribute)); } @@ -149,37 +104,67 @@ public void AddLine ( /// public void AddLine (StraightLine line) { - _cachedViewport = Rectangle.Empty; + _cachedBounds = Rectangle.Empty; _lines.Add (line); } + private Region? _exclusionRegion; + + /// + /// Causes the provided region to be excluded from and . + /// + /// + /// + /// Each call to this method will add to the exclusion region. To clear the exclusion region, call + /// . + /// + /// + public void Exclude (Region region) + { + _exclusionRegion ??= new (); + _exclusionRegion.Union (region); + } + + /// + /// Clears the exclusion region. After calling this method, and will + /// return all points in the canvas. + /// + public void ClearExclusions () { _exclusionRegion = null; } + /// Clears all lines from the LineCanvas. public void Clear () { - _cachedViewport = Rectangle.Empty; + _cachedBounds = Rectangle.Empty; _lines.Clear (); + ClearExclusions (); } /// - /// Clears any cached states from the canvas Call this method if you make changes to lines that have already been + /// Clears any cached states from the canvas. Call this method if you make changes to lines that have already been /// added. /// - public void ClearCache () { _cachedViewport = Rectangle.Empty; } + public void ClearCache () { _cachedBounds = Rectangle.Empty; } /// /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate /// intersection symbols. /// + /// + /// + /// Only the points within the of the canvas that are not in the exclusion region will be + /// returned. To exclude points from the map, use . + /// + /// /// A map of all the points within the canvas. public Dictionary GetCellMap () { Dictionary map = new (); // walk through each pixel of the bitmap - for (int y = Viewport.Y; y < Viewport.Y + Viewport.Height; y++) + for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) { - for (int x = Viewport.X; x < Viewport.X + Viewport.Width; x++) + for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) { IntersectionDefinition? [] intersects = _lines .Select (l => l.Intersects (x, y)) @@ -188,7 +173,7 @@ public void Clear () Cell? cell = GetCellForIntersects (Application.Driver, intersects); - if (cell is { }) + if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false) { map.Add (new (x, y), cell); } @@ -205,6 +190,12 @@ public void Clear () /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate /// intersection symbols. /// + /// + /// + /// Only the points within the of the canvas that are not in the exclusion region will be + /// returned. To exclude points from the map, use . + /// + /// /// A rectangle to constrain the search by. /// A map of the points within the canvas that intersect with . public Dictionary GetMap (Rectangle inArea) @@ -223,7 +214,7 @@ public Dictionary GetMap (Rectangle inArea) Rune? rune = GetRuneForIntersects (Application.Driver, intersects); - if (rune is { }) + if (rune is { } && _exclusionRegion?.Contains (x, y) is null or false) { map.Add (new (x, y), rune.Value); } @@ -238,8 +229,14 @@ public Dictionary GetMap (Rectangle inArea) /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate /// intersection symbols. /// + /// + /// + /// Only the points within the of the canvas that are not in the exclusion region will be + /// returned. To exclude points from the map, use . + /// + /// /// A map of all the points within the canvas. - public Dictionary GetMap () { return GetMap (Viewport); } + public Dictionary GetMap () { return GetMap (Bounds); } /// Merges one line canvas into this one. /// @@ -249,6 +246,12 @@ public void Merge (LineCanvas lineCanvas) { AddLine (line); } + + if (lineCanvas._exclusionRegion is { }) + { + _exclusionRegion ??= new (); + _exclusionRegion.Union (lineCanvas._exclusionRegion); + } } /// Removes the last line added to the canvas @@ -267,13 +270,13 @@ public StraightLine RemoveLastLine () /// /// Returns the contents of the line canvas rendered to a string. The string will include all columns and rows, - /// even if has negative coordinates. For example, if the canvas contains a single line that + /// even if has negative coordinates. For example, if the canvas contains a single line that /// starts at (-1,-1) with a length of 2, the rendered string will have a length of 2. /// /// The canvas rendered to a string. public override string ToString () { - if (Viewport.IsEmpty) + if (Bounds.IsEmpty) { return string.Empty; } @@ -282,13 +285,13 @@ public override string ToString () Dictionary runeMap = GetMap (); // Create the rune canvas - Rune [,] canvas = new Rune [Viewport.Height, Viewport.Width]; + Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width]; // Copy the rune map to the canvas, adjusting for any negative coordinates foreach (KeyValuePair kvp in runeMap) { - int x = kvp.Key.X - Viewport.X; - int y = kvp.Key.Y - Viewport.Y; + int x = kvp.Key.X - Bounds.X; + int y = kvp.Key.Y - Bounds.Y; canvas [y, x] = kvp.Value; } @@ -312,7 +315,10 @@ public override string ToString () return sb.ToString (); } - private bool All (IntersectionDefinition? [] intersects, Orientation orientation) { return intersects.All (i => i!.Line.Orientation == orientation); } + private static bool All (IntersectionDefinition? [] intersects, Orientation orientation) + { + return intersects.All (i => i!.Line.Orientation == orientation); + } private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e) { @@ -329,13 +335,55 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE /// /// /// - private bool Exactly (HashSet intersects, params IntersectionType [] types) { return intersects.SetEquals (types); } + private static bool Exactly (HashSet intersects, params IntersectionType [] types) { return intersects.SetEquals (types); } private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects) { - return Fill != null ? Fill.GetAttribute (intersects [0]!.Point) : intersects [0]!.Line.Attribute; + return Fill?.GetAttribute (intersects [0]!.Point) ?? intersects [0]!.Line.Attribute; } + private readonly Dictionary _runeResolvers = new () + { + { + IntersectionRuneType.ULCorner, + new ULIntersectionRuneResolver () + }, + { + IntersectionRuneType.URCorner, + new URIntersectionRuneResolver () + }, + { + IntersectionRuneType.LLCorner, + new LLIntersectionRuneResolver () + }, + { + IntersectionRuneType.LRCorner, + new LRIntersectionRuneResolver () + }, + { + IntersectionRuneType.TopTee, + new TopTeeIntersectionRuneResolver () + }, + { + IntersectionRuneType.LeftTee, + new LeftTeeIntersectionRuneResolver () + }, + { + IntersectionRuneType.RightTee, + new RightTeeIntersectionRuneResolver () + }, + { + IntersectionRuneType.BottomTee, + new BottomTeeIntersectionRuneResolver () + }, + { + IntersectionRuneType.Cross, + new CrossIntersectionRuneResolver () + } + + // TODO: Add other resolvers + }; + private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) @@ -677,7 +725,7 @@ private abstract class IntersectionRuneResolver internal Rune _thickBoth; internal Rune _thickH; internal Rune _thickV; - public IntersectionRuneResolver () { SetGlyphs (); } + protected IntersectionRuneResolver () { SetGlyphs (); } public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { @@ -853,4 +901,11 @@ public override void SetGlyphs () _normal = Glyphs.URCorner; } } + + /// + public void Dispose () + { + Applied -= ConfigurationManager_Applied; + GC.SuppressFinalize (this); + } } diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs new file mode 100644 index 0000000000..c3ef1d61b2 --- /dev/null +++ b/Terminal.Gui/Drawing/Region.cs @@ -0,0 +1,283 @@ +/// +/// Represents a region composed of one or more rectangles, providing methods for union, intersection, exclusion, and +/// complement operations. +/// +public class Region : IDisposable +{ + private List _rectangles; + + /// + /// Initializes a new instance of the class. + /// + public Region () { _rectangles = new (); } + + /// + /// Initializes a new instance of the class with the specified rectangle. + /// + /// The initial rectangle for the region. + public Region (Rectangle rectangle) { _rectangles = new () { rectangle }; } + + /// + /// Adds the specified rectangle to the region. + /// + /// The rectangle to add to the region. + public void Union (Rectangle rectangle) + { + _rectangles.Add (rectangle); + _rectangles = MergeRectangles (_rectangles); + } + + /// + /// Adds the specified region to this region. + /// + /// The region to add to this region. + public void Union (Region region) + { + _rectangles.AddRange (region._rectangles); + _rectangles = MergeRectangles (_rectangles); + } + + /// + /// Updates the region to be the intersection of itself with the specified rectangle. + /// + /// The rectangle to intersect with the region. + public void Intersect (Rectangle rectangle) + { + _rectangles = _rectangles.Select (r => Rectangle.Intersect (r, rectangle)).Where (r => !r.IsEmpty).ToList (); + } + + /// + /// Updates the region to be the intersection of itself with the specified region. + /// + /// The region to intersect with this region. + public void Intersect (Region region) + { + List intersections = new List (); + + foreach (Rectangle rect1 in _rectangles) + { + foreach (Rectangle rect2 in region._rectangles) + { + Rectangle intersected = Rectangle.Intersect (rect1, rect2); + + if (!intersected.IsEmpty) + { + intersections.Add (intersected); + } + } + } + + _rectangles = intersections; + } + + /// + /// Removes the specified rectangle from the region. + /// + /// The rectangle to exclude from the region. + public void Exclude (Rectangle rectangle) { _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rectangle)).ToList (); } + + /// + /// Removes the portion of the specified region from this region. + /// + /// The region to exclude from this region. + public void Exclude (Region region) + { + foreach (Rectangle rect in region._rectangles) + { + _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList (); + } + } + + /// + /// Updates the region to be the complement of itself within the specified bounds. + /// + /// The bounding rectangle to use for complementing the region. + public void Complement (Rectangle bounds) + { + if (bounds.IsEmpty || _rectangles.Count == 0) + { + _rectangles.Clear (); + + return; + } + + List complementRectangles = new List { bounds }; + + foreach (Rectangle rect in _rectangles) + { + complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList (); + } + + _rectangles = complementRectangles; + } + + /// + /// Creates an exact copy of the region. + /// + /// A new that is a copy of this instance. + public Region Clone () + { + var clone = new Region (); + clone._rectangles = new (_rectangles); + + return clone; + } + + /// + /// Gets a bounding rectangle for the entire region. + /// + /// A that bounds the region. + public Rectangle GetBounds () + { + if (_rectangles.Count == 0) + { + return Rectangle.Empty; + } + + int left = _rectangles.Min (r => r.Left); + int top = _rectangles.Min (r => r.Top); + int right = _rectangles.Max (r => r.Right); + int bottom = _rectangles.Max (r => r.Bottom); + + return new (left, top, right - left, bottom - top); + } + + /// + /// Determines whether the region is empty. + /// + /// true if the region is empty; otherwise, false. + public bool IsEmpty () { return !_rectangles.Any (); } + + /// + /// Determines whether the specified point is contained within the region. + /// + /// The x-coordinate of the point. + /// The y-coordinate of the point. + /// true if the point is contained within the region; otherwise, false. + public bool Contains (int x, int y) { return _rectangles.Any (r => r.Contains (x, y)); } + + /// + /// Determines whether the specified rectangle is contained within the region. + /// + /// The rectangle to check for containment. + /// true if the rectangle is contained within the region; otherwise, false. + public bool Contains (Rectangle rectangle) { return _rectangles.Any (r => r.Contains (rectangle)); } + + /// + /// Returns an array of rectangles that represent the region. + /// + /// An array of objects that make up the region. + public Rectangle [] GetRegionScans () { return _rectangles.ToArray (); } + + /// + /// Offsets all rectangles in the region by the specified amounts. + /// + /// The amount to offset along the x-axis. + /// The amount to offset along the y-axis. + public void Offset (int offsetX, int offsetY) + { + for (int i = 0; i < _rectangles.Count; i++) + { + var rect = _rectangles [i]; + _rectangles [i] = new Rectangle (rect.Left + offsetX, rect.Top + offsetY, rect.Width, rect.Height); + } + } + + /// + /// Merges overlapping rectangles into a minimal set of non-overlapping rectangles. + /// + /// The list of rectangles to merge. + /// A list of merged rectangles. + private List MergeRectangles (List rectangles) + { + // Simplified merging logic: this does not handle all edge cases for merging overlapping rectangles. + // For a full implementation, a plane sweep algorithm or similar would be needed. + List merged = new List (rectangles); + bool mergedAny; + + do + { + mergedAny = false; + + for (var i = 0; i < merged.Count; i++) + { + for (int j = i + 1; j < merged.Count; j++) + { + if (merged [i].IntersectsWith (merged [j])) + { + merged [i] = Rectangle.Union (merged [i], merged [j]); + merged.RemoveAt (j); + mergedAny = true; + + break; + } + } + + if (mergedAny) + { + break; + } + } + } + while (mergedAny); + + return merged; + } + + /// + /// Subtracts the specified rectangle from the original rectangle, returning the resulting rectangles. + /// + /// The original rectangle. + /// The rectangle to subtract from the original. + /// An enumerable collection of resulting rectangles after subtraction. + private IEnumerable SubtractRectangle (Rectangle original, Rectangle subtract) + { + if (!original.IntersectsWith (subtract)) + { + yield return original; + + yield break; + } + + // Top segment + if (original.Top < subtract.Top) + { + yield return new (original.Left, original.Top, original.Width, subtract.Top - original.Top); + } + + // Bottom segment + if (original.Bottom > subtract.Bottom) + { + yield return new (original.Left, subtract.Bottom, original.Width, original.Bottom - subtract.Bottom); + } + + // Left segment + if (original.Left < subtract.Left) + { + int top = Math.Max (original.Top, subtract.Top); + int bottom = Math.Min (original.Bottom, subtract.Bottom); + + if (bottom > top) + { + yield return new (original.Left, top, subtract.Left - original.Left, bottom - top); + } + } + + // Right segment + if (original.Right > subtract.Right) + { + int top = Math.Max (original.Top, subtract.Top); + int bottom = Math.Min (original.Bottom, subtract.Bottom); + + if (bottom > top) + { + yield return new (subtract.Right, top, original.Right - subtract.Right, bottom - top); + } + } + } + + /// + /// Releases all resources used by the . + /// + public void Dispose () { _rectangles.Clear (); } +} diff --git a/Terminal.Gui/Drawing/Ruler.cs b/Terminal.Gui/Drawing/Ruler.cs index d2551101d0..1eea7f263e 100644 --- a/Terminal.Gui/Drawing/Ruler.cs +++ b/Terminal.Gui/Drawing/Ruler.cs @@ -1,10 +1,11 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// Draws a ruler on the screen. /// /// /// -public class Ruler +internal class Ruler { /// Gets or sets the foreground and background color to use. public Attribute Attribute { get; set; } = new (); @@ -36,7 +37,7 @@ public void Draw (Point location, int start = 0) if (Orientation == Orientation.Horizontal) { string hrule = - _hTemplate.Repeat ((int)Math.Ceiling (Length + 2 / (double)_hTemplate.Length)) [start..(Length + start)]; + _hTemplate.Repeat ((int)Math.Ceiling (Length + 2 / (double)_hTemplate.Length))! [start..(Length + start)]; // Top Application.Driver?.Move (location.X, location.Y); @@ -45,7 +46,7 @@ public void Draw (Point location, int start = 0) else { string vrule = - _vTemplate.Repeat ((int)Math.Ceiling ((Length + 2) / (double)_vTemplate.Length)) + _vTemplate.Repeat ((int)Math.Ceiling ((Length + 2) / (double)_vTemplate.Length))! [start..(Length + start)]; for (int r = location.Y; r < location.Y + Length; r++) diff --git a/Terminal.Gui/Drawing/StraightLine.cs b/Terminal.Gui/Drawing/StraightLine.cs index fe2ccdc1d0..c2b1e034be 100644 --- a/Terminal.Gui/Drawing/StraightLine.cs +++ b/Terminal.Gui/Drawing/StraightLine.cs @@ -6,11 +6,11 @@ public class StraightLine { /// Creates a new instance of the class. - /// - /// - /// - /// - /// + /// The start location. + /// The length of the line. + /// The orientation of the line. + /// The line style. + /// The attribute to be used for rendering the line. public StraightLine ( Point start, int length, @@ -43,11 +43,11 @@ public StraightLine ( /// /// 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. + /// the furthest left/top and Size is defined by the line that extends the furthest right/bottom. /// - // PERF: Probably better to store the rectangle rather than make a new one on every single access to Viewport. - internal Rectangle Viewport + // PERF: Probably better to store the rectangle rather than make a new one on every single access to Bounds. + internal Rectangle Bounds { get { diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index d96ab73eab..0950f872bb 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -1,4 +1,5 @@ -using System.Numerics; +#nullable enable +using System.Numerics; using System.Text.Json.Serialization; namespace Terminal.Gui; @@ -15,10 +16,7 @@ namespace Terminal.Gui; /// with the thickness widths subtracted. /// /// -/// Use the helper API ( to draw the frame with the specified thickness. -/// -/// -/// Thickness uses intenrally. As a result, there is a potential precision loss for very +/// Thickness uses internally. As a result, there is a potential precision loss for very /// large numbers. This is typically not an issue for UI dimensions but could be relevant in other contexts. /// /// @@ -82,15 +80,16 @@ public bool Contains (in Rectangle outside, in Point location) /// Draws the rectangle with an optional diagnostics label. /// /// If is set to - /// then 'T', 'L', 'R', and 'B' glyphs will be used instead of + /// then 'T', 'L', 'R', and 'B' glyphs will be used instead of /// space. If is set to /// then a ruler will be drawn on the outer edge of the /// Thickness. /// /// The location and size of the rectangle that bounds the thickness rectangle, in screen coordinates. + /// /// The diagnostics label to draw on the bottom of the . /// The inner rectangle remaining to be drawn. - public Rectangle Draw (Rectangle rect, string label = null) + public Rectangle Draw (Rectangle rect, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, string? label = null) { if (rect.Size.Width < 1 || rect.Size.Height < 1) { @@ -103,7 +102,7 @@ public Rectangle Draw (Rectangle rect, string label = null) Rune topChar = clearChar; Rune bottomChar = clearChar; - if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Padding)) + if (diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)) { leftChar = (Rune)'L'; rightChar = (Rune)'R'; @@ -133,29 +132,29 @@ public Rectangle Draw (Rectangle rect, string label = null) if (Right > 0) { Application.Driver?.FillRect ( - rect with - { - X = Math.Max (0, rect.X + rect.Width - Right), - Width = Math.Min (rect.Width, Right) - }, - rightChar - ); + rect with + { + X = Math.Max (0, rect.X + rect.Width - Right), + Width = Math.Min (rect.Width, Right) + }, + rightChar + ); } // Draw the Bottom side if (Bottom > 0) { Application.Driver?.FillRect ( - rect with - { - Y = rect.Y + Math.Max (0, rect.Height - Bottom), - Height = Bottom - }, - bottomChar - ); + rect with + { + Y = rect.Y + Math.Max (0, rect.Height - Bottom), + Height = Bottom + }, + bottomChar + ); } - if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler)) + if (diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler)) { // PERF: This can almost certainly be simplified down to a single point offset and fewer calls to Draw // Top @@ -187,10 +186,11 @@ rect with } } - if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Padding)) + if (diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)) { // Draw the diagnostics label on the bottom string text = label is null ? string.Empty : $"{label} {this}"; + var tf = new TextFormatter { Text = text, diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 08813a50f8..21bfa3b9ea 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -109,6 +109,9 @@ Strings.Designer.cs + + + @@ -143,9 +146,7 @@ - + $(MSBuildThisFileDirectory)..\local_packages\ @@ -166,10 +167,7 @@ - + diff --git a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs index 9602776f5c..b068b099a2 100644 --- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs @@ -42,7 +42,7 @@ public override View HostControl public override void ClearSuggestions () { base.ClearSuggestions (); - textField.SetNeedsDisplay (); + textField.SetNeedsDraw (); } /// @@ -106,12 +106,12 @@ public override void RenderOverlay (Point renderAt) } // draw it like it's selected, even though it's not - Application.Driver?.SetAttribute ( - new Attribute ( - ColorScheme.Normal.Foreground, - textField.ColorScheme.Focus.Background - ) - ); + textField.SetAttribute ( + new Attribute ( + ColorScheme.Normal.Foreground, + textField.ColorScheme.Focus.Background + ) + ); textField.Move (textField.Text.Length, 0); Suggestion suggestion = Suggestions.ElementAt (SelectedIdx); @@ -183,7 +183,7 @@ private bool CycleSuggestion (int direction) SelectedIdx = Suggestions.Count () - 1; } - textField.SetNeedsDisplay (); + textField.SetNeedsDraw (); return true; } diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs index 39bdd7522d..2abe2091d0 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs @@ -15,14 +15,16 @@ public Popup (PopupAutocomplete autoComplete) private readonly PopupAutocomplete _autoComplete; - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (!_autoComplete.LastPopupPos.HasValue) { - return; + return true; } _autoComplete.RenderOverlay (_autoComplete.LastPopupPos.Value); + + return true; } protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { return _autoComplete.OnMouseEvent (mouseEvent); } diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs index 4f332bd541..34e058dab5 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -120,7 +120,7 @@ public override bool OnMouseEvent (MouseEventArgs me, bool fromHost = false) if (Visible && Suggestions.Count == 0) { Visible = false; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); return true; } @@ -128,7 +128,7 @@ public override bool OnMouseEvent (MouseEventArgs me, bool fromHost = false) if (!Visible && Suggestions.Count > 0) { Visible = true; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); Application.UngrabMouse (); return false; @@ -141,7 +141,7 @@ public override bool OnMouseEvent (MouseEventArgs me, bool fromHost = false) _closed = false; } - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); return false; } @@ -395,11 +395,11 @@ public override void RenderOverlay (Point renderAt) { if (i == SelectedIdx - ScrollOffset) { - Application.Driver?.SetAttribute (ColorScheme.Focus); + _popup.SetAttribute (ColorScheme.Focus); } else { - Application.Driver?.SetAttribute (ColorScheme.Normal); + _popup.SetAttribute (ColorScheme.Normal); } _popup.Move (0, i); @@ -424,7 +424,7 @@ protected void Close () ClearSuggestions (); Visible = false; _closed = true; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); //RemovePopupFromTop (); } @@ -469,7 +469,7 @@ protected void MoveDown () } EnsureSelectedIdxIsValid (); - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); } /// Moves the selection in the Autocomplete context menu up one @@ -483,7 +483,7 @@ protected void MoveUp () } EnsureSelectedIdxIsValid (); - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); } /// Render the current selection in the Autocomplete context menu by the mouse reporting. @@ -512,7 +512,7 @@ protected bool ReopenSuggestions () { Visible = true; _closed = false; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); return true; } diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 5748ae0fbb..2938b69bc0 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -68,7 +68,10 @@ public void Draw ( return; } - driver ??= Application.Driver; + if (driver is null) + { + driver = Application.Driver; + } driver?.SetAttribute (normalColor); diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index ca90eed765..3b407d575d 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -1,5 +1,4 @@ #nullable enable -using System.ComponentModel; using Terminal.Gui; using Attribute = Terminal.Gui.Attribute; @@ -15,7 +14,7 @@ /// mouse input. Each can be customized by manipulating their Subviews. /// /// -public class Adornment : View +public class Adornment : View, IDesignable { /// public Adornment () @@ -27,7 +26,7 @@ public Adornment () /// public Adornment (View parent) { - // By default Adornments can't get focus; has to be enabled specifically. + // By default, Adornments can't get focus; has to be enabled specifically. CanFocus = false; TabStop = TabBehavior.NoStop; Parent = parent; @@ -42,6 +41,15 @@ public Adornment (View parent) #region Thickness + /// + /// Gets or sets whether the Adornment will draw diagnostic information. This is a bit-field of + /// . + /// + /// + /// The static property is used as the default value for this property. + /// + public new ViewDiagnosticFlags Diagnostics { get; set; } = View.Diagnostics; + private Thickness _thickness = Thickness.Empty; /// Defines the rectangle that the will use to draw its content. @@ -51,20 +59,14 @@ public Thickness Thickness set { Thickness current = _thickness; + _thickness = value; if (current != _thickness) { - if (Parent?.IsInitialized == false) - { - // When initialized Parent.LayoutSubViews will cause a LayoutAdornments - Parent?.LayoutAdornments (); - } - else - { - Parent?.SetNeedsLayout (); - Parent?.LayoutSubviews (); - } + Parent?.SetAdornmentFrames (); + SetNeedsLayout (); + SetNeedsDraw (); OnThicknessChanged (); } @@ -72,14 +74,10 @@ public Thickness Thickness } /// Fired whenever the property changes. - [CanBeNull] public event EventHandler? ThicknessChanged; /// Called whenever the property changes. - public void OnThicknessChanged () - { - ThicknessChanged?.Invoke (this, EventArgs.Empty); - } + public void OnThicknessChanged () { ThicknessChanged?.Invoke (this, EventArgs.Empty); } #endregion Thickness @@ -89,23 +87,16 @@ public void OnThicknessChanged () /// Adornments cannot be used as sub-views (see ); setting this property will throw /// . /// + /// + /// While there are no real use cases for an Adornment being a subview, it is not explicitly dis-allowed to support + /// testing. E.g. in AllViewsTester. + /// public override View? SuperView { - get => null!; + get => base.SuperView!; set => throw new InvalidOperationException (@"Adornments can not be Subviews or have SuperViews. Use Parent instead."); } - //internal override Adornment CreateAdornment (Type adornmentType) - //{ - // /* Do nothing - Adornments do not have Adornments */ - // return null; - //} - - internal override void LayoutAdornments () - { - /* Do nothing - Adornments do not have Adornments */ - } - /// /// Gets the rectangle that describes the area of the Adornment. The Location is always (0,0). /// The size is the size of the . @@ -116,7 +107,7 @@ internal override void LayoutAdornments () /// public override Rectangle Viewport { - get => Frame with { Location = Point.Empty }; + get => base.Viewport; set => throw new InvalidOperationException (@"The Viewport of an Adornment cannot be modified."); } @@ -125,6 +116,15 @@ public override Rectangle FrameToScreen () { if (Parent is null) { + // While there are no real use cases for an Adornment being a subview, we support it for + // testing. E.g. in AllViewsTester. + if (SuperView is { }) + { + Point super = SuperView.ViewportToScreen (Frame.Location); + + return new (super, Frame.Size); + } + return Frame; } @@ -140,58 +140,51 @@ public override Rectangle FrameToScreen () /// public override Point ScreenToFrame (in Point location) { - return Parent!.ScreenToFrame (new (location.X - Frame.X, location.Y - Frame.Y)); - } - - /// Does nothing for Adornment - /// - public override bool OnDrawAdornments () { return false; } + View? parentOrSuperView = Parent; - /// Redraws the Adornments that comprise the . - public override void OnDrawContent (Rectangle viewport) - { - if (Thickness == Thickness.Empty) + if (parentOrSuperView is null) { - return; - } - - Rectangle prevClip = SetClip (); - - Rectangle screen = ViewportToScreen (viewport); - Attribute normalAttr = GetNormalColor (); - Driver.SetAttribute (normalAttr); - - // This just draws/clears the thickness, not the insides. - Thickness.Draw (screen, ToString ()); + // While there are no real use cases for an Adornment being a subview, we support it for + // testing. E.g. in AllViewsTester. + parentOrSuperView = SuperView; - if (!string.IsNullOrEmpty (TextFormatter.Text)) - { - if (TextFormatter is { }) + if (parentOrSuperView is null) { - TextFormatter.ConstrainToSize = Frame.Size; - TextFormatter.NeedsFormat = true; + return Point.Empty; } } - TextFormatter?.Draw (screen, normalAttr, normalAttr, Rectangle.Empty); + return parentOrSuperView.ScreenToFrame (new (location.X - Frame.X, location.Y - Frame.Y)); + } - if (Subviews.Count > 0) + /// + /// Called when the of the Adornment is to be cleared. + /// + /// to stop further clearing. + protected override bool OnClearingViewport () + { + if (Thickness == Thickness.Empty) { - base.OnDrawContent (viewport); + return true; } - if (Driver is { }) - { - Driver.Clip = prevClip; - } + // This just draws/clears the thickness, not the insides. + Thickness.Draw (ViewportToScreen (Viewport), Diagnostics, ToString ()); + + NeedsDraw = true; - ClearLayoutNeeded (); - ClearNeedsDisplay (); + return true; } + /// + protected override bool OnDrawingText () { return Thickness == Thickness.Empty; } + + /// + protected override bool OnDrawingSubviews () { return Thickness == Thickness.Empty; } + /// Does nothing for Adornment /// - public override bool OnRenderLineCanvas () { return false; } + protected override bool OnRenderingLineCanvas () { return true; } /// /// Adornments only render to their 's or Parent's SuperView's LineCanvas, so setting this @@ -199,64 +192,56 @@ public override void OnDrawContent (Rectangle viewport) /// public override bool SuperViewRendersLineCanvas { - get => false; + get => false; set => throw new InvalidOperationException (@"Adornment can only render to their Parent or Parent's Superview."); } - #endregion View Overrides - - #region Mouse Support - + /// + protected override void OnDrawComplete () { } /// - /// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. + /// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. /// /// /// The is relative to the PARENT's SuperView. /// /// - /// if the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. + /// + /// if the specified Parent's SuperView-relative coordinates are within the Adornment's + /// Thickness. + /// public override bool Contains (in Point location) { - if (Parent is null) + View? parentOrSuperView = Parent; + + if (parentOrSuperView is null) { - return false; + // While there are no real use cases for an Adornment being a subview, we support it for + // testing. E.g. in AllViewsTester. + parentOrSuperView = SuperView; + + if (parentOrSuperView is null) + { + return false; + } } Rectangle outside = Frame; - outside.Offset (Parent.Frame.Location); + outside.Offset (parentOrSuperView.Frame.Location); return Thickness.Contains (outside, location); } - ///// - //protected override bool OnMouseEnter (CancelEventArgs mouseEvent) - //{ - // // Invert Normal - // if (Diagnostics.HasFlag (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) - // { - // var cs = new ColorScheme (ColorScheme) - // { - // Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) - // }; - // ColorScheme = cs; - // } - - // return false; - //} - - ///// - //protected override void OnMouseLeave () - //{ - // // Invert Normal - // if (Diagnostics.FastHasFlags (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null) - // { - // var cs = new ColorScheme (ColorScheme) - // { - // Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground) - // }; - // ColorScheme = cs; - // } - //} - #endregion Mouse Support + #endregion View Overrides + + /// + bool IDesignable.EnableForDesign () + { + // This enables AllViewsTester to show something useful. + Thickness = new (3); + Frame = new (0, 0, 10, 10); + Diagnostics = ViewDiagnosticFlags.Thickness; + + return true; + } } diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index add7527e66..3a51eb54bb 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -64,6 +64,51 @@ public Border (View parent) : base (parent) HighlightStyle |= HighlightStyle.Pressed; Highlight += Border_Highlight; + + 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 SpinnerView () + { + 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 @@ -80,6 +125,7 @@ public override void BeginInit () { base.BeginInit (); + ShowHideDrawIndicator (); #if SUBVIEW_BASED_BORDER if (Parent is { }) { @@ -130,19 +176,11 @@ private void OnLayoutStarted (object sender, LayoutEventArgs e) /// public override ColorScheme? ColorScheme { - get - { - if (base.ColorScheme is { }) - { - return base.ColorScheme; - } - - return Parent?.ColorScheme; - } + get => base.ColorScheme ?? Parent?.ColorScheme; set { base.ColorScheme = value; - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); } } @@ -191,7 +229,7 @@ public LineStyle LineStyle // 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; + return Parent?.SuperView?.BorderStyle ?? LineStyle.None; } set => _lineStyle = value; } @@ -213,7 +251,7 @@ public BorderSettings Settings _settings = value; - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); } } @@ -254,7 +292,7 @@ private void Border_Highlight (object? sender, CancelEventArgs e ColorScheme = cs; } - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); e.Cancel = true; } @@ -266,9 +304,9 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3312 if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) - // HACK: Prevents Window from being draggable if it's Top - //&& Parent is Toplevel { Modal: true } - ) + // HACK: Prevents Window from being draggable if it's Top + //&& Parent is Toplevel { Modal: true } + ) { Parent!.SetFocus (); @@ -437,11 +475,11 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) if (Parent!.SuperView is null) { // Redraw the entire app window. - Application.Top!.SetNeedsDisplay (); + Application.Top!.SetNeedsDraw (); } else { - Parent.SuperView.SetNeedsDisplay (); + Parent.SuperView.SetNeedsDraw (); } _dragPosition = mouseEvent.Position; @@ -449,8 +487,8 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) Point parentLoc = Parent.SuperView?.ScreenToViewport (new (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y)) ?? mouseEvent.ScreenPosition; - int minHeight = Thickness.Vertical + Parent!.Margin.Thickness.Bottom; - int minWidth = Thickness.Horizontal + Parent!.Margin.Thickness.Right; + int minHeight = Thickness.Vertical + Parent!.Margin!.Thickness.Bottom; + int minWidth = Thickness.Horizontal + Parent!.Margin!.Thickness.Right; // TODO: This code can be refactored to be more readable and maintainable. switch (_arranging) @@ -565,7 +603,6 @@ out int ny break; } - Application.Refresh (); return true; } @@ -604,19 +641,14 @@ private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e) #endregion Mouse Support /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - if (Thickness == Thickness.Empty) { - return; + return true; } - //Driver.SetAttribute (Colors.ColorSchemes ["Error"].Normal); - Rectangle screenBounds = ViewportToScreen (viewport); - - //OnDrawSubviews (bounds); + Rectangle screenBounds = ViewportToScreen (Viewport); // TODO: v2 - this will eventually be two controls: "BorderView" and "Label" (for the title) @@ -633,12 +665,15 @@ public override void OnDrawContent (Rectangle viewport) int maxTitleWidth = Math.Max ( 0, Math.Min ( - Parent!.TitleTextFormatter.FormatAndGetSize ().Width, + Parent?.TitleTextFormatter.FormatAndGetSize ().Width ?? 0, Math.Min (screenBounds.Width - 4, borderBounds.Width - 4) ) ); - Parent.TitleTextFormatter.ConstrainToSize = new (maxTitleWidth, 1); + if (Parent is { }) + { + Parent.TitleTextFormatter.ConstrainToSize = new (maxTitleWidth, 1); + } int sideLineLength = borderBounds.Height; bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 }; @@ -677,20 +712,22 @@ public override void OnDrawContent (Rectangle viewport) } } - if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title) && !string.IsNullOrEmpty (Parent?.Title)) + if (Parent is { } && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title) && !string.IsNullOrEmpty (Parent?.Title)) { Attribute focus = Parent.GetNormalColor (); if (Parent.SuperView is { } && Parent.SuperView?.Subviews!.Count (s => s.CanFocus) > 1) { // Only use focus color if there are multiple focusable views - focus = Parent.GetFocusColor (); + focus = GetFocusColor (); } - Parent.TitleTextFormatter.Draw ( - new (borderBounds.X + 2, titleY, maxTitleWidth, 1), - Parent.HasFocus ? focus : Parent.GetNormalColor (), - Parent.HasFocus ? focus : Parent.GetHotNormalColor ()); + Rectangle titleRect = new (borderBounds.X + 2, titleY, maxTitleWidth, 1); + Parent.TitleTextFormatter.Draw (titleRect + , + Parent.HasFocus ? focus : GetNormalColor (), + Parent.HasFocus ? focus : GetHotNormalColor ()); + Parent?.LineCanvas.Exclude(new(titleRect)); } if (canDrawBorder && LineStyle != LineStyle.None) @@ -702,15 +739,15 @@ public override void OnDrawContent (Rectangle viewport) 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 prevAttr = Driver?.GetAttribute () ?? Attribute.Default; if (ColorScheme is { }) { - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } else { - Driver.SetAttribute (Parent!.GetNormalColor ()); + SetAttribute (Parent!.GetNormalColor ()); } if (drawTop) @@ -725,7 +762,7 @@ public override void OnDrawContent (Rectangle viewport) borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } else @@ -740,7 +777,7 @@ public override void OnDrawContent (Rectangle viewport) Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -754,7 +791,7 @@ public override void OnDrawContent (Rectangle viewport) Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); lc?.AddLine ( @@ -762,7 +799,7 @@ public override void OnDrawContent (Rectangle viewport) Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -773,7 +810,7 @@ public override void OnDrawContent (Rectangle viewport) 2, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add a vert line for ╔╡ @@ -782,7 +819,7 @@ public override void OnDrawContent (Rectangle viewport) titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add a vert line for ╞ @@ -797,7 +834,7 @@ public override void OnDrawContent (Rectangle viewport) titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add the right hand line for ╞═════╗ @@ -812,7 +849,7 @@ public override void OnDrawContent (Rectangle viewport) borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } } @@ -826,7 +863,7 @@ public override void OnDrawContent (Rectangle viewport) sideLineLength, Orientation.Vertical, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } #endif @@ -838,7 +875,7 @@ public override void OnDrawContent (Rectangle viewport) borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -849,11 +886,11 @@ public override void OnDrawContent (Rectangle viewport) sideLineLength, Orientation.Vertical, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } - Driver.SetAttribute (prevAttr); + SetAttribute (prevAttr); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if (Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler)) @@ -906,8 +943,15 @@ public override void OnDrawContent (Rectangle viewport) lc!.Fill = null; } } + + return true; ; } + /// + /// Gets the subview used to render . + /// + public SpinnerView? DrawIndicator { get; private set; } = null; + private void SetupGradientLineCanvas (LineCanvas lc, Rectangle rect) { GetAppealingGradientColors (out List stops, out List steps); @@ -1034,7 +1078,7 @@ private static void GetAppealingGradientColors (out List stops, out List< NoPadding = true, ShadowStyle = ShadowStyle.None, Text = $"{Glyphs.SizeVertical}", - X = Pos.Center () + Parent!.Margin.Thickness.Horizontal, + X = Pos.Center () + Parent!.Margin!.Thickness.Horizontal, Y = 0, Visible = false, Data = ViewArrangement.TopResizable @@ -1057,7 +1101,7 @@ private static void GetAppealingGradientColors (out List stops, out List< ShadowStyle = ShadowStyle.None, Text = $"{Glyphs.SizeHorizontal}", X = Pos.AnchorEnd (), - Y = Pos.Center () + Parent!.Margin.Thickness.Vertical / 2, + Y = Pos.Center () + Parent!.Margin!.Thickness.Vertical / 2, Visible = false, Data = ViewArrangement.RightResizable }; @@ -1079,7 +1123,7 @@ private static void GetAppealingGradientColors (out List stops, out List< ShadowStyle = ShadowStyle.None, Text = $"{Glyphs.SizeHorizontal}", X = 0, - Y = Pos.Center () + Parent!.Margin.Thickness.Vertical / 2, + Y = Pos.Center () + Parent!.Margin!.Thickness.Vertical / 2, Visible = false, Data = ViewArrangement.LeftResizable }; @@ -1100,7 +1144,7 @@ private static void GetAppealingGradientColors (out List stops, out List< NoPadding = true, ShadowStyle = ShadowStyle.None, Text = $"{Glyphs.SizeVertical}", - X = Pos.Center () + Parent!.Margin.Thickness.Horizontal / 2, + X = Pos.Center () + Parent!.Margin!.Thickness.Horizontal / 2, Y = Pos.AnchorEnd (), Visible = false, Data = ViewArrangement.BottomResizable @@ -1249,8 +1293,6 @@ private void AddArrangeModeKeyBindings () } } - Application.Refresh (); - return true; }); @@ -1273,8 +1315,6 @@ private void AddArrangeModeKeyBindings () Parent!.Height = Parent.Height! + 1; } - Application.Refresh (); - return true; }); @@ -1300,8 +1340,6 @@ private void AddArrangeModeKeyBindings () } } - Application.Refresh (); - return true; }); @@ -1324,8 +1362,6 @@ private void AddArrangeModeKeyBindings () Parent!.Width = Parent.Width! + 1; } - Application.Refresh (); - return true; }); diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs index 1f6cc81540..19bf76f372 100644 --- a/Terminal.Gui/View/Adornment/Margin.cs +++ b/Terminal.Gui/View/Adornment/Margin.cs @@ -4,10 +4,21 @@ namespace Terminal.Gui; /// The Margin for a . Accessed via /// +/// +/// The Margin is transparent by default. This can be overriden by explicitly setting . +/// +/// +/// Margins are drawn after all other Views in the application View hierarchy are drawn. +/// /// See the class. /// public class Margin : Adornment { + private const int SHADOW_WIDTH = 1; + private const int SHADOW_HEIGHT = 1; + private const int PRESS_MOVE_HORIZONTAL = 1; + private const int PRESS_MOVE_VERTICAL = 0; + /// public Margin () { /* Do nothing; A parameter-less constructor is required to support all views unit tests. */ @@ -19,18 +30,66 @@ public Margin (View parent) : base (parent) /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */ // BUGBUG: We should not set HighlightStyle.Pressed here, but wherever it is actually needed - // HighlightStyle |= HighlightStyle.Pressed; + // HighlightStyle |= HighlightStyle.Pressed; Highlight += Margin_Highlight; - LayoutStarted += Margin_LayoutStarted; + SubviewLayout += Margin_LayoutStarted; // Margin should not be focusable CanFocus = false; } - private bool _pressed; + // When the Parent is drawn, we cache the clip region so we can draw the Margin after all other Views + // QUESTION: Why can't this just be the NeedsDisplay region? + private Region? _cachedClip; - private ShadowView? _bottomShadow; - private ShadowView? _rightShadow; + internal Region? GetCachedClip () { return _cachedClip; } + + internal void ClearCachedClip () { _cachedClip = null; } + + internal void CacheClip () + { + if (Thickness != Thickness.Empty) + { + // PERFORMANCE: How expensive are these clones? + _cachedClip = GetClip ()?.Clone (); + } + } + + // PERFORMANCE: Margins are ALWAYS drawn. This may be an issue for apps that have a large number of views with shadows. + /// + /// INTERNAL API - Draws the margins for the specified views. This is called by the on each + /// iteration of the main loop after all Views have been drawn. + /// + /// + /// + internal static bool DrawMargins (IEnumerable margins) + { + Stack stack = new (margins); + + while (stack.Count > 0) + { + var view = stack.Pop (); + + if (view.Margin?.GetCachedClip() != null) + { + view.Margin.NeedsDraw = true; + Region? saved = GetClip (); + View.SetClip (view.Margin.GetCachedClip ()); + view.Margin.Draw (); + View.SetClip (saved); + view.Margin.ClearCachedClip (); + } + + view.NeedsDraw = false; + + foreach (var subview in view.Subviews) + { + stack.Push (subview); + } + } + + return true; + } /// public override void BeginInit () @@ -43,32 +102,10 @@ public override void BeginInit () } ShadowStyle = base.ShadowStyle; - - Add ( - _rightShadow = new () - { - X = Pos.AnchorEnd (1), - Y = 0, - Width = 1, - Height = Dim.Fill (), - ShadowStyle = ShadowStyle, - Orientation = Orientation.Vertical - }, - _bottomShadow = new () - { - X = 0, - Y = Pos.AnchorEnd (1), - Width = Dim.Fill (), - Height = 1, - ShadowStyle = ShadowStyle, - Orientation = Orientation.Horizontal - } - ); } /// - /// The color scheme for the Margin. If set to , gets the 's - /// scheme. color scheme. + /// The color scheme for the Margin. If set to (the default), the margin will be transparent. /// public override ColorScheme? ColorScheme { @@ -84,86 +121,95 @@ public override ColorScheme? ColorScheme set { base.ColorScheme = value; - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); } } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnClearingViewport () { - if (!NeedsDisplay) + if (Thickness == Thickness.Empty) { - return; + return true; } - Rectangle screen = ViewportToScreen (viewport); - Attribute normalAttr = GetNormalColor (); + Rectangle screen = ViewportToScreen (Viewport); - Driver?.SetAttribute (normalAttr); - - if (ShadowStyle != ShadowStyle.None) + // This just draws/clears the thickness, not the insides. + if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || base.ColorScheme is { }) { - screen = Rectangle.Inflate (screen, -1, -1); + Thickness.Draw (screen, Diagnostics, ToString ()); } - // This just draws/clears the thickness, not the insides. - Thickness.Draw (screen, ToString ()); - - if (Subviews.Count > 0) + if (ShadowStyle != ShadowStyle.None) { - // Draw subviews - // TODO: Implement OnDrawSubviews (cancelable); - if (Subviews is { } && SubViewNeedsDisplay) - { - IEnumerable subviewsNeedingDraw = Subviews.Where ( - view => view.Visible - && (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded) - ); - - foreach (View view in subviewsNeedingDraw) - { - if (view.LayoutNeeded) - { - view.LayoutSubviews (); - } - - view.Draw (); - } - } + // Don't clear where the shadow goes + screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT); } + + return true; } + #region Shadow + + private bool _pressed; + private ShadowView? _bottomShadow; + private ShadowView? _rightShadow; + /// /// Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the /// Margin. /// public ShadowStyle SetShadow (ShadowStyle style) { - if (ShadowStyle == style) + if (_rightShadow is { }) + { + Remove (_rightShadow); + _rightShadow.Dispose (); + _rightShadow = null; + } + + if (_bottomShadow is { }) { - // return style; + Remove (_bottomShadow); + _bottomShadow.Dispose (); + _bottomShadow = null; } if (ShadowStyle != ShadowStyle.None) { // Turn off shadow - Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - 1, Thickness.Bottom - 1); + Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT); } if (style != ShadowStyle.None) { // Turn on shadow - Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + 1, Thickness.Bottom + 1); + Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT); } - if (_rightShadow is { }) - { - _rightShadow.ShadowStyle = style; - } - - if (_bottomShadow is { }) + if (style != ShadowStyle.None) { - _bottomShadow.ShadowStyle = style; + _rightShadow = new () + { + X = Pos.AnchorEnd (SHADOW_WIDTH), + Y = 0, + Width = SHADOW_WIDTH, + Height = Dim.Fill (), + ShadowStyle = style, + Orientation = Orientation.Vertical + }; + + _bottomShadow = new () + { + X = 0, + Y = Pos.AnchorEnd (SHADOW_HEIGHT), + Width = Dim.Fill (), + Height = SHADOW_HEIGHT, + ShadowStyle = style, + Orientation = Orientation.Horizontal + }; + Add (_rightShadow, _bottomShadow); } return style; @@ -176,52 +222,59 @@ public override ShadowStyle ShadowStyle set => base.ShadowStyle = SetShadow (value); } - private const int PRESS_MOVE_HORIZONTAL = 1; - private const int PRESS_MOVE_VERTICAL = 0; - private void Margin_Highlight (object? sender, CancelEventArgs e) { - if (ShadowStyle != ShadowStyle.None) + if (Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None) + { + return; + } + + if (_pressed && e.NewValue == HighlightStyle.None) { - if (_pressed && e.NewValue == HighlightStyle.None) + // If the view is pressed and the highlight is being removed, move the shadow back. + // Note, for visual effects reasons, we only move horizontally. + // TODO: Add a setting or flag that lets the view move vertically as well. + Thickness = new ( + Thickness.Left - PRESS_MOVE_HORIZONTAL, + Thickness.Top - PRESS_MOVE_VERTICAL, + Thickness.Right + PRESS_MOVE_HORIZONTAL, + Thickness.Bottom + PRESS_MOVE_VERTICAL); + + if (_rightShadow is { }) { - // If the view is pressed and the highlight is being removed, move the shadow back. - // Note, for visual effects reasons, we only move horizontally. - // TODO: Add a setting or flag that lets the view move vertically as well. - Thickness = new (Thickness.Left - PRESS_MOVE_HORIZONTAL, Thickness.Top - PRESS_MOVE_VERTICAL, Thickness.Right + PRESS_MOVE_HORIZONTAL, Thickness.Bottom + PRESS_MOVE_VERTICAL); + _rightShadow.Visible = true; + } - if (_rightShadow is { }) - { - _rightShadow.Visible = true; - } + if (_bottomShadow is { }) + { + _bottomShadow.Visible = true; + } - if (_bottomShadow is { }) - { - _bottomShadow.Visible = true; - } + _pressed = false; - _pressed = false; + return; + } - return; + if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed)) + { + // If the view is not pressed and we want highlight move the shadow + // Note, for visual effects reasons, we only move horizontally. + // TODO: Add a setting or flag that lets the view move vertically as well. + Thickness = new ( + Thickness.Left + PRESS_MOVE_HORIZONTAL, + Thickness.Top + PRESS_MOVE_VERTICAL, + Thickness.Right - PRESS_MOVE_HORIZONTAL, + Thickness.Bottom - PRESS_MOVE_VERTICAL); + _pressed = true; + + if (_rightShadow is { }) + { + _rightShadow.Visible = false; } - if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed)) + if (_bottomShadow is { }) { - // If the view is not pressed and we want highlight move the shadow - // Note, for visual effects reasons, we only move horizontally. - // TODO: Add a setting or flag that lets the view move vertically as well. - Thickness = new (Thickness.Left + PRESS_MOVE_HORIZONTAL, Thickness.Top+ PRESS_MOVE_VERTICAL, Thickness.Right - PRESS_MOVE_HORIZONTAL, Thickness.Bottom - PRESS_MOVE_VERTICAL); - _pressed = true; - - if (_rightShadow is { }) - { - _rightShadow.Visible = false; - } - - if (_bottomShadow is { }) - { - _bottomShadow.Visible = false; - } + _bottomShadow.Visible = false; } } } @@ -235,21 +288,27 @@ private void Margin_LayoutStarted (object? sender, LayoutEventArgs e) { case ShadowStyle.Transparent: // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner. - _rightShadow.Y = Parent!.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; + _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; + break; case ShadowStyle.Opaque: // BUGBUG: This doesn't work right for all Border.Top sizes - Need an API on Border that gives top-right location of line corner. - _rightShadow.Y = Parent!.Border.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; + _rightShadow.Y = Parent!.Border!.Thickness.Top > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).Y + 1 : 0; _bottomShadow.X = Parent.Border.Thickness.Left > 0 ? ScreenToViewport (Parent.Border.GetBorderRectangle ().Location).X + 1 : 0; + break; case ShadowStyle.None: default: _rightShadow.Y = 0; _bottomShadow.X = 0; + break; } } } + + #endregion Shadow + } diff --git a/Terminal.Gui/View/Adornment/Padding.cs b/Terminal.Gui/View/Adornment/Padding.cs index 7dbc1cd704..2c7e8d1ab1 100644 --- a/Terminal.Gui/View/Adornment/Padding.cs +++ b/Terminal.Gui/View/Adornment/Padding.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// The Padding for a . Accessed via /// @@ -21,21 +22,13 @@ public Padding (View parent) : base (parent) /// The color scheme for the Padding. If set to , gets the /// scheme. color scheme. /// - public override ColorScheme ColorScheme + public override ColorScheme? ColorScheme { - get - { - if (base.ColorScheme is { }) - { - return base.ColorScheme; - } - - return Parent?.ColorScheme; - } + get => base.ColorScheme ?? Parent?.ColorScheme; set { base.ColorScheme = value; - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); } } @@ -62,7 +55,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) if (Parent.CanFocus && !Parent.HasFocus) { Parent.SetFocus (); - Parent.SetNeedsDisplay (); + Parent.SetNeedsDraw (); return mouseEvent.Handled = true; } } diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index 3ca31898c7..fa158cbdfe 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -14,42 +14,56 @@ internal class ShadowView : View /// public override Attribute GetNormalColor () { - if (SuperView is Adornment adornment) + if (SuperView is not Adornment adornment) { - var attr = Attribute.Default; + return base.GetNormalColor (); + } - if (adornment.Parent?.SuperView is { }) - { - attr = adornment.Parent.SuperView.GetNormalColor (); - } - else if (Application.Top is { }) - { - attr = Application.Top.GetNormalColor (); - } + var attr = Attribute.Default; - return new ( - new Attribute ( - ShadowStyle == ShadowStyle.Opaque ? Color.Black : attr.Foreground.GetDarkerColor (), - ShadowStyle == ShadowStyle.Opaque ? attr.Background : attr.Background.GetDarkerColor ())); + if (adornment.Parent?.SuperView is { }) + { + attr = adornment.Parent.SuperView.GetNormalColor (); } + else if (Application.Top is { }) + { + attr = Application.Top.GetNormalColor (); + } + + return new ( + new Attribute ( + ShadowStyle == ShadowStyle.Opaque ? Color.Black : attr.Foreground.GetDarkerColor (), + ShadowStyle == ShadowStyle.Opaque ? attr.Background : attr.Background.GetDarkerColor ())); + + } + + /// + /// + protected override bool OnDrawingText () + { + return true; + } - return base.GetNormalColor (); + /// + protected override bool OnClearingViewport () + { + // Prevent clearing (so we can have transparency) + return true; } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - //base.OnDrawContent (viewport); switch (ShadowStyle) { case ShadowStyle.Opaque: if (Orientation == Orientation.Vertical) { - DrawVerticalShadowOpaque (viewport); + DrawVerticalShadowOpaque (Viewport); } else { - DrawHorizontalShadowOpaque (viewport); + DrawHorizontalShadowOpaque (Viewport); } break; @@ -57,21 +71,23 @@ public override void OnDrawContent (Rectangle viewport) case ShadowStyle.Transparent: //Attribute prevAttr = Driver.GetAttribute (); //var attr = new Attribute (prevAttr.Foreground, prevAttr.Background); - //Driver.SetAttribute (attr); + //SetAttribute (attr); if (Orientation == Orientation.Vertical) { - DrawVerticalShadowTransparent (viewport); + DrawVerticalShadowTransparent (Viewport); } else { - DrawHorizontalShadowTransparent (viewport); + DrawHorizontalShadowTransparent (Viewport); } - //Driver.SetAttribute (prevAttr); + //SetAttribute (prevAttr); break; } + + return true; } /// @@ -106,16 +122,18 @@ private void DrawHorizontalShadowOpaque (Rectangle rectangle) private void DrawHorizontalShadowTransparent (Rectangle viewport) { - Rectangle screen = ViewportToScreen (viewport); + Rectangle screen = ViewportToScreen (Viewport); - // Fill the rest of the rectangle - note we skip the last since vertical will draw it - for (int i = Math.Max(0, screen.X + 1); i < screen.X + screen.Width - 1; i++) + for (int r = Math.Max (0, screen.Y); r < screen.Y + screen.Height; r++) { - Driver.Move (i, screen.Y); - - if (i < Driver.Contents!.GetLength (1) && screen.Y < Driver.Contents.GetLength (0)) + for (int c = Math.Max (0, screen.X + 1); c < screen.X + screen.Width; c++) { - Driver.AddRune (Driver.Contents [screen.Y, i].Rune); + Driver?.Move (c, r); + + if (c < Driver?.Contents!.GetLength (1) && r < Driver?.Contents?.GetLength (0)) + { + Driver.AddRune (Driver.Contents [r, c].Rune); + } } } } @@ -126,7 +144,7 @@ private void DrawVerticalShadowOpaque (Rectangle viewport) AddRune (0, 0, Glyphs.ShadowVerticalStart); // Fill the rest of the rectangle with the glyph - for (var i = 1; i < viewport.Height; i++) + for (var i = 1; i < viewport.Height - 1; i++) { AddRune (0, i, Glyphs.ShadowVertical); } @@ -134,16 +152,19 @@ private void DrawVerticalShadowOpaque (Rectangle viewport) private void DrawVerticalShadowTransparent (Rectangle viewport) { - Rectangle screen = ViewportToScreen (viewport); + Rectangle screen = ViewportToScreen (Viewport); // Fill the rest of the rectangle - for (int i = Math.Max (0, screen.Y); i < screen.Y + viewport.Height; i++) + for (int c = Math.Max (0, screen.X); c < screen.X + screen.Width; c++) { - Driver.Move (screen.X, i); - - if (Driver.Contents is { } && screen.X < Driver.Contents.GetLength (1) && i < Driver.Contents.GetLength (0)) + for (int r = Math.Max (0, screen.Y); r < screen.Y + viewport.Height; r++) { - Driver.AddRune (Driver.Contents [i, screen.X].Rune); + Driver?.Move (c, r); + + if (Driver?.Contents is { } && screen.X < Driver.Contents.GetLength (1) && r < Driver.Contents.GetLength (0)) + { + Driver.AddRune (Driver.Contents [r, c].Rune); + } } } } diff --git a/Terminal.Gui/View/DrawEventArgs.cs b/Terminal.Gui/View/DrawEventArgs.cs index 32c07c711d..f3cfc7747a 100644 --- a/Terminal.Gui/View/DrawEventArgs.cs +++ b/Terminal.Gui/View/DrawEventArgs.cs @@ -1,7 +1,9 @@ -namespace Terminal.Gui; +using System.ComponentModel; + +namespace Terminal.Gui; /// Event args for draw events -public class DrawEventArgs : EventArgs +public class DrawEventArgs : CancelEventArgs { /// Creates a new instance of the class. /// @@ -18,9 +20,6 @@ public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport) OldViewport = oldViewport; } - /// If set to true, the draw operation will be canceled, if applicable. - public bool Cancel { get; set; } - /// Gets the Content-relative rectangle describing the old visible viewport into the . public Rectangle OldViewport { get; } diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs index 83d169e2cf..849a29e2ab 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -22,6 +22,14 @@ namespace Terminal.Gui; /// /// /// +/// +/// +/// +/// Creates a that is a fixed size. +/// +/// +/// +/// /// /// /// @@ -182,9 +190,9 @@ public abstract record Dim : IEqualityOperators /// /// A reference to this instance. /// - public bool Has (out Dim dim) where T : Dim + public bool Has (out T dim) where T : Dim { - dim = this; + dim = (this as T)!; return this switch { diff --git a/Terminal.Gui/View/Layout/DimAuto.cs b/Terminal.Gui/View/Layout/DimAuto.cs index f159b6476c..2d7f044796 100644 --- a/Terminal.Gui/View/Layout/DimAuto.cs +++ b/Terminal.Gui/View/Layout/DimAuto.cs @@ -70,15 +70,15 @@ internal override int Calculate (int location, int superviewContentSize, View us if (Style.FastHasFlags (DimAutoStyle.Content)) { - if (!us.ContentSizeTracksViewport) + maxCalculatedSize = textSize; + + if (us is { ContentSizeTracksViewport: false, Subviews.Count: 0 }) { // ContentSize was explicitly set. Use `us.ContentSize` to determine size. maxCalculatedSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height; } else { - maxCalculatedSize = textSize; - // TOOD: All the below is a naive implementation. It may be possible to optimize this. List includedSubviews = us.Subviews.ToList (); @@ -130,7 +130,10 @@ internal override int Calculate (int location, int superviewContentSize, View us { notDependentSubViews = includedSubviews.Where ( v => v.Width is { } - && (v.X is PosAbsolute or PosFunc || v.Width is DimAuto or DimAbsolute or DimFunc) // BUGBUG: We should use v.X.Has and v.Width.Has? + && (v.X is PosAbsolute or PosFunc + || v.Width is DimAuto + or DimAbsolute + or DimFunc) // BUGBUG: We should use v.X.Has and v.Width.Has? && !v.X.Has (out _) && !v.X.Has (out _) && !v.X.Has (out _) @@ -143,7 +146,10 @@ internal override int Calculate (int location, int superviewContentSize, View us { notDependentSubViews = includedSubviews.Where ( v => v.Height is { } - && (v.Y is PosAbsolute or PosFunc || v.Height is DimAuto or DimAbsolute or DimFunc) // BUGBUG: We should use v.Y.Has and v.Height.Has? + && (v.Y is PosAbsolute or PosFunc + || v.Height is DimAuto + or DimAbsolute + or DimFunc) // BUGBUG: We should use v.Y.Has and v.Height.Has? && !v.Y.Has (out _) && !v.Y.Has (out _) && !v.Y.Has (out _) @@ -168,6 +174,7 @@ internal override int Calculate (int location, int superviewContentSize, View us { int width = v.Width!.Calculate (0, superviewContentSize, v, dimension); size = v.X.GetAnchor (0) + width; + } else { @@ -237,22 +244,14 @@ internal override int Calculate (int location, int superviewContentSize, View us List groupIds = includedSubviews.Select ( v => { - if (dimension == Dimension.Width) + return dimension switch { - if (v.X.Has (out Pos posAlign)) - { - return ((PosAlign)posAlign).GroupId; - } - } - else - { - if (v.Y.Has (out Pos posAlign)) - { - return ((PosAlign)posAlign).GroupId; - } - } - - return -1; + Dimension.Width when v.X.Has (out PosAlign posAlign) => + ((PosAlign)posAlign).GroupId, + Dimension.Height when v.Y.Has (out PosAlign posAlign) => + ((PosAlign)posAlign).GroupId, + _ => -1 + }; }) .Distinct () .ToList (); @@ -260,18 +259,7 @@ internal override int Calculate (int location, int superviewContentSize, View us foreach (int groupId in groupIds.Where (g => g != -1)) { // PERF: If this proves a perf issue, consider caching a ref to this list in each item - List posAlignsInGroup = includedSubviews.Where ( - v => - { - return dimension switch - { - Dimension.Width when v.X is PosAlign alignX => alignX.GroupId - == groupId, - Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId - == groupId, - _ => false - }; - }) + List posAlignsInGroup = includedSubviews.Where (v => PosAlign.HasGroupId (v, dimension, groupId)) .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign) .ToList (); @@ -349,16 +337,9 @@ internal override int Calculate (int location, int superviewContentSize, View us // BUGBUG: The order may not be correct. May need to call TopologicalSort? // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL). - if (dimension == Dimension.Width) - { - v.SetRelativeLayout (new (maxCalculatedSize, 0)); - } - else - { - v.SetRelativeLayout (new (0, maxCalculatedSize)); - } - - int maxPosView = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; + int maxPosView = dimension == Dimension.Width + ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension) + : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension); if (maxPosView > maxCalculatedSize) { @@ -390,16 +371,9 @@ internal override int Calculate (int location, int superviewContentSize, View us // BUGBUG: The order may not be correct. May need to call TopologicalSort? // TODO: Figure out a way to not have Calculate change the state of subviews (calling SRL). - if (dimension == Dimension.Width) - { - v.SetRelativeLayout (new (maxCalculatedSize, 0)); - } - else - { - v.SetRelativeLayout (new (0, maxCalculatedSize)); - } - - int maxDimView = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; + int maxDimView = dimension == Dimension.Width + ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension) + : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension); if (maxDimView > maxCalculatedSize) { @@ -410,15 +384,11 @@ internal override int Calculate (int location, int superviewContentSize, View us #endregion DimView #region DimAuto + // [ ] DimAuto - Dimension is internally calculated List dimAutoSubViews; - if (dimension == Dimension.Width && us.GetType ().Name == "Bar" && us.Subviews.Count == 3) - { - - } - if (dimension == Dimension.Width) { dimAutoSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (out _)).ToList (); @@ -432,16 +402,9 @@ internal override int Calculate (int location, int superviewContentSize, View us { View v = dimAutoSubViews [i]; - if (dimension == Dimension.Width) - { - v.SetRelativeLayout (new (maxCalculatedSize, 0)); - } - else - { - v.SetRelativeLayout (new (0, maxCalculatedSize)); - } - - int maxDimAuto= dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; + int maxDimAuto = dimension == Dimension.Width + ? v.Frame.X + v.Width!.Calculate (0, maxCalculatedSize, v, dimension) + : v.Frame.Y + v.Height!.Calculate (0, maxCalculatedSize, v, dimension); if (maxDimAuto > maxCalculatedSize) { @@ -450,6 +413,45 @@ internal override int Calculate (int location, int superviewContentSize, View us } #endregion + + + #region DimFill + + //// [ ] DimFill - Dimension is internally calculated + + //List DimFillSubViews; + + //if (dimension == Dimension.Width) + //{ + // DimFillSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has (out _)).ToList (); + //} + //else + //{ + // DimFillSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has (out _)).ToList (); + //} + + //for (var i = 0; i < DimFillSubViews.Count; i++) + //{ + // View v = DimFillSubViews [i]; + + // if (dimension == Dimension.Width) + // { + // v.SetRelativeLayout (new (maxCalculatedSize, 0)); + // } + // else + // { + // v.SetRelativeLayout (new (0, maxCalculatedSize)); + // } + + // int maxDimFill = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; + + // if (maxDimFill > maxCalculatedSize) + // { + // maxCalculatedSize = maxDimFill; + // } + //} + + #endregion } } diff --git a/Terminal.Gui/View/Layout/DimFunc.cs b/Terminal.Gui/View/Layout/DimFunc.cs index c51406f40e..5fbc28dc2d 100644 --- a/Terminal.Gui/View/Layout/DimFunc.cs +++ b/Terminal.Gui/View/Layout/DimFunc.cs @@ -8,7 +8,7 @@ namespace Terminal.Gui; /// This is a low-level API that is typically used internally by the layout system. Use the various static /// methods on the class to create objects instead. /// -/// The function that computes the dimension. +/// The function that computes the dimension. If this function throws ... public record DimFunc (Func Fn) : Dim { /// diff --git a/Terminal.Gui/View/Layout/LayoutEventArgs.cs b/Terminal.Gui/View/Layout/LayoutEventArgs.cs index dac959af07..264122c664 100644 --- a/Terminal.Gui/View/Layout/LayoutEventArgs.cs +++ b/Terminal.Gui/View/Layout/LayoutEventArgs.cs @@ -1,6 +1,6 @@ namespace Terminal.Gui; -/// Event arguments for the event. +/// Event arguments for the event. public class LayoutEventArgs : EventArgs { /// Creates a new instance of the class. diff --git a/Terminal.Gui/View/Layout/LayoutException.cs b/Terminal.Gui/View/Layout/LayoutException.cs new file mode 100644 index 0000000000..389b5a5ce1 --- /dev/null +++ b/Terminal.Gui/View/Layout/LayoutException.cs @@ -0,0 +1,30 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents an exception that is thrown when a layout operation fails. +/// +[Serializable] +public class LayoutException : Exception +{ + + /// + /// Creates a new instance of . + /// + public LayoutException () { } + + /// + /// Creates a new instance of . + /// + /// + public LayoutException (string? message) : base (message) { } + + /// + /// Creates a new instance of . + /// + /// + /// + public LayoutException (string? message, Exception? innerException) + : base (message, innerException) + { } +} diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs index b5233a6dcb..6ffe9b8d9c 100644 --- a/Terminal.Gui/View/Layout/Pos.cs +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -251,22 +251,22 @@ public static Pos Percent (int percent) /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Top (View? view) { return new PosView (view, Side.Top); } + public static Pos Top (View view) { return new PosView (view, Side.Top); } /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Y (View? view) { return new PosView (view, Side.Top); } + public static Pos Y (View view) { return new PosView (view, Side.Top); } /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Left (View? view) { return new PosView (view, Side.Left); } + public static Pos Left (View view) { return new PosView (view, Side.Left); } /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos X (View? view) { return new PosView (view, Side.Left); } + public static Pos X (View view) { return new PosView (view, Side.Left); } /// /// Creates a object that tracks the Bottom (Y+Height) coordinate of the specified @@ -274,7 +274,7 @@ public static Pos Percent (int percent) /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Bottom (View? view) { return new PosView (view, Side.Bottom); } + public static Pos Bottom (View view) { return new PosView (view, Side.Bottom); } /// /// Creates a object that tracks the Right (X+Width) coordinate of the specified @@ -282,7 +282,7 @@ public static Pos Percent (int percent) /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Right (View? view) { return new PosView (view, Side.Right); } + public static Pos Right (View view) { return new PosView (view, Side.Right); } #endregion static Pos creation methods @@ -335,9 +335,9 @@ public static Pos Percent (int percent) /// /// A reference to this instance. /// - public bool Has (out Pos pos) where T : Pos + public bool Has (out T pos) where T : Pos { - pos = this; + pos = (this as T)!; return this switch { diff --git a/Terminal.Gui/View/Layout/PosAlign.cs b/Terminal.Gui/View/Layout/PosAlign.cs index 197621cf01..d488d0bd85 100644 --- a/Terminal.Gui/View/Layout/PosAlign.cs +++ b/Terminal.Gui/View/Layout/PosAlign.cs @@ -1,6 +1,7 @@ #nullable enable using System.ComponentModel; +using System.Text.RegularExpressions; namespace Terminal.Gui; @@ -10,7 +11,7 @@ namespace Terminal.Gui; /// /// /// Updating the properties of is supported, but will not automatically cause re-layout to -/// happen. +/// happen. /// must be called on the SuperView. /// /// @@ -28,7 +29,7 @@ public record PosAlign : Pos /// /// The cached location. Used to store the calculated location to minimize recalculating it. /// - public int? _cachedLocation; + internal int? _cachedLocation; private readonly Aligner? _aligner; @@ -63,17 +64,7 @@ public static int CalculateMinDimension (int groupId, IList views, Dimensi List dimensionsList = new (); // PERF: If this proves a perf issue, consider caching a ref to this list in each item - List viewsInGroup = views.Where ( - v => - { - return dimension switch - { - Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId, - Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId, - _ => false - }; - }) - .ToList (); + List viewsInGroup = views.Where (v => HasGroupId (v, dimension, groupId)).ToList (); if (viewsInGroup.Count == 0) { @@ -99,6 +90,16 @@ public static int CalculateMinDimension (int groupId, IList views, Dimensi return dimensionsList.Sum (); } + internal static bool HasGroupId (View v, Dimension dimension, int groupId) + { + return dimension switch + { + Dimension.Width when v.X.Has (out PosAlign pos) => pos.GroupId == groupId, + Dimension.Height when v.Y.Has (out PosAlign pos) => pos.GroupId == groupId, + _ => false + }; + } + /// /// Gets the identifier of a set of views that should be aligned together. When only a single /// set of views in a SuperView is aligned, setting is not needed because it defaults to 0. @@ -110,17 +111,23 @@ public static int CalculateMinDimension (int groupId, IList views, Dimensi internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) { - if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension) + if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension && !us.NeedsLayout) { return _cachedLocation.Value; } - if (us?.SuperView is null) + IList? groupViews; + if (us.SuperView is null) { - return 0; + groupViews = new List (); + groupViews.Add (us); + } + else + { + groupViews = us.SuperView!.Subviews.Where (v => HasGroupId (v, dimension, GroupId)).ToList (); } - AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension); + AlignAndUpdateGroup (GroupId, groupViews, dimension, superviewDimension); if (_cachedLocation.HasValue) { @@ -145,31 +152,9 @@ private static void AlignAndUpdateGroup (int groupId, IList views, Dimensi List dimensionsList = new (); // PERF: If this proves a perf issue, consider caching a ref to this list in each item - List posAligns = views.Select ( - v => - { - switch (dimension) - { - case Dimension.Width when v.X.Has (out Pos pos): - - if (pos is PosAlign posAlignX && posAlignX.GroupId == groupId) - { - return posAlignX; - } - - break; - case Dimension.Height when v.Y.Has (out Pos pos): - if (pos is PosAlign posAlignY && posAlignY.GroupId == groupId) - { - return posAlignY; - } - - break; - } - - return null; - }) - .ToList (); + List posAligns = views.Where (v => PosAlign.HasGroupId (v, dimension, groupId)) + .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign) + .ToList (); // PERF: We iterate over viewsInGroup multiple times here. @@ -185,7 +170,9 @@ private static void AlignAndUpdateGroup (int groupId, IList views, Dimensi firstInGroup = posAligns [index]!.Aligner; } - dimensionsList.Add (dimension == Dimension.Width ? views [index].Frame.Width : views [index].Frame.Height); + dimensionsList.Add (dimension == Dimension.Width + ? views [index].Width!.Calculate(0, size, views [index], dimension) + : views [index].Height!.Calculate (0, size, views [index], dimension)); } } diff --git a/Terminal.Gui/View/Layout/PosAnchorEnd.cs b/Terminal.Gui/View/Layout/PosAnchorEnd.cs index 9156e2230b..3b66923ff2 100644 --- a/Terminal.Gui/View/Layout/PosAnchorEnd.cs +++ b/Terminal.Gui/View/Layout/PosAnchorEnd.cs @@ -36,7 +36,7 @@ public record PosAnchorEnd : Pos public bool UseDimForOffset { get; } /// - public override string ToString () { return UseDimForOffset ? "AnchorEnd()" : $"AnchorEnd({Offset})"; } + public override string ToString () { return UseDimForOffset ? "AnchorEnd" : $"AnchorEnd({Offset})"; } internal override int GetAnchor (int size) { diff --git a/Terminal.Gui/View/Layout/PosFunc.cs b/Terminal.Gui/View/Layout/PosFunc.cs index 0bd819ad5d..cb7f99c145 100644 --- a/Terminal.Gui/View/Layout/PosFunc.cs +++ b/Terminal.Gui/View/Layout/PosFunc.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; /// /// Represents a position that is computed by executing a function that returns an integer position. /// -/// The function that computes the position. +/// The function that computes the dimension. If this function throws ... public record PosFunc (Func Fn) : Pos { /// diff --git a/Terminal.Gui/View/Layout/PosView.cs b/Terminal.Gui/View/Layout/PosView.cs index 92748d88b3..9e6a3d7c65 100644 --- a/Terminal.Gui/View/Layout/PosView.cs +++ b/Terminal.Gui/View/Layout/PosView.cs @@ -1,4 +1,6 @@ #nullable enable +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -10,19 +12,35 @@ namespace Terminal.Gui; /// methods on the class to create objects instead. /// /// -/// The View the position is anchored to. -/// The side of the View the position is anchored to. -public record PosView (View? View, Side Side) : Pos +public record PosView : Pos { + /// + /// Represents a position that is anchored to the side of another view. + /// + /// + /// + /// This is a low-level API that is typically used internally by the layout system. Use the various static + /// methods on the class to create objects instead. + /// + /// + /// The View the position is anchored to. + /// The side of the View the position is anchored to. + public PosView (View view, Side side) + { + ArgumentNullException.ThrowIfNull (view); + Target = view; + Side = side; + } + /// /// Gets the View the position is anchored to. /// - public View? Target { get; } = View; + public View Target { get; } /// /// Gets the side of the View the position is anchored to. /// - public Side Side { get; } = Side; + public Side Side { get; } /// public override string ToString () diff --git a/Terminal.Gui/View/View.Adornments.cs b/Terminal.Gui/View/View.Adornments.cs index f3f37d40f8..7058fb0635 100644 --- a/Terminal.Gui/View/View.Adornments.cs +++ b/Terminal.Gui/View/View.Adornments.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; public partial class View // Adornments { @@ -7,9 +8,10 @@ public partial class View // Adornments /// private void SetupAdornments () { - //// TODO: Move this to Adornment as a static factory method + // TODO: Move this to Adornment as a static factory method if (this is not Adornment) { + // TODO: Make the Adornments Lazy and only create them when needed Margin = new (this); Border = new (this); Padding = new (this); @@ -46,6 +48,9 @@ private void DisposeAdornments () /// /// /// + /// The margin is typically transparent. This can be overriden by explicitly setting . + /// + /// /// Enabling will change the Thickness of the Margin to include the shadow. /// /// @@ -54,11 +59,11 @@ private void DisposeAdornments () /// /// /// Changing the size of an adornment (, , or ) will - /// change the size of and trigger to update the layout of the + /// change the size of which will call to update the layout of the /// and its . /// /// - public Margin Margin { get; private set; } + public Margin? Margin { get; private set; } private ShadowStyle _shadowStyle; @@ -109,12 +114,12 @@ public virtual ShadowStyle ShadowStyle /// View's content and are not clipped by the View's Clip Area. /// /// - /// Changing the size of a frame (, , or ) will - /// change the size of the and trigger to update the layout of the + /// Changing the size of an adornment (, , or ) will + /// change the size of which will call to update the layout of the /// and its . /// /// - public Border Border { get; private set; } + public Border? Border { get; private set; } /// Gets or sets whether the view has a one row/col thick border. /// @@ -127,6 +132,10 @@ public virtual ShadowStyle ShadowStyle /// Setting this property to is equivalent to setting 's /// to `0` and to . /// + /// + /// Calls and raises , which allows change + /// to be cancelled. + /// /// For more advanced customization of the view's border, manipulate see directly. /// public LineStyle BorderStyle @@ -134,38 +143,49 @@ public LineStyle BorderStyle get => Border?.LineStyle ?? LineStyle.Single; set { + if (Border is null) + { + return; + } + LineStyle old = Border?.LineStyle ?? LineStyle.None; + + // It's tempting to try to optimize this by checking that old != value and returning. + // Do not. + CancelEventArgs e = new (ref old, ref value); - OnBorderStyleChanging (e); + + if (OnBorderStyleChanging (e) || e.Cancel) + { + return; + } + + BorderStyleChanging?.Invoke (this, e); + + if (e.Cancel) + { + return; + } + + SetBorderStyle (e.NewValue); + SetAdornmentFrames (); + SetNeedsLayout (); } } /// - /// Called when the is changing. Invokes , which allows the - /// event to be cancelled. + /// Called when the is changing. /// /// - /// Override to prevent the from changing. + /// Set e.Cancel to true to prevent the from changing. /// /// - protected void OnBorderStyleChanging (CancelEventArgs e) - { - if (Border is null) - { - return; - } - - BorderStyleChanging?.Invoke (this, e); + protected virtual bool OnBorderStyleChanging (CancelEventArgs e) { return false; } - if (e.Cancel) - { - return; - } - - SetBorderStyle (e.NewValue); - LayoutAdornments (); - SetNeedsLayout (); - } + /// + /// Fired when the is changing. Allows the event to be cancelled. + /// + public event EventHandler>? BorderStyleChanging; /// /// Sets the of the view to the specified value. @@ -188,25 +208,19 @@ public virtual void SetBorderStyle (LineStyle value) { if (value != LineStyle.None) { - if (Border.Thickness == Thickness.Empty) + if (Border!.Thickness == Thickness.Empty) { Border.Thickness = new (1); } } else { - Border.Thickness = new (0); + Border!.Thickness = new (0); } Border.LineStyle = value; } - /// - /// Fired when the is changing. Allows the event to be cancelled. - /// - [CanBeNull] - public event EventHandler> BorderStyleChanging; - /// /// The inside of the view that offsets the /// from the . @@ -217,12 +231,12 @@ public virtual void SetBorderStyle (LineStyle value) /// View's content and are not clipped by the View's Clip Area. /// /// - /// Changing the size of a frame (, , or ) will - /// change the size of the and trigger to update the layout of the + /// Changing the size of an adornment (, , or ) will + /// change the size of which will call to update the layout of the /// and its . /// /// - public Padding Padding { get; private set; } + public Padding? Padding { get; private set; } /// /// Gets the thickness describing the sum of the Adornments' thicknesses. @@ -235,78 +249,48 @@ public virtual void SetBorderStyle (LineStyle value) /// A thickness that describes the sum of the Adornments' thicknesses. public Thickness GetAdornmentsThickness () { - if (Margin is null) - { - return Thickness.Empty; - } - - return Margin.Thickness + Border.Thickness + Padding.Thickness; - } + Thickness result = Thickness.Empty; - /// Lays out the Adornments of the View. - /// - /// Overriden by to do nothing, as does not have adornments. - /// - internal virtual void LayoutAdornments () - { - if (Margin is null) + if (Margin is { }) { - return; // CreateAdornments () has not been called yet + result += Margin.Thickness; } - if (Margin.Frame.Size != Frame.Size) + if (Border is { }) { - Margin.SetFrame (Rectangle.Empty with { Size = Frame.Size }); - Margin.X = 0; - Margin.Y = 0; - Margin.Width = Frame.Size.Width; - Margin.Height = Frame.Size.Height; + result += Border.Thickness; } - Margin.SetNeedsLayout (); - Margin.SetNeedsDisplay (); - - if (IsInitialized) + if (Padding is { }) { - Margin.LayoutSubviews (); + result += Padding.Thickness; } - Rectangle border = Margin.Thickness.GetInside (Margin.Frame); + return result; + } - if (border != Border.Frame) + /// Sets the Frame's of the Margin, Border, and Padding. + internal void SetAdornmentFrames () + { + if (this is Adornment) { - Border.SetFrame (border); - Border.X = border.Location.X; - Border.Y = border.Location.Y; - Border.Width = border.Size.Width; - Border.Height = border.Size.Height; + // Adornments do not have Adornments + return; } - Border.SetNeedsLayout (); - Border.SetNeedsDisplay (); - - if (IsInitialized) + if (Margin is { }) { - Border.LayoutSubviews (); + Margin!.Frame = Rectangle.Empty with { Size = Frame.Size }; } - Rectangle padding = Border.Thickness.GetInside (Border.Frame); - - if (padding != Padding.Frame) + if (Border is { } && Margin is { }) { - Padding.SetFrame (padding); - Padding.X = padding.Location.X; - Padding.Y = padding.Location.Y; - Padding.Width = padding.Size.Width; - Padding.Height = padding.Size.Height; + Border!.Frame = Margin!.Thickness.GetInside (Margin!.Frame); } - Padding.SetNeedsLayout (); - Padding.SetNeedsDisplay (); - - if (IsInitialized) + if (Padding is { } && Border is { }) { - Padding.LayoutSubviews (); + Padding!.Frame = Border!.Thickness.GetInside (Border!.Frame); } } } diff --git a/Terminal.Gui/View/View.Arrangement.cs b/Terminal.Gui/View/View.Arrangement.cs index aec9e65530..e11b1d3894 100644 --- a/Terminal.Gui/View/View.Arrangement.cs +++ b/Terminal.Gui/View/View.Arrangement.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; public partial class View { diff --git a/Terminal.Gui/View/View.Attribute.cs b/Terminal.Gui/View/View.Attribute.cs new file mode 100644 index 0000000000..20e201b664 --- /dev/null +++ b/Terminal.Gui/View/View.Attribute.cs @@ -0,0 +1,121 @@ +#nullable enable +namespace Terminal.Gui; + +public partial class View +{ + // TODO: Rename "Color"->"Attribute" given we'll soon have non-color information in Attributes? + // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/457 + + #region ColorScheme + + private ColorScheme? _colorScheme; + + /// The color scheme for this view, if it is not defined, it returns the 's color scheme. + public virtual ColorScheme? ColorScheme + { + get => _colorScheme ?? SuperView?.ColorScheme; + set + { + if (_colorScheme == value) + { + return; + } + + _colorScheme = value; + + // BUGBUG: This should be in Border.cs somehow + if (Border is { } && Border.LineStyle != LineStyle.None && Border.ColorScheme is { }) + { + Border.ColorScheme = _colorScheme; + } + + SetNeedsDraw (); + } + } + + /// Determines the current based on the value. + /// + /// if is or + /// if is . If it's + /// overridden can return other values. + /// + public virtual Attribute GetFocusColor () + { + ColorScheme? cs = ColorScheme ?? new (); + + return Enabled ? GetColor (cs.Focus) : cs.Disabled; + } + + /// Determines the current based on the value. + /// + /// if is or + /// if is . If it's + /// overridden can return other values. + /// + public virtual Attribute GetHotFocusColor () + { + ColorScheme? cs = ColorScheme ?? new (); + + return Enabled ? GetColor (cs.HotFocus) : cs.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 () + { + ColorScheme? cs = ColorScheme ?? new (); + + return Enabled ? GetColor (cs.HotNormal) : cs.Disabled; + } + + /// Determines the current based on the value. + /// + /// if is or + /// if is . If it's + /// overridden can return other values. + /// + public virtual Attribute GetNormalColor () + { + ColorScheme? cs = ColorScheme ?? new (); + + Attribute disabled = new (cs.Disabled.Foreground, cs.Disabled.Background); + + if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) + { + disabled = new (disabled.Foreground.GetDarkerColor (), disabled.Background.GetDarkerColor ()); + } + + return Enabled ? GetColor (cs.Normal) : disabled; + } + + private Attribute GetColor (Attribute inputAttribute) + { + Attribute attr = inputAttribute; + + if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) + { + attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ()); + } + + return attr; + } + + #endregion ColorScheme + + #region Attribute + + /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. + /// + /// THe Attribute to set. + public Attribute SetAttribute (Attribute attribute) { return Driver?.SetAttribute (attribute) ?? Attribute.Default; } + + /// Gets the current . + /// The current attribute. + public Attribute GetAttribute () { return Driver?.GetAttribute () ?? Attribute.Default; } + + #endregion Attribute +} diff --git a/Terminal.Gui/View/View.Content.cs b/Terminal.Gui/View/View.Content.cs index 95433903ec..5f2818c783 100644 --- a/Terminal.Gui/View/View.Content.cs +++ b/Terminal.Gui/View/View.Content.cs @@ -151,10 +151,7 @@ public bool ContentSizeTracksViewport if (e.Cancel != true) { - OnResizeNeeded (); - - //SetNeedsLayout (); - //SetNeedsDisplay (); + SetNeedsLayout (); } return e.Cancel; @@ -173,7 +170,6 @@ public bool ContentSizeTracksViewport public Point ContentToScreen (in Point location) { // Subtract the ViewportOffsetFromFrame to get the Viewport-relative location. - Point viewportOffset = GetViewportOffsetFromFrame (); Point contentRelativeToViewport = location; contentRelativeToViewport.Offset (-Viewport.X, -Viewport.Y); @@ -268,7 +264,7 @@ public ViewportSettings ViewportSettings /// /// /// Altering the Viewport Size will eventually (when the view is next laid out) cause the - /// and methods to be called. + /// and methods to be called. /// /// public virtual Rectangle Viewport @@ -312,6 +308,8 @@ private void SetViewport (Rectangle viewport) { _viewportLocation = viewport.Location; SetNeedsLayout (); + //SetNeedsDraw(); + //SetSubViewNeedsDraw(); } OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); diff --git a/Terminal.Gui/View/View.Diagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs index 8145af8fc2..8b67e50a87 100644 --- a/Terminal.Gui/View/View.Diagnostics.cs +++ b/Terminal.Gui/View/View.Diagnostics.cs @@ -9,24 +9,37 @@ public enum ViewDiagnosticFlags : uint Off = 0b_0000_0000, /// - /// When enabled, will draw a ruler in the Thickness. + /// When enabled, will draw a ruler in the Thickness. See . /// Ruler = 0b_0000_0001, /// - /// When enabled, will draw the first letter of the Adornment name ('M', 'B', or 'P') - /// in the Thickness. + /// When enabled, will draw the first letter of the Adornment name ('M', 'B', or 'P') + /// in the Thickness. See . /// - Padding = 0b_0000_0010, + Thickness = 0b_0000_0010, /// /// When enabled the View's colors will be darker when the mouse is hovering over the View (See and . /// - Hover = 0b_0000_00100 + Hover = 0b_0000_00100, + + /// + /// When enabled a draw indicator will be shown; the indicator will change each time the View's Draw method is called with NeedsDraw set to true. + /// + DrawIndicator = 0b_0000_01000, } public partial class View { - /// Flags to enable/disable diagnostics. + /// Gets or sets whether diagnostic information will be drawn. This is a bit-field of .e diagnostics. + /// + /// + /// gets set to this property by default, enabling and . + /// + /// + /// and are enabled for all Views independently of Adornments. + /// + /// public static ViewDiagnosticFlags Diagnostics { get; set; } } diff --git a/Terminal.Gui/View/View.Drawing.Clipping.cs b/Terminal.Gui/View/View.Drawing.Clipping.cs new file mode 100644 index 0000000000..2996751f93 --- /dev/null +++ b/Terminal.Gui/View/View.Drawing.Clipping.cs @@ -0,0 +1,167 @@ +#nullable enable +namespace Terminal.Gui; + +public partial class View +{ + /// + /// Gets the current Clip region. + /// + /// + /// + /// There is a single clip region for the entire application. + /// + /// + /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, it is + /// recommended to clone it first. + /// + /// + /// The current Clip. + public static Region? GetClip () { return Application.Driver?.Clip; } + + /// + /// Sets the Clip to the specified region. + /// + /// + /// + /// There is a single clip region for the entire application. This method sets the clip region to the specified + /// region. + /// + /// + /// + public static void SetClip (Region? region) + { + if (Driver is { } && region is { }) + { + Driver.Clip = region; + } + } + + /// + /// Sets the Clip to be the rectangle of the screen. + /// + /// + /// + /// There is a single clip region for the entire application. This method sets the clip region to the screen. + /// + /// + /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, it is + /// recommended to clone it first. + /// + /// + /// + /// The current Clip, which can be then re-applied + /// + public static Region? SetClipToScreen () + { + Region? previous = GetClip (); + + if (Driver is { }) + { + Driver.Clip = new (Application.Screen); + } + + return previous; + } + + /// + /// Removes the specified rectangle from the Clip. + /// + /// + /// + /// There is a single clip region for the entire application. + /// + /// + /// + public static void ExcludeFromClip (Rectangle rectangle) { Driver?.Clip?.Exclude (rectangle); } + + /// + /// Changes the Clip to the intersection of the current Clip and the of this View. + /// + /// + /// + /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, it is + /// recommended to clone it first. + /// + /// + /// + /// The current Clip, which can be then re-applied + /// + internal Region? ClipFrame () + { + if (Driver is null) + { + return null; + } + + Region previous = GetClip () ?? new (Application.Screen); + + Region frameRegion = previous.Clone (); + + // Translate viewportRegion to screen-relative coords + Rectangle screenRect = FrameToScreen (); + frameRegion.Intersect (screenRect); + + if (this is Adornment adornment && adornment.Thickness != Thickness.Empty) + { + // Ensure adornments can't draw outside their thickness + frameRegion.Exclude (adornment.Thickness.GetInside (Frame)); + } + + SetClip (frameRegion); + + return previous; + } + + /// Changes the Clip to the intersection of the current Clip and the of this View. + /// + /// + /// By default, sets the Clip to the intersection of the current clip region and the + /// . This ensures that drawing is constrained to the viewport, but allows + /// content to be drawn beyond the viewport. + /// + /// + /// If has set, clipping will be + /// applied to just the visible content area. + /// + /// + /// + /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, it + /// is recommended to clone it first. + /// + /// + /// + /// + /// The current Clip, which can be then re-applied + /// + public Region? ClipViewport () + { + if (Driver is null) + { + return null; + } + + Region previous = GetClip () ?? new (Application.Screen); + + Region viewportRegion = previous.Clone (); + + Rectangle viewport = ViewportToScreen (new Rectangle (Point.Empty, Viewport.Size)); + viewportRegion?.Intersect (viewport); + + if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly)) + { + // Clamp the Clip to the just content area that is within the viewport + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); + viewportRegion?.Intersect (visibleContent); + } + + if (this is Adornment adornment && adornment.Thickness != Thickness.Empty) + { + // Ensure adornments can't draw outside their thickness + viewportRegion?.Exclude (adornment.Thickness.GetInside (viewport)); + } + + SetClip (viewportRegion); + + return previous; + } +} diff --git a/Terminal.Gui/View/View.Drawing.Primitives.cs b/Terminal.Gui/View/View.Drawing.Primitives.cs new file mode 100644 index 0000000000..ba3245cea7 --- /dev/null +++ b/Terminal.Gui/View/View.Drawing.Primitives.cs @@ -0,0 +1,172 @@ +using static Terminal.Gui.SpinnerStyle; + +namespace Terminal.Gui; + +public partial class View +{ + /// Moves the drawing cursor to the specified -relative location in the view. + /// + /// + /// If the provided coordinates are outside the visible content area, this method does nothing. + /// + /// + /// The top-left corner of the visible content area is ViewPort.Location. + /// + /// + /// Column (viewport-relative). + /// Row (viewport-relative). + public bool Move (int col, int row) + { + if (Driver is null || Driver?.Rows == 0) + { + return false; + } + + if (col < 0 || row < 0 || col >= Viewport.Width || row >= Viewport.Height) + { + return false; + } + + Point screen = ViewportToScreen (new Point (col, row)); + Driver?.Move (screen.X, screen.Y); + + return true; + } + + /// Draws the specified character at the current draw position. + /// The Rune. + public void AddRune (Rune rune) + { + Driver?.AddRune (rune); + } + + + /// + /// Adds the specified to the display at the current cursor position. This method is a + /// convenience method that calls with the constructor. + /// + /// Character to add. + public void AddRune (char c) { AddRune (new Rune (c)); } + + /// Draws the specified character in the specified viewport-relative column and row of the View. + /// + /// If the provided coordinates are outside the visible content area, this method does nothing. + /// + /// + /// The top-left corner of the visible content area is ViewPort.Location. + /// + /// Column (viewport-relative). + /// Row (viewport-relative). + /// The Rune. + public void AddRune (int col, int row, Rune rune) + { + if (Move (col, row)) + { + Driver?.AddRune (rune); + } + } + + + /// Adds the to the display at the current draw position. + /// + /// + /// When the method returns, the draw position will be incremented by the number of columns + /// required, unless the new column value is outside the or . + /// + /// If requires more columns than are available, the output will be clipped. + /// + /// String. + public void AddStr (string str) + { + Driver?.AddStr (str); + } + /// 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 (string text, Attribute hotColor, Attribute normalColor) + { + Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; + SetAttribute (normalColor); + + foreach (Rune rune in text.EnumerateRunes ()) + { + if (rune == new Rune (hotkeySpec.Value)) + { + SetAttribute (hotColor); + + continue; + } + + AddRune (rune); + 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. + /// + public void DrawHotString (string text, bool focused) + { + if (focused) + { + DrawHotString (text, GetHotFocusColor (), GetFocusColor ()); + } + else + { + DrawHotString ( + text, + Enabled ? GetHotNormalColor () : ColorScheme!.Disabled, + Enabled ? GetNormalColor () : ColorScheme!.Disabled + ); + } + } + + /// Fills the specified -relative rectangle with the specified color. + /// The Viewport-relative rectangle to clear. + /// The color to use to fill the rectangle. If not provided, the Normal background color will be used. + public void FillRect (Rectangle rect, Color? color = null) + { + if (Driver is null) + { + return; + } + + Region prevClip = ClipViewport (); + Rectangle toClear = ViewportToScreen (rect); + Attribute prev = SetAttribute (new (color ?? GetNormalColor ().Background)); + Driver.FillRect (toClear); + SetAttribute (prev); + SetClip (prevClip); + } + + /// Fills the specified -relative rectangle. + /// The Viewport-relative rectangle to clear. + /// The Rune to fill with. + public void FillRect (Rectangle rect, Rune rune) + { + if (Driver is null) + { + return; + } + + Region prevClip = ClipViewport (); + Rectangle toClear = ViewportToScreen (rect); + Driver.FillRect (toClear, rune); + SetClip (prevClip); + } + +} diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs index 4401d29580..79ab737f7c 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -1,280 +1,294 @@ #nullable enable +using System.ComponentModel; + namespace Terminal.Gui; public partial class View // Drawing APIs { - private ColorScheme? _colorScheme; - - /// The color scheme for this view, if it is not defined, it returns the 's color scheme. - public virtual ColorScheme? ColorScheme + /// + /// Draws a set of views. + /// + /// The peer views to draw. + /// If , will be called on each view to force it to be drawn. + internal static void Draw (IEnumerable views, bool force) { - get + IEnumerable viewsArray = views as View [] ?? views.ToArray (); + + foreach (View view in viewsArray) { - if (_colorScheme is null) + if (force) { - return SuperView?.ColorScheme; + view.SetNeedsDraw (); } - return _colorScheme; + view.Draw (); } - set - { - if (_colorScheme != value) - { - _colorScheme = value; - if (Border is { } && Border.LineStyle != LineStyle.None && Border.ColorScheme is { }) - { - Border.ColorScheme = _colorScheme; - } - SetNeedsDisplay (); - } - } + Margin.DrawMargins (viewsArray); } - /// 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 LineCanvas LineCanvas { get; } = new (); + /// + /// Draws the view if it needs to be drawn. + /// + /// + /// + /// The view will only be drawn if it is visible, and has any of , + /// , + /// or set. + /// + /// + /// See the View Drawing Deep Dive for more information: . + /// + /// + public void Draw () + { + if (!CanBeVisible (this)) + { + return; + } - // The view-relative region that needs to be redrawn. Marked internal for unit tests. - internal Rectangle _needsDisplayRect = Rectangle.Empty; + Region? saved = GetClip (); - /// Gets or sets whether the view needs to be redrawn. - public bool NeedsDisplay - { - get => _needsDisplayRect != Rectangle.Empty; - set + // TODO: This can be further optimized by checking NeedsDraw below and only clearing, drawing text, drawing content, etc. if it is true. + if (NeedsDraw || SubViewNeedsDraw) { - if (value) - { - SetNeedsDisplay (); - } - else + // Draw the Border and Padding. + // We clip to the frame to prevent drawing outside the frame. + saved = ClipFrame (); + DoDrawBorderAndPadding (); + SetClip (saved); + + // Draw the content within the Viewport + // By default, we clip to the viewport preventing drawing outside the viewport + // We also clip to the content, but if a developer wants to draw outside the viewport, they can do + // so via settings. SetClip honors the ViewportSettings.DisableVisibleContentClipping flag. + // Get our Viewport in screen coordinates + + saved = ClipViewport (); + + // Clear the viewport + // TODO: Simplify/optimize SetAttribute system. + DoSetAttribute (); + DoClearViewport (); + + // Draw the subviews + if (SubViewNeedsDraw) { - ClearNeedsDisplay (); + DoSetAttribute (); + DoDrawSubviews (); } + + // Draw the text + DoSetAttribute (); + DoDrawText (); + + // Draw the content + DoSetAttribute (); + DoDrawContent (); + + // Restore the clip before rendering the line canvas and adornment subviews + // because they may draw outside the viewport. + SetClip (saved); + + saved = ClipFrame (); + + // Draw the line canvas + DoRenderLineCanvas (); + + // Re-draw the border and padding subviews + // HACK: This is a hack to ensure that the border and padding subviews are drawn after the line canvas. + DoDrawBorderAndPaddingSubViews (); + + // Advance the diagnostics draw indicator + Border?.AdvanceDrawIndicator (); + + ClearNeedsDraw (); } - } - /// Gets whether any Subviews need to be redrawn. - public bool SubViewNeedsDisplay { get; private set; } + // This causes the Margin to be drawn in a second pass + // PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop. + Margin?.CacheClip (); - /// - /// Gets or sets whether this View will use it's SuperView's for rendering any - /// lines. If the rendering of any borders drawn by this Frame will be done by its parent's - /// SuperView. If (the default) this View's method will be - /// called to render the borders. - /// - public virtual bool SuperViewRendersLineCanvas { get; set; } = false; + // We're done drawing + DoDrawComplete (); - /// Draws the specified character in the specified viewport-relative column and row of the View. - /// - /// If the provided coordinates are outside the visible content area, this method does nothing. - /// - /// - /// The top-left corner of the visible content area is ViewPort.Location. - /// - /// Column (viewport-relative). - /// Row (viewport-relative). - /// The Rune. - public void AddRune (int col, int row, Rune rune) - { - if (Move (col, row)) + // QUESTION: Should this go before DoDrawComplete? What is more correct? + SetClip (saved); + + // Exclude this view (not including Margin) from the Clip + if (this is not Adornment) { - Driver.AddRune (rune); + Rectangle borderFrame = FrameToScreen (); + + if (Border is { }) + { + borderFrame = Border.FrameToScreen (); + } + + ExcludeFromClip (borderFrame); } } - /// Clears with the normal background. - /// - /// - /// If has only - /// the portion of the content - /// area that is visible within the will be cleared. This is useful for views that have - /// a - /// content area larger than the Viewport (e.g. when is - /// enabled) and want - /// the area outside the content to be visually distinct. - /// - /// - public void Clear () + #region DrawAdornments + + private void DoDrawBorderAndPaddingSubViews () { - if (Driver is null) + if (Border?.Subviews is { } && Border.Thickness != Thickness.Empty) { - return; - } - - // Get screen-relative coords - Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); + // PERFORMANCE: Get the check for DrawIndicator out of this somehow. + foreach (View subview in Border.Subviews.Where (v => v.Visible || v.Id == "DrawIndicator")) + { + if (subview.Id != "DrawIndicator") + { + subview.SetNeedsDraw (); + } - Rectangle prevClip = Driver.Clip; + LineCanvas.Exclude (new (subview.FrameToScreen())); + } - if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) - { - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); - toClear = Rectangle.Intersect (toClear, visibleContent); + Region? saved = Border?.ClipFrame (); + Border?.DoDrawSubviews (); + SetClip (saved); } - Attribute prev = Driver.SetAttribute (GetNormalColor ()); - Driver.FillRect (toClear); - Driver.SetAttribute (prev); + if (Padding?.Subviews is { } && Padding.Thickness != Thickness.Empty) + { + foreach (View subview in Padding.Subviews) + { + subview.SetNeedsDraw (); + } - Driver.Clip = prevClip; + Region? saved = Padding?.ClipFrame (); + Padding?.DoDrawSubviews (); + SetClip (saved); + } } - /// Fills the specified -relative rectangle with the specified color. - /// The Viewport-relative rectangle to clear. - /// The color to use to fill the rectangle. If not provided, the Normal background color will be used. - public void FillRect (Rectangle rect, Color? color = null) + private void DoDrawBorderAndPadding () { - if (Driver is null) + if (OnDrawingBorderAndPadding ()) { return; } - // Get screen-relative coords - Rectangle toClear = ViewportToScreen (rect); + // TODO: add event. - Rectangle prevClip = Driver.Clip; - - Driver.Clip = Rectangle.Intersect (prevClip, ViewportToScreen (Viewport with { Location = new (0, 0) })); - - Attribute prev = Driver.SetAttribute (new (color ?? GetNormalColor ().Background)); - Driver.FillRect (toClear); - Driver.SetAttribute (prev); - - Driver.Clip = prevClip; + DrawBorderAndPadding (); } - /// Sets the 's clip region to . + /// + /// Causes and to be drawn. + /// /// /// - /// By default, the clip rectangle is set to the intersection of the current clip region and the - /// . This ensures that drawing is constrained to the viewport, but allows - /// content to be drawn beyond the viewport. - /// - /// - /// If has set, clipping will be - /// applied to just the visible content area. + /// is drawn in a separate pass. /// /// - /// - /// The current screen-relative clip region, which can be then re-applied by setting - /// . - /// - public Rectangle SetClip () + public void DrawBorderAndPadding () { - if (Driver is null) + // We do not attempt to draw Margin. It is drawn in a separate pass. + + // Each of these renders lines to this View's LineCanvas + // Those lines will be finally rendered in OnRenderLineCanvas + if (Border is { } && Border.Thickness != Thickness.Empty) { - return Rectangle.Empty; + Border?.Draw (); } - Rectangle previous = Driver.Clip; - - // Clamp the Clip to the entire visible area - Rectangle clip = Rectangle.Intersect (ViewportToScreen (Viewport with { Location = Point.Empty }), previous); - - if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly)) + if (Padding is { } && Padding.Thickness != Thickness.Empty) { - // Clamp the Clip to the just content area that is within the viewport - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); - clip = Rectangle.Intersect (clip, visibleContent); + Padding?.Draw (); } - Driver.Clip = clip; - - return previous; } /// - /// Draws the view if it needs to be drawn. Causes the following virtual methods to be called (along with their related events): - /// , . + /// Called when the View's Adornments are to be drawn. Prepares . If + /// is true, only the + /// of this view's subviews will be rendered. If is + /// false (the default), this method will cause the be prepared to be rendered. /// - /// - /// - /// The view will only be drawn if it is visible, and has any of , , - /// or set. - /// - /// - /// 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 property, as this will cause the driver to clip the entire - /// region. - /// - /// - public void Draw () + /// to stop further drawing of the Adornments. + protected virtual bool OnDrawingBorderAndPadding () { return false; } + + #endregion DrawAdornments + + #region SetAttribute + + private void DoSetAttribute () { - if (!CanBeVisible (this)) + if (OnSettingAttribute ()) { return; } - // TODO: This ensures overlapped views are drawn correctly. However, this is inefficient. - // TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413 - if (Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - SetNeedsDisplay (); - } + var args = new CancelEventArgs (); + SettingAttribute?.Invoke (this, args); - if (!NeedsDisplay && !SubViewNeedsDisplay && !LayoutNeeded) + if (args.Cancel) { return; } - OnDrawAdornments (); + SetNormalAttribute (); + } + + /// + /// Called when the normal attribute for the View is to be set. This is called before the View is drawn. + /// + /// to stop default behavior. + protected virtual bool OnSettingAttribute () { return false; } + + /// Raised when the normal attribute for the View is to be set. This is raised before the View is drawn. + /// + /// Set to to stop default behavior. + /// + public event EventHandler? SettingAttribute; + /// + /// Sets the attribute for the View. This is called before the View is drawn. + /// + public void SetNormalAttribute () + { if (ColorScheme is { }) { - //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); - Driver?.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } + } - // By default, we clip to the viewport preventing drawing outside the viewport - // We also clip to the content, but if a developer wants to draw outside the viewport, they can do - // so via settings. SetClip honors the ViewportSettings.DisableVisibleContentClipping flag. - Rectangle prevClip = SetClip (); + #endregion - // Invoke DrawContentEvent - var dev = new DrawEventArgs (Viewport, Rectangle.Empty); - DrawContent?.Invoke (this, dev); + #region ClearViewport - if (!dev.Cancel) + private void DoClearViewport () + { + if (OnClearingViewport ()) { - OnDrawContent (Viewport); + return; } - if (Driver is { }) + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + ClearingViewport?.Invoke (this, dev); + + if (dev.Cancel) { - Driver.Clip = prevClip; + return; } - OnRenderLineCanvas (); - - // TODO: This is a hack to force the border subviews to draw. - if (Border?.Subviews is { }) + if (!NeedsDraw) { - foreach (View view in Border.Subviews) - { - view.SetNeedsDisplay (); - view.Draw (); - } + return; } - // Invoke DrawContentCompleteEvent - OnDrawContentComplete (Viewport); - - // BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details. - ClearLayoutNeeded (); - ClearNeedsDisplay (); + ClearViewport (); } + /// + /// Called when the is to be cleared. + /// + /// to stop further clearing. + protected virtual bool OnClearingViewport () { return false; } + /// Event invoked when the content area of the View is to be drawn. /// /// Will be invoked before any subviews added with have been drawn. @@ -283,341 +297,276 @@ public void Draw () /// . /// /// - public event EventHandler? DrawContent; + public event EventHandler? ClearingViewport; - /// 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; - - /// 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. + /// Clears with the normal background. /// /// - /// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by - /// default. + /// If has only + /// the portion of the content + /// area that is visible within the will be cleared. This is useful for views that have + /// a + /// content area larger than the Viewport (e.g. when is + /// enabled) and want + /// the area outside the content to be visually distinct. /// - /// The hotkey specifier can be changed via /// - public void DrawHotString (string text, Attribute hotColor, Attribute normalColor) + public void ClearViewport () { - Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; - Application.Driver?.SetAttribute (normalColor); - - foreach (Rune rune in text.EnumerateRunes ()) + if (Driver is null) { - if (rune == new Rune (hotkeySpec.Value)) - { - Application.Driver?.SetAttribute (hotColor); + return; + } - continue; - } + // Get screen-relative coords + Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); - Application.Driver?.AddRune (rune); - Application.Driver?.SetAttribute (normalColor); + if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) + { + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); + toClear = Rectangle.Intersect (toClear, visibleContent); } + + Attribute prev = SetAttribute (GetNormalColor ()); + Driver.FillRect (toClear); + SetAttribute (prev); + SetNeedsDraw (); } - /// - /// 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. - /// - public void DrawHotString (string text, bool focused) + #endregion ClearViewport + + #region DrawText + + private void DoDrawText () { - if (focused) + if (OnDrawingText ()) { - DrawHotString (text, GetHotFocusColor (), GetFocusColor ()); + return; } - else + + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingText?.Invoke (this, dev); + + if (dev.Cancel) { - DrawHotString ( - text, - Enabled ? GetHotNormalColor () : ColorScheme!.Disabled, - Enabled ? GetNormalColor () : ColorScheme!.Disabled - ); + return; } - } - /// Determines the current based on the value. - /// - /// if is or - /// if is . If it's - /// overridden can return other values. - /// - public virtual Attribute GetFocusColor () - { - ColorScheme? cs = ColorScheme; - - if (cs is null) + if (!NeedsDraw) { - cs = new (); + return; } - return Enabled ? GetColor (cs.Focus) : cs.Disabled; + DrawText (); } - /// Determines the current based on the value. - /// - /// if is or - /// if is . If it's - /// overridden can return other values. - /// - public virtual Attribute GetHotFocusColor () - { - ColorScheme? cs = ColorScheme ?? new (); + /// + /// Called when the of the View is to be drawn. + /// + /// to stop further drawing of . + protected virtual bool OnDrawingText () { return false; } - return Enabled ? GetColor (cs.HotFocus) : cs.Disabled; - } - /// Determines the current based on the value. +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved + /// Raised when the of the View is to be drawn. /// - /// if is or - /// if is . If it's - /// overridden can return other values. + /// Set to to stop further drawing of + /// . /// - public virtual Attribute GetHotNormalColor () - { - ColorScheme? cs = ColorScheme; + public event EventHandler? DrawingText; +#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved - if (cs is null) + /// + /// Draws the of the View using the . + /// + public void DrawText () + { + if (!string.IsNullOrEmpty (TextFormatter.Text)) { - cs = new (); + TextFormatter.NeedsFormat = true; } - return Enabled ? GetColor (cs.HotNormal) : cs.Disabled; - } + // TODO: If the output is not in the Viewport, do nothing + var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); - /// Determines the current based on the value. - /// - /// if is or - /// if is . If it's - /// overridden can return other values. - /// - public virtual Attribute GetNormalColor () - { - ColorScheme? cs = ColorScheme; + TextFormatter?.Draw ( + drawRect, + HasFocus ? GetFocusColor () : GetNormalColor (), + HasFocus ? GetHotFocusColor () : GetHotNormalColor (), + Rectangle.Empty + ); - if (cs is null) - { - cs = new (); - } - - Attribute disabled = new (cs.Disabled.Foreground, cs.Disabled.Background); - if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) - { - disabled = new (disabled.Foreground.GetDarkerColor (), disabled.Background.GetDarkerColor ()); - } - return Enabled ? GetColor (cs.Normal) : disabled; + // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn. + SetSubViewNeedsDraw (); } - private Attribute GetColor (Attribute inputAttribute) + #endregion DrawText + + #region DrawContent + + private void DoDrawContent () { - Attribute attr = inputAttribute; - if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) + if (OnDrawingContent ()) { - attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ()); + return; } - return attr; + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingContent?.Invoke (this, dev); + + if (dev.Cancel) + { } + + // Do nothing. } - /// Moves the drawing cursor to the specified -relative location in the view. + /// + /// Called when the View's content is to be drawn. The default implementation does nothing. + /// /// + /// + /// to stop further drawing content. + protected virtual bool OnDrawingContent () { return false; } + + /// Raised when the View's content is to be drawn. + /// + /// Will be invoked before any subviews added with have been drawn. /// - /// If the provided coordinates are outside the visible content area, this method does nothing. - /// - /// - /// The top-left corner of the visible content area is ViewPort.Location. + /// Rect provides the view-relative rectangle describing the currently visible viewport into the + /// . /// /// - /// Column (viewport-relative). - /// Row (viewport-relative). - public bool Move (int col, int row) + public event EventHandler? DrawingContent; + + #endregion DrawContent + + #region DrawSubviews + + private void DoDrawSubviews () { - if (Driver is null || Driver?.Rows == 0) + if (OnDrawingSubviews ()) { - return false; + return; } - if (col < 0 || row < 0 || col >= Viewport.Width || row >= Viewport.Height) + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingSubviews?.Invoke (this, dev); + + if (dev.Cancel) { - return false; + return; } - Point screen = ViewportToScreen (new Point (col, row)); - Driver?.Move (screen.X, screen.Y); + if (!SubViewNeedsDraw) + { + return; + } - return true; + DrawSubviews (); } - // TODO: Make this cancelable /// - /// Prepares . If is true, only the - /// of this view's subviews will be rendered. If is - /// false (the default), this method will cause the be prepared to be rendered. + /// Called when the are to be drawn. /// - /// - public virtual bool OnDrawAdornments () - { - if (!IsInitialized) - { - return false; - } + /// to stop further drawing of . + protected virtual bool OnDrawingSubviews () { return false; } - // Each of these renders lines to either this View's LineCanvas - // Those lines will be finally rendered in OnRenderLineCanvas - Margin?.OnDrawContent (Margin.Viewport); - Border?.OnDrawContent (Border.Viewport); - Padding?.OnDrawContent (Padding.Viewport); - return true; - } +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved + /// Raised when the are to be drawn. + /// + /// + /// + /// Set to to stop further drawing of + /// . + /// + public event EventHandler? DrawingSubviews; +#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved /// - /// Draws the view's content, including Subviews. + /// Draws the . /// - /// - /// - /// The parameter is provided as a convenience; it has the same values as the - /// property. - /// - /// - /// The Location and Size indicate what part of the View's content, defined - /// by , is visible and should be drawn. The coordinates taken by - /// and - /// are relative to , thus if ViewPort.Location.Y is 5 - /// the 6th row of the content should be drawn using MoveTo (x, 5). - /// - /// - /// If is larger than ViewPort.Size drawing code should use - /// - /// to constrain drawing for better performance. - /// - /// - /// The may define smaller area than ; complex drawing code - /// can be more - /// efficient by using to constrain drawing for better performance. - /// - /// - /// Overrides should loop through the subviews and call . - /// - /// - /// - /// The rectangle describing the currently visible viewport into the ; has the same value as - /// . - /// - public virtual void OnDrawContent (Rectangle viewport) + public void DrawSubviews () { - if (NeedsDisplay) + if (_subviews is null) { - if (!CanBeVisible (this)) - { - return; - } + return; + } - // BUGBUG: this clears way too frequently. Need to optimize this. - if (SuperView is { } || Arrangement.HasFlag (ViewArrangement.Overlapped)) + // Draw the subviews in reverse order to leverage clipping. + foreach (View view in _subviews.Where (view => view.Visible).Reverse ()) + { + // TODO: HACK - This enables auto line join to work, but is brute force. + if (view.SuperViewRendersLineCanvas) { - Clear (); + view.SetNeedsDraw (); } + view.Draw (); - if (!string.IsNullOrEmpty (TextFormatter.Text)) + if (view.SuperViewRendersLineCanvas) { - if (TextFormatter is { }) - { - TextFormatter.NeedsFormat = true; - } + LineCanvas.Merge (view.LineCanvas); + view.LineCanvas.Clear (); } + } + } - // This should NOT clear - // TODO: If the output is not in the Viewport, do nothing - var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); - - TextFormatter?.Draw ( - drawRect, - HasFocus ? GetFocusColor () : GetNormalColor (), - HasFocus ? GetHotFocusColor () : GetHotNormalColor (), - Rectangle.Empty - ); - SetSubViewNeedsDisplay (); - } - - // TODO: Move drawing of subviews to a separate OnDrawSubviews virtual method - // Draw subviews - // TODO: Implement OnDrawSubviews (cancelable); - if (_subviews is { } && SubViewNeedsDisplay) - { - IEnumerable subviewsNeedingDraw = _subviews.Where ( - view => view.Visible - && (view.NeedsDisplay - || view.SubViewNeedsDisplay - || view.LayoutNeeded - || view.Arrangement.HasFlag (ViewArrangement.Overlapped) - )); - - foreach (View view in subviewsNeedingDraw) - { - if (view.LayoutNeeded) - { - view.LayoutSubviews (); - } + #endregion DrawSubviews - // TODO: This ensures overlapped views are drawn correctly. However, this is inefficient. - // TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413 - if (view.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - view.SetNeedsDisplay (); - } + #region DrawLineCanvas - view.Draw (); - } + private void DoRenderLineCanvas () + { + if (OnRenderingLineCanvas ()) + { + return; } + + // TODO: Add event + + RenderLineCanvas (); } /// - /// Called after to enable overrides. + /// Called when the is to be rendered. See . /// - /// - /// The viewport-relative rectangle describing the currently visible viewport into the - /// - /// - public virtual void OnDrawContentComplete (Rectangle viewport) { DrawContentComplete?.Invoke (this, new (viewport, Rectangle.Empty)); } + /// to stop further drawing of . + protected virtual bool OnRenderingLineCanvas () { return false; } + + /// 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 LineCanvas LineCanvas { get; } = new (); - // TODO: Make this cancelable /// - /// Renders . If is true, only the + /// Gets or sets whether this View will use it's SuperView's for rendering any + /// lines. If the rendering of any borders drawn by this Frame will be done by its parent's + /// SuperView. If (the default) this View's method will + /// be + /// called to render the borders. + /// + public virtual bool SuperViewRendersLineCanvas { get; set; } = false; + + /// + /// Causes the contents of to be drawn. + /// If is true, only the /// of this view's subviews will be rendered. If is /// false (the default), this method will cause the to be rendered. /// - /// - public virtual bool OnRenderLineCanvas () + public void RenderLineCanvas () { - if (!IsInitialized || Driver is null) + if (Driver is null) { - return false; + return; } - // If we have a SuperView, it'll render our frames. - if (!SuperViewRendersLineCanvas && LineCanvas.Viewport != Rectangle.Empty) + if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rectangle.Empty) { foreach (KeyValuePair p in LineCanvas.GetCellMap ()) { // Get the entire map if (p.Value is { }) { - Driver.SetAttribute (p.Value.Value.Attribute ?? ColorScheme!.Normal); + SetAttribute (p.Value.Value.Attribute ?? ColorScheme!.Normal); Driver.Move (p.Key.X, p.Key.Y); // TODO: #2616 - Support combining sequences that don't normalize @@ -627,116 +576,212 @@ public virtual bool OnRenderLineCanvas () LineCanvas.Clear (); } + } - if (Subviews.Any (s => s.SuperViewRendersLineCanvas)) + #endregion DrawLineCanvas + + #region DrawComplete + + private void DoDrawComplete () + { + OnDrawComplete (); + + DrawComplete?.Invoke (this, new (Viewport, Viewport)); + + // Default implementation does nothing. + } + + /// + /// Called when the View is completed drawing. + /// + protected virtual void OnDrawComplete () { } + + /// Raised when the View is completed drawing. + /// + /// + public event EventHandler? DrawComplete; + + #endregion DrawComplete + + #region NeedsDraw + + // TODO: Change NeedsDraw to use a Region instead of Rectangle + // TODO: Make _needsDrawRect nullable instead of relying on Empty + // TODO: If null, it means ? + // TODO: If Empty, it means no need to redraw + // TODO: If not Empty, it means the region that needs to be redrawn + // The viewport-relative region that needs to be redrawn. Marked internal for unit tests. + internal Rectangle _needsDrawRect = Rectangle.Empty; + + /// Gets or sets whether the view needs to be redrawn. + /// + /// + /// Will be if the property is or if + /// any part of the view's needs to be redrawn. + /// + /// + /// Setting has no effect on . + /// + /// + public bool NeedsDraw + { + // TODO: Figure out if we can decouple NeedsDraw from NeedsLayout. + get => Visible && (_needsDrawRect != Rectangle.Empty || NeedsLayout); + set { - foreach (View subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) + if (value) { - // Combine the LineCanvas' - LineCanvas.Merge (subview.LineCanvas); - subview.LineCanvas.Clear (); + SetNeedsDraw (); } - - foreach (KeyValuePair p in LineCanvas.GetCellMap ()) + else { - // Get the entire map - if (p.Value is { }) - { - Driver.SetAttribute (p.Value.Value.Attribute ?? ColorScheme!.Normal); - Driver.Move (p.Key.X, p.Key.Y); - - // TODO: #2616 - Support combining sequences that don't normalize - Driver.AddRune (p.Value.Value.Rune); - } + ClearNeedsDraw (); } - - LineCanvas.Clear (); } - - return true; } - /// Sets the area of this view needing to be redrawn to . + /// Gets whether any Subviews need to be redrawn. + public bool SubViewNeedsDraw { get; private set; } + + /// Sets that the of this View needs to be redrawn. /// /// If the view has not been initialized ( is ), this method /// does nothing. /// - public void SetNeedsDisplay () { SetNeedsDisplay (Viewport); } + public void SetNeedsDraw () + { + Rectangle viewport = Viewport; + + if (!Visible || (_needsDrawRect != Rectangle.Empty && viewport.IsEmpty)) + { + // This handles the case where the view has not been initialized yet + return; + } + + SetNeedsDraw (viewport); + } - /// Expands the area of this view needing to be redrawn to include . + /// Expands the area of this view needing to be redrawn to include . /// /// - /// The location of is relative to the View's content, bound by Size.Empty and - /// . + /// The location of is relative to the View's . /// /// /// If the view has not been initialized ( is ), the area to be - /// redrawn will be the . + /// redrawn will be the . /// /// - /// The content-relative region that needs to be redrawn. - public void SetNeedsDisplay (Rectangle region) + /// The relative region that needs to be redrawn. + public void SetNeedsDraw (Rectangle viewPortRelativeRegion) { - if (_needsDisplayRect.IsEmpty) + if (!Visible) + { + return; + } + + if (_needsDrawRect.IsEmpty) { - _needsDisplayRect = region; + _needsDrawRect = viewPortRelativeRegion; } else { - int x = Math.Min (_needsDisplayRect.X, region.X); - int y = Math.Min (_needsDisplayRect.Y, region.Y); - int w = Math.Max (_needsDisplayRect.Width, region.Width); - int h = Math.Max (_needsDisplayRect.Height, region.Height); - _needsDisplayRect = new (x, y, w, h); + int x = Math.Min (Viewport.X, viewPortRelativeRegion.X); + int y = Math.Min (Viewport.Y, viewPortRelativeRegion.Y); + int w = Math.Max (Viewport.Width, viewPortRelativeRegion.Width); + int h = Math.Max (Viewport.Height, viewPortRelativeRegion.Height); + _needsDrawRect = new (x, y, w, h); + } + + // Do not set on Margin - it will be drawn in a separate pass. + + if (Border is { } && Border.Thickness != Thickness.Empty) + { + Border?.SetNeedsDraw (); + } + + if (Padding is { } && Padding.Thickness != Thickness.Empty) + { + Padding?.SetNeedsDraw (); } - Margin?.SetNeedsDisplay (); - Border?.SetNeedsDisplay (); - Padding?.SetNeedsDisplay (); + SuperView?.SetSubViewNeedsDraw (); - SuperView?.SetSubViewNeedsDisplay (); + if (this is Adornment adornment) + { + adornment.Parent?.SetSubViewNeedsDraw (); + } foreach (View subview in Subviews) { - if (subview.Frame.IntersectsWith (region)) + if (subview.Frame.IntersectsWith (viewPortRelativeRegion)) { - Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, region); + Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, viewPortRelativeRegion); subviewRegion.X -= subview.Frame.X; subviewRegion.Y -= subview.Frame.Y; - subview.SetNeedsDisplay (subviewRegion); + subview.SetNeedsDraw (subviewRegion); } } } - /// Sets to for this View and all Superviews. - public void SetSubViewNeedsDisplay () + /// Sets to for this View and all Superviews. + public void SetSubViewNeedsDraw () { - SubViewNeedsDisplay = true; + if (!Visible) + { + return; + } + + SubViewNeedsDraw = true; if (this is Adornment adornment) { - adornment.Parent?.SetSubViewNeedsDisplay (); + adornment.Parent?.SetSubViewNeedsDraw (); } - if (SuperView is { SubViewNeedsDisplay: false }) + if (SuperView is { SubViewNeedsDraw: false }) { - SuperView.SetSubViewNeedsDisplay (); + SuperView.SetSubViewNeedsDraw (); } } - /// Clears and . - protected void ClearNeedsDisplay () + /// Clears and . + protected void ClearNeedsDraw () { - _needsDisplayRect = Rectangle.Empty; - SubViewNeedsDisplay = false; + _needsDrawRect = Rectangle.Empty; + SubViewNeedsDraw = false; + + if (Margin is { } && Margin.Thickness != Thickness.Empty) + { + Margin?.ClearNeedsDraw (); + } + + if (Border is { } && Border.Thickness != Thickness.Empty) + { + Border?.ClearNeedsDraw (); + } - Margin?.ClearNeedsDisplay (); - Border?.ClearNeedsDisplay (); - Padding?.ClearNeedsDisplay (); + if (Padding is { } && Padding.Thickness != Thickness.Empty) + { + Padding?.ClearNeedsDraw (); + } foreach (View subview in Subviews) { - subview.ClearNeedsDisplay (); + subview.ClearNeedsDraw (); + } + + if (SuperView is { }) + { + SuperView.SubViewNeedsDraw = false; } + + // This ensures LineCanvas' get redrawn + if (!SuperViewRendersLineCanvas) + { + LineCanvas.Clear (); + } + } + + #endregion NeedsDraw } diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs index 01cf01bebd..5f45236b39 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -47,8 +47,12 @@ public virtual View? SuperView /// /// The view to add. /// The view that was added. - public virtual View Add (View view) + public virtual View? Add (View? view) { + if (view is null) + { + return null; + } if (_subviews is null) { _subviews = []; @@ -73,7 +77,6 @@ public virtual View Add (View view) if (view.Enabled && !Enabled) { - view._oldEnabled = true; view.Enabled = false; } @@ -85,9 +88,8 @@ public virtual View Add (View view) view.EndInit (); } - CheckDimAuto (); + SetNeedsDraw (); SetNeedsLayout (); - SetNeedsDisplay (); return view; } @@ -126,7 +128,6 @@ public virtual void OnAdded (SuperViewChangedEventArgs e) { View view = e.SubView; view.IsAdded = true; - view.OnResizeNeeded (); view.Added?.Invoke (this, e); } @@ -150,8 +151,13 @@ public virtual void OnRemoved (SuperViewChangedEventArgs e) /// /// The removed View. if the View could not be removed. /// - public virtual View? Remove (View view) + public virtual View? Remove (View? view) { + if (view is null) + { + return null; + } + if (_subviews is null) { return view; @@ -179,13 +185,13 @@ public virtual void OnRemoved (SuperViewChangedEventArgs e) view._superView = null; SetNeedsLayout (); - SetNeedsDisplay (); + SetNeedsDraw (); foreach (View v in _subviews) { if (v.Frame.IntersectsWith (touched)) { - view.SetNeedsDisplay (); + view.SetNeedsDraw (); } } @@ -339,8 +345,8 @@ private void PerformActionForSubview (View subview, Action action) } // BUGBUG: this is odd. Why is this needed? - SetNeedsDisplay (); - subview.SetNeedsDisplay (); + SetNeedsDraw (); + subview.SetNeedsDraw (); } #endregion SubViewOrdering diff --git a/Terminal.Gui/View/View.Layout.cs b/Terminal.Gui/View/View.Layout.cs index 318b5c993a..f317b5bcb1 100644 --- a/Terminal.Gui/View/View.Layout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -1,11 +1,12 @@ #nullable enable using System.Diagnostics; -using Microsoft.CodeAnalysis; namespace Terminal.Gui; public partial class View // Layout APIs { + #region Frame/Position/Dimension + /// /// Indicates whether the specified SuperView-relative coordinates are within the View's . /// @@ -13,162 +14,7 @@ public partial class View // Layout APIs /// if the specified SuperView-relative coordinates are within the View. public virtual bool Contains (in Point location) { return Frame.Contains (location); } - // BUGBUG: This method interferes with Dialog/MessageBox default min/max size. - /// - /// Gets a new location of the that is within the Viewport of the 's - /// (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates. - /// - /// - /// If does not have a or it's SuperView is not - /// the position will be bound by the and - /// . - /// - /// The View that is to be moved. - /// The target x location. - /// The target y location. - /// The new x location that will ensure will be fully visible. - /// The new y location that will ensure will be fully visible. - /// - /// Either (if does not have a Super View) or - /// 's SuperView. This can be used to ensure LayoutSubviews is called on the correct View. - /// - internal static View? GetLocationEnsuringFullVisibility ( - View viewToMove, - int targetX, - int targetY, - out int nx, - out int ny - //, - // out StatusBar? statusBar - ) - { - int maxDimension; - View? superView; - //statusBar = null!; - - if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) - { - maxDimension = Driver.Cols; - superView = Application.Top; - } - else - { - // Use the SuperView's Viewport, not Frame - maxDimension = viewToMove!.SuperView.Viewport.Width; - superView = viewToMove.SuperView; - } - - if (superView?.Margin is { } && superView == viewToMove!.SuperView) - { - maxDimension -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; - } - - if (viewToMove!.Frame.Width <= maxDimension) - { - nx = Math.Max (targetX, 0); - nx = nx + viewToMove.Frame.Width > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Width, 0) : nx; - - if (nx > viewToMove.Frame.X + viewToMove.Frame.Width) - { - nx = Math.Max (viewToMove.Frame.Right, 0); - } - } - else - { - nx = targetX; - } - - //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); - var menuVisible = false; - var statusVisible = false; - - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) - { - menuVisible = Application.Top?.MenuBar?.Visible == true; - } - else - { - View? t = viewToMove!.SuperView; - - while (t is { } and not Toplevel) - { - t = t.SuperView; - } - - if (t is Toplevel topLevel) - { - menuVisible = topLevel.MenuBar?.Visible == true; - } - } - - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) - { - maxDimension = menuVisible ? 1 : 0; - } - else - { - maxDimension = 0; - } - - ny = Math.Max (targetY, maxDimension); - - //if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) - //{ - // statusVisible = Application.Top?.StatusBar?.Visible == true; - // statusBar = Application.Top?.StatusBar!; - //} - //else - //{ - // View? t = viewToMove!.SuperView; - - // while (t is { } and not Toplevel) - // { - // t = t.SuperView; - // } - - // if (t is Toplevel topLevel) - // { - // statusVisible = topLevel.StatusBar?.Visible == true; - // statusBar = topLevel.StatusBar!; - // } - //} - - if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) - { - maxDimension = statusVisible ? Driver.Rows - 1 : Driver.Rows; - } - else - { - maxDimension = statusVisible ? viewToMove!.SuperView.Viewport.Height - 1 : viewToMove!.SuperView.Viewport.Height; - } - - if (superView?.Margin is { } && superView == viewToMove?.SuperView) - { - maxDimension -= superView.GetAdornmentsThickness ().Top + superView.GetAdornmentsThickness ().Bottom; - } - - ny = Math.Min (ny, maxDimension); - - if (viewToMove?.Frame.Height <= maxDimension) - { - ny = ny + viewToMove.Frame.Height > maxDimension - ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0) - : ny; - - if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) - { - ny = Math.Max (viewToMove.Frame.Bottom, 0); - } - } - - //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); - - return superView!; - } - - #region Frame - - private Rectangle _frame; + private Rectangle? _frame; /// Gets or sets the absolute location and dimension of the view. /// @@ -185,45 +31,55 @@ out int ny /// . /// /// - /// Setting Frame will set , , , and to the - /// values of the corresponding properties of the parameter. + /// Setting Frame will set , , , and to + /// absolute values. /// /// - /// Altering the Frame will eventually (when the view hierarchy is next laid out via see - /// cref="LayoutSubviews"/>) cause and - /// - /// methods to be called. + /// Changing this property will result in and to be set, + /// resulting in the + /// view being laid out and redrawn as appropriate in the next iteration of the . /// /// public Rectangle Frame { - get => _frame; - set + get { - if (_frame == value) + if (NeedsLayout) { - return; + //Debug.WriteLine("Frame_get with _layoutNeeded"); } - SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) }); - - // If Frame gets set, set all Pos/Dim to Absolute values. - _x = _frame.X; - _y = _frame.Y; - _width = _frame.Width; - _height = _frame.Height; - - if (IsInitialized) + return _frame ?? Rectangle.Empty; + } + set + { + // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged + if (SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) })) { - OnResizeNeeded (); + // If Frame gets set, set all Pos/Dim to Absolute values. + _x = _frame!.Value.X; + _y = _frame!.Value.Y; + _width = _frame!.Value.Width; + _height = _frame!.Value.Height; + + // Implicit layout is ok here because we are setting the Frame directly. + Layout (); } - - SetNeedsDisplay (); } } - private void SetFrame (in Rectangle frame) + /// + /// INTERNAL API - Sets _frame, calls SetsNeedsLayout, and raises OnViewportChanged/ViewportChanged + /// + /// + /// if the frame was changed. + private bool SetFrame (in Rectangle frame) { + if (_frame == frame) + { + return false; + } + var oldViewport = Rectangle.Empty; if (IsInitialized) @@ -234,7 +90,15 @@ private void SetFrame (in Rectangle frame) // This is the only place where _frame should be set directly. Use Frame = or SetFrame instead. _frame = frame; + SetAdornmentFrames (); + + SetNeedsDraw (); + SetNeedsLayout (); + + // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK? OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); + + return true; } /// Gets the with a screen-relative location. @@ -256,7 +120,7 @@ public virtual Rectangle FrameToScreen () // Now add our Frame location parentScreen.Offset (screen.X, screen.Y); - return parentScreen; + return parentScreen with { Size = Frame.Size }; } Point viewportOffset = current.GetViewportOffsetFromFrame (); @@ -294,6 +158,24 @@ public virtual Point ScreenToFrame (in Point location) return frame; } + // helper for X, Y, Width, Height setters to ensure consistency + private void PosDimSet () + { + SetNeedsLayout (); + + if (_x is PosAbsolute && _y is PosAbsolute && _width is DimAbsolute && _height is DimAbsolute) + { + // Implicit layout is ok here because all Pos/Dim are Absolute values. + Layout (); + + if (SuperView is { } || this is Adornment { Parent: null }) + { + // Ensure the next Application iteration tries to layout again + SetNeedsLayout (); + } + } + } + private Pos _x = Pos.Absolute (0); /// Gets or sets the X position for the view (the column). @@ -309,12 +191,12 @@ public virtual Point ScreenToFrame (in Point location) /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the view has been - /// initialized ( is true) and has been - /// called. + /// laid out (e.g. has been called). /// /// - /// Changing this property will eventually (when the view is next drawn) cause the - /// and methods to be called. + /// Changing this property will result in and to be set, + /// resulting in the + /// view being laid out and redrawn as appropriate in the next iteration of the . /// /// /// Changing this property will cause to be updated. @@ -333,7 +215,7 @@ public Pos X _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null"); - OnResizeNeeded (); + PosDimSet (); } } @@ -352,12 +234,12 @@ public Pos X /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the view has been - /// initialized ( is true) and has been - /// called. + /// laid out (e.g. has been called). /// /// - /// Changing this property will eventually (when the view is next drawn) cause the - /// and methods to be called. + /// Changing this property will result in and to be set, + /// resulting in the + /// view being laid out and redrawn as appropriate in the next iteration of the . /// /// /// Changing this property will cause to be updated. @@ -375,7 +257,7 @@ public Pos Y } _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null"); - OnResizeNeeded (); + PosDimSet (); } } @@ -390,17 +272,16 @@ public Pos Y /// /// /// The dimension is relative to the 's Content, which is bound by - /// - /// . + /// . /// /// - /// If set to a relative value (e.g. ) the value is indeterminate until the view has - /// been initialized ( is true) and has been - /// called. + /// If set to a relative value (e.g. ) the value is indeterminate until the view has been + /// laid out (e.g. has been called). /// /// - /// Changing this property will eventually (when the view is next drawn) cause the - /// and methods to be called. + /// Changing this property will result in and to be set, + /// resulting in the + /// view being laid out and redrawn as appropriate in the next iteration of the . /// /// /// Changing this property will cause to be updated. @@ -417,18 +298,12 @@ public Dim? Height return; } - if (_height is { } && _height.Has (out _)) - { - // Reset ContentSize to Viewport - _contentSize = null; - } - _height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null"); // Reset TextFormatter - Will be recalculated in SetTextFormatterSize TextFormatter.ConstrainToHeight = null; - OnResizeNeeded (); + PosDimSet (); } } @@ -447,13 +322,13 @@ public Dim? Height /// . /// /// - /// If set to a relative value (e.g. ) the value is indeterminate until the view has - /// been initialized ( is true) and has been - /// called. + /// If set to a relative value (e.g. ) the value is indeterminate until the view has been + /// laid out (e.g. has been called). /// /// - /// Changing this property will eventually (when the view is next drawn) cause the - /// and methods to be called. + /// Changing this property will result in and to be set, + /// resulting in the + /// view being laid out and redrawn as appropriate in the next iteration of the . /// /// /// Changing this property will cause to be updated. @@ -470,44 +345,91 @@ public Dim? Width return; } - if (_width is { } && _width.Has (out _)) - { - // Reset ContentSize to Viewport - _contentSize = null; - } - _width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null"); // Reset TextFormatter - Will be recalculated in SetTextFormatterSize TextFormatter.ConstrainToWidth = null; - - OnResizeNeeded (); + PosDimSet (); } } - #endregion Frame + #endregion Frame/Position/Dimension + + #region Core Layout API + + /// + /// INTERNAL API - Performs layout of the specified views within the specified content size. Called by the Application + /// main loop. + /// + /// The views to layout. + /// The size to bound the views by. + /// If any of the views needed to be laid out. + internal static bool Layout (IEnumerable views, Size contentSize) + { + var neededLayout = false; + + foreach (View v in views) + { + if (v.NeedsLayout) + { + neededLayout = true; + v.Layout (contentSize); + } + } - #region Layout Engine + return neededLayout; + } - /// Fired after the View's method has completed. + /// + /// Performs layout of the view and its subviews within the specified content size. + /// /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has - /// otherwise changed. + /// + /// See the View Layout Deep Dive for more information: + /// + /// + /// + /// This method is intended to be called by the layout engine to + /// prepare the view for layout and is exposed as a public API primarily for testing purposes. + /// /// - public event EventHandler? LayoutComplete; + /// + /// If the view could not be laid out (typically because a dependencies was not ready). + public bool Layout (Size contentSize) + { + if (SetRelativeLayout (contentSize)) + { + LayoutSubviews (); - /// Fired after the View's method has completed. + // Debug.Assert(!NeedsLayout); + return true; + } + + return false; + } + + /// + /// Performs layout of the view and its subviews using the content size of either the or + /// . + /// /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has - /// otherwise changed. + /// + /// See the View Layout Deep Dive for more information: + /// + /// + /// + /// This method is intended to be called by the layout engine to + /// prepare the view for layout and is exposed as a public API primarily for testing purposes. + /// /// - public event EventHandler? LayoutStarted; + /// If the view could not be laid out (typically because dependency was not ready). + public bool Layout () { return Layout (GetContainerSize ()); } /// - /// Adjusts given the SuperView's ContentSize (nominally the same as - /// this.SuperView.GetContentSize ()) - /// and the position (, ) and dimension (, and - /// ). + /// Sets the position and size of this view, relative to the SuperView's ContentSize (nominally the same as + /// this.SuperView.GetContentSize ()) based on the values of , , + /// , + /// and . /// /// /// @@ -516,15 +438,18 @@ public Dim? Width /// are left unchanged. /// /// - /// If any of the view's subviews have a position or dimension dependent on either or - /// other subviews, on - /// will be called for that subview. + /// This method does not arrange subviews or adornments. It is intended to be called by the layout engine to + /// prepare the view for layout and is exposed as a public API primarily for testing purposes. + /// + /// + /// Some subviews may have SetRelativeLayout called on them as a side effect, particularly in DimAuto scenarios. /// /// /// /// The size of the SuperView's content (nominally the same as this.SuperView.GetContentSize ()). /// - internal void SetRelativeLayout (Size superviewContentSize) + /// if successful. means a dependent View still needs layout. + public bool SetRelativeLayout (Size superviewContentSize) { Debug.Assert (_x is { }); Debug.Assert (_y is { }); @@ -532,32 +457,54 @@ internal void SetRelativeLayout (Size superviewContentSize) Debug.Assert (_height is { }); CheckDimAuto (); + + // TODO: Should move to View.LayoutSubviews? SetTextFormatterSize (); int newX, newW, newY, newH; - // Calculate the new X, Y, Width, and Height - // If the Width or Height is Dim.Auto, calculate the Width or Height first. Otherwise, calculate the X or Y first. - if (_width is DimAuto) + try { - newW = _width.Calculate (0, superviewContentSize.Width, this, Dimension.Width); - newX = _x.Calculate (superviewContentSize.Width, newW, this, Dimension.Width); - } - else - { - newX = _x.Calculate (superviewContentSize.Width, _width, this, Dimension.Width); - newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width); - } + // Calculate the new X, Y, Width, and Height + // If the Width or Height is Dim.Auto, calculate the Width or Height first. Otherwise, calculate the X or Y first. + if (_width.Has (out _)) + { + newW = _width.Calculate (0, superviewContentSize.Width, this, Dimension.Width); + newX = _x.Calculate (superviewContentSize.Width, newW, this, Dimension.Width); - if (_height is DimAuto) - { - newH = _height.Calculate (0, superviewContentSize.Height, this, Dimension.Height); - newY = _y.Calculate (superviewContentSize.Height, newH, this, Dimension.Height); + if (newW != Frame.Width) + { + // Pos.Calculate gave us a new position. We need to redo dimension + newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width); + } + } + else + { + newX = _x.Calculate (superviewContentSize.Width, _width, this, Dimension.Width); + newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width); + } + + if (_height.Has (out _)) + { + newH = _height.Calculate (0, superviewContentSize.Height, this, Dimension.Height); + newY = _y.Calculate (superviewContentSize.Height, newH, this, Dimension.Height); + + if (newH != Frame.Height) + { + // Pos.Calculate gave us a new position. We need to redo dimension + newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height); + } + } + else + { + newY = _y.Calculate (superviewContentSize.Height, _height, this, Dimension.Height); + newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height); + } } - else + catch (LayoutException) { - newY = _y.Calculate (superviewContentSize.Height, _height, this, Dimension.Height); - newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height); + //Debug.WriteLine ($"A Dim/PosFunc function threw (typically this is because a dependent View was not laid out)\n{le}."); + return false; } Rectangle newFrame = new (newX, newY, newW, newH); @@ -565,6 +512,7 @@ internal void SetRelativeLayout (Size superviewContentSize) if (Frame != newFrame) { // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height + // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged SetFrame (newFrame); if (_x is PosAbsolute) @@ -592,8 +540,7 @@ internal void SetRelativeLayout (Size superviewContentSize) SetTitleTextFormatterSize (); } - SetNeedsLayout (); - SetNeedsDisplay (); + SuperView?.SetNeedsDraw (); } if (TextFormatter.ConstrainToWidth is null) @@ -605,11 +552,13 @@ internal void SetRelativeLayout (Size superviewContentSize) { TextFormatter.ConstrainToHeight = GetContentSize ().Height; } + + return true; } /// - /// Invoked when the dimensions of the view have changed, for example in response to the container view or terminal - /// resizing. + /// INTERNAL API - Causes the view's subviews and adornments to be laid out within the view's content areas. Assumes + /// the view's relative layout has been set via . /// /// /// @@ -620,16 +569,11 @@ internal void SetRelativeLayout (Size superviewContentSize) /// The position and dimensions of the view are indeterminate until the view has been initialized. Therefore, the /// behavior of this method is indeterminate if is . /// - /// Raises the event before it returns. + /// Raises the event before it returns. /// - public virtual void LayoutSubviews () + internal void LayoutSubviews () { - if (!IsInitialized) - { - Debug.WriteLine ($"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}"); - } - - if (!LayoutNeeded) + if (!NeedsLayout) { return; } @@ -637,9 +581,25 @@ public virtual void LayoutSubviews () CheckDimAuto (); Size contentSize = GetContentSize (); - OnLayoutStarted (new (contentSize)); - LayoutAdornments (); + OnSubviewLayout (new (contentSize)); + SubviewLayout?.Invoke (this, new (contentSize)); + + // The Adornments already have their Frame's set by SetRelativeLayout so we call LayoutSubViews vs. Layout here. + if (Margin is { Subviews.Count: > 0 }) + { + Margin.LayoutSubviews (); + } + + if (Border is { Subviews.Count: > 0 }) + { + Border.LayoutSubviews (); + } + + if (Padding is { Subviews.Count: > 0 }) + { + Padding.LayoutSubviews (); + } // Sort out the dependencies of the X, Y, Width, Height properties HashSet nodes = new (); @@ -647,120 +607,197 @@ public virtual void LayoutSubviews () CollectAll (this, ref nodes, ref edges); List ordered = TopologicalSort (SuperView!, nodes, edges); + List redo = new (); + foreach (View v in ordered) { - LayoutSubview (v, contentSize); + if (!v.Layout (contentSize)) + { + redo.Add (v); + } + } + + var layoutStillNeeded = false; + + if (redo.Count > 0) + { + foreach (View v in ordered) + { + if (!v.Layout (contentSize)) + { + layoutStillNeeded = true; + } + } } // If the 'to' is rooted to 'from' it's a special-case. - // Use LayoutSubview with the Frame of the 'from'. - if (SuperView is { } && GetTopSuperView () is { } && LayoutNeeded && edges.Count > 0) + // Use Layout with the ContentSize of the 'from'. + // See the Nested_SubViews_Ref_Topmost_SuperView unit test + if (edges.Count > 0 && GetTopSuperView () is { }) { foreach ((View from, View to) in edges) { - LayoutSubview (to, from.GetContentSize ()); + // QUESTION: Do we test this with adornments well enough? + to.Layout (from.GetContentSize ()); } } - LayoutNeeded = false; + NeedsLayout = layoutStillNeeded; - OnLayoutComplete (new (contentSize)); + OnSubviewsLaidOut (new (contentSize)); + SubviewsLaidOut?.Invoke (this, new (contentSize)); } - private void LayoutSubview (View v, Size contentSize) - { - // Note, SetRelativeLayout calls SetTextFormatterSize - v.SetRelativeLayout (contentSize); - v.LayoutSubviews (); - v.LayoutNeeded = false; - } - - /// Indicates that the view does not need to be laid out. - protected void ClearLayoutNeeded () { LayoutNeeded = false; } + /// + /// Called from before any subviews + /// have been laid out. + /// + /// + /// Override to perform tasks when the layout is changing. + /// + protected virtual void OnSubviewLayout (LayoutEventArgs args) { } /// - /// Raises the event. Called from before all sub-views + /// Raised by before any subviews /// have been laid out. /// - internal virtual void OnLayoutComplete (LayoutEventArgs args) { LayoutComplete?.Invoke (this, args); } + /// + /// Subscribe to this event to perform tasks when the layout is changing. + /// + public event EventHandler? SubviewLayout; /// - /// Raises the event. Called from before any subviews + /// Called from after all sub-views /// have been laid out. /// - internal virtual void OnLayoutStarted (LayoutEventArgs args) { LayoutStarted?.Invoke (this, args); } + /// + /// Override to perform tasks after the has been resized or the layout has + /// otherwise changed. + /// + protected virtual void OnSubviewsLaidOut (LayoutEventArgs args) { } + + /// Raised after all sub-views have been laid out. + /// + /// Subscribe to this event to perform tasks after the has been resized or the layout has + /// otherwise changed. + /// + public event EventHandler? SubviewsLaidOut; + + #endregion Core Layout API + + #region NeedsLayout + + // We expose no setter for this to ensure that the ONLY place it's changed is in SetNeedsLayout + + /// + /// Indicates the View's Frame or the layout of the View's subviews (including Adornments) have + /// changed since the last time the View was laid out. + /// + /// + /// + /// Used to prevent from needlessly computing + /// layout. + /// + /// + /// + /// if layout is needed. + /// + public bool NeedsLayout { get; private set; } = true; /// - /// Called whenever the view needs to be resized. This is called whenever , - /// , , , or changes. + /// Sets to return , indicating this View and all of it's subviews + /// (including adornments) need to be laid out in the next Application iteration. /// /// /// - /// Determines the relative bounds of the and its s, and then calls - /// to update the view. + /// The will cause to be called on the next + /// so there is normally no reason to call see . /// /// - internal void OnResizeNeeded () + public void SetNeedsLayout () { - // TODO: Identify a real-world use-case where this API should be virtual. - // TODO: Until then leave it `internal` and non-virtual + NeedsLayout = true; - // Determine our container's ContentSize - - // First try SuperView.Viewport, then Application.Top, then Driver.Viewport. - // Finally, if none of those are valid, use 2048 (for Unit tests). - Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.GetContentSize () : - Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.GetContentSize () : - Application.Screen.Size; + if (Margin is { Subviews.Count: > 0 }) + { + Margin.SetNeedsLayout (); + } - SetRelativeLayout (superViewContentSize); + if (Border is { Subviews.Count: > 0 }) + { + Border.SetNeedsLayout (); + } - if (IsInitialized) + if (Padding is { Subviews.Count: > 0 }) { - LayoutAdornments (); + Padding.SetNeedsLayout (); } - SetNeedsLayout (); + // Use a stack to avoid recursion + Stack stack = new (Subviews); - // TODO: This ensures overlapped views are drawn correctly. However, this is inefficient. - // TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413 - if (Arrangement.HasFlag (ViewArrangement.Overlapped)) + while (stack.Count > 0) { - foreach (Toplevel v in Application.TopLevels) + View current = stack.Pop (); + + if (!current.NeedsLayout) { - if (v.Visible && v != this) + current.NeedsLayout = true; + + if (current.Margin is { Subviews.Count: > 0 }) + { + current.Margin.SetNeedsLayout (); + } + + if (current.Border is { Subviews.Count: > 0 }) + { + current.Border.SetNeedsLayout (); + } + + if (current.Padding is { Subviews.Count: > 0 }) { - v.SetNeedsDisplay (); + current.Padding.SetNeedsLayout (); + } + + foreach (View subview in current.Subviews) + { + stack.Push (subview); } } } - } - internal bool LayoutNeeded { get; private set; } = true; + TextFormatter.NeedsFormat = true; - /// - /// Sets for this View and all of it's subviews and it's SuperView. - /// The main loop will call SetRelativeLayout and LayoutSubviews for any view with set. - /// - internal void SetNeedsLayout () - { - if (LayoutNeeded) + if (SuperView is { NeedsLayout: false }) { - return; + SuperView?.SetNeedsLayout (); } - LayoutNeeded = true; + if (SuperView is null) + { + foreach (Toplevel tl in Application.TopLevels) + { + // tl.SetNeedsDraw (); + } + } - foreach (View view in Subviews) + if (this is not Adornment adornment) { - view.SetNeedsLayout (); + return; } - TextFormatter.NeedsFormat = true; - SuperView?.SetNeedsLayout (); + if (adornment.Parent is { NeedsLayout: false }) + { + adornment.Parent?.SetNeedsLayout (); + } } + #endregion NeedsLayout + + #region Topological Sort + /// - /// Collects all views and their dependencies from a given starting view for layout purposes. Used by + /// INTERNAL API - Collects all views and their dependencies from a given starting view for layout purposes. Used by /// to create an ordered list of views to layout. /// /// The starting view from which to collect dependencies. @@ -782,7 +819,7 @@ internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View } /// - /// Collects dimension (where Width or Height is `DimView`) dependencies for a given view. + /// INTERNAL API - Collects dimension (where Width or Height is `DimView`) dependencies for a given view. /// /// The dimension (width or height) to collect dependencies for. /// The view for which to collect dimension dependencies. @@ -793,29 +830,23 @@ internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View /// internal void CollectDim (Dim? dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { - switch (dim) + if (dim!.Has (out DimView dv)) { - case DimView dv: - // See #2461 - //if (!from.InternalSubviews.Contains (dv.Target)) { - // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}"); - //} - if (dv.Target != this) - { - nEdges.Add ((dv.Target!, from)); - } - - return; - case DimCombine dc: - CollectDim (dc.Left, from, ref nNodes, ref nEdges); - CollectDim (dc.Right, from, ref nNodes, ref nEdges); + if (dv.Target != this) + { + nEdges.Add ((dv.Target!, from)); + } + } - break; + if (dim!.Has (out DimCombine dc)) + { + CollectDim (dc.Left, from, ref nNodes, ref nEdges); + CollectDim (dc.Right, from, ref nNodes, ref nEdges); } } /// - /// Collects position (where X or Y is `PosView`) dependencies for a given view. + /// INTERNAL API - Collects position (where X or Y is `PosView`) dependencies for a given view. /// /// The position (X or Y) to collect dependencies for. /// The view for which to collect position dependencies. @@ -826,13 +857,12 @@ internal void CollectDim (Dim? dim, View from, ref HashSet nNodes, ref Has /// internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { + // TODO: Use Pos.Has instead. switch (pos) { case PosView pv: - // See #2461 - //if (!from.InternalSubviews.Contains (pv.Target)) { - // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}"); - //} + Debug.Assert (pv.Target is { }); + if (pv.Target != this) { nEdges.Add ((pv.Target!, from)); @@ -926,16 +956,16 @@ internal static List TopologicalSort ( { if (ReferenceEquals (from.SuperView, to)) { - throw new InvalidOperationException ( - $"ComputedLayout for \"{superView}\": \"{to}\" " - + $"references a SubView (\"{from}\")." - ); + throw new LayoutException ( + $"ComputedLayout for \"{superView}\": \"{to}\" " + + $"references a SubView (\"{from}\")." + ); } - throw new InvalidOperationException ( - $"ComputedLayout for \"{superView}\": \"{from}\" " - + $"linked with \"{to}\" was not found. Did you forget to add it to {superView}?" - ); + throw new LayoutException ( + $"ComputedLayout for \"{superView}\": \"{from}\" " + + $"linked with \"{to}\" was not found. Did you forget to add it to {superView}?" + ); } } @@ -943,6 +973,159 @@ internal static List TopologicalSort ( return result; } // TopologicalSort + #endregion Topological Sort + + #region Utilities + + /// + /// INTERNAL API - Gets the size of the SuperView's content (nominally the same as + /// the SuperView's ) or the screen size if there's no SuperView. + /// + /// + private Size GetContainerSize () + { + // TODO: Get rid of refs to Top + Size superViewContentSize = SuperView?.GetContentSize () + ?? (Application.Top is { } && Application.Top != this && Application.Top.IsInitialized + ? Application.Top.GetContentSize () + : Application.Screen.Size); + + return superViewContentSize; + } + + // BUGBUG: This method interferes with Dialog/MessageBox default min/max size. + // TODO: Get rid of MenuBar coupling as part of https://github.com/gui-cs/Terminal.Gui/issues/2975 + /// + /// Gets a new location of the that is within the Viewport of the 's + /// (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates. + /// + /// + /// If does not have a or it's SuperView is not + /// the position will be bound by . + /// + /// The View that is to be moved. + /// The target x location. + /// The target y location. + /// The new x location that will ensure will be fully visible. + /// The new y location that will ensure will be fully visible. + /// + /// Either (if does not have a Super View) or + /// 's SuperView. This can be used to ensure LayoutSubviews is called on the correct View. + /// + internal static View? GetLocationEnsuringFullVisibility ( + View viewToMove, + int targetX, + int targetY, + out int nx, + out int ny + ) + { + int maxDimension; + View? superView; + + if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + { + maxDimension = Application.Screen.Width; + superView = Application.Top; + } + else + { + // Use the SuperView's Viewport, not Frame + maxDimension = viewToMove!.SuperView.Viewport.Width; + superView = viewToMove.SuperView; + } + + if (superView?.Margin is { } && superView == viewToMove!.SuperView) + { + maxDimension -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; + } + + if (viewToMove!.Frame.Width <= maxDimension) + { + nx = Math.Max (targetX, 0); + nx = nx + viewToMove.Frame.Width > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Width, 0) : nx; + + if (nx > viewToMove.Frame.X + viewToMove.Frame.Width) + { + nx = Math.Max (viewToMove.Frame.Right, 0); + } + } + else + { + nx = targetX; + } + + //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); + var menuVisible = false; + var statusVisible = false; + + if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + { + menuVisible = Application.Top?.MenuBar?.Visible == true; + } + else + { + View? t = viewToMove!.SuperView; + + while (t is { } and not Toplevel) + { + t = t.SuperView; + } + + if (t is Toplevel topLevel) + { + menuVisible = topLevel.MenuBar?.Visible == true; + } + } + + if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + { + maxDimension = menuVisible ? 1 : 0; + } + else + { + maxDimension = 0; + } + + ny = Math.Max (targetY, maxDimension); + + if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + { + maxDimension = statusVisible ? Application.Screen.Height - 1 : Application.Screen.Height; + } + else + { + maxDimension = statusVisible ? viewToMove!.SuperView.Viewport.Height - 1 : viewToMove!.SuperView.Viewport.Height; + } + + if (superView?.Margin is { } && superView == viewToMove?.SuperView) + { + maxDimension -= superView.GetAdornmentsThickness ().Top + superView.GetAdornmentsThickness ().Bottom; + } + + ny = Math.Min (ny, maxDimension); + + if (viewToMove?.Frame.Height <= maxDimension) + { + ny = ny + viewToMove.Frame.Height > maxDimension + ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0) + : ny; + + if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) + { + ny = Math.Max (viewToMove.Frame.Bottom, 0); + } + } + + //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); + + return superView!; + } + + #endregion Utilities + + #region Diagnostics and Verification + // Diagnostics to highlight when X or Y is read before the view has been initialized private Pos VerifyIsInitialized (Pos pos, string member) { @@ -1058,13 +1241,13 @@ void ThrowInvalid (View view, object? checkPosDim, string name) if (bad != null) { - throw new InvalidOperationException ( - $"{view.GetType ().Name}.{name} = {bad.GetType ().Name} " - + $"which depends on the SuperView's dimensions and the SuperView uses Dim.Auto." - ); + throw new LayoutException ( + $"{view.GetType ().Name}.{name} = {bad.GetType ().Name} " + + $"which depends on the SuperView's dimensions and the SuperView uses Dim.Auto." + ); } } } - #endregion Layout Engine + #endregion Diagnostics and Verification } diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index a76de77b34..4dbb65e38c 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -664,17 +664,20 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) Adornment? found = null; - if (start.Margin.Contains (currentLocation)) + if (start is not Adornment) { - found = start.Margin; - } - else if (start.Border.Contains (currentLocation)) - { - found = start.Border; - } - else if (start.Padding.Contains (currentLocation)) - { - found = start.Padding; + if (start.Margin is {} && start.Margin.Contains (currentLocation)) + { + found = start.Margin; + } + else if (start.Border is {} && start.Border.Contains (currentLocation)) + { + found = start.Border; + } + else if (start.Padding is { } && start.Padding.Contains(currentLocation)) + { + found = start.Padding; + } } Point viewportOffset = start.GetViewportOffsetFromFrame (); diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index e68b3c7d31..cbd2963c44 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -594,7 +594,7 @@ public bool SetFocus () // Focus work is done. Notify. RaiseFocusChanged (HasFocus, currentFocusedView, this); - SetNeedsDisplay (); + SetNeedsDraw (); // Post-conditions - prove correctness if (HasFocus == previousValue) @@ -847,21 +847,21 @@ private void SetHasFocusFalse (View? newFocusedView, bool traversingDown = false throw new InvalidOperationException ("SetHasFocusFalse and the HasFocus value did not change."); } - SetNeedsDisplay (); + SetNeedsDraw (); } - private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) + private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - if (newHasFocus && focusedVew?.Focused is null) + if (newHasFocus && focusedView?.Focused is null) { - Application.Navigation?.SetFocused (focusedVew); + Application.Navigation?.SetFocused (focusedView); } // Call the virtual method - OnHasFocusChanged (newHasFocus, previousFocusedView, focusedVew); + OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); // Raise the event - var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedVew); + var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedView); HasFocusChanged?.Invoke (this, args); } @@ -876,8 +876,8 @@ private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, Vie /// /// The new value of . /// - /// The view that is now focused. May be - protected virtual void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { } + /// The view that is now focused. May be + protected virtual void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { } /// Raised after has changed. /// diff --git a/Terminal.Gui/View/View.Text.cs b/Terminal.Gui/View/View.Text.cs index 508637b567..971a8ac83a 100644 --- a/Terminal.Gui/View/View.Text.cs +++ b/Terminal.Gui/View/View.Text.cs @@ -4,21 +4,20 @@ namespace Terminal.Gui; public partial class View // Text Property APIs { - private string _text = null!; + private string _text = string.Empty; /// /// Called when the has changed. Fires the event. /// public void OnTextChanged () { TextChanged?.Invoke (this, EventArgs.Empty); } - // TODO: Make this non-virtual. Nobody overrides it. /// /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved /// or not when is enabled. /// If trailing spaces at the end of wrapped lines will be removed when /// is formatted for display. The default is . /// - public virtual bool PreserveTrailingSpaces + public bool PreserveTrailingSpaces { get => TextFormatter.PreserveTrailingSpaces; set @@ -27,6 +26,7 @@ public virtual bool PreserveTrailingSpaces { TextFormatter.PreserveTrailingSpaces = value; TextFormatter.NeedsFormat = true; + SetNeedsLayout (); } } } @@ -58,11 +58,16 @@ public virtual string Text get => _text; set { + if (_text == value) + { + return; + } + string old = _text; _text = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); #if DEBUG if (_text is { } && string.IsNullOrEmpty (Id)) { @@ -92,7 +97,7 @@ public virtual Alignment TextAlignment { TextFormatter.Alignment = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); } } @@ -143,7 +148,7 @@ public virtual Alignment VerticalTextAlignment set { TextFormatter.VerticalAlignment = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -229,9 +234,9 @@ private void UpdateTextDirection (TextDirection newDirection) { TextFormatter.ConstrainToWidth = null; TextFormatter.ConstrainToHeight = null; - OnResizeNeeded (); + SetNeedsLayout (); } - SetNeedsDisplay (); + SetNeedsDraw (); } } diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 706409c5b0..00e6df6ad4 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -69,12 +69,14 @@ namespace Terminal.Gui; /// /// /// To flag a region of the View's to be redrawn call -/// +/// /// . -/// To flag the entire view for redraw call . +/// To flag the entire view for redraw call . /// /// -/// The method is invoked when the size or layout of a view has changed. +/// The method is called when the size or layout of a view has changed. The will +/// cause to be called on the next so there is normally no reason to direclty call +/// see . /// /// /// Views have a property that defines the default colors that subviews should use for @@ -122,7 +124,7 @@ public partial class View : Responder, ISupportInitializeNotification /// Points to the current driver in use by the view, it is a convenience property for simplifying the development /// of new views. /// - public static ConsoleDriver Driver => Application.Driver!; + public static ConsoleDriver? Driver => Application.Driver; /// Initializes a new instance of . /// @@ -142,7 +144,6 @@ public View () //SetupMouse (); SetupText (); - } /// @@ -229,7 +230,6 @@ public virtual void EndInit () // These calls were moved from BeginInit as they access Viewport which is indeterminate until EndInit is called. UpdateTextDirection (TextDirection); UpdateTextFormatterText (); - OnResizeNeeded (); if (_subviews is { }) { @@ -242,6 +242,10 @@ public virtual void EndInit () } } + // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop + Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)). + SetNeedsLayout (); + Initialized?.Invoke (this, EventArgs.Empty); } @@ -251,9 +255,6 @@ public virtual void EndInit () private bool _enabled = true; - // This is a cache of the Enabled property so that we can restore it when the superview is re-enabled. - private bool _oldEnabled; - /// Gets or sets a value indicating whether this can respond to user interaction. public bool Enabled { @@ -282,7 +283,12 @@ public bool Enabled } OnEnabledChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); + + if (Border is { }) + { + Border.Enabled = _enabled; + } if (_subviews is null) { @@ -291,18 +297,7 @@ public bool Enabled foreach (View view in _subviews) { - if (!_enabled) - { - view._oldEnabled = view.Enabled; - view.Enabled = _enabled; - } - else - { - view.Enabled = view._oldEnabled; -#if AUTO_CANFOCUS - view._addingViewSoCanFocusAlsoUpdatesSuperView = _enabled; -#endif - } + view.Enabled = Enabled; } } } @@ -363,7 +358,10 @@ public virtual bool Visible OnVisibleChanged (); VisibleChanged?.Invoke (this, EventArgs.Empty); - SetNeedsDisplay (); + SetNeedsLayout (); + SuperView?.SetNeedsLayout(); + SetNeedsDraw (); + SuperView?.SetNeedsDraw(); } } @@ -469,7 +467,7 @@ public string Title SetTitleTextFormatterSize (); SetHotKeyFromTitle (); - SetNeedsDisplay (); + SetNeedsDraw (); #if DEBUG if (string.IsNullOrEmpty (Id)) { diff --git a/Terminal.Gui/View/ViewportSettings.cs b/Terminal.Gui/View/ViewportSettings.cs index f023d3f756..f7e8488c12 100644 --- a/Terminal.Gui/View/ViewportSettings.cs +++ b/Terminal.Gui/View/ViewportSettings.cs @@ -94,7 +94,7 @@ public enum ViewportSettings ClipContentOnly = 16, /// - /// If set will clear only the portion of the content + /// If set will clear only the portion of the content /// area that is visible within the . This is useful for views that have a /// content area larger than the Viewport and want the area outside the content to be visually distinct. /// diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 1511cd68af..bece840786 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -31,7 +31,7 @@ public Bar (IEnumerable shortcuts) _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); - Initialized += Bar_Initialized; + // Initialized += Bar_Initialized; MouseEvent += OnMouseEvent; if (shortcuts is null) @@ -77,17 +77,21 @@ private void OnMouseEvent (object? sender, MouseEventArgs e) } } - private void Bar_Initialized (object? sender, EventArgs e) + /// + public override void EndInit () { + base.EndInit (); ColorScheme = Colors.ColorSchemes ["Menu"]; - LayoutBarItems (GetContentSize ()); } /// public override void SetBorderStyle (LineStyle value) { - // The default changes the thickness. We don't want that. We just set the style. - Border.LineStyle = value; + if (Border is { }) + { + // The default changes the thickness. We don't want that. We just set the style. + Border.LineStyle = value; + } } #region IOrientation members @@ -119,7 +123,8 @@ public Orientation Orientation /// public void OnOrientationChanged (Orientation newOrientation) { - SetNeedsLayout (); + // BUGBUG: this should not be SuperView.GetContentSize + LayoutBarItems (SuperView?.GetContentSize () ?? Application.Screen.Size); } #endregion @@ -135,6 +140,7 @@ public AlignmentModes AlignmentModes set { _alignmentModes = value; + //SetNeedsDraw (); SetNeedsLayout (); } } @@ -162,7 +168,8 @@ public void AddShortcutAt (int index, Shortcut item) } } - SetNeedsDisplay (); + //SetNeedsDraw (); + SetNeedsLayout (); } // TODO: Move this to View @@ -185,17 +192,16 @@ public void AddShortcutAt (int index, Shortcut item) if (toRemove is { }) { Remove (toRemove); - SetNeedsDisplay (); + //SetNeedsDraw (); + SetNeedsLayout (); } return toRemove as Shortcut; } /// - internal override void OnLayoutStarted (LayoutEventArgs args) + protected override void OnSubviewLayout (LayoutEventArgs args) { - base.OnLayoutStarted (args); - LayoutBarItems (args.OldContentSize); } @@ -217,62 +223,72 @@ private void LayoutBarItems (Size contentSize) break; case Orientation.Vertical: - // Set the overall size of the Bar and arrange the views vertically - - var minKeyWidth = 0; - - List shortcuts = Subviews.Where (s => s is Shortcut && s.Visible).Cast ().ToList (); - foreach (Shortcut shortcut in shortcuts) + if (Width!.Has (out _)) { - // Let DimAuto do its thing to get the minimum width of each CommandView and HelpView - //shortcut.CommandView.SetRelativeLayout (new Size (int.MaxValue, int.MaxValue)); - minKeyWidth = int.Max (minKeyWidth, shortcut.KeyView.Text.GetColumns ()); - } + // Set the overall size of the Bar and arrange the views vertically - var maxBarItemWidth = 0; - var totalHeight = 0; - - for (var index = 0; index < Subviews.Count; index++) - { - View barItem = Subviews [index]; + var minKeyWidth = 0; - barItem.ColorScheme = ColorScheme; + List shortcuts = Subviews.Where (s => s is Shortcut && s.Visible).Cast ().ToList (); - if (!barItem.Visible) + foreach (Shortcut shortcut in shortcuts) { - continue; + // Get the largest width of all KeyView's + minKeyWidth = int.Max (minKeyWidth, shortcut.KeyView.Text.GetColumns ()); } - if (barItem is Shortcut scBarItem) - { - scBarItem.MinimumKeyTextSize = minKeyWidth; - maxBarItemWidth = Math.Max (maxBarItemWidth, scBarItem.Frame.Width); - } + var _maxBarItemWidth = 0; - if (prevBarItem == null) + for (var index = 0; index < Subviews.Count; index++) { - barItem.Y = 0; + View barItem = Subviews [index]; + + barItem.X = 0; + + barItem.ColorScheme = ColorScheme; + + if (!barItem.Visible) + { + continue; + } + + if (barItem is Shortcut scBarItem) + { + scBarItem.MinimumKeyTextSize = minKeyWidth; + scBarItem.Width = scBarItem.GetWidthDimAuto (); + barItem.Layout (Application.Screen.Size); + _maxBarItemWidth = Math.Max (_maxBarItemWidth, barItem.Frame.Width); + } + + if (prevBarItem == null) + { + // TODO: Just use Pos.Align! + barItem.Y = 0; + } + else + { + // TODO: Just use Pos.Align! + // Align the view to the bottom of the previous view + barItem.Y = Pos.Bottom (prevBarItem); + } + + prevBarItem = barItem; + } - else + + foreach (var subView in Subviews) { - // Align the view to the bottom of the previous view - barItem.Y = Pos.Bottom (prevBarItem); + subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: _maxBarItemWidth); } - - prevBarItem = barItem; - - barItem.X = 0; - totalHeight += barItem.Frame.Height; } - - - foreach (View barItem in Subviews) + else { - barItem.Width = maxBarItemWidth; + foreach (var subView in Subviews) + { + subView.Width = Dim.Fill(); + } } - Height = Dim.Auto (DimAutoStyle.Content, totalHeight); - break; } } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index f642f23d9c..eaf2d3956e 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -191,7 +191,7 @@ public bool IsDefault _isDefault = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); } } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index af58ecedd9..04156637e1 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -153,7 +153,7 @@ public CheckState CheckedState _checkedState = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); EventArgs args = new (in _checkedState); OnCheckedStateChanged (args); diff --git a/Terminal.Gui/Views/ColorBar.cs b/Terminal.Gui/Views/ColorBar.cs index 581606d175..824b499584 100644 --- a/Terminal.Gui/Views/ColorBar.cs +++ b/Terminal.Gui/Views/ColorBar.cs @@ -78,30 +78,30 @@ public int Value void IColorBar.SetValueWithoutRaisingEvent (int v) { _value = v; - SetNeedsDisplay (); + SetNeedsDraw (); } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - var xOffset = 0; if (!string.IsNullOrWhiteSpace (Text)) { Move (0, 0); - Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); - Driver.AddStr (Text); + SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + Driver?.AddStr (Text); // TODO: is there a better method than this? this is what it is in TableView xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ()); } - _barWidth = viewport.Width - xOffset; + _barWidth = Viewport.Width - xOffset; _barStartsAt = xOffset; DrawBar (xOffset, 0, _barWidth); + + return true; } /// @@ -198,7 +198,7 @@ private void DrawBar (int xOffset, int yOffset, int width) if (isSelectedCell) { // Draw the triangle at the closest position - Application.Driver?.SetAttribute (new (triangleColor, color)); + SetAttribute (new (triangleColor, color)); AddRune (x + xOffset, yOffset, new ('▲')); // Record for tests @@ -206,7 +206,7 @@ private void DrawBar (int xOffset, int yOffset, int width) } else { - Application.Driver?.SetAttribute (new (color, color)); + SetAttribute (new (color, color)); AddRune (x + xOffset, yOffset, new ('█')); } } @@ -215,7 +215,7 @@ private void DrawBar (int xOffset, int yOffset, int width) private void OnValueChanged () { ValueChanged?.Invoke (this, new (in _value)); - SetNeedsDisplay (); + SetNeedsDraw (); } private bool? SetMax () diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index c1cb72cc23..39a02f351e 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -67,8 +67,12 @@ public Point Cursor /// Moves the selected item index to the next row. /// - public virtual bool MoveDown () + private bool MoveDown (CommandContext ctx) { + if (RaiseSelecting (ctx) == true) + { + return true; + } if (Cursor.Y < _rows - 1) { SelectedColor += _cols; @@ -79,8 +83,13 @@ public virtual bool MoveDown () /// Moves the selected item index to the previous column. /// - public virtual bool MoveLeft () + private bool MoveLeft (CommandContext ctx) { + if (RaiseSelecting (ctx) == true) + { + return true; + } + if (Cursor.X > 0) { SelectedColor--; @@ -91,8 +100,12 @@ public virtual bool MoveLeft () /// Moves the selected item index to the next column. /// - public virtual bool MoveRight () + private bool MoveRight (CommandContext ctx) { + if (RaiseSelecting (ctx) == true) + { + return true; + } if (Cursor.X < _cols - 1) { SelectedColor++; @@ -103,8 +116,12 @@ public virtual bool MoveRight () /// Moves the selected item index to the previous row. /// - public virtual bool MoveUp () + private bool MoveUp (CommandContext ctx) { + if (RaiseSelecting (ctx) == true) + { + return true; + } if (Cursor.Y > 0) { SelectedColor -= _cols; @@ -114,16 +131,14 @@ public virtual bool MoveUp () } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - - Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); + SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); var colorIndex = 0; - for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++) + for (var y = 0; y < Math.Max (2, Viewport.Height / BoxHeight); y++) { - for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++) + for (var x = 0; x < Math.Max (8, Viewport.Width / BoxWidth); x++) { int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; @@ -132,12 +147,14 @@ public override void OnDrawContent (Rectangle viewport) continue; } - Driver.SetAttribute (new ((ColorName16)foregroundColorIndex, (ColorName16)colorIndex)); + SetAttribute (new ((ColorName16)foregroundColorIndex, (ColorName16)colorIndex)); bool selected = x == Cursor.X && y == Cursor.Y; DrawColorBox (x, y, selected); colorIndex++; } } + + return true; } /// Selected color. @@ -157,20 +174,31 @@ public ColorName16 SelectedColor this, new (value) ); - SetNeedsDisplay (); + SetNeedsDraw (); } } /// Add the commands. private void AddCommands () { - AddCommand (Command.Left, () => MoveLeft ()); - AddCommand (Command.Right, () => MoveRight ()); - AddCommand (Command.Up, () => MoveUp ()); - AddCommand (Command.Down, () => MoveDown ()); + AddCommand (Command.Left, (ctx) => MoveLeft (ctx)); + AddCommand (Command.Right, (ctx) => MoveRight (ctx)); + AddCommand (Command.Up, (ctx) => MoveUp (ctx)); + AddCommand (Command.Down, (ctx) => MoveDown (ctx)); + + AddCommand (Command.Select, (ctx) => + { + bool set = false; + if (ctx.Data is MouseEventArgs me) + { + Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight); + set = true; + } + return RaiseAccepting (ctx) == true || set; + }); } - /// Add the KeyBindinds. + /// Add the KeyBindings. private void AddKeyBindings () { KeyBindings.Add (Key.CursorLeft, Command.Left); @@ -181,15 +209,6 @@ private void AddKeyBindings () // TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor. - private void ColorPicker_MouseClick (object sender, MouseEventArgs me) - { - // if (CanFocus) - { - Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight); - SetFocus (); - me.Handled = true; - } - } /// Draw a box for one color. /// X location. @@ -203,8 +222,7 @@ private void DrawColorBox (int x, int y, bool selected) { for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++) { - Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY); - Driver.AddRune ((Rune)' '); + AddRune (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY, (Rune)' '); index++; } } @@ -265,7 +283,5 @@ private void SetInitialProperties () Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); - - MouseClick += ColorPicker_MouseClick; } } diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index a4209ffe42..415fc1acd1 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -90,10 +90,7 @@ public void ApplyStyleChanges () CreateTextField (); SelectedColor = oldValue; - if (IsInitialized) - { - LayoutSubviews (); - } + SetNeedsLayout (); } /// @@ -102,13 +99,14 @@ public void ApplyStyleChanges () public event EventHandler? ColorChanged; /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); Attribute normal = GetNormalColor (); - Driver.SetAttribute (new (SelectedColor, normal.Background)); + SetAttribute (new (SelectedColor, normal.Background)); int y = _bars.Count + (Style.ShowColorName ? 1 : 0); AddRune (13, y, (Rune)'■'); + + return true; } /// @@ -275,6 +273,8 @@ private void SyncSubViewValues (bool syncBars) { _tfHex.Text = colorHex; } + + SetNeedsLayout (); } private void UpdateSingleBarValueFromTextField (object? sender, HasFocusEventArgs e) diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 996056068c..abfee0fde5 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -57,7 +57,7 @@ public ComboBox () Initialized += (s, e) => ProcessLayout (); // On resize - LayoutComplete += (sender, a) => ProcessLayout (); + SubviewsLaidOut += (sender, a) => ProcessLayout (); Added += (s, e) => { @@ -73,7 +73,7 @@ public ComboBox () } SetNeedsLayout (); - SetNeedsDisplay (); + SetNeedsDraw (); ShowHideList (Text); }; @@ -118,7 +118,7 @@ public ComboBox () { _listview.ColorScheme = value; base.ColorScheme = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -198,7 +198,7 @@ public IListDataSource Source if (SuperView is { } && SuperView.Subviews.Contains (this)) { Text = string.Empty; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -294,18 +294,21 @@ protected override bool OnMouseEvent (MouseEventArgs me) public virtual void OnCollapsed () { Collapsed?.Invoke (this, EventArgs.Empty); } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); if (!_autoHide) { - return; + return true; + } + + if (ColorScheme != null) + { + SetAttribute (ColorScheme.Focus); } + AddRune (Viewport.Right - 1, 0, Glyphs.DownArrow); - Driver.SetAttribute (ColorScheme.Focus); - Move (Viewport.Right - 1, 0); - Driver.AddRune (Glyphs.DownArrow); + return true; } @@ -504,11 +507,13 @@ private void HideList () } Reset (true); - _listview.Clear (); + _listview.ClearViewport (); _listview.TabStop = TabBehavior.NoStop; SuperView?.MoveSubviewToStart (this); + + // BUGBUG: SetNeedsDraw takes Viewport relative coordinates, not Screen Rectangle rect = _listview.ViewportToScreen (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty); - SuperView?.SetNeedsDisplay (rect); + SuperView?.SetNeedsDraw (rect); OnCollapsed (); } @@ -803,7 +808,7 @@ private void ShowList () _listview.SetSource (_searchSet); _listview.ResumeSuspendCollectionChangedEvent (); - _listview.Clear (); + _listview.ClearViewport (); _listview.Height = CalculateHeight (); SuperView?.MoveSubviewToStart (this); } @@ -872,7 +877,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (isMousePositionValid) { _highlighted = Math.Min (TopItem + me.Position.Y, Source.Count); - SetNeedsDisplay (); + SetNeedsDraw (); } _isFocusing = false; @@ -883,10 +888,10 @@ protected override bool OnMouseEvent (MouseEventArgs me) return res; } - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - Attribute current = ColorScheme.Focus; - Driver.SetAttribute (current); + Attribute current = ColorScheme?.Focus ?? Attribute.Default; + SetAttribute (current); Move (0, 0); Rectangle f = Frame; int item = TopItem; @@ -916,7 +921,7 @@ public override void OnDrawContent (Rectangle viewport) if (newcolor != current) { - Driver.SetAttribute (newcolor); + SetAttribute (newcolor); current = newcolor; } @@ -926,7 +931,7 @@ public override void OnDrawContent (Rectangle viewport) { for (var c = 0; c < f.Width; c++) { - Driver.AddRune ((Rune)' '); + AddRune (0, row, (Rune)' '); } } else @@ -937,24 +942,26 @@ public override void OnDrawContent (Rectangle viewport) if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute) { current = (Attribute)rowEventArgs.RowAttribute; - Driver.SetAttribute (current); + SetAttribute (current); } if (AllowsMarking) { - Driver.AddRune ( + AddRune ( Source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected : AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected ); - Driver.AddRune ((Rune)' '); + AddRune ((Rune)' '); } - Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start); + Source.Render (this, isSelected, item, col, row, f.Width - col, start); } } + + return true; } - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView) { if (newHasFocus) { diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index aca0c086ee..2962464ec0 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -200,8 +200,9 @@ private void SetInitialProperties (DateTime date) ShowHeaders = true, ShowHorizontalBottomline = true, ShowVerticalCellLines = true, - ExpandLastColumn = true - } + ExpandLastColumn = true, + }, + MultiSelect = false }; _dateField = new DateField (DateTime.Now) @@ -286,6 +287,9 @@ private void SetInitialProperties (DateTime date) Add (_dateLabel, _dateField, _calendar, _previousMonthButton, _nextMonthButton); } + /// + protected override bool OnDrawingText () { return true; } + private static string StandardizeDateFormat (string format) { return format switch diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 44cf5a168f..259638baf2 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -89,13 +89,13 @@ public Dialog () /// public override Attribute GetNormalColor () { - return ColorScheme.Normal; + return ColorScheme!.Normal; } /// public override Attribute GetFocusColor () { - return ColorScheme.Normal; + return ColorScheme!.Normal; } private bool _canceled; diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 2ded19418f..b80dcacabe 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -8,7 +8,7 @@ namespace Terminal.Gui; /// Modal dialog for selecting files/directories. Has auto-complete and expandable navigation pane (Recent, Root /// drives etc). /// -public class FileDialog : Dialog +public class FileDialog : Dialog, IDesignable { private const int alignmentGroupInput = 32; private const int alignmentGroupComplete = 55; @@ -78,20 +78,34 @@ internal FileDialog (IFileSystem fileSystem) Y = Pos.AnchorEnd (), IsDefault = true, Text = Style.OkButtonText }; - _btnOk.Accepting += (s, e) => Accept (true); + _btnOk.Accepting += (s, e) => + { + if (e.Cancel) + { + return; + } + + Accept (true); + }; _btnCancel = new Button { X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, alignmentGroupComplete), - Y = Pos.AnchorEnd(), + Y = Pos.AnchorEnd (), Text = Strings.btnCancel }; _btnCancel.Accepting += (s, e) => { - Canceled = true; - Application.RequestStop (); + if (e.Cancel) + { + return; + } + if (Modal) + { + Application.RequestStop (); + } }; _btnUp = new Button { X = 0, Y = 1, NoPadding = true }; @@ -163,7 +177,7 @@ internal FileDialog (IFileSystem fileSystem) ColumnStyle typeStyle = Style.TableStyle.GetOrCreateColumnStyle (3); typeStyle.MinWidth = 6; typeStyle.ColorGetter = ColorGetter; - + _treeView = new TreeView { Width = Dim.Fill (), Height = Dim.Fill () }; var fileDialogTreeBuilder = new FileSystemTreeBuilder (); @@ -189,12 +203,12 @@ internal FileDialog (IFileSystem fileSystem) bool newState = !tile.ContentView.Visible; tile.ContentView.Visible = newState; _btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState); - LayoutSubviews (); + SetNeedsLayout (); }; _tbFind = new TextField { - X = Pos.Align (Alignment.Start,AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), + X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), CaptionColor = new Color (Color.Black), Width = 30, Y = Pos.Top (_btnToggleSplitterCollapse), @@ -240,7 +254,7 @@ internal FileDialog (IFileSystem fileSystem) _tableView.KeyBindings.ReplaceCommands (Key.End, Command.End); _tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.StartExtend); _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.EndExtend); - + AllowsMultipleSelection = false; UpdateNavigationVisibility (); @@ -254,8 +268,8 @@ internal FileDialog (IFileSystem fileSystem) Add (_tbFind); Add (_spinnerView); - Add(_btnOk); - Add(_btnCancel); + Add (_btnOk); + Add (_btnCancel); } /// @@ -368,10 +382,8 @@ public bool IsCompatibleWithAllowedExtensions (IFileInfo file) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - if (!string.IsNullOrWhiteSpace (_feedback)) { int feedbackWidth = _feedback.EnumerateRunes ().Sum (c => c.GetColumns ()); @@ -386,11 +398,13 @@ public override void OnDrawContent (Rectangle viewport) Move (0, Viewport.Height / 2); - Driver.SetAttribute (new Attribute (Color.Red, ColorScheme.Normal.Background)); + SetAttribute (new Attribute (Color.Red, ColorScheme.Normal.Background)); Driver.AddStr (new string (' ', feedbackPadLeft)); Driver.AddStr (_feedback); Driver.AddStr (new string (' ', feedbackPadRight)); } + + return true; } /// @@ -461,7 +475,7 @@ public override void OnLoaded () AllowedTypeMenuClicked (0); // TODO: Using v1's menu bar here is a hack. Need to upgrade this. - _allowedTypeMenuBar.DrawContentComplete += (s, e) => + _allowedTypeMenuBar.DrawingContent += (s, e) => { _allowedTypeMenuBar.Move (e.NewViewport.Width - 1, 0); Driver.AddRune (Glyphs.DownArrow); @@ -493,7 +507,9 @@ public override void OnLoaded () _btnOk.X = Pos.Right (_btnCancel) + 1; MoveSubviewTowardsStart (_btnCancel); } - LayoutSubviews (); + + SetNeedsDraw (); + SetNeedsLayout (); } /// @@ -634,7 +650,7 @@ private void Accept (IFileInfo f) if (!IsCompatibleWithOpenMode (f.FullName, out string reason)) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -661,7 +677,7 @@ private void Accept (bool allowMulti) if (reason is { }) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); } return; @@ -827,7 +843,11 @@ private void FinishAccept () } Canceled = false; - Application.RequestStop (); + + if (Modal) + { + Application.RequestStop (); + } } private string GetBackButtonText () { return Glyphs.LeftArrow + "-"; } @@ -1115,7 +1135,7 @@ private void PushState ( _tableView.RowOffset = 0; _tableView.SelectedRow = 0; - SetNeedsDisplay (); + SetNeedsDraw (); UpdateNavigationVisibility (); } finally @@ -1400,7 +1420,7 @@ out reason if (reason is { }) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); } return false; @@ -1588,9 +1608,16 @@ private void UpdateChildrenToFound () Parent.WriteStateToTableView (); Parent._spinnerView.Visible = true; - Parent._spinnerView.SetNeedsDisplay (); + Parent._spinnerView.SetNeedsDraw (); } ); } } + + bool IDesignable.EnableForDesign () + { + Modal = false; + OnLoaded (); + return true; + } } diff --git a/Terminal.Gui/Views/GraphView/Annotations.cs b/Terminal.Gui/Views/GraphView/Annotations.cs index 7dbc8836e8..02b67c974b 100644 --- a/Terminal.Gui/Views/GraphView/Annotations.cs +++ b/Terminal.Gui/Views/GraphView/Annotations.cs @@ -127,6 +127,17 @@ public LegendAnnotation (Rectangle legendBounds) /// Returns false i.e. Legends render after series public bool BeforeSeries => false; + // BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent + // BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing. + /// + protected override bool OnDrawingText () { return true; } + + // BUGBUG: Legend annotations are subviews. But for some reason the are rendered directly in OnDrawContent + // BUGBUG: instead of just being normal subviews. They get rendered as blank rects and thus we disable subview drawing. + /// + protected override bool OnClearingViewport () { return true; } + + /// Draws the Legend and all entries into the area within /// public void Render (GraphView graph) @@ -139,8 +150,8 @@ public void Render (GraphView graph) if (BorderStyle != LineStyle.None) { - OnDrawAdornments (); - OnRenderLineCanvas (); + DrawBorderAndPadding (); + RenderLineCanvas (); } var linesDrawn = 0; @@ -149,7 +160,7 @@ public void Render (GraphView graph) { if (entry.Item1.Color.HasValue) { - Application.Driver?.SetAttribute (entry.Item1.Color.Value); + SetAttribute (entry.Item1.Color.Value); } else { @@ -206,7 +217,7 @@ public class PathAnnotation : IAnnotation /// public void Render (GraphView graph) { - View.Driver.SetAttribute (LineColor ?? graph.ColorScheme.Normal); + graph.SetAttribute (LineColor ?? graph.ColorScheme.Normal); foreach (LineF line in PointsToLines ()) { diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index d810957dd5..93dcc1992b 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; /// View for rendering graphs (bar, scatter, etc...). -public class GraphView : View +public class GraphView : View, IDesignable { /// Creates a new graph with a 1 to 1 graph space with absolute layout. public GraphView () @@ -197,7 +197,7 @@ public Point GraphSpaceToScreen (PointF location) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (CellSize.X == 0 || CellSize.Y == 0) { @@ -212,13 +212,13 @@ public override void OnDrawContent (Rectangle viewport) for (var i = 0; i < Viewport.Height; i++) { Move (0, i); - Driver.AddStr (new string (' ', Viewport.Width)); + Driver?.AddStr (new string (' ', Viewport.Width)); } // If there is no data do not display a graph if (!Series.Any () && !Annotations.Any ()) { - return; + return true; } // The drawable area of the graph (anything that isn't in the margins) @@ -228,7 +228,7 @@ public override void OnDrawContent (Rectangle viewport) // if the margins take up the full draw bounds don't render if (graphScreenWidth < 0 || graphScreenHeight < 0) { - return; + return true; } // Draw 'before' annotations @@ -275,6 +275,7 @@ public override void OnDrawContent (Rectangle viewport) { a.Render (this); } + return true; } /// Scrolls the graph down 1 page. @@ -296,7 +297,7 @@ public void Reset () Series.Clear (); Annotations.Clear (); GraphColor = null; - SetNeedsDisplay (); + SetNeedsDraw (); } /// Returns the section of the graph that is represented by the given screen position. @@ -337,12 +338,56 @@ public void Scroll (float offsetX, float offsetY) ScrollOffset.Y + offsetY ); - SetNeedsDisplay (); + SetNeedsDraw (); } /// /// Sets the color attribute of to the (if defined) or /// otherwise. /// - public void SetDriverColorToGraphColor () { Driver.SetAttribute (GraphColor ?? GetNormalColor ()); } + public void SetDriverColorToGraphColor () { SetAttribute (GraphColor ?? GetNormalColor ()); } + + bool IDesignable.EnableForDesign () + { + Title = "Sine Wave"; + + var points = new ScatterSeries (); + var line = new PathAnnotation (); + + // Draw line first so it does not draw over top of points or axis labels + line.BeforeSeries = true; + + // Generate line graph with 2,000 points + for (float x = -500; x < 500; x += 0.5f) + { + points.Points.Add (new (x, (float)Math.Sin (x))); + line.Points.Add (new (x, (float)Math.Sin (x))); + } + + Series.Add (points); + Annotations.Add (line); + + // How much graph space each cell of the console depicts + CellSize = new (0.1f, 0.1f); + + // leave space for axis labels + MarginBottom = 2; + MarginLeft = 3; + + // One axis tick/label per + AxisX.Increment = 0.5f; + AxisX.ShowLabelsEvery = 2; + AxisX.Text = "X →"; + AxisX.LabelGetter = v => v.Value.ToString ("N2"); + + AxisY.Increment = 0.2f; + AxisY.ShowLabelsEvery = 2; + AxisY.Text = "↑Y"; + AxisY.LabelGetter = v => v.Value.ToString ("N2"); + + ScrollOffset = new (-2.5f, -1); + + return true; + } + } diff --git a/Terminal.Gui/Views/GraphView/Series.cs b/Terminal.Gui/Views/GraphView/Series.cs index f7c02e1749..194bfeac22 100644 --- a/Terminal.Gui/Views/GraphView/Series.cs +++ b/Terminal.Gui/Views/GraphView/Series.cs @@ -33,7 +33,7 @@ public void DrawSeries (GraphView graph, Rectangle drawBounds, RectangleF graphB { if (Fill.Color.HasValue) { - Application.Driver?.SetAttribute (Fill.Color.Value); + graph.SetAttribute (Fill.Color.Value); } foreach (PointF p in Points.Where (p => graphBounds.Contains (p))) @@ -261,7 +261,7 @@ protected virtual void DrawBarLine (GraphView graph, Point start, Point end, Bar if (adjusted.Color.HasValue) { - Application.Driver?.SetAttribute (adjusted.Color.Value); + graph.SetAttribute (adjusted.Color.Value); } graph.DrawLine (start, end, adjusted.Rune); diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 8c614062b0..cc0ba4df56 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -104,7 +104,7 @@ public HexView (Stream? source) KeyBindings.Remove (Key.Space); KeyBindings.Remove (Key.Enter); - LayoutComplete += HexView_LayoutComplete; + SubviewsLaidOut += HexView_LayoutComplete; } /// Initializes a class. @@ -198,7 +198,8 @@ public Stream? Source Address = 0; } - SetNeedsDisplay (); + SetNeedsLayout (); + SetNeedsDraw (); } } @@ -270,7 +271,8 @@ public int AddressWidth } _addressWidth = value; - SetNeedsDisplay (); + SetNeedsDraw (); + SetNeedsLayout (); } } @@ -291,7 +293,7 @@ internal void SetDisplayStart (long value) _displayStart = value; } - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -317,7 +319,7 @@ public void ApplyEdits (Stream? stream = null) } _edits = new (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -413,35 +415,35 @@ protected override bool OnMouseEvent (MouseEventArgs me) } } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (Source is null) { - return; + return true; } - Attribute currentAttribute; + Attribute currentAttribute = Attribute.Default; Attribute current = GetFocusColor (); - Driver.SetAttribute (current); + SetAttribute (current); Move (0, 0); int nBlocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN; - var data = new byte [nBlocks * NUM_BYTES_PER_HEX_COLUMN * viewport.Height]; + var data = new byte [nBlocks * NUM_BYTES_PER_HEX_COLUMN * Viewport.Height]; Source.Position = _displayStart; int n = _source!.Read (data, 0, data.Length); Attribute selectedAttribute = GetHotNormalColor (); Attribute editedAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background); Attribute editingAttribute = new Attribute (GetFocusColor ().Background, GetFocusColor ().Foreground); - for (var line = 0; line < viewport.Height; line++) + for (var line = 0; line < Viewport.Height; line++) { - Rectangle lineRect = new (0, line, viewport.Width, 1); + Rectangle lineRect = new (0, line, Viewport.Width, 1); if (!Viewport.Contains (lineRect)) { @@ -450,13 +452,13 @@ public override void OnDrawContent (Rectangle viewport) Move (0, line); currentAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background); - Driver.SetAttribute (currentAttribute); + SetAttribute (currentAttribute); var address = $"{_displayStart + line * nBlocks * NUM_BYTES_PER_HEX_COLUMN:x8}"; - Driver.AddStr ($"{address.Substring (8 - AddressWidth)}"); + Driver?.AddStr ($"{address.Substring (8 - AddressWidth)}"); if (AddressWidth > 0) { - Driver.AddStr (" "); + Driver?.AddStr (" "); } SetAttribute (GetNormalColor ()); @@ -478,12 +480,12 @@ public override void OnDrawContent (Rectangle viewport) SetAttribute (edited ? editedAttribute : GetNormalColor ()); } - Driver.AddStr (offset >= n && !edited ? " " : $"{value:x2}"); + Driver?.AddStr (offset >= n && !edited ? " " : $"{value:x2}"); SetAttribute (GetNormalColor ()); - Driver.AddRune (_spaceCharRune); + Driver?.AddRune (_spaceCharRune); } - Driver.AddStr (block + 1 == nBlocks ? " " : $"{_columnSeparatorRune} "); + Driver?.AddStr (block + 1 == nBlocks ? " " : $"{_columnSeparatorRune} "); } for (var byteIndex = 0; byteIndex < nBlocks * NUM_BYTES_PER_HEX_COLUMN; byteIndex++) @@ -536,22 +538,24 @@ public override void OnDrawContent (Rectangle viewport) SetAttribute (edited ? editedAttribute : GetNormalColor ()); } - Driver.AddRune (c); + Driver?.AddRune (c); for (var i = 1; i < utf8BytesConsumed; i++) { byteIndex++; - Driver.AddRune (_periodCharRune); + Driver?.AddRune (_periodCharRune); } } } + return true; + void SetAttribute (Attribute attribute) { if (currentAttribute != attribute) { currentAttribute = attribute; - Driver.SetAttribute (attribute); + SetAttribute (attribute); } } } @@ -577,6 +581,8 @@ protected virtual void OnEdited (HexViewEditEventArgs e) { } /// protected void RaisePositionChanged () { + SetNeedsDraw (); + HexViewEventArgs args = new (Address, Position, BytesPerLine); OnPositionChanged (args); PositionChanged?.Invoke (this, args); @@ -598,6 +604,11 @@ protected override bool OnKeyDownNotHandled (Key keyEvent) return false; } + if (keyEvent.IsAlt) + { + return false; + } + if (_leftSideHasFocus) { int value; @@ -775,7 +786,7 @@ private bool MoveDown (int bytes) if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + bytes); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -793,7 +804,7 @@ private bool MoveEnd () if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (Address); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -807,7 +818,7 @@ private bool MoveEndOfLine () { // This lets address go past the end of the stream one, enabling adding to the stream. Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, GetEditedSize ()); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -815,7 +826,7 @@ private bool MoveEndOfLine () private bool MoveHome () { DisplayStart = 0; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -844,7 +855,7 @@ private bool MoveLeft () if (Address - 1 < DisplayStart) { SetDisplayStart (_displayStart - BytesPerLine); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -881,7 +892,7 @@ private bool MoveRight () if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + BytesPerLine); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -906,7 +917,7 @@ private long GetEditedSize () private bool MoveLeftStart () { Address = Address / BytesPerLine * BytesPerLine; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -923,7 +934,7 @@ private bool MoveUp (int bytes) if (Address < DisplayStart) { SetDisplayStart (DisplayStart - bytes); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -943,7 +954,7 @@ private void RedisplayLine (long pos) var delta = (int)(pos - DisplayStart); int line = delta / BytesPerLine; - SetNeedsDisplay (new (0, line, Viewport.Width, 1)); + SetNeedsDraw (new (0, line, Viewport.Width, 1)); } /// diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 730d5a1aab..82b7499e19 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -64,39 +64,40 @@ public override void SetBorderStyle (LineStyle value) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { LineCanvas lc = LineCanvas; if (SuperViewRendersLineCanvas) { - lc = SuperView.LineCanvas; + lc = SuperView?.LineCanvas; } if (SuperView is Adornment adornment) { - lc = adornment.Parent.LineCanvas; + lc = adornment.Parent?.LineCanvas; } - Point pos = ViewportToScreen (viewport).Location; + Point pos = ViewportToScreen (Viewport).Location; int length = Orientation == Orientation.Horizontal ? Frame.Width : Frame.Height; - if (SuperViewRendersLineCanvas && Orientation == Orientation.Horizontal) + if (SuperView is {} && SuperViewRendersLineCanvas && Orientation == Orientation.Horizontal) { pos.Offset (-SuperView.Border.Thickness.Left, 0); length += SuperView.Border.Thickness.Horizontal; } - if (SuperViewRendersLineCanvas && Orientation == Orientation.Vertical) + if (SuperView is { } && SuperViewRendersLineCanvas && Orientation == Orientation.Vertical) { pos.Offset (0, -SuperView.Border.Thickness.Top); length += SuperView.Border.Thickness.Vertical; } - lc.AddLine ( + lc?.AddLine ( pos, length, Orientation, BorderStyle ); + return true; } } diff --git a/Terminal.Gui/Views/LineView.cs b/Terminal.Gui/Views/LineView.cs index 4191ed2131..ccb65d3fd2 100644 --- a/Terminal.Gui/Views/LineView.cs +++ b/Terminal.Gui/Views/LineView.cs @@ -54,12 +54,10 @@ public LineView (Orientation orientation) public Rune? StartingAnchor { get; set; } /// Draws the line including any starting/ending anchors - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - Move (0, 0); - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); int hLineWidth = Math.Max (1, Glyphs.HLine.GetColumns ()); @@ -87,7 +85,8 @@ public override void OnDrawContent (Rectangle viewport) rune = EndingAnchor ?? LineRune; } - Driver.AddRune (rune); + Driver?.AddRune (rune); } + return true; } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 925bc203ce..87f9db948a 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -32,8 +32,7 @@ public interface IListDataSource : IDisposable /// This method is invoked to render a specified item, the method should cover the entire provided width. /// The render. - /// The list view to render. - /// The console driver to render. + /// The list view to render. /// Describes whether the item being rendered is currently selected by the user. /// The index of the item to render, zero for the first item and so on. /// The column where the rendering will start @@ -45,8 +44,7 @@ public interface IListDataSource : IDisposable /// or not. /// void Render ( - ListView container, - ConsoleDriver driver, + ListView listView, bool selected, int item, int col, @@ -88,7 +86,7 @@ void Render ( /// /// /// To change the contents of the ListView, set the property (when providing custom -/// rendering via ) or call an is being +/// rendering via ) or call an is being /// used. /// /// @@ -123,11 +121,24 @@ public ListView () // Things this view knows how to do // - // BUGBUG: Should return false if selection doesn't change (to support nav to next view) - AddCommand (Command.Up, () => MoveUp ()); - // BUGBUG: Should return false if selection doesn't change (to support nav to next view) - AddCommand (Command.Down, () => MoveDown ()); + AddCommand (Command.Up, (ctx) => + { + if (RaiseSelecting (ctx) == true) + { + return true; + } + return MoveUp (); + }); + AddCommand (Command.Down, (ctx) => + { + if (RaiseSelecting (ctx) == true) + { + return true; + } + return MoveDown (); + }); + // TODO: add RaiseSelecting to all of these AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); AddCommand (Command.ScrollDown, () => ScrollVertical (1)); AddCommand (Command.PageUp, () => MovePageUp ()); @@ -213,6 +224,13 @@ public ListView () // Use the form of Add that lets us pass context to the handler KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, true)); KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, false)); + + SubviewsLaidOut += ListView_LayoutComplete; + } + + private void ListView_LayoutComplete (object sender, LayoutEventArgs e) + { + SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); } /// Gets or sets whether this allows items to be marked. @@ -227,7 +245,7 @@ public bool AllowsMarking set { _allowsMarking = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -254,7 +272,7 @@ public bool AllowsMultipleSelection } } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -282,7 +300,7 @@ public int LeftItem } Viewport = Viewport with { X = value }; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -313,7 +331,7 @@ public int SelectedItem /// Gets or sets the backing this , enabling custom rendering. /// The source. - /// Use to set a new source. + /// Use to set a new source. public IListDataSource Source { get => _source; @@ -335,16 +353,17 @@ public IListDataSource Source SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); if (IsInitialized) { - Viewport = Viewport with { Y = 0 }; + // Viewport = Viewport with { Y = 0 }; } KeystrokeNavigator.Collection = _source?.ToList (); _selected = -1; _lastSelectedItem = -1; - SetNeedsDisplay (); + SetNeedsDraw (); } } + private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e) { SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width)); @@ -354,7 +373,7 @@ private void Source_CollectionChanged (object sender, NotifyCollectionChangedEve SelectedItem = Source.Count - 1; } - SetNeedsDisplay (); + SetNeedsDraw (); OnCollectionChanged (e); } @@ -446,11 +465,11 @@ public void EnsureSelectedItemVisible () Viewport = Viewport with { Y = _selected - Viewport.Height + 1 }; } - LayoutStarted -= ListView_LayoutStarted; + SubviewLayout -= ListView_LayoutStarted; } else { - LayoutStarted += ListView_LayoutStarted; + SubviewLayout += ListView_LayoutStarted; } } @@ -461,7 +480,7 @@ public bool MarkUnmarkSelectedItem () if (UnmarkAllButSelected ()) { Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); - SetNeedsDisplay (); + SetNeedsDraw (); return Source.IsMarked (SelectedItem); } @@ -537,7 +556,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); if (me.Flags == MouseFlags.Button1DoubleClicked) { @@ -564,7 +583,7 @@ public virtual bool MoveDown () // This can occur if the backing data source changes. _selected = _source.Count - 1; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_selected + 1 < _source.Count) { @@ -581,17 +600,17 @@ public virtual bool MoveDown () } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_selected == 0) { OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_selected >= Viewport.Y + Viewport.Height) { Viewport = Viewport with { Y = _source.Count - Viewport.Height }; - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -616,7 +635,7 @@ public virtual bool MoveEnd () } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -631,7 +650,7 @@ public virtual bool MoveHome () _selected = 0; Viewport = Viewport with { Y = _selected }; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -670,7 +689,7 @@ public virtual bool MovePageDown () } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -692,7 +711,7 @@ public virtual bool MovePageUp () _selected = n; Viewport = Viewport with { Y = _selected }; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -715,7 +734,7 @@ public virtual bool MoveUp () // This can occur if the backing data source changes. _selected = _source.Count - 1; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_selected > 0) { @@ -736,24 +755,22 @@ public virtual bool MoveUp () } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_selected < Viewport.Y) { Viewport = Viewport with { Y = _selected }; - SetNeedsDisplay (); + SetNeedsDraw (); } return true; } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - - Attribute current = ColorScheme.Focus; - Driver.SetAttribute (current); + Attribute current = ColorScheme?.Focus ?? Attribute.Default; + SetAttribute (current); Move (0, 0); Rectangle f = Viewport; int item = Viewport.Y; @@ -770,7 +787,7 @@ public override void OnDrawContent (Rectangle viewport) if (newcolor != current) { - Driver.SetAttribute (newcolor); + SetAttribute (newcolor); current = newcolor; } @@ -780,7 +797,7 @@ public override void OnDrawContent (Rectangle viewport) { for (var c = 0; c < f.Width; c++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } } else @@ -791,21 +808,22 @@ public override void OnDrawContent (Rectangle viewport) if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute) { current = (Attribute)rowEventArgs.RowAttribute; - Driver.SetAttribute (current); + SetAttribute (current); } if (_allowsMarking) { - Driver.AddRune ( + Driver?.AddRune ( _source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected : AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected ); - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } - Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start); + Source.Render (this, isSelected, item, col, row, f.Width - col, start); } } + return true; } /// @@ -866,7 +884,7 @@ protected override bool OnKeyDown (Key a) { SelectedItem = (int)newItem; EnsureSelectedItemVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -900,20 +918,20 @@ public virtual bool OnSelectedChanged () /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item. public event EventHandler OpenSelectedItem; - /// - public override Point? PositionCursor () - { - int x = 0; - int y = _selected - Viewport.Y; - if (!_allowsMarking) - { - x = Viewport.Width - 1; - } + ///// + //public override Point? PositionCursor () + //{ + // int x = 0; + // int y = _selected - Viewport.Y; + // if (!_allowsMarking) + // { + // x = Viewport.Width - 1; + // } - Move (x, y); + // Move (x, y); - return null; // Don't show the cursor - } + // return null; // Don't show the cursor + //} /// This event is invoked when this is being drawn before rendering. public event EventHandler RowRender; @@ -1096,7 +1114,6 @@ private void CheckAndResizeMarksIfRequired () /// public void Render ( ListView container, - ConsoleDriver driver, bool marked, int item, int col, @@ -1113,17 +1130,17 @@ public void Render ( if (t is null) { - RenderUstr (driver, "", col, line, width); + RenderUstr (container, "", col, line, width); } else { if (t is string s) { - RenderUstr (driver, s, col, line, width, start); + RenderUstr (container, s, col, line, width, start); } else { - RenderUstr (driver, t.ToString (), col, line, width, start); + RenderUstr (container, t.ToString (), col, line, width, start); } } } @@ -1219,7 +1236,7 @@ private int GetMaxLengthItem () return maxLength; } - private void RenderUstr (ConsoleDriver driver, string ustr, int col, int line, int width, int start = 0) + private void RenderUstr (View driver, string ustr, int col, int line, int width, int start = 0) { string str = start > ustr.GetColumns () ? string.Empty : ustr.Substring (Math.Min (start, ustr.ToRunes ().Length - 1)); string u = TextFormatter.ClipAndJustify (str, width, Alignment.Start); diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index a8c50755b3..fdfab4d982 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -70,7 +70,18 @@ public override void BeginInit () { base.BeginInit (); - Frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); + var frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); + + if (Frame.X != frame.X) + { + X = frame.X; + } + if (Frame.Y != frame.Y) + { + Y = frame.Y; + } + Width = frame.Width; + Height = frame.Height; if (_barItems.Children is { }) { @@ -148,7 +159,7 @@ public Menu () { if (Application.Top is { }) { - Application.Top.DrawContentComplete += Current_DrawContentComplete; + Application.Top.DrawComplete += Top_DrawComplete; Application.Top.SizeChanging += Current_TerminalResized; } @@ -279,7 +290,7 @@ private bool ExpandCollapse (MenuItem? menuItem) if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); return true; @@ -382,19 +393,26 @@ internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); } - public override void OnDrawContent (Rectangle viewport) + // By doing this we draw last, over everything else. + private void Top_DrawComplete (object? sender, DrawEventArgs e) { + if (!Visible) + { + return; + } + if (_barItems!.Children is null) { return; } - Rectangle savedClip = Driver.Clip; - Driver.Clip = new (0, 0, Driver.Cols, Driver.Rows); - Driver.SetAttribute (GetNormalColor ()); + DrawBorderAndPadding (); + RenderLineCanvas (); + + // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. + Region? savedClip = View.SetClipToScreen (); - OnDrawAdornments (); - OnRenderLineCanvas (); + SetAttribute (GetNormalColor ()); for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) { @@ -410,7 +428,7 @@ public override void OnDrawContent (Rectangle viewport) MenuItem? item = _barItems.Children [i]; - Driver.SetAttribute ( + SetAttribute ( // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract item is null ? GetNormalColor () : i == _currentChild ? GetFocusColor () : GetNormalColor () @@ -427,7 +445,7 @@ public override void OnDrawContent (Rectangle viewport) Move (0, i); } - Driver.SetAttribute (DetermineColorSchemeFor (item, i)); + SetAttribute (DetermineColorSchemeFor (item, i)); for (int p = Viewport.X; p < Frame.Width - 2; p++) { @@ -561,17 +579,7 @@ public override void OnDrawContent (Rectangle viewport) } } - Driver.Clip = savedClip; - - // PositionCursor (); - } - - private void Current_DrawContentComplete (object? sender, DrawEventArgs e) - { - if (Visible) - { - OnDrawContent (Viewport); - } + View.SetClip (savedClip); } public override Point? PositionCursor () @@ -601,7 +609,7 @@ public void Run (Action? action) Application.UngrabMouse (); _host.CloseAllMenus (); Application.Driver!.ClearContents (); - Application.Refresh (); + Application.LayoutAndDraw (); _host.Run (action); } @@ -702,7 +710,7 @@ private bool MoveDown () } while (_barItems?.Children? [_currentChild] is null || disabled); - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); if (!_host.UseSubMenusSingleFrame) @@ -783,7 +791,7 @@ private bool MoveUp () } while (_barItems.Children [_currentChild] is null || disabled); - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); if (!_host.UseSubMenusSingleFrame) @@ -800,12 +808,12 @@ private void SetParentSetNeedsDisplay () { foreach (Menu menu in _host._openSubMenu) { - menu.SetNeedsDisplay (); + menu.SetNeedsDraw (); } } - _host._openMenu?.SetNeedsDisplay (); - _host.SetNeedsDisplay (); + _host._openMenu?.SetNeedsDraw (); + _host.SetNeedsDraw (); } protected override bool OnMouseEvent (MouseEventArgs me) @@ -888,7 +896,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); return me.Handled = true; @@ -935,7 +943,7 @@ internal bool CheckSubMenu () } else { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); } @@ -948,7 +956,7 @@ protected override void Dispose (bool disposing) if (Application.Top is { }) { - Application.Top.DrawContentComplete -= Current_DrawContentComplete; + Application.Top.DrawComplete -= Top_DrawComplete; Application.Top.SizeChanging -= Current_TerminalResized; } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index a15d1e4633..7bba8b7143 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -244,7 +244,7 @@ public bool UseSubMenusSingleFrame if (value && UseKeysUpDownAsKeysLeftRight) { _useKeysUpDownAsKeysLeftRight = false; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -297,12 +297,8 @@ internal Menu? OpenCurrentMenu public event EventHandler? MenuOpening; /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - Driver.SetAttribute (GetNormalColor ()); - - Clear (); - var pos = 0; for (var i = 0; i < Menus.Length; i++) @@ -338,6 +334,7 @@ public override void OnDrawContent (Rectangle viewport) } //PositionCursor (); + return true; } /// Virtual method that will invoke the . @@ -381,7 +378,7 @@ public virtual void OnMenuOpened () mi = parent?.Children?.Length > 0 ? parent.Children [_openMenu!._currentChild] : null; } } - + MenuOpened?.Invoke (this, new (parent, mi)); } @@ -411,7 +408,7 @@ public void OpenMenu () } _selected = 0; - SetNeedsDisplay (); + SetNeedsDraw (); _previousFocused = (SuperView is null ? Application.Top?.Focused : SuperView.Focused)!; OpenMenu (_selected); @@ -478,7 +475,7 @@ internal void Activate (int idx, int sIdx = -1, MenuBarItem? subMenu = null!) } OpenMenu (idx, sIdx, subMenu); - SetNeedsDisplay (); + SetNeedsDraw (); } internal void CleanUp () @@ -500,7 +497,7 @@ internal void CleanUp () _lastFocused.SetFocus (); } - SetNeedsDisplay (); + SetNeedsDraw (); if (Application.MouseGrabView is { } && Application.MouseGrabView is MenuBar && Application.MouseGrabView != this) { @@ -593,7 +590,7 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing Application.Top?.Remove (_openMenu); } - SetNeedsDisplay (); + SetNeedsDraw (); if (_previousFocused is Menu && _openMenu is { } && _previousFocused.ToString () != OpenCurrentMenu!.ToString ()) { @@ -655,7 +652,7 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing case true: _selectedSub = -1; - SetNeedsDisplay (); + SetNeedsDraw (); RemoveAllOpensSubMenus (); OpenCurrentMenu!._previousSubFocused!.SetFocus (); _openSubMenu = null; @@ -773,7 +770,7 @@ out OpenCurrentMenu._currentChild return; } - SetNeedsDisplay (); + SetNeedsDraw (); if (UseKeysUpDownAsKeysLeftRight) { @@ -861,6 +858,7 @@ internal void OpenMenu (int index, int sIndex = -1, MenuBarItem? subMenu = null! if (Application.Top is { }) { Application.Top.Add (_openMenu); + // _openMenu.SetRelativeLayout (Application.Screen.Size); } else { @@ -991,7 +989,7 @@ internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingle { _selectedSub--; RemoveSubMenu (_selectedSub, ignoreUseSubMenusSingleFrame); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -1119,7 +1117,7 @@ internal bool SelectItem (MenuItem? item) Application.UngrabMouse (); CloseAllMenus (); - Application.Refresh (); + Application.LayoutAndDraw (); _openedByAltKey = true; return Run (item.Action); @@ -1138,7 +1136,7 @@ private void CloseMenuBar () LastFocused?.SetFocus (); } - SetNeedsDisplay (); + SetNeedsDraw (); } private Point GetLocationOffset () @@ -1167,14 +1165,14 @@ private void MoveLeft () } OpenMenu (_selected); - SetNeedsDisplay (); + SetNeedsDraw (); } private void MoveRight () { _selected = (_selected + 1) % Menus.Length; OpenMenu (_selected); - SetNeedsDisplay (); + SetNeedsDraw (); } private bool ProcessMenu (int i, MenuBarItem mi) @@ -1217,7 +1215,7 @@ out OpenCurrentMenu._currentChild } } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1324,7 +1322,7 @@ public bool UseKeysUpDownAsKeysLeftRight if (value && UseSubMenusSingleFrame) { UseSubMenusSingleFrame = false; - SetNeedsDisplay (); + SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/MenuBarv2.cs b/Terminal.Gui/Views/MenuBarv2.cs index d4c598dfed..7eb470968b 100644 --- a/Terminal.Gui/Views/MenuBarv2.cs +++ b/Terminal.Gui/Views/MenuBarv2.cs @@ -22,7 +22,7 @@ public MenuBarv2 (IEnumerable shortcuts) : base (shortcuts) ColorScheme = Colors.ColorSchemes ["Menu"]; Orientation = Orientation.Horizontal; - LayoutStarted += MenuBarv2_LayoutStarted; + SubviewLayout += MenuBarv2_LayoutStarted; } // MenuBarv2 arranges the items horizontally. diff --git a/Terminal.Gui/Views/Menuv2.cs b/Terminal.Gui/Views/Menuv2.cs index e8d371e22d..609fb962c2 100644 --- a/Terminal.Gui/Views/Menuv2.cs +++ b/Terminal.Gui/Views/Menuv2.cs @@ -46,7 +46,8 @@ private void Menuv2_Initialized (object sender, EventArgs e) // Menuv2 arranges the items horizontally. // The first item has no left border, the last item has no right border. // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart). - internal override void OnLayoutStarted (LayoutEventArgs args) + /// + protected override void OnSubviewLayout (LayoutEventArgs args) { for (int index = 0; index < Subviews.Count; index++) { @@ -58,7 +59,7 @@ internal override void OnLayoutStarted (LayoutEventArgs args) } } - base.OnLayoutStarted (args); + base.OnSubviewLayout (args); } /// diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index 4e73d87c5d..5d3d602cea 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -64,7 +64,7 @@ public NumericUpDown () Text = Value?.ToString () ?? "Err", X = Pos.Right (_down), Y = Pos.Top (_down), - Width = Dim.Auto (minimumContentDim: Dim.Func (() => string.Format (Format, Value).Length)), + Width = Dim.Auto (minimumContentDim: Dim.Func (() => string.Format (Format, Value).GetColumns())), Height = 1, TextAlignment = Alignment.Center, CanFocus = true, @@ -93,13 +93,18 @@ public NumericUpDown () AddCommand ( Command.ScrollUp, - () => + (ctx) => { if (type == typeof (object)) { return false; } + if (RaiseSelecting (ctx) is true) + { + return true; + } + if (Value is { } && Increment is { }) { Value = (dynamic)Value + (dynamic)Increment; @@ -110,13 +115,18 @@ public NumericUpDown () AddCommand ( Command.ScrollDown, - () => + (ctx) => { if (type == typeof (object)) { return false; } + if (RaiseSelecting (ctx) is true) + { + return true; + } + if (Value is { } && Increment is { }) { Value = (dynamic)Value - (dynamic)Increment; @@ -251,6 +261,10 @@ public T? Increment /// Raised when has changed. /// public event EventHandler>? IncrementChanged; + + // Prevent the drawing of Text + /// + protected override bool OnDrawingText () { return true; } } /// diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs index c2021d954f..e86d99d2c3 100644 --- a/Terminal.Gui/Views/ProgressBar.cs +++ b/Terminal.Gui/Views/ProgressBar.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// Specifies the style that a uses to indicate the progress of an operation. public enum ProgressBarStyle @@ -37,30 +38,29 @@ public enum ProgressBarFormat /// public class ProgressBar : View, IDesignable { - private int [] _activityPos; - private bool _bidirectionalMarquee = true; + private int []? _activityPos; private int _delta; private float _fraction; private bool _isActivity; private ProgressBarStyle _progressBarStyle = ProgressBarStyle.Blocks; - private ProgressBarFormat _progressBarFormat = ProgressBarFormat.Simple; - private Rune _segmentCharacter = Glyphs.BlocksMeterSegment; /// /// Initializes a new instance of the class, starts in percentage mode and uses relative /// layout. /// - public ProgressBar () { SetInitialProperties (); } + public ProgressBar () + { + Width = Dim.Auto (DimAutoStyle.Content); + Height = Dim.Auto (DimAutoStyle.Content, 1); + CanFocus = false; + _fraction = 0; + } /// /// Specifies if the or the /// styles is unidirectional or bidirectional. /// - public bool BidirectionalMarquee - { - get => _bidirectionalMarquee; - set => _bidirectionalMarquee = value; - } + public bool BidirectionalMarquee { get; set; } = true; /// Gets or sets the fraction to display, must be a value between 0 and 1. /// The fraction representing the progress. @@ -71,16 +71,12 @@ public float Fraction { _fraction = Math.Min (value, 1); _isActivity = false; - SetNeedsDisplay (); + SetNeedsDraw (); } } /// Specifies the format that a uses to indicate the visual presentation. - public ProgressBarFormat ProgressBarFormat - { - get => _progressBarFormat; - set => _progressBarFormat = value; - } + public ProgressBarFormat ProgressBarFormat { get; set; } = ProgressBarFormat.Simple; /// Gets/Sets the progress bar style based on the public ProgressBarStyle ProgressBarStyle @@ -109,16 +105,13 @@ public ProgressBarStyle ProgressBarStyle break; } - SetNeedsDisplay (); + + SetNeedsDraw (); } } /// Segment indicator for meter views. - public Rune SegmentCharacter - { - get => _segmentCharacter; - set => _segmentCharacter = value; - } + public Rune SegmentCharacter { get; set; } = Glyphs.BlocksMeterSegment; /// /// Gets or sets the text displayed on the progress bar. If set to an empty string and @@ -130,8 +123,7 @@ public override string Text get => string.IsNullOrEmpty (base.Text) ? $"{_fraction * 100:F0}%" : base.Text; set { - if (ProgressBarStyle == ProgressBarStyle.MarqueeBlocks - || ProgressBarStyle == ProgressBarStyle.MarqueeContinuous) + if (ProgressBarStyle is ProgressBarStyle.MarqueeBlocks or ProgressBarStyle.MarqueeContinuous) { base.Text = value; } @@ -139,9 +131,9 @@ public override string Text } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - Driver.SetAttribute (GetHotNormalColor ()); + SetAttribute (GetHotNormalColor ()); Move (0, 0); @@ -149,13 +141,13 @@ public override void OnDrawContent (Rectangle viewport) { for (var i = 0; i < Viewport.Width; i++) { - if (Array.IndexOf (_activityPos, i) != -1) + if (Array.IndexOf (_activityPos!, i) != -1) { - Driver.AddRune (SegmentCharacter); + Driver?.AddRune (SegmentCharacter); } else { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } } } @@ -166,32 +158,34 @@ public override void OnDrawContent (Rectangle viewport) for (i = 0; (i < mid) & (i < Viewport.Width); i++) { - Driver.AddRune (SegmentCharacter); + Driver?.AddRune (SegmentCharacter); } for (; i < Viewport.Width; i++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } } if (ProgressBarFormat != ProgressBarFormat.Simple && !_isActivity) { var tf = new TextFormatter { Alignment = Alignment.Center, Text = Text }; - var attr = new Attribute (ColorScheme.HotNormal.Foreground, ColorScheme.HotNormal.Background); + var attr = new Attribute (ColorScheme!.HotNormal.Foreground, ColorScheme.HotNormal.Background); if (_fraction > .5) { - attr = new Attribute (ColorScheme.HotNormal.Background, ColorScheme.HotNormal.Foreground); + attr = new (ColorScheme.HotNormal.Background, ColorScheme.HotNormal.Foreground); } - tf?.Draw ( - ViewportToScreen (Viewport), - attr, - ColorScheme.Normal, - SuperView?.ViewportToScreen (SuperView.Viewport) ?? default (Rectangle) - ); + tf.Draw ( + ViewportToScreen (Viewport), + attr, + ColorScheme.Normal, + SuperView?.ViewportToScreen (SuperView.Viewport) ?? default (Rectangle) + ); } + + return true; } /// Notifies the that some progress has taken place. @@ -234,7 +228,7 @@ public void Pulse () } else if (_activityPos [0] >= Viewport.Width) { - if (_bidirectionalMarquee) + if (BidirectionalMarquee) { for (var i = 0; i < _activityPos.Length; i++) { @@ -250,7 +244,7 @@ public void Pulse () } } - SetNeedsDisplay (); + SetNeedsDraw (); } private void PopulateActivityPos () @@ -263,29 +257,13 @@ private void PopulateActivityPos () } } - private void ProgressBar_Initialized (object sender, EventArgs e) - { - //ColorScheme = new ColorScheme (ColorScheme ?? SuperView?.ColorScheme ?? Colors.ColorSchemes ["Base"]) - //{ - // HotNormal = new Attribute (Color.BrightGreen, Color.Gray) - //}; - } - - private void SetInitialProperties () - { - Width = Dim.Auto (DimAutoStyle.Content); - Height = Dim.Auto (DimAutoStyle.Content, minimumContentDim: 1); - CanFocus = false; - _fraction = 0; - Initialized += ProgressBar_Initialized; - } - - /// + /// public bool EnableForDesign () { Width = Dim.Fill (); - Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1); + Height = Dim.Auto (DimAutoStyle.Text, 1); Fraction = 0.75f; + return true; } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index f929d0ce5a..ac2867a3af 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -52,9 +52,9 @@ public RadioGroup () // Accept (Enter key) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, RaiseAccepting); - // Hotkey - ctx may indicate a radio item hotkey was pressed. Beahvior depends on HasFocus + // Hotkey - ctx may indicate a radio item hotkey was pressed. Behavior depends on HasFocus // If HasFocus and it's this.HotKey invoke Select command - DO NOT raise Accept - // If it's a radio item HotKey select that item and raise Seelcted event - DO NOT raise Accept + // If it's a radio item HotKey select that item and raise Selected event - DO NOT raise Accept // If nothing is selected, select first and raise Selected event - DO NOT raise Accept AddCommand (Command.HotKey, ctx => @@ -175,7 +175,7 @@ public RadioGroup () SetupKeyBindings (); - LayoutStarted += RadioGroup_LayoutStarted; + SubviewLayout += RadioGroup_LayoutStarted; HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; @@ -354,17 +354,15 @@ private bool ChangeSelectedItem (int value) OnSelectedItemChanged (value, SelectedItem); SelectedItemChanged?.Invoke (this, new (SelectedItem, savedSelected)); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); for (var i = 0; i < _radioLabels.Count; i++) { @@ -381,8 +379,8 @@ public override void OnDrawContent (Rectangle viewport) } string rl = _radioLabels [i]; - Driver.SetAttribute (GetNormalColor ()); - Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} "); + SetAttribute (GetNormalColor ()); + Driver?.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} "); TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey); if (hotPos != -1 && hotKey != Key.Empty) @@ -395,7 +393,7 @@ public override void OnDrawContent (Rectangle viewport) if (j == hotPos && i == Cursor) { - Application.Driver?.SetAttribute ( + SetAttribute ( HasFocus ? ColorScheme!.HotFocus : GetHotNormalColor () @@ -403,11 +401,11 @@ public override void OnDrawContent (Rectangle viewport) } else if (j == hotPos && i != Cursor) { - Application.Driver?.SetAttribute (GetHotNormalColor ()); + SetAttribute (GetHotNormalColor ()); } else if (HasFocus && i == Cursor) { - Application.Driver?.SetAttribute (GetFocusColor ()); + SetAttribute (GetFocusColor ()); } if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) @@ -417,7 +415,7 @@ public override void OnDrawContent (Rectangle viewport) if (i == Cursor) { - Application.Driver?.SetAttribute ( + SetAttribute ( HasFocus ? ColorScheme!.HotFocus : GetHotNormalColor () @@ -425,12 +423,12 @@ public override void OnDrawContent (Rectangle viewport) } else if (i != Cursor) { - Application.Driver?.SetAttribute (GetHotNormalColor ()); + SetAttribute (GetHotNormalColor ()); } } Application.Driver?.AddRune (rune); - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } } else @@ -438,6 +436,7 @@ public override void OnDrawContent (Rectangle viewport) DrawHotString (rl, HasFocus && i == Cursor); } } + return true; } #region IOrientation @@ -524,7 +523,7 @@ private bool MoveDownRight () if (Cursor + 1 < _radioLabels.Count) { Cursor++; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -552,7 +551,7 @@ private bool MoveUpLeft () if (Cursor > 0) { Cursor--; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index f6e8da2961..8873e5ba78 100644 --- a/Terminal.Gui/Views/ScrollBarView.cs +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -5,6 +5,8 @@ // Miguel de Icaza (miguel@gnome.org) // +using System.Diagnostics; + namespace Terminal.Gui; /// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical @@ -113,7 +115,7 @@ public bool AutoHideScrollBars if (_autoHideScrollBars != value) { _autoHideScrollBars = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -259,7 +261,7 @@ public int Size { SetRelativeLayout (SuperView?.Frame.Size ?? Host.Frame.Size); ShowHideScrollBars (false); - SetNeedsDisplay (); + SetNeedsLayout (); } } } @@ -444,7 +446,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) public virtual void OnChangedPosition () { ChangedPosition?.Invoke (this, EventArgs.Empty); } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (ColorScheme is null || ((!ShowScrollIndicator || Size == 0) && AutoHideScrollBars && Visible)) { @@ -453,21 +455,21 @@ public override void OnDrawContent (Rectangle viewport) ShowHideScrollBars (false); } - return; + return false; } if (Size == 0 || (_vertical && Viewport.Height == 0) || (!_vertical && Viewport.Width == 0)) { - return; + return false; } - Driver.SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); + SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); if (_vertical) { if (Viewport.Right < Viewport.Width - 1) { - return; + return true; } int col = Viewport.Width - 1; @@ -575,7 +577,7 @@ public override void OnDrawContent (Rectangle viewport) { if (Viewport.Bottom < Viewport.Height - 1) { - return; + return true; } int row = Viewport.Height - 1; @@ -661,6 +663,8 @@ public override void OnDrawContent (Rectangle viewport) Driver.AddRune (Glyphs.RightArrow); } } + + return false; } @@ -761,11 +765,11 @@ private bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = fa private void ContentBottomRightCorner_DrawContent (object sender, DrawEventArgs e) { - Driver.SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); + SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ()); // I'm forced to do this here because the Clear method is // changing the color attribute and is different of this one - Driver.FillRect (Driver.Clip); + Driver.FillRect (Driver.Clip.GetBounds()); e.Cancel = true; } @@ -823,7 +827,7 @@ private void CreateBottomRightCorner (View host) _contentBottomRightCorner.Width = 1; _contentBottomRightCorner.Height = 1; _contentBottomRightCorner.MouseClick += ContentBottomRightCorner_MouseClick; - _contentBottomRightCorner.DrawContent += ContentBottomRightCorner_DrawContent; + _contentBottomRightCorner.DrawingContent += ContentBottomRightCorner_DrawContent; } } @@ -899,7 +903,7 @@ private void SetPosition (int newPosition) if (newPosition < 0) { _position = 0; - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -924,7 +928,7 @@ private void SetPosition (int newPosition) } OnChangedPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); } // BUGBUG: v2 - rationalize this with View.SetMinWidthHeight diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index 35d4409e5d..aeae539add 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -191,7 +191,7 @@ public bool AutoHideScrollBars _horizontal.AutoHideScrollBars = value; } - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -228,7 +228,7 @@ public Point ContentOffset // _contentView.Frame = new Rectangle (_contentOffset, value); // _vertical.Size = GetContentSize ().Height; // _horizontal.Size = GetContentSize ().Width; - // SetNeedsDisplay (); + // SetNeedsDraw (); // } // } //} @@ -372,19 +372,23 @@ public override View Add (View view) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - SetViewsNeedsDisplay (); + SetViewsNeedsDraw (); // TODO: It's bad practice for views to always clear a view. It negates clipping. - Clear (); + ClearViewport (); if (!string.IsNullOrEmpty (_contentView.Text) || _contentView.Subviews.Count > 0) { + Region? saved = ClipFrame(); _contentView.Draw (); + View.SetClip (saved); } DrawScrollBars (); + + return true; } /// @@ -467,7 +471,7 @@ public override View Remove (View view) return view; } - SetNeedsDisplay (); + SetNeedsDraw (); View container = view?.SuperView; if (container == this) @@ -580,18 +584,24 @@ private void DrawScrollBars () { if (ShowVerticalScrollIndicator) { + Region? saved = View.SetClipToScreen (); _vertical.Draw (); + View.SetClip (saved); } if (ShowHorizontalScrollIndicator) { + Region? saved = View.SetClipToScreen (); _horizontal.Draw (); + View.SetClip (saved); } if (ShowVerticalScrollIndicator && ShowHorizontalScrollIndicator) { SetContentBottomRightCornerVisibility (); + Region? saved = View.SetClipToScreen (); _contentBottomRightCorner.Draw (); + View.SetClip (saved); } } } @@ -626,14 +636,14 @@ private void SetContentOffset (Point offset) { _horizontal.Position = Math.Max (0, -_contentOffset.X); } - SetNeedsDisplay (); + SetNeedsDraw (); } - private void SetViewsNeedsDisplay () + private void SetViewsNeedsDraw () { foreach (View view in _contentView.Subviews) { - view.SetNeedsDisplay (); + view.SetNeedsDraw (); } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index f23ec23e33..b7b860df0f 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -96,6 +96,12 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText HighlightStyle = HighlightStyle.None; CanFocus = true; + + if (Border is { }) + { + Border.Settings &= ~BorderSettings.Title; + } + Width = GetWidthDimAuto (); Height = Dim.Auto (DimAutoStyle.Content, 1); @@ -109,64 +115,43 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText CommandView = new () { + Id = "CommandView", Width = Dim.Auto (), - Height = Dim.Auto (DimAutoStyle.Auto, 1) + Height = Dim.Fill() }; + Title = commandText ?? string.Empty; HelpView.Id = "_helpView"; HelpView.CanFocus = false; HelpView.Text = helpText ?? string.Empty; - Add (HelpView); KeyView.Id = "_keyView"; KeyView.CanFocus = false; - Add (KeyView); - - LayoutStarted += OnLayoutStarted; - Initialized += OnInitialized; - key ??= Key.Empty; Key = key; - Title = commandText ?? string.Empty; - Action = action; - return; - - void OnInitialized (object? sender, EventArgs e) - { - SuperViewRendersLineCanvas = true; - Border.Settings &= ~BorderSettings.Title; - - ShowHide (); - - // Force Width to DimAuto to calculate natural width and then set it back - Dim savedDim = Width; - Width = GetWidthDimAuto (); - _minimumDimAutoWidth = Frame.Width; - Width = savedDim; - - SetCommandViewDefaultLayout (); - SetHelpViewDefaultLayout (); - SetKeyViewDefaultLayout (); + Action = action; - SetColors (); - } + SubviewLayout += OnLayoutStarted; - // Helper to set Width consistently - Dim GetWidthDimAuto () - { - // TODO: PosAlign.CalculateMinDimension is a hack. Need to figure out a better way of doing this. - return Dim.Auto ( - DimAutoStyle.Content, - Dim.Func (() => PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width)), - Dim.Func (() => PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width)))!; - } + ShowHide (); } + // Helper to set Width consistently + internal Dim GetWidthDimAuto () + { + return Dim.Auto ( + DimAutoStyle.Content, + minimumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0), + maximumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0))!; +} + private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast; - // This is used to calculate the minimum width of the Shortcut when the width is NOT Dim.Auto - private int? _minimumDimAutoWidth; + // This is used to calculate the minimum width of the Shortcut when Width is NOT Dim.Auto + // It is calculated by setting Width to DimAuto temporarily and forcing layout. + // Once Frame.Width gets below this value, LayoutStarted makes HelpView an KeyView smaller. + private int? _minimumNaturalWidth; /// protected override bool OnHighlight (CancelEventArgs args) @@ -210,101 +195,84 @@ internal void ShowHide () if (CommandView.Visible) { Add (CommandView); + SetCommandViewDefaultLayout (); } if (HelpView.Visible && !string.IsNullOrEmpty (HelpView.Text)) { Add (HelpView); + SetHelpViewDefaultLayout (); } if (KeyView.Visible && Key != Key.Empty) { Add (KeyView); + SetKeyViewDefaultLayout (); } + + SetColors (); } - private Thickness GetMarginThickness () + // Force Width to DimAuto to calculate natural width and then set it back + private void ForceCalculateNaturalWidth () { - if (Orientation == Orientation.Vertical) - { - return new (1, 0, 1, 0); - } + // Get the natural size of each subview + CommandView.SetRelativeLayout (Application.Screen.Size); + HelpView.SetRelativeLayout (Application.Screen.Size); + KeyView.SetRelativeLayout (Application.Screen.Size); + _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width); + + // Reset our relative layout + SetRelativeLayout (SuperView?.GetContentSize() ?? Application.Screen.Size); + } + + // TODO: Enable setting of the margin thickness + private Thickness GetMarginThickness () + { return new (1, 0, 1, 0); } // When layout starts, we need to adjust the layout of the HelpView and KeyView private void OnLayoutStarted (object? sender, LayoutEventArgs e) { - if (Width is DimAuto widthAuto) + ShowHide (); + ForceCalculateNaturalWidth (); + + if (Width is DimAuto widthAuto || HelpView!.Margin is null) { - _minimumDimAutoWidth = Frame.Width; + return; } - else - { - if (string.IsNullOrEmpty (HelpView.Text)) - { - return; - } - - int currentWidth = Frame.Width; - - // If our width is smaller than the natural width then reduce width of HelpView first. - // Then KeyView. - // Don't ever reduce CommandView (it should spill). - // When Horizontal, Key is first, then Help, then Command. - // When Vertical, Command is first, then Help, then Key. - // BUGBUG: This does not do what the above says. - // TODO: Add Unit tests for this. - if (currentWidth < _minimumDimAutoWidth) - { - int delta = _minimumDimAutoWidth.Value - currentWidth; - int maxHelpWidth = int.Max (0, HelpView.Text.GetColumns () + Margin.Thickness.Horizontal - delta); - - switch (maxHelpWidth) - { - case 0: - // Hide HelpView - HelpView.Visible = false; - HelpView.X = 0; - - break; - case 1: - // Scrunch it by removing margins - HelpView.Margin.Thickness = new (0, 0, 0, 0); + // Frame.Width is smaller than the natural width. Reduce width of HelpView. + _maxHelpWidth = int.Max (0, GetContentSize ().Width - CommandView.Frame.Width - KeyView.Frame.Width); - break; + if (_maxHelpWidth < 3) + { + Thickness t = GetMarginThickness (); - case 2: - // Scrunch just the right margin - Thickness t = GetMarginThickness (); - HelpView.Margin.Thickness = new (t.Right, t.Top, t.Left - 1, t.Bottom); + switch (_maxHelpWidth) + { + case 0: + case 1: + // Scrunch it by removing both margins + HelpView.Margin.Thickness = new (t.Right - 1, t.Top, t.Left - 1, t.Bottom); - break; + break; - default: - // Default margin - HelpView.Margin.Thickness = GetMarginThickness (); + case 2: - break; - } + // Scrunch just the right margin + HelpView.Margin.Thickness = new (t.Right, t.Top, t.Left - 1, t.Bottom); - if (maxHelpWidth > 0) - { - HelpView.X = Pos.Align (Alignment.End, AlignmentModes); - - // Leverage Dim.Auto's max: - HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: maxHelpWidth); - HelpView.Visible = true; - } - } - else - { - // Reset to default - SetHelpViewDefaultLayout (); + break; } } + else + { + // Reset to default + HelpView.Margin.Thickness = GetMarginThickness (); + } } @@ -387,14 +355,7 @@ private void AddCommands () /// . /// /// - /// - /// Horizontal orientation arranges the command, help, and key parts of each s from right to - /// left - /// Vertical orientation arranges the command, help, and key parts of each s from left to - /// right. - /// /// - public Orientation Orientation { get => _orientationHelper.Orientation; @@ -504,13 +465,9 @@ public View CommandView Title = _commandView.Text; _commandView.Selecting += CommandViewOnSelecting; - _commandView.Accepting += CommandViewOnAccepted; - SetCommandViewDefaultLayout (); - SetHelpViewDefaultLayout (); - SetKeyViewDefaultLayout (); - ShowHide (); + //ShowHide (); UpdateKeyBindings (Key.Empty); return; @@ -536,10 +493,17 @@ void CommandViewOnSelecting (object? sender, CommandEventArgs e) private void SetCommandViewDefaultLayout () { - CommandView.Margin.Thickness = GetMarginThickness (); + if (CommandView.Margin is { }) + { + CommandView.Margin.Thickness = GetMarginThickness (); + } + CommandView.X = Pos.Align (Alignment.End, AlignmentModes); - CommandView.Y = 0; //Pos.Center (); - HelpView.HighlightStyle = HighlightStyle.None; + + CommandView.VerticalTextAlignment = Alignment.Center; + CommandView.TextAlignment = Alignment.Start; + CommandView.TextFormatter.WordWrap = false; + CommandView.HighlightStyle = HighlightStyle.None; } private void Shortcut_TitleChanged (object? sender, EventArgs e) @@ -554,21 +518,30 @@ private void Shortcut_TitleChanged (object? sender, EventArgs e) #region Help + // The maximum width of the HelpView. Calculated in OnLayoutStarted and used in HelpView.Width (Dim.Auto/Func). + private int _maxHelpWidth = 0; + /// /// The subview that displays the help text for the command. Internal for unit testing. /// - internal View HelpView { get; } = new (); + public View HelpView { get; } = new (); private void SetHelpViewDefaultLayout () { - HelpView.Margin.Thickness = GetMarginThickness (); + if (HelpView.Margin is { }) + { + HelpView.Margin.Thickness = GetMarginThickness (); + } + HelpView.X = Pos.Align (Alignment.End, AlignmentModes); - HelpView.Y = 0; //Pos.Center (); - HelpView.Width = Dim.Auto (DimAutoStyle.Text); - HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + _maxHelpWidth = HelpView.Text.GetColumns (); + HelpView.Width = Dim.Auto (DimAutoStyle.Text, maximumContentDim: Dim.Func ((() => _maxHelpWidth))); + HelpView.Height = Dim.Fill (); HelpView.Visible = true; HelpView.VerticalTextAlignment = Alignment.Center; + HelpView.TextAlignment = Alignment.Start; + HelpView.TextFormatter.WordWrap = false; HelpView.HighlightStyle = HighlightStyle.None; } @@ -660,7 +633,7 @@ public KeyBindingScope KeyBindingScope /// Gets the subview that displays the key. Internal for unit testing. /// - internal View KeyView { get; } = new (); + public View KeyView { get; } = new (); private int _minimumKeyTextSize; @@ -679,22 +652,26 @@ public int MinimumKeyTextSize _minimumKeyTextSize = value; SetKeyViewDefaultLayout (); - CommandView.SetNeedsLayout (); - HelpView.SetNeedsLayout (); - KeyView.SetNeedsLayout (); - SetSubViewNeedsDisplay (); + + //// TODO: Prob not needed + //CommandView.SetNeedsLayout (); + //HelpView.SetNeedsLayout (); + //KeyView.SetNeedsLayout (); + //SetSubViewNeedsDraw (); } } - private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; } private void SetKeyViewDefaultLayout () { - KeyView.Margin.Thickness = GetMarginThickness (); + if (KeyView.Margin is { }) + { + KeyView.Margin.Thickness = GetMarginThickness (); + } + KeyView.X = Pos.Align (Alignment.End, AlignmentModes); - KeyView.Y = 0; - KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize)); - KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + KeyView.Width = Dim.Auto (DimAutoStyle.Text, minimumContentDim: Dim.Func (() => MinimumKeyTextSize)); + KeyView.Height = Dim.Fill (); KeyView.Visible = true; @@ -742,23 +719,23 @@ public override ColorScheme? ColorScheme get => base.ColorScheme; set { - base.ColorScheme = value; + base.ColorScheme = _nonFocusColorScheme = value; SetColors (); } } + private ColorScheme? _nonFocusColorScheme; /// /// internal void SetColors (bool highlight = false) { - // Border should match superview. - if (Border is { }) - { - Border.ColorScheme = SuperView?.ColorScheme; - } - if (HasFocus || highlight) { + if (_nonFocusColorScheme is null) + { + _nonFocusColorScheme = base.ColorScheme; + } + base.ColorScheme ??= new (Attribute.Default); // When we have focus, we invert the colors @@ -772,7 +749,15 @@ internal void SetColors (bool highlight = false) } else { - base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; + if (_nonFocusColorScheme is { }) + { + base.ColorScheme = _nonFocusColorScheme; + //_nonFocusColorScheme = null; + } + else + { + base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; + } } // Set KeyView's colors to show "hot" @@ -785,6 +770,20 @@ internal void SetColors (bool highlight = false) }; KeyView.ColorScheme = cs; } + + if (CommandView.Margin is { }) + { + CommandView.Margin.ColorScheme = base.ColorScheme; + } + if (HelpView.Margin is { }) + { + HelpView.Margin.ColorScheme = base.ColorScheme; + } + + if (KeyView.Margin is { }) + { + KeyView.Margin.ColorScheme = base.ColorScheme; + } } /// @@ -802,7 +801,6 @@ public bool EnableForDesign () return true; } - /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index 9cbc785d17..27d17fa6bc 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -59,7 +59,7 @@ private void SetInitialProperties ( // BUGBUG: This should not be needed - Need to ensure SetRelativeLayout gets called during EndInit Initialized += (s, e) => { SetContentSize (); }; - LayoutStarted += (s, e) => { SetContentSize (); }; + SubviewLayout += (s, e) => { SetContentSize (); }; } // TODO: Make configurable via ConfigurationManager @@ -222,7 +222,7 @@ public SliderType Type // Todo: Custom logic to preserve options. _setOptions.Clear (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -353,7 +353,7 @@ public bool UseMinimumSize public virtual void OnOptionsChanged () { OptionsChanged?.Invoke (this, new (GetSetOptionDictionary ())); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Event raised When the option is hovered with the keys or the mouse. @@ -775,13 +775,13 @@ internal bool TryGetOptionByPosition (int x, int y, int threshold, out int optio #region Drawing /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { // TODO: make this more surgical to reduce repaint if (_options is null || _options.Count == 0) { - return; + return true; } // Draw Slider @@ -797,6 +797,8 @@ public override void OnDrawContent (Rectangle viewport) { AddRune (_moveRenderPosition.Value.X, _moveRenderPosition.Value.Y, Style.DragChar.Rune); } + + return true; } private string AlignText (string text, int width, Alignment alignment) @@ -839,7 +841,7 @@ private string AlignText (string text, int width, Alignment alignment) private void DrawSlider () { // TODO: be more surgical on clear - Clear (); + ClearViewport (); // Attributes @@ -848,8 +850,8 @@ private void DrawSlider () if (IsInitialized) { - normalAttr = ColorScheme?.Normal ?? Application.Top.ColorScheme.Normal; - setAttr = Style.SetChar.Attribute ?? ColorScheme!.HotNormal; + normalAttr = GetNormalColor(); + setAttr = Style.SetChar.Attribute ?? GetHotNormalColor (); } bool isVertical = _config._sliderOrientation == Orientation.Vertical; @@ -864,7 +866,7 @@ private void DrawSlider () // Left Spacing if (_config._showEndSpacing && _config._startSpacing > 0) { - Driver?.SetAttribute ( + SetAttribute ( isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr @@ -887,7 +889,7 @@ private void DrawSlider () } else { - Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); + SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < _config._startSpacing; i++) { @@ -940,7 +942,7 @@ private void DrawSlider () } // Draw Option - Driver?.SetAttribute ( + SetAttribute ( isSet && _setOptions.Contains (i) ? Style.SetChar.Attribute ?? setAttr : drawRange ? Style.RangeChar.Attribute ?? setAttr : Style.OptionChar.Attribute ?? normalAttr ); @@ -978,7 +980,7 @@ private void DrawSlider () if (_config._showEndSpacing || i < _options.Count - 1) { // Skip if is the Last Spacing. - Driver?.SetAttribute ( + SetAttribute ( drawRange && isSet ? Style.RangeChar.Attribute ?? setAttr : Style.SpaceChar.Attribute ?? normalAttr @@ -1006,7 +1008,7 @@ private void DrawSlider () // Right Spacing if (_config._showEndSpacing) { - Driver?.SetAttribute ( + SetAttribute ( isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr @@ -1029,7 +1031,7 @@ private void DrawSlider () } else { - Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); + SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < remaining; i++) { @@ -1056,8 +1058,8 @@ private void DrawLegends () if (IsInitialized) { - normalAttr = Style.LegendAttributes.NormalAttribute ?? ColorScheme?.Normal ?? ColorScheme.Disabled; - setAttr = Style.LegendAttributes.SetAttribute ?? ColorScheme?.HotNormal ?? ColorScheme.Normal; + normalAttr = Style.LegendAttributes.NormalAttribute ?? GetNormalColor (); + setAttr = Style.LegendAttributes.SetAttribute ?? GetHotNormalColor (); spaceAttr = Style.LegendAttributes.EmptyAttribute ?? normalAttr; } @@ -1221,7 +1223,7 @@ private void DrawLegends () } // Legend - Driver?.SetAttribute (isOptionSet ? setAttr : normalAttr); + SetAttribute (isOptionSet ? setAttr : normalAttr); foreach (Rune c in text.EnumerateRunes ()) { @@ -1247,7 +1249,7 @@ private void DrawLegends () } // Option Right Spacing of Option - Driver?.SetAttribute (spaceAttr); + SetAttribute (spaceAttr); if (isTextVertical) { @@ -1309,7 +1311,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) Application.GrabMouse (this); } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1343,7 +1345,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) } } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1377,7 +1379,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) } } - SetNeedsDisplay (); + SetNeedsDraw (); mouseEvent.Handled = true; diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs b/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs index 09d4358935..fac2005c9e 100644 --- a/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs +++ b/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs @@ -54,7 +54,7 @@ public abstract class SpinnerStyle /// /// /// This is the maximum speed the spinner will rotate at. You still need to call - /// or to advance/start animation. + /// or to advance/start animation. /// public abstract int SpinDelay { get; } diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs index 474ef57f57..a16f88a269 100644 --- a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs +++ b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs @@ -4,14 +4,16 @@ // . //------------------------------------------------------------------------------ +using System.Diagnostics; + namespace Terminal.Gui; /// A which displays (by default) a spinning line character. /// -/// By default animation only occurs when you call . Use -/// to make the automate calls to . +/// By default animation only occurs when you call . Use +/// to make the automate calls to . /// -public class SpinnerView : View +public class SpinnerView : View, IDesignable { private const int DEFAULT_DELAY = 130; private static readonly SpinnerStyle DEFAULT_STYLE = new SpinnerStyle.Line (); @@ -87,7 +89,7 @@ public bool SpinBounce /// Gets or sets the number of milliseconds to wait between characters in the animation. /// /// This is the maximum speed the spinner will rotate at. You still need to call - /// or to advance/start animation. + /// or to advance/start animation. /// public int SpinDelay { @@ -113,11 +115,11 @@ public SpinnerStyle Style /// ignored based on . /// /// Ensure this method is called on the main UI thread e.g. via - public void AdvanceAnimation () + public void AdvanceAnimation (bool setNeedsDraw = true) { if (DateTime.Now - _lastRender > TimeSpan.FromMilliseconds (SpinDelay)) { - if (Sequence is { } && Sequence.Length > 1) + if (Sequence is { Length: > 1 }) { var d = 1; @@ -169,14 +171,37 @@ public void AdvanceAnimation () _currentIdx = Sequence.Length - 1; } } - - Text = "" + Sequence [_currentIdx]; //.EnumerateRunes; } _lastRender = DateTime.Now; } - SetNeedsDisplay (); + if (setNeedsDraw) + { + SetNeedsDraw (); + } + } + + /// + protected override bool OnClearingViewport () { return true; } + + /// + protected override bool OnDrawingContent () + { + Render (); + return true; + } + + /// + /// Renders the current frame of the spinner. + /// + public void Render () + { + if (Sequence is { Length: > 0 } && _currentIdx < Sequence.Length) + { + Move (Viewport.X, Viewport.Y); + View.Driver?.AddStr (Sequence [_currentIdx]); + } } /// @@ -198,7 +223,7 @@ private void AddAutoSpinTimeout () TimeSpan.FromMilliseconds (SpinDelay), () => { - Application.Invoke (AdvanceAnimation); + Application.Invoke (() => AdvanceAnimation()); return true; } @@ -289,4 +314,12 @@ private void SetStyle (SpinnerStyle style) Width = GetSpinnerWidth (); } } + + bool IDesignable.EnableForDesign () + { + Style = new SpinnerStyle.Points (); + SpinReverse = true; + AutoSpin = true; + return true; + } } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 975d2c7ad4..84914ab558 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -26,7 +26,7 @@ public StatusBar (IEnumerable shortcuts) : base (shortcuts) BorderStyle = LineStyle.Dashed; ColorScheme = Colors.ColorSchemes ["Menu"]; - LayoutStarted += StatusBar_LayoutStarted; + SubviewLayout += StatusBar_LayoutStarted; } // StatusBar arranges the items horizontally. diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/Tab.cs index eb42a59b99..b683b04b6f 100644 --- a/Terminal.Gui/Views/Tab.cs +++ b/Terminal.Gui/Views/Tab.cs @@ -1,9 +1,10 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// A single tab in a . public class Tab : View { - private string _displayText; + private string? _displayText; /// Creates a new unnamed tab with no controls inside. public Tab () @@ -21,11 +22,11 @@ public string DisplayText set { _displayText = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } /// The control to display when the tab is selected. /// - public View View { get; set; } + public View? View { get; set; } } diff --git a/Terminal.Gui/Views/TabMouseEventArgs.cs b/Terminal.Gui/Views/TabMouseEventArgs.cs index 22a23ad017..30b64d15ac 100644 --- a/Terminal.Gui/Views/TabMouseEventArgs.cs +++ b/Terminal.Gui/Views/TabMouseEventArgs.cs @@ -1,7 +1,10 @@ -namespace Terminal.Gui; +#nullable enable +using System.ComponentModel; + +namespace Terminal.Gui; /// Describes a mouse event over a specific in a . -public class TabMouseEventArgs : EventArgs +public class TabMouseEventArgs : HandledEventArgs { /// Creates a new instance of the class. /// that the mouse was over when the event occurred. @@ -13,7 +16,7 @@ public TabMouseEventArgs (Tab tab, MouseEventArgs mouseEvent) } /// - /// Gets the actual mouse event. Use to cancel this event and perform custom + /// Gets the actual mouse event. Use to cancel this event and perform custom /// behavior (e.g. show a context menu). /// public MouseEventArgs MouseEvent { get; } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index d605b6fa05..99d90bec28 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; - +#nullable enable namespace Terminal.Gui; /// Control that hosts multiple sub views, presenting a single one at once. @@ -19,8 +18,8 @@ public class TabView : View /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. private readonly TabRowView _tabsBar; - private Tab _selectedTab; - private TabToRender [] _tabLocations; + private Tab? _selectedTab; + private TabToRender []? _tabLocations; private int _tabScrollOffset; /// Initializes a class. @@ -48,7 +47,7 @@ public TabView () () => { TabScrollOffset = 0; - SelectedTab = Tabs.FirstOrDefault (); + SelectedTab = Tabs.FirstOrDefault ()!; return true; } @@ -59,7 +58,7 @@ public TabView () () => { TabScrollOffset = Tabs.Count - 1; - SelectedTab = Tabs.LastOrDefault (); + SelectedTab = Tabs.LastOrDefault ()!; return true; } @@ -69,7 +68,7 @@ public TabView () Command.PageDown, () => { - TabScrollOffset += _tabLocations.Length; + TabScrollOffset += _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -80,7 +79,7 @@ public TabView () Command.PageUp, () => { - TabScrollOffset -= _tabLocations.Length; + TabScrollOffset -= _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -104,19 +103,20 @@ public TabView () /// The currently selected member of chosen by the user. /// - public Tab SelectedTab + public Tab? SelectedTab { get => _selectedTab; set { UnSetCurrentTabs (); - Tab old = _selectedTab; + Tab? old = _selectedTab; if (_selectedTab is { }) { if (_selectedTab.View is { }) { + _selectedTab.View.CanFocusChanged -= ContentViewCanFocus!; // remove old content _contentView.Remove (_selectedTab.View); } @@ -124,35 +124,53 @@ public Tab SelectedTab _selectedTab = value; - if (value is { }) + // add new content + if (_selectedTab?.View != null) { - // add new content - if (_selectedTab.View is { }) - { - _contentView.Add (_selectedTab.View); - // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; - } + _selectedTab.View.CanFocusChanged += ContentViewCanFocus!; + _contentView.Add (_selectedTab.View); + // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; } - _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + ContentViewCanFocus (null!, null!); EnsureSelectedTabIsVisible (); - if (old != value) + if (old != _selectedTab) { if (old?.HasFocus == true) { SelectedTab?.SetFocus (); } - OnSelectedTabChanged (old, value); + OnSelectedTabChanged (old!, _selectedTab!); } + SetNeedsLayout (); } } + private void ContentViewCanFocus (object sender, EventArgs eventArgs) + { + _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + } + + private TabStyle _style = new (); + /// Render choices for how to display tabs. After making changes, call . /// - public TabStyle Style { get; set; } = new (); + public TabStyle Style + { + get => _style; + set + { + if (_style == value) + { + return; + } + _style = value; + SetNeedsLayout (); + } + } /// All tabs currently hosted by the control. /// @@ -163,7 +181,11 @@ public Tab SelectedTab public int TabScrollOffset { get => _tabScrollOffset; - set => _tabScrollOffset = EnsureValidScrollOffsets (value); + set + { + _tabScrollOffset = EnsureValidScrollOffsets (value); + SetNeedsLayout (); + } } /// Adds the given to . @@ -188,13 +210,13 @@ public void AddTab (Tab tab, bool andSelect) tab.View?.SetFocus (); } - SetNeedsDisplay (); + SetNeedsLayout (); } /// /// Updates the control to use the latest state settings in . This can change the size of the /// client area of the tab (for rendering the selected tab's content). This method includes a call to - /// . + /// . /// public void ApplyStyleChanges () { @@ -233,7 +255,7 @@ public void ApplyStyleChanges () int tabHeight = GetTabHeight (true); //move content down to make space for tabs - _contentView.Y = Pos.Bottom (_tabsBar) ; + _contentView.Y = Pos.Bottom (_tabsBar); // Fill client area leaving space at bottom for border _contentView.Height = Dim.Fill (); @@ -245,12 +267,7 @@ public void ApplyStyleChanges () // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 } - if (IsInitialized) - { - LayoutSubviews (); - } - - SetNeedsDisplay (); + SetNeedsLayout (); } /// Updates to ensure that is visible. @@ -271,34 +288,48 @@ public void EnsureSelectedTabIsVisible () /// Updates to be a valid index of . /// The value to validate. - /// Changes will not be immediately visible in the display until you call . + /// Changes will not be immediately visible in the display until you call . /// The valid for the given value. public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } - /// - public override void OnDrawContent (Rectangle viewport) + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - Driver.SetAttribute (GetNormalColor ()); + if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this) + { + SelectedTab?.SetFocus (); + return; + } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); + } + + /// + protected override bool OnDrawingContent () + { if (Tabs.Any ()) { - Rectangle savedClip = SetClip (); - _tabsBar.OnDrawContent (viewport); - _contentView.SetNeedsDisplay (); + // Region savedClip = SetClip (); + _tabsBar.Draw (); + _contentView.SetNeedsDraw (); _contentView.Draw (); - Driver.Clip = savedClip; + + //if (Driver is { }) + //{ + // Driver.Clip = savedClip; + //} } - } - /// - public override void OnDrawContentComplete (Rectangle viewport) { _tabsBar.OnDrawContentComplete (viewport); } + return true; + } /// /// Removes the given from . Caller is responsible for disposing the /// tab's hosted if appropriate. /// /// - public void RemoveTab (Tab tab) + public void RemoveTab (Tab? tab) { if (tab is null || !_tabs.Contains (tab)) { @@ -327,11 +358,11 @@ public void RemoveTab (Tab tab) } EnsureSelectedTabIsVisible (); - SetNeedsDisplay (); + SetNeedsLayout (); } /// Event for when changes. - public event EventHandler SelectedTabChanged; + public event EventHandler? SelectedTabChanged; /// /// Changes the by the given . Positive for right, negative for @@ -349,7 +380,6 @@ public bool SwitchTabBy (int amount) if (Tabs.Count == 1 || SelectedTab is null) { SelectedTab = Tabs.ElementAt (0); - SetNeedsDisplay (); return SelectedTab is { }; } @@ -360,8 +390,6 @@ public bool SwitchTabBy (int amount) if (currentIdx == -1) { SelectedTab = Tabs.ElementAt (0); - SetNeedsDisplay (); - return true; } @@ -373,7 +401,6 @@ public bool SwitchTabBy (int amount) } SelectedTab = _tabs [newIdx]; - SetNeedsDisplay (); EnsureSelectedTabIsVisible (); @@ -384,7 +411,7 @@ public bool SwitchTabBy (int amount) /// Event fired when a is clicked. Can be used to cancel navigation, show context menu (e.g. on /// right click) etc. /// - public event EventHandler TabClicked; + public event EventHandler? TabClicked; /// Disposes the control and all . /// @@ -417,7 +444,7 @@ private IEnumerable CalculateViewport (Rectangle bounds) UnSetCurrentTabs (); var i = 1; - View prevTab = null; + View? prevTab = null; // Starting at the first or scrolled to tab foreach (Tab tab in Tabs.Skip (TabScrollOffset)) @@ -452,9 +479,9 @@ private IEnumerable CalculateViewport (Rectangle bounds) if (maxWidth == 0) { tab.Visible = true; - tab.MouseClick += Tab_MouseClick; + tab.MouseClick += Tab_MouseClick!; - yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0); + yield return new TabToRender (tab, string.Empty, Equals (SelectedTab, tab)); break; } @@ -478,9 +505,9 @@ private IEnumerable CalculateViewport (Rectangle bounds) // there is enough space! tab.Visible = true; - tab.MouseClick += Tab_MouseClick; + tab.MouseClick += Tab_MouseClick!; - yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth); + yield return new TabToRender (tab, text, Equals (SelectedTab, tab)); i += tabTextWidth + 1; } @@ -519,7 +546,7 @@ private void UnSetCurrentTabs () { foreach (TabToRender tabToRender in _tabLocations) { - tabToRender.Tab.MouseClick -= Tab_MouseClick; + tabToRender.Tab.MouseClick -= Tab_MouseClick!; tabToRender.Tab.Visible = false; } @@ -554,7 +581,7 @@ public TabRowView (TabView host) Visible = false, Text = Glyphs.RightArrow.ToString () }; - _rightScrollIndicator.MouseClick += _host.Tab_MouseClick; + _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; _leftScrollIndicator = new View { @@ -564,14 +591,14 @@ public TabRowView (TabView host) Visible = false, Text = Glyphs.LeftArrow.ToString () }; - _leftScrollIndicator.MouseClick += _host.Tab_MouseClick; + _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!; Add (_rightScrollIndicator, _leftScrollIndicator); } protected override bool OnMouseEvent (MouseEventArgs me) { - Tab hit = me.View is Tab ? (Tab)me.View : null; + Tab? hit = me.View as Tab; if (me.IsSingleClicked) { @@ -611,7 +638,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) { _host.SwitchTabBy (scrollIndicatorHit); - SetNeedsDisplay (); + SetNeedsLayout (); return true; } @@ -619,7 +646,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (hit is { }) { _host.SelectedTab = hit; - SetNeedsDisplay (); + SetNeedsLayout (); return true; } @@ -628,20 +655,37 @@ protected override bool OnMouseEvent (MouseEventArgs me) return false; } - public override void OnDrawContent (Rectangle viewport) + /// + protected override bool OnClearingViewport () { - _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - // clear any old text - Clear (); + ClearViewport (); + + return true; + } + + protected override bool OnDrawingContent () + { + _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); RenderTabLine (); RenderUnderline (); - Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + + SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + + return true; + } + + /// + protected override bool OnDrawingSubviews () + { + // RenderTabLine (); + + return false; } - public override void OnDrawContentComplete (Rectangle viewport) + protected override void OnDrawComplete () { if (_host._tabLocations is null) { @@ -1171,7 +1215,9 @@ public override void OnDrawContentComplete (Rectangle viewport) } tab.LineCanvas.Merge (lc); - tab.OnRenderLineCanvas (); + tab.RenderLineCanvas (); + + // RenderUnderline (); } } @@ -1188,21 +1234,15 @@ private int GetUnderlineYPosition () /// Renders the line with the tab names in it. private void RenderTabLine () { - TabToRender [] tabLocations = _host._tabLocations; - int y; + TabToRender []? tabLocations = _host._tabLocations; - if (_host.Style.TabsOnBottom) + if (tabLocations is null) { - y = 1; - } - else - { - y = _host.Style.ShowTopLine ? 1 : 0; + return; } - View selected = null; + View? selected = null; int topLine = _host.Style.ShowTopLine ? 1 : 0; - int width = Viewport.Width; foreach (TabToRender toRender in tabLocations) { @@ -1236,7 +1276,7 @@ private void RenderTabLine () tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } - tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1); + tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } else { @@ -1251,16 +1291,17 @@ private void RenderTabLine () tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } - tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1); + tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } tab.Text = toRender.TextToRender; - LayoutSubviews (); + // BUGBUG: Layout should only be called from Mainloop iteration! + Layout (); - tab.OnDrawAdornments (); + tab.DrawBorderAndPadding (); - Attribute prevAttr = Driver.GetAttribute (); + Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default; // if tab is the selected one and focus is inside this control if (toRender.IsSelected && _host.HasFocus) @@ -1283,9 +1324,10 @@ private void RenderTabLine () ColorScheme.HotNormal ); - tab.OnRenderLineCanvas (); + tab.DrawBorderAndPadding (); + - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } } @@ -1294,7 +1336,7 @@ private void RenderUnderline () { int y = GetUnderlineYPosition (); - TabToRender selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected); + TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected); if (selected is null) { @@ -1340,17 +1382,15 @@ private void RenderUnderline () } } - private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } + private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } } private class TabToRender { - public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width) + public TabToRender (Tab tab, string textToRender, bool isSelected) { - X = x; Tab = tab; IsSelected = isSelected; - Width = width; TextToRender = textToRender; } @@ -1360,7 +1400,5 @@ public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int wi public Tab Tab { get; } public string TextToRender { get; } - public int Width { get; } - public int X { get; set; } } } diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs index abbb2c704b..c3c47a64c9 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -147,7 +147,7 @@ private void TableView_CellToggled (object sender, CellToggledEventArgs e) } e.Cancel = true; - tableView.SetNeedsDisplay (); + tableView.SetNeedsDraw (); } private void TableView_MouseClick (object sender, MouseEventArgs e) @@ -171,7 +171,7 @@ private void TableView_MouseClick (object sender, MouseEventArgs e) // otherwise it ticks all rows ToggleAllRows (); e.Handled = true; - tableView.SetNeedsDisplay (); + tableView.SetNeedsDraw (); } else if (hit.HasValue && hit.Value.X == 0) { @@ -186,7 +186,7 @@ private void TableView_MouseClick (object sender, MouseEventArgs e) } e.Handled = true; - tableView.SetNeedsDisplay (); + tableView.SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/TableView/ListTableSource.cs b/Terminal.Gui/Views/TableView/ListTableSource.cs index 1286e2660a..6a117698f1 100644 --- a/Terminal.Gui/Views/TableView/ListTableSource.cs +++ b/Terminal.Gui/Views/TableView/ListTableSource.cs @@ -38,7 +38,7 @@ public ListTableSource (IList list, TableView tableView, ListColumnStyle style) DataTable = CreateTable (CalculateColumns ()); // TODO: Determine the best event for this - tableView.DrawContent += TableView_DrawContent; + tableView.DrawingContent += TableView_DrawContent; } /// diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index a33903d0b1..468b851f65 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Globalization; namespace Terminal.Gui; @@ -16,7 +17,7 @@ namespace Terminal.Gui; /// View for tabular data based on a . /// See TableView Deep Dive for more information. /// -public class TableView : View +public class TableView : View, IDesignable { /// /// The default maximum cell width for and @@ -319,8 +320,13 @@ public int ColumnOffset //try to prevent this being set to an out of bounds column set { + int prev = columnOffset; columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns - 1, value)); - SetNeedsDisplay (); + + if (prev != columnOffset) + { + SetNeedsDraw (); + } } } @@ -357,7 +363,16 @@ public int ColumnOffset public int RowOffset { get => rowOffset; - set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows - 1, value)); + set + { + int prev = rowOffset; + rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows - 1, value)); + + if (rowOffset != prev) + { + SetNeedsDraw (); + } + } } /// The index of in that the user has currently selected @@ -582,7 +597,7 @@ public void ChangeSelectionToStartOfTable (bool extend) /// not been set. /// /// - /// Changes will not be immediately visible in the display until you call + /// Changes will not be immediately visible in the display until you call /// public void EnsureSelectedCellIsVisible () { @@ -643,7 +658,7 @@ public void EnsureSelectedCellIsVisible () /// (by adjusting them to the nearest existing cell). Has no effect if has not been set. /// /// - /// Changes will not be immediately visible in the display until you call + /// Changes will not be immediately visible in the display until you call /// public void EnsureValidScrollOffsets () { @@ -662,7 +677,7 @@ public void EnsureValidScrollOffsets () /// has not been set. /// /// - /// Changes will not be immediately visible in the display until you call + /// Changes will not be immediately visible in the display until you call /// public void EnsureValidSelection () { @@ -829,28 +844,28 @@ protected override bool OnMouseEvent (MouseEventArgs me) case MouseFlags.WheeledDown: RowOffset++; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + //SetNeedsDraw (); return true; case MouseFlags.WheeledUp: RowOffset--; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + //SetNeedsDraw (); return true; case MouseFlags.WheeledRight: ColumnOffset++; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + //SetNeedsDraw (); return true; case MouseFlags.WheeledLeft: ColumnOffset--; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + //SetNeedsDraw (); return true; } @@ -866,7 +881,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) { ColumnOffset--; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + SetNeedsDraw (); } if (scrollRightPoint != null @@ -875,7 +890,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) { ColumnOffset++; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + SetNeedsDraw (); } Point? hit = ScreenToCell (boundsX, boundsY); @@ -910,10 +925,8 @@ protected override bool OnMouseEvent (MouseEventArgs me) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - Move (0, 0); scrollRightPoint = null; @@ -922,7 +935,7 @@ public override void OnDrawContent (Rectangle viewport) // What columns to render at what X offset in viewport ColumnToRender [] columnsToRender = CalculateViewport (Viewport).ToArray (); - Driver?.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); //invalidate current row (prevents scrolling around leaving old characters in the frame Driver?.AddStr (new string (' ', Viewport.Width)); @@ -985,6 +998,8 @@ public override void OnDrawContent (Rectangle viewport) RenderRow (line, rowToRender, columnsToRender); } + + return true; } /// @@ -1226,12 +1241,12 @@ public void SetSelection (int col, int row, bool extendExistingSelection) /// Updates the view to reflect changes to and to ( / /// ) etc /// - /// This always calls + /// This always calls public void Update () { if (!IsInitialized || TableIsNullOrInvisible ()) { - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -1241,7 +1256,7 @@ public void Update () EnsureSelectedCellIsVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Invokes the event @@ -1280,20 +1295,20 @@ protected virtual void RenderCell (Attribute cellColor, string render, bool isPr if (render.Length > 0) { // invert the color of the current cell for the first character - Driver.SetAttribute (new Attribute (cellColor.Background, cellColor.Foreground)); - Driver.AddRune ((Rune)render [0]); + SetAttribute (new Attribute (cellColor.Background, cellColor.Foreground)); + Driver?.AddRune ((Rune)render [0]); if (render.Length > 1) { - Driver.SetAttribute (cellColor); - Driver.AddStr (render.Substring (1)); + SetAttribute (cellColor); + Driver?.AddStr (render.Substring (1)); } } } else { - Driver.SetAttribute (cellColor); - Driver.AddStr (render); + SetAttribute (cellColor); + Driver?.AddStr (render); } } @@ -1323,7 +1338,7 @@ internal int GetHeaderHeight () private void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) { Move (col, row); - d.AddRune (ch); + d?.AddRune (ch); } /// @@ -1505,8 +1520,12 @@ private IEnumerable CalculateViewport (Rectangle bounds, int pad /// private void ClearLine (int row, int width) { + if (Driver is null) + { + return; + } Move (0, row); - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); Driver.AddStr (new string (' ', width)); } @@ -1580,7 +1599,7 @@ private bool CycleToNextTableEntryBeginningWith (Key key) SelectedRow = match; EnsureValidSelection (); EnsureSelectedCellIsVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1728,7 +1747,7 @@ private void RenderHeaderMidline (int row, ColumnToRender [] columnsToRender) Move (current.X, row); - Driver.AddStr (TruncateOrPad (colName, colName, current.Width, colStyle)); + Driver?.AddStr (TruncateOrPad (colName, colName, current.Width, colStyle)); if (Style.ExpandLastColumn == false && current.IsVeryLast) { @@ -1776,7 +1795,10 @@ private void RenderHeaderOverline (int row, int availableWidth, ColumnToRender [ } } - AddRuneAt (Driver, c, row, rune); + if (Driver is { }) + { + AddRuneAt (Driver, c, row, rune); + } } } @@ -1885,19 +1907,22 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen //start by clearing the entire line Move (0, row); - Attribute color; + Attribute? color; if (FullRowSelect && IsSelected (0, rowToRender)) { - color = focused ? rowScheme.Focus : rowScheme.HotNormal; + color = focused ? rowScheme?.Focus : rowScheme?.HotNormal; } else { - color = Enabled ? rowScheme.Normal : rowScheme.Disabled; + color = Enabled ? rowScheme?.Normal : rowScheme?.Disabled; } - Driver.SetAttribute (color); - Driver.AddStr (new string (' ', Viewport.Width)); + if (color is { }) + { + SetAttribute (color.Value); + } + Driver?.AddStr (new string (' ', Viewport.Width)); // Render cells for each visible header for the current row for (var i = 0; i < columnsToRender.Length; i++) @@ -1948,15 +1973,15 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen scheme = rowScheme; } - Attribute cellColor; + Attribute? cellColor; if (isSelectedCell) { - cellColor = focused ? scheme.Focus : scheme.HotNormal; + cellColor = focused ? scheme?.Focus : scheme?.HotNormal; } else { - cellColor = Enabled ? scheme.Normal : scheme.Disabled; + cellColor = Enabled ? scheme?.Normal : scheme?.Disabled; } string render = TruncateOrPad (val, representation, current.Width, colStyle); @@ -1964,7 +1989,10 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen // While many cells can be selected (see MultiSelectedRegions) only one cell is the primary (drives navigation etc) bool isPrimaryCell = current.Column == selectedColumn && rowToRender == selectedRow; - RenderCell (cellColor, render, isPrimaryCell); + if (cellColor.HasValue) + { + RenderCell (cellColor.Value, render, isPrimaryCell); + } // Reset color scheme to normal for drawing separators if we drew text with custom scheme if (scheme != rowScheme) @@ -1978,18 +2006,24 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen color = Enabled ? rowScheme.Normal : rowScheme.Disabled; } - Driver.SetAttribute (color); + SetAttribute (color.Value); } // If not in full row select mode always, reset color scheme to normal and render the vertical line (or space) at the end of the cell if (!FullRowSelect) { - Driver.SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled); + if (rowScheme is { }) + { + SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled); + } } if (style.AlwaysUseNormalColorForVerticalCellLines && style.ShowVerticalCellLines) { - Driver.SetAttribute (rowScheme.Normal); + if (rowScheme is { }) + { + SetAttribute (rowScheme.Normal); + } } RenderSeparator (current.X - 1, row, false); @@ -2002,7 +2036,10 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen if (style.ShowVerticalCellLines) { - Driver.SetAttribute (rowScheme.Normal); + if (rowScheme is { }) + { + SetAttribute (rowScheme.Normal); + } //render start and end of line AddRune (0, row, Glyphs.VLine); @@ -2297,4 +2334,62 @@ public ColumnToRender (int col, int x, int width, bool isVeryLast) /// The horizontal position to begin rendering the column at public int X { get; set; } } + + bool IDesignable.EnableForDesign () + { + var dt = BuildDemoDataTable (5, 5); + Table = new DataTableSource (dt); + return true; + } + + /// + /// Generates a new demo with the given number of (min 5) and + /// + /// + /// + /// + /// + public static DataTable BuildDemoDataTable (int cols, int rows) + { + var dt = new DataTable (); + + var explicitCols = 6; + dt.Columns.Add (new DataColumn ("StrCol", typeof (string))); + dt.Columns.Add (new DataColumn ("DateCol", typeof (DateTime))); + dt.Columns.Add (new DataColumn ("IntCol", typeof (int))); + dt.Columns.Add (new DataColumn ("DoubleCol", typeof (double))); + dt.Columns.Add (new DataColumn ("NullsCol", typeof (string))); + dt.Columns.Add (new DataColumn ("Unicode", typeof (string))); + + for (var i = 0; i < cols - explicitCols; i++) + { + dt.Columns.Add ("Column" + (i + explicitCols)); + } + + var r = new Random (100); + + for (var i = 0; i < rows; i++) + { + List row = new () + { + $"Demo text in row {i}", + new DateTime (2000 + i, 12, 25), + r.Next (i), + r.NextDouble () * i - 0.5 /*add some negatives to demo styles*/, + DBNull.Value, + "Les Mise" + + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + + "rables" + }; + + for (var j = 0; j < cols - explicitCols; j++) + { + row.Add ("SomeValue" + r.Next (100)); + } + + dt.Rows.Add (row.ToArray ()); + } + + return dt; + } } diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index c3458b32d3..9125c0c953 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -162,7 +162,7 @@ private void Table_KeyPress (object sender, Key e) if (e.Handled) { _tree.InvalidateLineMap (); - _tableView.SetNeedsDisplay (); + _tableView.SetNeedsDraw (); } } @@ -197,7 +197,7 @@ private void Table_MouseClick (object sender, MouseEventArgs e) if (e.Handled) { _tree.InvalidateLineMap (); - _tableView.SetNeedsDisplay (); + _tableView.SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 0b08e1f04b..c12a16bd9c 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -562,7 +562,7 @@ public string SelectedText } Adjust (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -585,7 +585,7 @@ public void ClearAllSelection () _selectedText = null; _start = 0; SelectedLength = 0; - SetNeedsDisplay (); + SetNeedsDraw (); } /// Allows clearing the items updating the original text. @@ -627,7 +627,7 @@ public void DeleteAll () _selectedStart = 0; MoveEndExtend (); DeleteCharLeft (false); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Deletes the character to the left. @@ -909,7 +909,7 @@ protected override bool OnMouseEvent (MouseEventArgs ev) ShowContextMenu (); } - //SetNeedsDisplay (); + //SetNeedsDraw (); return true; @@ -931,14 +931,14 @@ public void MoveEnd () } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { _isDrawing = true; var selColor = new Attribute (GetFocusColor ().Background, GetFocusColor ().Foreground); SetSelectedStartSelectedLength (); - Driver?.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); Move (0, 0); int p = ScrollOffset; @@ -954,11 +954,11 @@ public override void OnDrawContent (Rectangle viewport) if (idx == _cursorPosition && HasFocus && !Used && SelectedLength == 0 && !ReadOnly) { - Driver?.SetAttribute (selColor); + SetAttribute (selColor); } else if (ReadOnly) { - Driver?.SetAttribute ( + SetAttribute ( idx >= _start && SelectedLength > 0 && idx < _start + SelectedLength ? selColor : roc @@ -966,15 +966,15 @@ public override void OnDrawContent (Rectangle viewport) } else if (!HasFocus && Enabled) { - Driver?.SetAttribute (GetFocusColor ()); + SetAttribute (GetFocusColor ()); } else if (!Enabled) { - Driver?.SetAttribute (roc); + SetAttribute (roc); } else { - Driver?.SetAttribute ( + SetAttribute ( idx >= _start && SelectedLength > 0 && idx < _start + SelectedLength ? selColor : ColorScheme.Focus @@ -997,11 +997,11 @@ public override void OnDrawContent (Rectangle viewport) } } - Driver.SetAttribute (GetFocusColor ()); + SetAttribute (GetFocusColor ()); for (int i = col; i < width; i++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } PositionCursor (); @@ -1010,6 +1010,8 @@ public override void OnDrawContent (Rectangle viewport) DrawAutocomplete (); _isDrawing = false; + + return true; } /// @@ -1093,7 +1095,7 @@ public virtual void Paste () _cursorPosition = Math.Min (selStart + cbTxt.GetRuneCount (), _text.Count); ClearAllSelection (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -1142,7 +1144,7 @@ public void SelectAll () _selectedStart = 0; MoveEndExtend (); - SetNeedsDisplay (); + SetNeedsDraw (); } ///// @@ -1191,7 +1193,7 @@ private void Adjust () //SetContentSize(new (TextModel.DisplaySize (_text).size, 1)); int offB = OffSetBackground (); - bool need = NeedsDisplay || !Used; + bool need = NeedsDraw || !Used; if (_cursorPosition < ScrollOffset) { @@ -1216,7 +1218,7 @@ private void Adjust () if (need) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -1711,7 +1713,7 @@ private void PrepareSelection (int x, int direction = 0) _selectedText = null; } - SetNeedsDisplay (); + SetNeedsDraw (); } else if (SelectedLength > 0 || _selectedText is { }) { @@ -1767,7 +1769,7 @@ private void RenderCaption () } var color = new Attribute (CaptionColor, GetNormalColor ().Background); - Driver.SetAttribute (color); + SetAttribute (color); Move (0, 0); string render = Caption; @@ -1777,7 +1779,7 @@ private void RenderCaption () render = render [..Viewport.Width]; } - Driver.AddStr (render); + Driver?.AddStr (render); } private void SetClipboard (IEnumerable text) @@ -1791,7 +1793,7 @@ private void SetClipboard (IEnumerable text) private void SetOverwrite (bool overwrite) { Used = overwrite; - SetNeedsDisplay (); + SetNeedsDraw (); } private void SetSelectedStartSelectedLength () diff --git a/Terminal.Gui/Views/TextValidateField.cs b/Terminal.Gui/Views/TextValidateField.cs index 289855c30a..e1560c0771 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -526,7 +526,7 @@ public ITextValidateProvider Provider _provider.Text = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -544,7 +544,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) _cursorPosition = c; SetFocus (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -553,14 +553,14 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (_provider is null) { Move (0, 0); - Driver.AddStr ("Error: ITextValidateProvider not set!"); + Driver?.AddStr ("Error: ITextValidateProvider not set!"); - return; + return true; } Color bgcolor = !IsValid ? new Color (Color.BrightRed) : ColorScheme.Focus.Background; @@ -571,29 +571,31 @@ public override void OnDrawContent (Rectangle viewport) Move (0, 0); // Left Margin - Driver.SetAttribute (textColor); + SetAttribute (textColor); for (var i = 0; i < margin_left; i++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } // Content - Driver.SetAttribute (textColor); + SetAttribute (textColor); // Content for (var i = 0; i < _provider.DisplayText.Length; i++) { - Driver.AddRune ((Rune)_provider.DisplayText [i]); + Driver?.AddRune ((Rune)_provider.DisplayText [i]); } // Right Margin - Driver.SetAttribute (textColor); + SetAttribute (textColor); for (var i = 0; i < margin_right; i++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } + + return true; } /// @@ -655,7 +657,7 @@ private bool BackspaceKeyHandler () _cursorPosition = _provider.CursorLeft (_cursorPosition); _provider.Delete (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -671,7 +673,7 @@ private bool CursorLeft () int current = _cursorPosition; _cursorPosition = _provider.CursorLeft (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return current != _cursorPosition; } @@ -687,7 +689,7 @@ private bool CursorRight () int current = _cursorPosition; _cursorPosition = _provider.CursorRight (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return current != _cursorPosition; } @@ -702,7 +704,7 @@ private bool DeleteKeyHandler () } _provider.Delete (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -712,7 +714,7 @@ private bool DeleteKeyHandler () private bool EndKeyHandler () { _cursorPosition = _provider.CursorEnd (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -743,7 +745,7 @@ private bool EndKeyHandler () private bool HomeKeyHandler () { _cursorPosition = _provider.CursorStart (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 132f907a35..2ac3cf0981 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1901,7 +1901,7 @@ public TextView () Added += TextView_Added!; - LayoutComplete += TextView_LayoutComplete; + SubviewsLaidOut += TextView_LayoutComplete; // Things this view knows how to do @@ -2457,7 +2457,7 @@ public bool AllowsReturn AllowsTab = false; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2489,7 +2489,7 @@ public bool AllowsTab _tabWidth = 0; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2522,7 +2522,7 @@ public Point CursorPosition CurrentRow = value.Y < 0 ? 0 : value.Y > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value.Y; - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } } @@ -2605,12 +2605,12 @@ public bool Multiline _model.LoadString (Text); } - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_multiline && _savedHeight is { }) { Height = _savedHeight; - SetNeedsDisplay (); + SetNeedsDraw (); } KeyBindings.Remove (Key.Enter); @@ -2629,7 +2629,7 @@ public bool ReadOnly { _isReadOnly = value; - SetNeedsDisplay (); + SetNeedsDraw (); WrapTextModel (); Adjust (); } @@ -2683,7 +2683,7 @@ public int SelectionStartColumn _selectionStartColumn = value < 0 ? 0 : value > line.Count ? line.Count : value; IsSelecting = true; - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } } @@ -2697,7 +2697,7 @@ public int SelectionStartRow _selectionStartRow = value < 0 ? 0 : value > _model.Count - 1 ? Math.Max (_model.Count - 1, 0) : value; IsSelecting = true; - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } } @@ -2715,7 +2715,7 @@ public int TabWidth AllowsTab = true; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2747,7 +2747,7 @@ public override string Text } OnTextChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); _historyText.Clear (_model.GetAllLines ()); } @@ -2795,7 +2795,7 @@ public bool WordWrap _model = _wrapManager.Model; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2809,7 +2809,7 @@ public bool CloseFile () SetWrapModel (); bool res = _model.CloseFile (); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); return res; @@ -2978,7 +2978,7 @@ public void DeleteAll () _selectionStartRow = 0; MoveBottomEndExtend (); DeleteCharLeft (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Deletes all the selected or a single character at left from the position of the cursor. @@ -3196,7 +3196,7 @@ public void InsertText (string toAdd) InsertText (key); - if (NeedsDisplay) + if (NeedsDraw) { Adjust (); } @@ -3225,7 +3225,7 @@ public bool Load (string path) finally { UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -3243,7 +3243,7 @@ public void Load (Stream stream) _model.LoadStream (stream); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -3255,7 +3255,7 @@ public void Load (List cells) _model.LoadCells (cells, ColorScheme?.Focus); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); InheritsPreviousAttribute = true; } @@ -3269,7 +3269,7 @@ public void Load (List> cellsList) _model.LoadListCells (cellsList, ColorScheme?.Focus); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -3325,7 +3325,7 @@ protected override bool OnMouseEvent (MouseEventArgs ev) } else { - SetNeedsDisplay (); + SetNeedsDraw (); } _lastWasKill = false; @@ -3534,7 +3534,7 @@ public void MoveHome () _leftColumn = 0; TrackColumn (); PositionCursor (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -3550,7 +3550,7 @@ public virtual void OnContentsChanged () } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { _isDrawing = true; @@ -3616,7 +3616,7 @@ public override void OnDrawContent (Rectangle viewport) cols = Math.Max (cols, 1); } - if (!TextModel.SetCol (ref col, viewport.Right, cols)) + if (!TextModel.SetCol (ref col, Viewport.Right, cols)) { break; } @@ -3639,12 +3639,14 @@ public override void OnDrawContent (Rectangle viewport) if (row < bottom) { SetNormalColor (); - ClearRegion (viewport.Left, row, right, bottom); + ClearRegion (Viewport.Left, row, right, bottom); } //PositionCursor (); _isDrawing = false; + + return false; } /// @@ -3756,7 +3758,7 @@ public void Paste () HistoryText.LineStatus.Replaced ); - SetNeedsDisplay (); + SetNeedsDraw (); OnContentsChanged (); } else @@ -3778,7 +3780,7 @@ public void Paste () ); } - SetNeedsDisplay (); + SetNeedsDraw (); } UpdateWrapModel (); @@ -3801,8 +3803,8 @@ public void Paste () // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height); //var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Viewport.Height); - //SetNeedsDisplay (new (0, minRow, Viewport.Width, maxRow)); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, minRow, Viewport.Width, maxRow)); + SetNeedsDraw (); } List line = _model.GetLine (CurrentRow); @@ -3917,7 +3919,7 @@ public void ScrollTo (int idx, bool isRow = true) _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0); } - SetNeedsDisplay (); + SetNeedsDraw (); } /// Select all text. @@ -3933,7 +3935,7 @@ public void SelectAll () _selectionStartRow = 0; CurrentColumn = _model.GetLine (_model.Count - 1).Count; CurrentRow = _model.Count - 1; - SetNeedsDisplay (); + SetNeedsDraw (); } ///// Raised when the property of the changes. @@ -3960,7 +3962,7 @@ public void Undo () /// /// Sets the to an appropriate color for rendering the given /// of the current . Override to provide custom coloring by calling - /// Defaults to . + /// Defaults to . /// /// The line. /// The col index. @@ -3974,18 +3976,18 @@ protected virtual void OnDrawNormalColor (List line, int idxCol, int idxRo if (line [idxCol].Attribute is { }) { Attribute? attribute = line [idxCol].Attribute; - Driver.SetAttribute ((Attribute)attribute!); + SetAttribute ((Attribute)attribute!); } else { - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } } /// /// Sets the to an appropriate color for rendering the given /// of the current . Override to provide custom coloring by calling - /// Defaults to . + /// Defaults to . /// /// The line. /// The col index. @@ -4009,13 +4011,13 @@ protected virtual void OnDrawReadOnlyColor (List line, int idxCol, int idx attribute = new (cellAttribute.Value.Foreground, ColorScheme!.Focus.Background); } - Driver.SetAttribute (attribute); + SetAttribute (attribute); } /// /// Sets the to an appropriate color for rendering the given /// of the current . Override to provide custom coloring by calling - /// Defaults to . + /// Defaults to . /// /// The line. /// The col index. @@ -4031,13 +4033,13 @@ protected virtual void OnDrawSelectionColor (List line, int idxCol, int id { Attribute? attribute = line [idxCol].Attribute; - Driver.SetAttribute ( + SetAttribute ( new (attribute!.Value.Background, attribute.Value.Foreground) ); } else { - Driver.SetAttribute ( + SetAttribute ( new ( ColorScheme!.Focus.Background, ColorScheme!.Focus.Foreground @@ -4049,7 +4051,7 @@ protected virtual void OnDrawSelectionColor (List line, int idxCol, int id /// /// Sets the to an appropriate color for rendering the given /// of the current . Override to provide custom coloring by calling - /// Defaults to . + /// Defaults to . /// /// The line. /// The col index. @@ -4076,13 +4078,13 @@ protected virtual void OnDrawUsedColor (List line, int idxCol, int idxRow) /// Sets the driver to the default color for the control where no text is being rendered. Defaults to /// . /// - protected virtual void SetNormalColor () { Driver.SetAttribute (GetNormalColor ()); } + protected virtual void SetNormalColor () { SetAttribute (GetNormalColor ()); } private void Adjust () { (int width, int height) offB = OffSetBackground (); List line = GetCurrentLine (); - bool need = NeedsDisplay || _wrapNeeded || !Used; + bool need = NeedsDraw || _wrapNeeded || !Used; (int size, int length) tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth); (int size, int length) dSize = TextModel.DisplaySize (line, _leftColumn, CurrentColumn, true, TabWidth); @@ -4136,7 +4138,7 @@ private void Adjust () _wrapNeeded = false; } - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -4266,14 +4268,14 @@ private void ClearRegion () if (_wordWrap) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { //QUESTION: Is the below comment still relevant? // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1)); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1)); + SetNeedsDraw (); } _historyText.Add ( @@ -4315,7 +4317,7 @@ private void ClearRegion () UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); } private void ClearSelectedRegion () @@ -4363,13 +4365,13 @@ private bool DeleteTextBackwards () if (CurrentColumn < _leftColumn) { _leftColumn--; - SetNeedsDisplay (); + SetNeedsDraw (); } else { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (new (0, currentRow - topRow, 1, Viewport.Width)); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, currentRow - topRow, 1, Viewport.Width)); + SetNeedsDraw (); } } else @@ -4413,7 +4415,7 @@ private bool DeleteTextBackwards () ); CurrentColumn = prevCount; - SetNeedsDisplay (); + SetNeedsDraw (); } UpdateWrapModel (); @@ -4460,7 +4462,7 @@ private bool DeleteTextForwards () _wrapNeeded = true; } - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1)); } else { @@ -4479,7 +4481,7 @@ private bool DeleteTextForwards () _wrapNeeded = true; } - DoSetNeedsDisplay ( + DoSetNeedsDraw ( new ( CurrentColumn - _leftColumn, CurrentRow - _topRow, @@ -4496,7 +4498,7 @@ private bool DeleteTextForwards () private void DoNeededAction () { - if (NeedsDisplay) + if (NeedsDraw) { Adjust (); } @@ -4506,17 +4508,17 @@ private void DoNeededAction () } } - private void DoSetNeedsDisplay (Rectangle rect) + private void DoSetNeedsDraw (Rectangle rect) { if (_wrapNeeded) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (rect); - SetNeedsDisplay (); + //SetNeedsDraw (rect); + SetNeedsDraw (); } } @@ -4789,8 +4791,8 @@ private void Insert (Cell cell) if (!_wrapNeeded) { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0))); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0))); + SetNeedsDraw (); } } @@ -4844,13 +4846,13 @@ private void InsertAllText (string text, bool fromClipboard = false) if (_wordWrap) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0))); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0))); + SetNeedsDraw (); } UpdateWrapModel (); @@ -4948,7 +4950,7 @@ private bool InsertText (Key a, Attribute? attribute = null) if (CurrentColumn >= _leftColumn + Viewport.Width) { _leftColumn++; - SetNeedsDisplay (); + SetNeedsDraw (); } } else @@ -5063,7 +5065,7 @@ [[.. GetCurrentLine ()]], UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); _lastWasKill = setLastWasKill; DoNeededAction (); @@ -5170,7 +5172,7 @@ [[.. GetCurrentLine ()]], UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); _lastWasKill = setLastWasKill; DoNeededAction (); @@ -5240,7 +5242,7 @@ [[.. GetCurrentLine ()]], UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); DoNeededAction (); } @@ -5299,7 +5301,7 @@ [[.. GetCurrentLine ()]], UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); DoNeededAction (); } @@ -5352,7 +5354,7 @@ private bool MoveDown () if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5395,7 +5397,7 @@ private bool MoveLeft () if (CurrentRow < _topRow) { _topRow--; - SetNeedsDisplay (); + SetNeedsDraw (); } List currentLine = GetCurrentLine (); @@ -5433,7 +5435,7 @@ private void MovePageDown () _topRow = CurrentRow >= _model.Count ? CurrentRow - nPageDnShift : _topRow + nPageDnShift; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5459,7 +5461,7 @@ private void MovePageUp () if (CurrentRow < _topRow) { _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5487,7 +5489,7 @@ private bool MoveRight () if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -5510,7 +5512,7 @@ private void MoveLeftStart () { if (_leftColumn > 0) { - SetNeedsDisplay (); + SetNeedsDraw (); } CurrentColumn = 0; @@ -5552,7 +5554,7 @@ private bool MoveUp () if (CurrentRow < _topRow) { _topRow--; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5684,7 +5686,7 @@ private bool ProcessBackTab () ); } - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -6184,12 +6186,12 @@ private bool ProcessEnterKey (CommandContext ctx) CurrentRow++; - var fullNeedsDisplay = false; + var fullNeedsDraw = false; if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - fullNeedsDisplay = true; + fullNeedsDraw = true; } CurrentColumn = 0; @@ -6202,19 +6204,19 @@ private bool ProcessEnterKey (CommandContext ctx) if (!_wordWrap && CurrentColumn < _leftColumn) { - fullNeedsDisplay = true; + fullNeedsDraw = true; _leftColumn = 0; } - if (fullNeedsDisplay) + if (fullNeedsDraw) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method. - //SetNeedsDisplay (new (0, currentRow - topRow, 2, Viewport.Height)); - SetNeedsDisplay (); + //SetNeedsDraw (new (0, currentRow - topRow, 2, Viewport.Height)); + SetNeedsDraw (); } UpdateWrapModel (); @@ -6337,7 +6339,7 @@ private bool SetFoundText ( else { UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -6355,15 +6357,15 @@ private bool SetFoundText ( private void SetOverwrite (bool overwrite) { Used = overwrite; - SetNeedsDisplay (); + SetNeedsDraw (); DoNeededAction (); } - private static void SetValidUsedColor (Attribute? attribute) + private void SetValidUsedColor (Attribute? attribute) { // BUGBUG: (v2 truecolor) This code depends on 8-bit color names; disabling for now //if ((colorScheme!.HotNormal.Foreground & colorScheme.Focus.Background) == colorScheme.Focus.Foreground) { - Driver.SetAttribute (new (attribute!.Value.Background, attribute!.Value.Foreground)); + SetAttribute (new (attribute!.Value.Background, attribute!.Value.Foreground)); } /// Restore from original model. @@ -6527,7 +6529,7 @@ private void UpdateWrapModel ([CallerMemberName] string? caller = null) _selectionStartColumn = nStartCol; _wrapNeeded = true; - SetNeedsDisplay (); + SetNeedsDraw (); } if (_currentCaller is { }) @@ -6558,7 +6560,7 @@ private void WrapTextModel () CurrentColumn = nCol; _selectionStartRow = nStartRow; _selectionStartColumn = nStartCol; - SetNeedsDisplay (); + SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/Tile.cs b/Terminal.Gui/Views/Tile.cs index 89149f0ec8..f93c55a6b1 100644 --- a/Terminal.Gui/Views/Tile.cs +++ b/Terminal.Gui/Views/Tile.cs @@ -31,7 +31,7 @@ public Tile () /// The that is contained in this . Add new child views to this /// member for multiple s within the . /// - public View ContentView { get; internal set; } + public View? ContentView { get; internal set; } /// /// Gets or Sets the minimum size you to allow when splitter resizing along parent diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index 60370b2cb5..2a834aa925 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// /// A consisting of a moveable bar that divides the display area into resizeable @@ -7,15 +8,13 @@ public class TileView : View { private Orientation _orientation = Orientation.Vertical; - private List _splitterDistances; - private List _splitterLines; - private List _tiles; - private TileView _parentTileView; + private List? _splitterDistances; + private List? _splitterLines; + private List? _tiles; + private TileView? _parentTileView; /// Creates a new instance of the class with 2 tiles (i.e. left and right). - public TileView () : this (2) - { - } + public TileView () : this (2) { } /// Creates a new instance of the class with number of tiles. /// @@ -23,6 +22,23 @@ public TileView (int tiles) { CanFocus = true; RebuildForTileCount (tiles); + + SubviewLayout += (_, _) => + { + Rectangle viewport = Viewport; + + if (HasBorder ()) + { + viewport = new ( + viewport.X + 1, + viewport.Y + 1, + Math.Max (0, viewport.Width - 2), + Math.Max (0, viewport.Height - 2) + ); + } + + Setup (viewport); + }; } /// The line style to use when drawing the splitter lines. @@ -34,20 +50,24 @@ public Orientation Orientation get => _orientation; set { - _orientation = value; - - if (IsInitialized) + if (_orientation == value) { - LayoutSubviews (); + return; } + + _orientation = value; + + SetNeedsDraw (); + SetNeedsLayout (); + } } /// The splitter locations. Note that there will be N-1 splitters where N is the number of . - public IReadOnlyCollection SplitterDistances => _splitterDistances.AsReadOnly (); + public IReadOnlyCollection SplitterDistances => _splitterDistances!.AsReadOnly (); /// The sub sections hosted by the view - public IReadOnlyCollection Tiles => _tiles.AsReadOnly (); + public IReadOnlyCollection Tiles => _tiles!.AsReadOnly (); // TODO: Update to use Key instead of KeyCode /// @@ -63,7 +83,7 @@ public Orientation Orientation /// /// Use to determine if the returned value is the root. /// - public TileView GetParentTileView () { return _parentTileView; } + public TileView? GetParentTileView () { return _parentTileView; } /// /// Returns the index of the first in which contains @@ -71,9 +91,9 @@ public Orientation Orientation /// public int IndexOf (View toFind, bool recursive = false) { - for (var i = 0; i < _tiles.Count; i++) + for (var i = 0; i < _tiles!.Count; i++) { - View v = _tiles [i].ContentView; + View v = _tiles [i].ContentView!; if (v == toFind) { @@ -102,14 +122,14 @@ public int IndexOf (View toFind, bool recursive = false) /// line /// /// - public Tile InsertTile (int idx) + public Tile? InsertTile (int idx) { Tile [] oldTiles = Tiles.ToArray (); RebuildForTileCount (oldTiles.Length + 1); - Tile toReturn = null; + Tile? toReturn = null; - for (var i = 0; i < _tiles.Count; i++) + for (var i = 0; i < _tiles?.Count; i++) { if (i != idx) { @@ -117,12 +137,12 @@ public Tile InsertTile (int idx) // remove the new empty View Remove (_tiles [i].ContentView); - _tiles [i].ContentView.Dispose (); + _tiles [i].ContentView?.Dispose (); _tiles [i].ContentView = null; // restore old Tile and View _tiles [i] = oldTile; - _tiles [i].ContentView.TabStop = TabStop; + _tiles [i].ContentView!.TabStop = TabStop; Add (_tiles [i].ContentView); } else @@ -131,12 +151,8 @@ public Tile InsertTile (int idx) } } - SetNeedsDisplay (); - - if (IsInitialized) - { - LayoutSubviews (); - } + SetNeedsDraw (); + SetNeedsLayout (); return toReturn; } @@ -155,44 +171,22 @@ public Tile InsertTile (int idx) /// public bool IsRootTileView () { return _parentTileView == null; } - /// - public override void LayoutSubviews () - { - if (!IsInitialized) - { - return; - } - - Rectangle viewport = Viewport; - - if (HasBorder ()) - { - viewport = new ( - viewport.X + 1, - viewport.Y + 1, - Math.Max (0, viewport.Width - 2), - Math.Max (0, viewport.Height - 2) - ); - } - - Setup (viewport); - base.LayoutSubviews (); - } - // BUG: v2 fix this hack // QUESTION: Does this need to be fixed before events are refactored? /// Overridden so no Frames get drawn /// - public override bool OnDrawAdornments () { return false; } + protected override bool OnDrawingBorderAndPadding () { return true; } /// - public override void OnDrawContent (Rectangle viewport) - { - Driver.SetAttribute (ColorScheme.Normal); - - Clear (); + protected override bool OnRenderingLineCanvas () { return false; } - base.OnDrawContent (viewport); + /// + protected override void OnDrawComplete () + { + if (ColorScheme is { }) + { + SetAttribute (ColorScheme.Normal); + } var lc = new LineCanvas (); @@ -207,14 +201,14 @@ public override void OnDrawContent (Rectangle viewport) lc.AddLine (Point.Empty, Viewport.Height, Orientation.Vertical, LineStyle); lc.AddLine ( - new Point (Viewport.Width - 1, Viewport.Height - 1), + new (Viewport.Width - 1, Viewport.Height - 1), -Viewport.Width, Orientation.Horizontal, LineStyle ); lc.AddLine ( - new Point (Viewport.Width - 1, Viewport.Height - 1), + new (Viewport.Width - 1, Viewport.Height - 1), -Viewport.Height, Orientation.Vertical, LineStyle @@ -223,7 +217,7 @@ public override void OnDrawContent (Rectangle viewport) foreach (TileViewLineView line in allLines) { - bool isRoot = _splitterLines.Contains (line); + bool isRoot = _splitterLines!.Contains (line); Rectangle screen = line.ViewportToScreen (Rectangle.Empty); Point origin = ScreenToFrame (screen.Location); @@ -247,7 +241,10 @@ public override void OnDrawContent (Rectangle viewport) } } - Driver.SetAttribute (ColorScheme.Normal); + if (ColorScheme is { }) + { + SetAttribute (ColorScheme.Normal); + } foreach (KeyValuePair p in lc.GetMap (Viewport)) { @@ -282,6 +279,8 @@ public override void OnDrawContent (Rectangle viewport) AddRune (renderAt.X + i, renderAt.Y, (Rune)title [i]); } } + + return; } //// BUGBUG: Why is this not handled by a key binding??? @@ -292,7 +291,7 @@ protected override bool OnKeyDownNotHandled (Key key) if (key.KeyCode == ToggleResizable) { - foreach (TileViewLineView l in _splitterLines) + foreach (TileViewLineView l in _splitterLines!) { bool iniBefore = l.IsInitialized; l.IsInitialized = false; @@ -319,8 +318,8 @@ protected override bool OnKeyDownNotHandled (Key key) /// public void RebuildForTileCount (int count) { - _tiles = new List (); - _splitterDistances = new List (); + _tiles = new (); + _splitterDistances = new (); if (_splitterLines is { }) { @@ -330,13 +329,13 @@ public void RebuildForTileCount (int count) } } - _splitterLines = new List (); + _splitterLines = new (); RemoveAll (); foreach (Tile tile in _tiles) { - tile.ContentView.Dispose (); + tile.ContentView?.Dispose (); tile.ContentView = null; } @@ -361,15 +360,14 @@ public void RebuildForTileCount (int count) var tile = new Tile (); _tiles.Add (tile); - tile.ContentView.Id = $"Tile.ContentView {i}"; + tile.ContentView!.Id = $"Tile.ContentView {i}"; Add (tile.ContentView); - tile.TitleChanged += (s, e) => SetNeedsDisplay (); - } - if (IsInitialized) - { - LayoutSubviews (); + // BUGBUG: This should not be needed: + tile.TitleChanged += (s, e) => SetNeedsLayout (); } + + SetNeedsLayout (); } /// @@ -378,7 +376,7 @@ public void RebuildForTileCount (int count) /// /// /// - public Tile RemoveTile (int idx) + public Tile? RemoveTile (int idx) { Tile [] oldTiles = Tiles.ToArray (); @@ -391,14 +389,14 @@ public Tile RemoveTile (int idx) RebuildForTileCount (oldTiles.Length - 1); - for (var i = 0; i < _tiles.Count; i++) + for (var i = 0; i < _tiles?.Count; i++) { int oldIdx = i >= idx ? i + 1 : i; Tile oldTile = oldTiles [oldIdx]; // remove the new empty View Remove (_tiles [i].ContentView); - _tiles [i].ContentView.Dispose (); + _tiles [i].ContentView?.Dispose (); _tiles [i].ContentView = null; // restore old Tile and View @@ -406,9 +404,6 @@ public Tile RemoveTile (int idx) Add (_tiles [i].ContentView); } - SetNeedsDisplay (); - LayoutSubviews (); - return removed; } @@ -439,15 +434,20 @@ public bool SetSplitterPos (int idx, Pos value) return false; } - _splitterDistances [idx] = value; - GetRootTileView ().LayoutSubviews (); + if (_splitterDistances is { }) + { + _splitterDistances [idx] = value; + } + OnSplitterMoved (idx); + SetNeedsDraw (); + SetNeedsLayout (); return true; } /// Invoked when any of the is changed. - public event SplitterEventHandler SplitterMoved; + public event SplitterEventHandler? SplitterMoved; /// /// Converts of element from a regular to a new @@ -469,10 +469,10 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) { // when splitting a view into 2 sub views we will need to migrate // the title too - Tile tile = _tiles [idx]; + Tile tile = _tiles! [idx]; string title = tile.Title; - View toMove = tile.ContentView; + View? toMove = tile.ContentView; if (toMove is TileView existing) { @@ -487,7 +487,7 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) }; // Take everything out of the View we are moving - View [] childViews = toMove.Subviews.ToArray (); + View [] childViews = toMove!.Subviews.ToArray (); toMove.RemoveAll (); // Remove the view itself and replace it with the new TileView @@ -499,16 +499,16 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) tile.ContentView = newContainer; - View newTileView1 = newContainer._tiles [0].ContentView; + View newTileView1 = newContainer!._tiles? [0].ContentView!; // Add the original content into the first view of the new container foreach (View childView in childViews) { - newTileView1.Add (childView); + newTileView1!.Add (childView); } // Move the title across too - newContainer._tiles [0].Title = title; + newContainer._tiles! [0].Title = title; tile.Title = string.Empty; result = newContainer; @@ -522,14 +522,14 @@ protected override void Dispose (bool disposing) foreach (Tile tile in Tiles) { Remove (tile.ContentView); - tile.ContentView.Dispose (); + tile.ContentView?.Dispose (); } base.Dispose (disposing); } /// Raises the event - protected virtual void OnSplitterMoved (int idx) { SplitterMoved?.Invoke (this, new SplitterEventArgs (this, idx, _splitterDistances [idx])); } + protected virtual void OnSplitterMoved (int idx) { SplitterMoved?.Invoke (this, new (this, idx, _splitterDistances! [idx])); } private List GetAllLineViewsRecursively (View v) { @@ -556,14 +556,14 @@ private List GetAllLineViewsRecursively (View v) return lines; } - private List GetAllTitlesToRenderRecursively (TileView v, int depth = 0) + private List GetAllTitlesToRenderRecursively (TileView? v, int depth = 0) { List titles = new (); - foreach (Tile sub in v.Tiles) + foreach (Tile sub in v!.Tiles) { // Don't render titles for invisible stuff! - if (!sub.ContentView.Visible) + if (!sub.ContentView!.Visible) { continue; } @@ -579,7 +579,7 @@ private List GetAllTitlesToRenderRecursively (TileView v, int { if (sub.Title.Length > 0) { - titles.Add (new TileTitleToRender (v, sub, depth)); + titles.Add (new (v, sub, depth)); } } } @@ -599,20 +599,20 @@ private TileView GetRootTileView () return root; } - private Dim GetTileWidthOrHeight (int i, int space, Tile [] visibleTiles, TileViewLineView [] visibleSplitterLines) + private Dim GetTileWidthOrHeight (int i, int space, Tile? [] visibleTiles, TileViewLineView? [] visibleSplitterLines) { // last tile if (i + 1 >= visibleTiles.Length) { - return Dim.Fill (HasBorder () ? 1 : 0); + return Dim.Fill (HasBorder () ? 1 : 0)!; } - TileViewLineView nextSplitter = visibleSplitterLines [i]; - Pos nextSplitterPos = Orientation == Orientation.Vertical ? nextSplitter.X : nextSplitter.Y; + TileViewLineView? nextSplitter = visibleSplitterLines [i]; + Pos? nextSplitterPos = Orientation == Orientation.Vertical ? nextSplitter!.X : nextSplitter!.Y; int nextSplitterDistance = nextSplitterPos.GetAnchor (space); - TileViewLineView lastSplitter = i >= 1 ? visibleSplitterLines [i - 1] : null; - Pos lastSplitterPos = Orientation == Orientation.Vertical ? lastSplitter?.X : lastSplitter?.Y; + TileViewLineView? lastSplitter = i >= 1 ? visibleSplitterLines [i - 1] : null; + Pos? lastSplitterPos = Orientation == Orientation.Vertical ? lastSplitter?.X : lastSplitter?.Y; int lastSplitterDistance = lastSplitterPos?.GetAnchor (space) ?? 0; int distance = nextSplitterDistance - lastSplitterDistance; @@ -629,19 +629,19 @@ private Dim GetTileWidthOrHeight (int i, int space, Tile [] visibleTiles, TileVi private void HideSplittersBasedOnTileVisibility () { - if (_splitterLines.Count == 0) + if (_splitterLines is { Count: 0 }) { return; } - foreach (TileViewLineView line in _splitterLines) + foreach (TileViewLineView line in _splitterLines!) { line.Visible = true; } - for (var i = 0; i < _tiles.Count; i++) + for (var i = 0; i < _tiles!.Count; i++) { - if (!_tiles [i].ContentView.Visible) + if (!_tiles [i].ContentView!.Visible) { // when a tile is not visible, prefer hiding // the splitter on it's left @@ -665,7 +665,7 @@ private void HideSplittersBasedOnTileVisibility () private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) { int newSize = value.GetAnchor (fullSpace); - bool isGettingBigger = newSize > _splitterDistances [idx].GetAnchor (fullSpace); + bool isGettingBigger = newSize > _splitterDistances! [idx].GetAnchor (fullSpace); int lastSplitterOrBorder = HasBorder () ? 1 : 0; int nextSplitterOrBorder = HasBorder () ? fullSpace - 1 : fullSpace; @@ -724,7 +724,7 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) } // don't grow if it would take us below min size of right panel - if (spaceForNext < _tiles [idx + 1].MinSize) + if (spaceForNext < _tiles! [idx + 1].MinSize) { return false; } @@ -740,7 +740,7 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) } // don't shrink if it would take us below min size of left panel - if (spaceForLast < _tiles [idx].MinSize) + if (spaceForLast < _tiles! [idx].MinSize) { return false; } @@ -774,7 +774,7 @@ private void Setup (Rectangle viewport) return; } - for (var i = 0; i < _splitterLines.Count; i++) + for (var i = 0; i < _splitterLines!.Count; i++) { TileViewLineView line = _splitterLines [i]; @@ -791,19 +791,19 @@ private void Setup (Rectangle viewport) if (_orientation == Orientation.Vertical) { - line.X = _splitterDistances [i]; + line.X = _splitterDistances! [i]; line.Y = 0; } else { - line.Y = _splitterDistances [i]; + line.Y = _splitterDistances! [i]; line.X = 0; } } HideSplittersBasedOnTileVisibility (); - Tile [] visibleTiles = _tiles.Where (t => t.ContentView.Visible).ToArray (); + Tile [] visibleTiles = _tiles!.Where (t => t.ContentView!.Visible).ToArray (); TileViewLineView [] visibleSplitterLines = _splitterLines.Where (l => l.Visible).ToArray (); for (var i = 0; i < visibleTiles.Length; i++) @@ -812,26 +812,27 @@ private void Setup (Rectangle viewport) if (Orientation == Orientation.Vertical) { - tile.ContentView.X = i == 0 ? viewport.X : Pos.Right (visibleSplitterLines [i - 1]); + tile.ContentView!.X = i == 0 ? viewport.X : Pos.Right (visibleSplitterLines [i - 1]); tile.ContentView.Y = viewport.Y; tile.ContentView.Height = viewport.Height; tile.ContentView.Width = GetTileWidthOrHeight (i, Viewport.Width, visibleTiles, visibleSplitterLines); } else { - tile.ContentView.X = viewport.X; + tile.ContentView!.X = viewport.X; tile.ContentView.Y = i == 0 ? viewport.Y : Pos.Bottom (visibleSplitterLines [i - 1]); tile.ContentView.Width = viewport.Width; tile.ContentView.Height = GetTileWidthOrHeight (i, Viewport.Height, visibleTiles, visibleSplitterLines); } + // BUGBUG: This should not be needed. If any of the pos/dim setters above actually changed values, NeedsDisplay should have already been set. - tile.ContentView.SetNeedsDisplay (); + tile.ContentView.SetNeedsDraw (); } } private class TileTitleToRender { - public TileTitleToRender (TileView parent, Tile tile, int depth) + public TileTitleToRender (TileView? parent, Tile tile, int depth) { Parent = parent; Tile = tile; @@ -839,8 +840,8 @@ public TileTitleToRender (TileView parent, Tile tile, int depth) } public int Depth { get; } - public TileView Parent { get; } - public Tile Tile { get; } + public TileView? Parent { get; } + public Tile? Tile { get; } /// /// Translates the title location from its local coordinate space @@ -848,21 +849,22 @@ public TileTitleToRender (TileView parent, Tile tile, int depth) /// public Point GetLocalCoordinateForTitle (TileView intoCoordinateSpace) { - Rectangle screen = Tile.ContentView.ViewportToScreen (Rectangle.Empty); + Rectangle screen = Tile!.ContentView!.ViewportToScreen (Rectangle.Empty); + return intoCoordinateSpace.ScreenToFrame (new (screen.X, screen.Y - 1)); } internal string GetTrimmedTitle () { - Dim spaceDim = Tile.ContentView.Width; + Dim? spaceDim = Tile?.ContentView?.Width; - int spaceAbs = spaceDim.GetAnchor (Parent.Viewport.Width); + int spaceAbs = spaceDim!.GetAnchor (Parent!.Viewport.Width); - var title = $" {Tile.Title} "; + var title = $" {Tile!.Title} "; if (title.Length > spaceAbs) { - return title.Substring (0, spaceAbs); + return title!.Substring (0, spaceAbs); } return title; @@ -873,7 +875,7 @@ private class TileViewLineView : LineView { public Point? moveRuneRenderLocation; - private Pos dragOrignalPos; + private Pos? dragOrignalPos; private Point? dragPosition; public TileViewLineView (TileView parent, int idx) @@ -883,13 +885,13 @@ public TileViewLineView (TileView parent, int idx) Parent = parent; Idx = idx; - AddCommand (Command.Right, () => { return MoveSplitter (1, 0); }); + AddCommand (Command.Right, () => MoveSplitter (1, 0)); - AddCommand (Command.Left, () => { return MoveSplitter (-1, 0); }); + AddCommand (Command.Left, () => MoveSplitter (-1, 0)); - AddCommand (Command.Up, () => { return MoveSplitter (0, -1); }); + AddCommand (Command.Up, () => MoveSplitter (0, -1)); - AddCommand (Command.Down, () => { return MoveSplitter (0, 1); }); + AddCommand (Command.Down, () => MoveSplitter (0, 1)); KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Add (Key.CursorLeft, Command.Left); @@ -956,7 +958,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Viewport.Height - 2, mouseEvent.Position.Y))); } - Parent.SetNeedsDisplay (); + Parent.SetNeedsLayout (); return true; } @@ -969,7 +971,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) //Driver.UncookMouse (); FinalisePosition ( - dragOrignalPos, + dragOrignalPos!, Orientation == Orientation.Horizontal ? Y : X ); dragPosition = null; @@ -979,11 +981,14 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) return false; } - public override void OnDrawContent (Rectangle viewport) - { - base.OnDrawContent (viewport); + /// + protected override bool OnClearingViewport () { return true; } + protected override bool OnDrawingContent () + { DrawSplitterSymbol (); + + return true; } public override Point? PositionCursor () @@ -1015,7 +1020,7 @@ private Pos ConvertToPosPercent (Pos p, int parentLength) float position = p.GetAnchor (parentLength) + 0.5f; // Calculate the percentage - int percent = (int)Math.Round ((position / parentLength) * 100); + var percent = (int)Math.Round (position / parentLength * 100); // Return a new PosPercent object return Pos.Percent (percent); @@ -1036,6 +1041,10 @@ private Pos ConvertToPosPercent (Pos p, int parentLength) /// private bool FinalisePosition (Pos oldValue, Pos newValue) { + SetNeedsDraw (); + + SetNeedsLayout (); + if (oldValue is PosPercent) { if (Orientation == Orientation.Horizontal) @@ -1078,10 +1087,10 @@ private bool MoveSplitter (int distanceX, int distanceY) private Pos Offset (Pos pos, int delta) { int posAbsolute = pos.GetAnchor ( - Orientation == Orientation.Horizontal - ? Parent.Viewport.Height - : Parent.Viewport.Width - ); + Orientation == Orientation.Horizontal + ? Parent.Viewport.Height + : Parent.Viewport.Width + ); return posAbsolute + delta; } @@ -1089,4 +1098,4 @@ private Pos Offset (Pos pos, int delta) } /// Represents a method that will handle splitter events. -public delegate void SplitterEventHandler (object sender, SplitterEventArgs e); +public delegate void SplitterEventHandler (object? sender, SplitterEventArgs e); diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index ecc94a7be4..932f67ddb6 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -106,7 +106,7 @@ public bool IsShortFormat SetText (Text); ReadOnly = ro; - SetNeedsDisplay (); + SetNeedsDraw (); } } diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index ddee6b0412..7861fcfd0b 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -235,7 +235,7 @@ out int ny return; } - var layoutSubviews = false; + //var layoutSubviews = false; var maxWidth = 0; if (superView.Margin is { } && superView == top.SuperView) @@ -251,36 +251,26 @@ out int ny if (top?.X is null or PosAbsolute && top?.Frame.X != nx) { top!.X = nx; - layoutSubviews = true; + //layoutSubviews = true; } if (top?.Y is null or PosAbsolute && top?.Frame.Y != ny) { top!.Y = ny; - layoutSubviews = true; + //layoutSubviews = true; } } - //// TODO: v2 - This is a hack to get the StatusBar to be positioned correctly. - //if (sb != null - // && !top!.Subviews.Contains (sb) - // && ny + top.Frame.Height != superView.Frame.Height - (sb.Visible ? 1 : 0) - // && top.Height is DimFill - // && -top.Height.GetAnchor (0) < 1) + + //if (superView.IsLayoutNeeded () || layoutSubviews) //{ - // top.Height = Dim.Fill (sb.Visible ? 1 : 0); - // layoutSubviews = true; + // superView.LayoutSubviews (); //} - if (superView.LayoutNeeded || layoutSubviews) - { - superView.LayoutSubviews (); - } - - if (LayoutNeeded) - { - LayoutSubviews (); - } + //if (IsLayoutNeeded ()) + //{ + // LayoutSubviews (); + //} } /// Invoked when the terminal has been resized. The new of the terminal is provided. diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 2b9f637d8d..e2d220aceb 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -237,7 +237,7 @@ public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, }; tree.OnDrawLine (e); - if (!e.Handled) + if (!e.Handled && driver != null) { foreach (Cell cell in cells) { @@ -246,7 +246,7 @@ public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, } } - driver.SetAttribute (colorScheme.Normal); + driver?.SetAttribute (colorScheme.Normal); } /// Expands the current branch if possible. diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 4e09eba2b5..728a411bdb 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -3,6 +3,7 @@ // and code to be used in this library under the MIT license. using System.Collections.ObjectModel; +using static Terminal.Gui.SpinnerStyle; namespace Terminal.Gui; @@ -18,15 +19,15 @@ public interface ITreeView /// Removes all objects from the tree and clears selection. void ClearObjects (); - /// Sets a flag indicating this view needs to be redisplayed because its state has changed. - void SetNeedsDisplay (); + /// Sets a flag indicating this view needs to be drawn because its state has changed. + void SetNeedsDraw (); } /// /// Convenience implementation of generic for any tree were all nodes implement /// . See TreeView Deep Dive for more information. /// -public class TreeView : TreeView +public class TreeView : TreeView, IDesignable { /// /// Creates a new instance of the tree control with absolute positioning and initialises @@ -39,6 +40,24 @@ public TreeView () TreeBuilder = new TreeNodeBuilder (); AspectGetter = o => o is null ? "Null" : o.Text ?? o?.ToString () ?? "Unnamed Node"; } + + + bool IDesignable.EnableForDesign () + { + var root1 = new TreeNode ("Root1"); + root1.Children.Add (new TreeNode ("Child1.1")); + root1.Children.Add (new TreeNode ("Child1.2")); + + var root2 = new TreeNode ("Root2"); + root2.Children.Add (new TreeNode ("Child2.1")); + root2.Children.Add (new TreeNode ("Child2.2")); + + AddObject (root1); + AddObject (root2); + + ExpandAll (); + return true; + } } /// @@ -358,7 +377,7 @@ public KeyCode ObjectActivationKey { KeyBindings.ReplaceKey (ObjectActivationKey, value); objectActivationKey = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -369,7 +388,7 @@ public KeyCode ObjectActivationKey /// The amount of tree view that has been scrolled to the right (horizontally). /// /// Setting a value of less than 0 will result in a offset of 0. To see changes in the UI call - /// . + /// . /// public int ScrollOffsetHorizontal { @@ -377,14 +396,14 @@ public int ScrollOffsetHorizontal set { scrollOffsetHorizontal = Math.Max (0, value); - SetNeedsDisplay (); + SetNeedsDraw (); } } /// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down). /// /// Setting a value of less than 0 will result in an offset of 0. To see changes in the UI call - /// . + /// . /// public int ScrollOffsetVertical { @@ -392,7 +411,7 @@ public int ScrollOffsetVertical set { scrollOffsetVertical = Math.Max (0, value); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -435,7 +454,7 @@ public void ClearObjects () multiSelectedRegions.Clear (); roots = new Dictionary> (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -471,7 +490,7 @@ public void AddObject (T o) { roots.Add (o, new Branch (this, null, o)); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -494,7 +513,7 @@ public void AddObjects (IEnumerable collection) if (objectsAdded) { InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -562,7 +581,7 @@ public void AdjustSelection (int offset, bool expandSelection = false) } } - SetNeedsDisplay (); + SetNeedsDraw (); } /// Moves the selection to the last child in the currently selected level. @@ -594,7 +613,7 @@ public void AdjustSelectionToBranchEnd () { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -636,7 +655,7 @@ public void AdjustSelectionToBranchStart () { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -697,7 +716,7 @@ public void CollapseAll () } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -753,7 +772,7 @@ public void Expand (T toExpand) ObjectToBranch (toExpand)?.Expand (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Expands the supplied object and all child objects. @@ -767,7 +786,7 @@ public void ExpandAll (T toExpand) ObjectToBranch (toExpand)?.ExpandAll (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -782,7 +801,7 @@ public void ExpandAll () } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -911,7 +930,7 @@ public int GetContentWidth (bool visible) /// /// Returns the index of the object if it is currently exposed (it's parent(s) have been - /// expanded). This can be used with and to + /// expanded). This can be used with and to /// scroll to a specific object. /// /// Uses the Equals method and returns the first index at which the object is found or -1 if it is not found. @@ -947,7 +966,7 @@ public void GoTo (T toSelect) SelectedObject = toSelect; EnsureVisible (toSelect); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Changes the to the last object in the tree and scrolls so that it is visible. @@ -957,7 +976,7 @@ public void GoToEnd () ScrollOffsetVertical = Math.Max (0, map.Count - Viewport.Height + 1); SelectedObject = map.LastOrDefault ()?.Model; - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -969,7 +988,7 @@ public void GoToFirst () ScrollOffsetVertical = 0; SelectedObject = roots.Keys.FirstOrDefault (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Clears any cached results of the tree state. @@ -1022,7 +1041,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (me.Flags == MouseFlags.WheeledRight) { ScrollOffsetHorizontal++; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1030,7 +1049,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (me.Flags == MouseFlags.WheeledLeft) { ScrollOffsetHorizontal--; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1079,7 +1098,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) multiSelectedRegions.Clear (); } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1098,7 +1117,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) // Double click changes the selection to the clicked node as well as triggering // activation otherwise it feels wierd SelectedObject = clickedBranch.Model; - SetNeedsDisplay (); + SetNeedsDraw (); // trigger activation event OnObjectActivated (new ObjectActivatedEventArgs (this, clickedBranch.Model)); @@ -1127,19 +1146,19 @@ protected override bool OnMouseEvent (MouseEventArgs me) public event EventHandler> ObjectActivated; /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (roots is null) { - return; + return true; } if (TreeBuilder is null) { Move (0, 0); - Driver.AddStr (NoBuilderError); + Driver?.AddStr (NoBuilderError); - return; + return true; } IReadOnlyCollection> map = BuildLineMap (); @@ -1158,10 +1177,12 @@ public override void OnDrawContent (Rectangle viewport) { // Else clear the line to prevent stale symbols due to scrolling etc Move (0, line); - Driver.SetAttribute (GetNormalColor ()); - Driver.AddStr (new string (' ', Viewport.Width)); + SetAttribute (GetNormalColor ()); + Driver?.AddStr (new string (' ', Viewport.Width)); } } + + return true; } /// @@ -1200,7 +1221,7 @@ protected override bool OnKeyDown (Key key) { SelectedObject = map.ElementAt ((int)newIndex).Model; EnsureVisible (selectedObject); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1241,7 +1262,7 @@ public void RebuildTree () } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -1262,7 +1283,7 @@ public void RefreshObject (T o, bool startAtTop = false) { branch.Refresh (startAtTop); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1276,7 +1297,7 @@ public void Remove (T o) { roots.Remove (o); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); if (Equals (SelectedObject, o)) { @@ -1291,7 +1312,7 @@ public void ScrollDown () if (ScrollOffsetVertical <= ContentHeight - 2) { ScrollOffsetVertical++; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1301,7 +1322,7 @@ public void ScrollUp () if (scrollOffsetVertical > 0) { ScrollOffsetVertical--; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1323,7 +1344,7 @@ public void SelectAll () } multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map)); - SetNeedsDisplay (); + SetNeedsDraw (); OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } @@ -1368,7 +1389,7 @@ protected void CollapseImpl (T toCollapse, bool all) } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -1396,7 +1417,7 @@ protected virtual void CursorLeft (bool ctrl) { SelectedObject = parent; AdjustSelection (0); - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -1529,7 +1550,7 @@ private void AdjustSelectionToNext (Func, bool> predicate) { SelectedObject = map.ElementAt (idxCur).Model; EnsureVisible (map.ElementAt (idxCur).Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -1593,4 +1614,5 @@ public TreeSelection (Branch from, int toIndex, IReadOnlyCollection public Branch Origin { get; } public bool Contains (T model) { return included.Contains (model); } + } diff --git a/Terminal.Gui/Views/TreeViewTextFilter.cs b/Terminal.Gui/Views/TreeViewTextFilter.cs index ba9b190925..907c1b5dc7 100644 --- a/Terminal.Gui/Views/TreeViewTextFilter.cs +++ b/Terminal.Gui/Views/TreeViewTextFilter.cs @@ -51,6 +51,6 @@ public bool IsMatch (T model) private void RefreshTreeView () { _forTree.InvalidateLineMap (); - _forTree.SetNeedsDisplay (); + _forTree.SetNeedsDraw (); } } diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index b9bc49bd58..254aece7a2 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -546,9 +546,6 @@ private void UpdateButtonsAndTitle () SizeStep (CurrentStep); SetNeedsLayout (); - LayoutSubviews (); - - //Draw (); } private void Wizard_Closing (object sender, ToplevelClosingEventArgs obj) diff --git a/Terminal.Gui/Views/Wizard/WizardStep.cs b/Terminal.Gui/Views/Wizard/WizardStep.cs index eacb7c9dfe..e3d94f6b3e 100644 --- a/Terminal.Gui/Views/Wizard/WizardStep.cs +++ b/Terminal.Gui/Views/Wizard/WizardStep.cs @@ -29,7 +29,7 @@ public class WizardStep : View // OnTitleChanged (old, title); // } // base.Title = value; - // SetNeedsDisplay (); + // SetNeedsDraw (); // } //} @@ -73,7 +73,7 @@ public WizardStep () // if (helpTextView.TopRow != scrollBar.Position) { // scrollBar.Position = helpTextView.TopRow; // } - // helpTextView.SetNeedsDisplay (); + // helpTextView.SetNeedsDraw (); //}; //scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { @@ -81,7 +81,7 @@ public WizardStep () // if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) { // scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn; // } - // helpTextView.SetNeedsDisplay (); + // helpTextView.SetNeedsDraw (); //}; //scrollBar.VisibleChanged += (s,e) => { @@ -130,7 +130,7 @@ public string HelpText { _helpTextView.Text = value; ShowHide (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -158,7 +158,7 @@ public override View Add (View view) /// public override View Remove (View view) { - SetNeedsDisplay (); + SetNeedsDraw (); View container = view?.SuperView; if (container == this) diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 219c41cbe8..835e9828bc 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -381,6 +381,10 @@ False True True + LL + LR + UL + UR False <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> diff --git a/UICatalog/BenchmarkResults.cs b/UICatalog/BenchmarkResults.cs new file mode 100644 index 0000000000..050fcf5c43 --- /dev/null +++ b/UICatalog/BenchmarkResults.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json.Serialization; + +namespace UICatalog; + +public class BenchmarkResults +{ + [JsonInclude] + public string Scenario { get; set; } + + [JsonInclude] + public TimeSpan Duration { get; set; } + + [JsonInclude] + public int IterationCount { get; set; } = 0; + [JsonInclude] + public int ClearedContentCount { get; set; } = 0; + [JsonInclude] + public int RefreshedCount { get; set; } = 0; + [JsonInclude] + public int UpdatedCount { get; set; } = 0; + [JsonInclude] + public int DrawCompleteCount { get; set; } = 0; + + [JsonInclude] + public int LaidOutCount { get; set; } = 0; +} diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs index 94aa4f91a1..54e8f521dc 100644 --- a/UICatalog/KeyBindingsDialog.cs +++ b/UICatalog/KeyBindingsDialog.cs @@ -103,7 +103,7 @@ private void SetTextBoxToShowBinding (Command cmd) _keyLabel.Text = "Key: None"; } - SetNeedsDisplay (); + SetNeedsDraw (); } /// Tracks views as they are created in UICatalog so that their keybindings can be managed. diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index 041a5c22dc..7ed43499ad 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -11,7 +11,7 @@ "commandName": "Project", "commandLineArgs": "--driver WindowsDriver" }, - "WSL : UICatalog": { + "WSL: UICatalog": { "commandName": "Executable", "executablePath": "wsl", "commandLineArgs": "dotnet UICatalog.dll", @@ -23,56 +23,30 @@ "commandLineArgs": "dotnet UICatalog.dll --driver NetDriver", "distributionName": "" }, - "Sliders": { + "Benchmark All": { "commandName": "Project", - "commandLineArgs": "Sliders" + "commandLineArgs": "--benchmark" }, - "Wizards": { + "Benchmark All --driver NetDriver": { "commandName": "Project", - "commandLineArgs": "Wizards" + "commandLineArgs": "--driver NetDriver --benchmark" }, - "Dialogs": { - "commandName": "Project", - "commandLineArgs": "Dialogs" - }, - "Buttons": { - "commandName": "Project", - "commandLineArgs": "Buttons" - }, - "WizardAsView": { - "commandName": "Project", - "commandLineArgs": "WizardAsView" - }, - "CollectionNavigatorTester": { - "commandName": "Project", - "commandLineArgs": "\"Search Collection Nav\"" - }, - "Charmap": { - "commandName": "Project", - "commandLineArgs": "\"Character Map\"" - }, - "All Views Tester": { - "commandName": "Project", - "commandLineArgs": "\"All Views Tester\"" - }, - "Windows & FrameViews": { - "commandName": "Project", - "commandLineArgs": "\"Windows & FrameViews\"" + "WSL: Benchmark All": { + "commandName": "Executable", + "executablePath": "wsl", + "commandLineArgs": "dotnet UICatalog.dll --benchmark", + "distributionName": "" }, "Docker": { "commandName": "Docker" }, - "MenuBarScenario": { - "commandName": "Project", - "commandLineArgs": "MenuBar" - }, - "ListView & ComboBox": { + "All Views Tester": { "commandName": "Project", - "commandLineArgs": "\"ListView & ComboBox\"" + "commandLineArgs": "\"All Views Tester\" -b" }, - "Frames Demo": { + "Charmap": { "commandName": "Project", - "commandLineArgs": "\"Frames Demo\"" + "commandLineArgs": "Bars -b" } } } \ No newline at end of file diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index f0a03f5ce4..1d62c4ad7a 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -1,6 +1,8 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using Terminal.Gui; @@ -20,12 +22,12 @@ namespace UICatalog; /// /// /// Annotate the derived class with a -/// attribute specifying the scenario's name and description. +/// attribute specifying the scenario's name and description. /// /// /// /// -/// Add one or more attributes to the class specifying +/// Add one or more attributes to the class specifying /// which categories the scenario belongs to. If you don't specify a category the scenario will show up /// in "_All". /// @@ -82,7 +84,13 @@ namespace UICatalog; public class Scenario : IDisposable { private static int _maxScenarioNameLen = 30; - public string TopLevelColorScheme = "Base"; + public string TopLevelColorScheme { get; set; } = "Base"; + + public BenchmarkResults BenchmarkResults + { + get { return _benchmarkResults; } + } + private bool _disposedValue; /// @@ -114,16 +122,19 @@ public class Scenario : IDisposable /// public static ObservableCollection GetScenarios () { - List objects = new (); + List objects = []; foreach (Type type in typeof (Scenario).Assembly.ExportedTypes .Where ( - myType => myType.IsClass - && !myType.IsAbstract + myType => myType is { IsClass: true, IsAbstract: false } && myType.IsSubclassOf (typeof (Scenario)) )) { - var scenario = (Scenario)Activator.CreateInstance (type); + if (Activator.CreateInstance (type) is not Scenario { } scenario) + { + continue; + } + objects.Add (scenario); _maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1); } @@ -137,6 +148,136 @@ public static ObservableCollection GetScenarios () /// public virtual void Main () { } + private const uint MAX_NATURAL_ITERATIONS = 500; // not including needed for demo keys + private const uint ABORT_TIMEOUT_MS = 2500; + private const int DEMO_KEY_PACING_MS = 1; // Must be non-zero + + private readonly object _timeoutLock = new (); + private object? _timeout; + private Stopwatch? _stopwatch; + private readonly BenchmarkResults _benchmarkResults = new BenchmarkResults (); + + public void StartBenchmark () + { + BenchmarkResults.Scenario = GetName (); + Application.InitializedChanged += OnApplicationOnInitializedChanged; + } + + public BenchmarkResults EndBenchmark () + { + Application.InitializedChanged -= OnApplicationOnInitializedChanged; + + lock (_timeoutLock) + { + if (_timeout is { }) + { + _timeout = null; + } + } + + return _benchmarkResults; + } + + private List _demoKeys; + private int _currentDemoKey = 0; + + private void OnApplicationOnInitializedChanged (object? s, EventArgs a) + { + if (a.CurrentValue) + { + lock (_timeoutLock!) + { + _timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (ABORT_TIMEOUT_MS), ForceCloseCallback); + } + + Application.Iteration += OnApplicationOnIteration; + Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++; + Application.Driver!.Refreshed += (sender, args) => + { + BenchmarkResults.RefreshedCount++; + + if (args.CurrentValue) + { + BenchmarkResults.UpdatedCount++; + } + }; + Application.NotifyNewRunState += OnApplicationNotifyNewRunState; + + + _stopwatch = Stopwatch.StartNew (); + } + else + { + Application.NotifyNewRunState -= OnApplicationNotifyNewRunState; + Application.Iteration -= OnApplicationOnIteration; + BenchmarkResults.Duration = _stopwatch!.Elapsed; + _stopwatch?.Stop (); + } + } + + private void OnApplicationOnIteration (object? s, IterationEventArgs a) + { + BenchmarkResults.IterationCount++; + if (BenchmarkResults.IterationCount > MAX_NATURAL_ITERATIONS + (_demoKeys.Count* DEMO_KEY_PACING_MS)) + { + Application.RequestStop (); + } + } + + private void OnApplicationNotifyNewRunState (object? sender, RunStateEventArgs e) + { + SubscribeAllSubviews (Application.Top!); + + _currentDemoKey = 0; + _demoKeys = GetDemoKeyStrokes (); + + Application.AddTimeout ( + new TimeSpan (0, 0, 0, 0, DEMO_KEY_PACING_MS), + () => + { + if (_currentDemoKey >= _demoKeys.Count) + { + return false; + } + + Application.RaiseKeyDownEvent (_demoKeys [_currentDemoKey++]); + + return true; + }); + + return; + + // Get a list of all subviews under Application.Top (and their subviews, etc.) + // and subscribe to their DrawComplete event + void SubscribeAllSubviews (View view) + { + view.DrawComplete += (s, a) => BenchmarkResults.DrawCompleteCount++; + view.SubviewsLaidOut += (s, a) => BenchmarkResults.LaidOutCount++; + foreach (View subview in view.Subviews) + { + SubscribeAllSubviews (subview); + } + } + } + + // If the scenario doesn't close within the abort time, this will force it to quit + private bool ForceCloseCallback () + { + lock (_timeoutLock) + { + if (_timeout is { }) + { + _timeout = null; + } + } + + Debug.WriteLine ($@" Failed to Quit with {Application.QuitKey} after {ABORT_TIMEOUT_MS}ms and {BenchmarkResults.IterationCount} iterations. Force quit."); + + Application.RequestStop (); + + return false; + } + /// Gets the Scenario Name + Description with the Description padded based on the longest known Scenario name. /// public override string ToString () { return $"{GetName ().PadRight (_maxScenarioNameLen)}{GetDescription ()}"; } @@ -170,8 +311,7 @@ internal static ObservableCollection GetAllCategories () aCategories = typeof (Scenario).Assembly.GetTypes () .Where ( - myType => myType.IsClass - && !myType.IsAbstract + myType => myType is { IsClass: true, IsAbstract: false } && myType.IsSubclassOf (typeof (Scenario))) .Select (type => System.Attribute.GetCustomAttributes (type).ToList ()) .Aggregate ( @@ -189,51 +329,9 @@ internal static ObservableCollection GetAllCategories () categories.Insert (0, "All Scenarios"); return categories; - } - /// Defines the category names used to categorize a - [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] - public class ScenarioCategory (string name) : System.Attribute - { - /// Static helper function to get the Categories given a Type - /// - /// list of category names - public static List GetCategories (Type t) - { - return GetCustomAttributes (t) - .ToList () - .Where (a => a is ScenarioCategory) - .Select (a => ((ScenarioCategory)a).Name) - .ToList (); - } - - /// Static helper function to get the Name given a Type - /// - /// Name of the category - public static string GetName (Type t) { return ((ScenarioCategory)GetCustomAttributes (t) [0]).Name; } - - /// Category Name - public string Name { get; set; } = name; } - /// Defines the metadata (Name and Description) for a - [AttributeUsage (AttributeTargets.Class)] - public class ScenarioMetadata (string name, string description) : System.Attribute - { - /// Description - public string Description { get; set; } = description; + public virtual List GetDemoKeyStrokes () => new List (); - /// Static helper function to get the Description given a Type - /// - /// - public static string GetDescription (Type t) { return ((ScenarioMetadata)GetCustomAttributes (t) [0]).Description; } - - /// Static helper function to get the Name given a Type - /// - /// - public static string GetName (Type t) { return ((ScenarioMetadata)GetCustomAttributes (t) [0]).Name; } - - /// Name - public string Name { get; set; } = name; - } -} +} \ No newline at end of file diff --git a/UICatalog/ScenarioCategory.cs b/UICatalog/ScenarioCategory.cs new file mode 100644 index 0000000000..3429744052 --- /dev/null +++ b/UICatalog/ScenarioCategory.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +namespace UICatalog; + +/// Defines the category names used to categorize a +[AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] +public class ScenarioCategory (string name) : System.Attribute +{ + /// Static helper function to get the Categories given a Type + /// + /// list of category names + public static List GetCategories (Type t) + { + return GetCustomAttributes (t) + .ToList () + .Where (a => a is ScenarioCategory) + .Select (a => ((ScenarioCategory)a).Name) + .ToList (); + } + + /// Static helper function to get the Name given a Type + /// + /// Name of the category + public static string GetName (Type t) + { + if (GetCustomAttributes (t).FirstOrDefault (a => a is ScenarioMetadata) is ScenarioMetadata { } metadata) + { + return metadata.Name; + } + + return string.Empty; + } + + /// Category Name + public string Name { get; set; } = name; +} diff --git a/UICatalog/ScenarioMetadata.cs b/UICatalog/ScenarioMetadata.cs new file mode 100644 index 0000000000..0e1ef4b164 --- /dev/null +++ b/UICatalog/ScenarioMetadata.cs @@ -0,0 +1,42 @@ +#nullable enable +using System; +using System.Linq; + +namespace UICatalog; + +/// Defines the metadata (Name and Description) for a +[AttributeUsage (AttributeTargets.Class)] +public class ScenarioMetadata (string name, string description) : System.Attribute +{ + /// Description + public string Description { get; set; } = description; + + /// Static helper function to get the Description given a Type + /// + /// + public static string GetDescription (Type t) + { + if (GetCustomAttributes (t).FirstOrDefault (a => a is ScenarioMetadata) is ScenarioMetadata { } metadata) + { + return metadata.Description; + } + + return string.Empty; + } + + /// Static helper function to get the Name given a Type + /// + /// + public static string GetName (Type t) + { + if (GetCustomAttributes (t).FirstOrDefault (a => a is ScenarioMetadata) is ScenarioMetadata { } metadata) + { + return metadata.Name; + } + + return string.Empty; + } + + /// Name + public string Name { get; set; } = name; +} diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs index 45889f3e8b..d7e181eae1 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -111,7 +111,7 @@ public void CustomInitialize () Add (_border, _fill, title); } - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView) { if (newHasFocus) { diff --git a/UICatalog/Scenarios/Adornments.cs b/UICatalog/Scenarios/Adornments.cs index d4d54b819f..55feb28ece 100644 --- a/UICatalog/Scenarios/Adornments.cs +++ b/UICatalog/Scenarios/Adornments.cs @@ -1,4 +1,5 @@ -using Terminal.Gui; +using System; +using Terminal.Gui; namespace UICatalog.Scenarios; @@ -13,7 +14,8 @@ public override void Main () Window app = new () { - Title = GetQuitKeyAndName () + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None }; var editor = new AdornmentsEditor @@ -34,9 +36,8 @@ public override void Main () Title = "The _Window", Arrangement = ViewArrangement.Movable, - // X = Pos.Center (), - Width = Dim.Percent (60), - Height = Dim.Percent (90) + Width = Dim.Fill (Dim.Func (() => editor.Frame.Width )), + Height = Dim.Fill () }; app.Add (window); @@ -79,17 +80,21 @@ public override void Main () Width = 40, Height = Dim.Percent (20), Text = "Label\nY=AnchorEnd(),Height=Dim.Percent(10)", - ColorScheme = Colors.ColorSchemes ["Error"] + ColorScheme = Colors.ColorSchemes ["Dialog"] }; window.Margin.Data = "Margin"; - window.Margin.Thickness = new (3); + window.Margin.Text = "Margin Text"; + window.Margin.Thickness = new (0); window.Border.Data = "Border"; - window.Border.Thickness = new (3); + window.Border.Text = "Border Text"; + window.Border.Thickness = new (0); window.Padding.Data = "Padding"; + window.Padding.Text = "Padding Text line 1\nPadding Text line 3\nPadding Text line 3\nPadding Text line 4\nPadding Text line 5"; window.Padding.Thickness = new (3); + window.Padding.ColorScheme = Colors.ColorSchemes ["Error"]; window.Padding.CanFocus = true; var longLabel = new Label @@ -99,18 +104,20 @@ public override void Main () longLabel.TextFormatter.WordWrap = true; window.Add (tf1, color, button, label, btnButtonInWindow, labelAnchorEnd, longLabel); - editor.Initialized += (s, e) => { editor.ViewToEdit = window; }; - window.Initialized += (s, e) => { - var labelInPadding = new Label { X = 1, Y = 0, Title = "_Text:" }; + editor.ViewToEdit = window; + + editor.ShowViewIdentifier = true; + + var labelInPadding = new Label { X = 0, Y = 1, Title = "_Text:" }; window.Padding.Add (labelInPadding); var textFieldInPadding = new TextField { X = Pos.Right (labelInPadding) + 1, - Y = Pos.Top (labelInPadding), Width = 15, - Text = "some text", + Y = Pos.Top (labelInPadding), Width = 10, + Text = "text (Y = 1)", CanFocus = true }; textFieldInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "TextField", textFieldInPadding.Text, "Ok"); @@ -119,9 +126,10 @@ public override void Main () var btnButtonInPadding = new Button { X = Pos.Center (), - Y = 0, - Text = "_Button in Padding", - CanFocus = true + Y = 1, + Text = "_Button in Padding Y = 1", + CanFocus = true, + HighlightStyle = HighlightStyle.None, }; btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInPadding.BorderStyle = LineStyle.Dashed; diff --git a/UICatalog/Scenarios/AdornmentsEditor.cs b/UICatalog/Scenarios/AdornmentsEditor.cs deleted file mode 100644 index d50714c5fc..0000000000 --- a/UICatalog/Scenarios/AdornmentsEditor.cs +++ /dev/null @@ -1,232 +0,0 @@ -#nullable enable -using System; -using System.Text; -using Terminal.Gui; - -namespace UICatalog.Scenarios; - -/// -/// Provides an editor UI for the Margin, Border, and Padding of a View. -/// -public class AdornmentsEditor : View -{ - public AdornmentsEditor () - { - //ColorScheme = Colors.ColorSchemes ["Dialog"]; - Title = "AdornmentsEditor"; - - Width = Dim.Auto (DimAutoStyle.Content); - Height = Dim.Auto (DimAutoStyle.Content); - - //SuperViewRendersLineCanvas = true; - - CanFocus = true; - - TabStop = TabBehavior.TabGroup; - - _expandButton = new () - { - Orientation = Orientation.Horizontal - }; - - Initialized += AdornmentsEditor_Initialized; - } - - private readonly ViewDiagnosticFlags _savedDiagnosticFlags = Diagnostics; - private View? _viewToEdit; - - private Label? _lblView; // Text describing the vi - - private MarginEditor? _marginEditor; - private BorderEditor? _borderEditor; - private PaddingEditor? _paddingEditor; - - // TODO: Move Diagnostics to a separate Editor class (DiagnosticsEditor?). - private CheckBox? _diagPaddingCheckBox; - private CheckBox? _diagRulerCheckBox; - - /// - /// Gets or sets whether the AdornmentsEditor should automatically select the View to edit - /// based on the values of and . - /// - public bool AutoSelectViewToEdit { get; set; } - - /// - /// Gets or sets the View that will scope the behavior of . - /// - public View? AutoSelectSuperView { get; set; } - - /// - /// Gets or sets whether auto select with the mouse will select Adornments or just Views. - /// - public bool AutoSelectAdornments { get; set; } - - public View? ViewToEdit - { - get => _viewToEdit; - set - { - if (_viewToEdit == value) - { - return; - } - - _viewToEdit = value; - - if (_viewToEdit is not Adornment) - { - _marginEditor!.AdornmentToEdit = _viewToEdit?.Margin ?? null; - _borderEditor!.AdornmentToEdit = _viewToEdit?.Border ?? null; - _paddingEditor!.AdornmentToEdit = _viewToEdit?.Padding ?? null; - } - - if (_lblView is { }) - { - _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; - } - } - } - - - private void NavigationOnFocusedChanged (object? sender, EventArgs e) - { - if (AutoSelectSuperView is null) - { - return; - } - - if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ())) - { - return; - } - - if (!ApplicationNavigation.IsInHierarchy (AutoSelectSuperView, Application.Navigation!.GetFocused ())) - { - return; - } - - ViewToEdit = Application.Navigation!.GetFocused (); - } - - private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) - { - if (e.Flags != MouseFlags.Button1Clicked || !AutoSelectViewToEdit) - { - return; - } - - if ((AutoSelectSuperView is { } && !AutoSelectSuperView.FrameToScreen ().Contains (e.Position)) - || FrameToScreen ().Contains (e.Position)) - { - return; - } - - View view = e.View; - - if (view is { }) - { - if (view is Adornment adornment) - { - ViewToEdit = AutoSelectAdornments ? adornment : adornment.Parent; - } - else - { - ViewToEdit = view; - } - } - } - - /// - protected override void Dispose (bool disposing) - { - Diagnostics = _savedDiagnosticFlags; - base.Dispose (disposing); - } - - private readonly ExpanderButton? _expandButton; - - public ExpanderButton? ExpandButton => _expandButton; - - private void AdornmentsEditor_Initialized (object? sender, EventArgs e) - { - BorderStyle = LineStyle.Dotted; - - Border.Add (_expandButton!); - - _lblView = new () - { - X = 0, - Y = 0, - Height = 2 - }; - _lblView.TextFormatter.WordWrap = true; - _lblView.TextFormatter.MultiLine = true; - _lblView.HotKeySpecifier = (Rune)'\uffff'; - Add (_lblView); - - _marginEditor = new () - { - X = 0, - Y = Pos.Bottom (_lblView), - SuperViewRendersLineCanvas = true - }; - Add (_marginEditor); - - _lblView.Width = Dim.Width (_marginEditor); - - _borderEditor = new () - { - X = Pos.Left (_marginEditor), - Y = Pos.Bottom (_marginEditor), - SuperViewRendersLineCanvas = true - }; - Add (_borderEditor); - - _paddingEditor = new () - { - X = Pos.Left (_borderEditor), - Y = Pos.Bottom (_borderEditor), - SuperViewRendersLineCanvas = true - }; - Add (_paddingEditor); - - _diagPaddingCheckBox = new () { Text = "_Diagnostic Padding" }; - _diagPaddingCheckBox.CheckedState = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Padding) ? CheckState.Checked : CheckState.UnChecked; - - _diagPaddingCheckBox.CheckedStateChanging += (s, e) => - { - if (e.NewValue == CheckState.Checked) - { - Diagnostics |= ViewDiagnosticFlags.Padding; - } - else - { - Diagnostics &= ~ViewDiagnosticFlags.Padding; - } - }; - - Add (_diagPaddingCheckBox); - _diagPaddingCheckBox.Y = Pos.Bottom (_paddingEditor); - - _diagRulerCheckBox = new () { Text = "_Diagnostic Ruler" }; - _diagRulerCheckBox.CheckedState = Diagnostics.FastHasFlags (ViewDiagnosticFlags.Ruler) ? CheckState.Checked : CheckState.UnChecked; - - _diagRulerCheckBox.CheckedStateChanging += (s, e) => - { - if (e.NewValue == CheckState.Checked) - { - Diagnostics |= ViewDiagnosticFlags.Ruler; - } - else - { - Diagnostics &= ~ViewDiagnosticFlags.Ruler; - } - }; - - Add (_diagRulerCheckBox); - _diagRulerCheckBox.Y = Pos.Bottom (_diagPaddingCheckBox); - - Application.MouseEvent += ApplicationOnMouseEvent; - Application.Navigation!.FocusedChanged += NavigationOnFocusedChanged; - } -} diff --git a/UICatalog/Scenarios/AdvancedClipping.cs b/UICatalog/Scenarios/AdvancedClipping.cs new file mode 100644 index 0000000000..8408867027 --- /dev/null +++ b/UICatalog/Scenarios/AdvancedClipping.cs @@ -0,0 +1,166 @@ +using System.Text; +using System.Timers; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("AdvancedClipping", "AdvancedClipping Tester")] +[ScenarioCategory ("AdvancedClipping")] +public class AdvancedClipping : Scenario +{ + private int _hotkeyCount; + + public override void Main () + { + Application.Init (); + + Window app = new () + { + Title = GetQuitKeyAndName (), + //BorderStyle = LineStyle.None + }; + + app.DrawingContent += (s, e) => + { + app!.FillRect (app!.Viewport, CM.Glyphs.Dot); + e.Cancel = true; + }; + + var arrangementEditor = new ArrangementEditor () + { + X = Pos.AnchorEnd (), + Y = 0, + AutoSelectViewToEdit = true, + }; + app.Add (arrangementEditor); + + View tiledView1 = CreateTiledView (1, 0, 0); + + tiledView1.Width = 30; + + ProgressBar tiledProgressBar1 = new () + { + X = 0, + Y = Pos.AnchorEnd (), + Width = Dim.Fill (), + Id = "tiledProgressBar", + BidirectionalMarquee = true, + }; + tiledView1.Add (tiledProgressBar1); + + View tiledView2 = CreateTiledView (2, 4, 2); + + ProgressBar tiledProgressBar2 = new () + { + X = 0, + Y = Pos.AnchorEnd (), + Width = Dim.Fill (), + Id = "tiledProgressBar", + BidirectionalMarquee = true, + ProgressBarStyle = ProgressBarStyle.MarqueeBlocks + // BorderStyle = LineStyle.Rounded + }; + tiledView2.Add (tiledProgressBar2); + + app.Add (tiledView1); + app.Add (tiledView2); + + View tiledView3 = CreateTiledView (3, 8, 4); + app.Add (tiledView3); + + // View overlappedView1 = CreateOverlappedView (1, 30, 2); + + //ProgressBar progressBar = new () + //{ + // X = Pos.AnchorEnd (), + // Y = Pos.AnchorEnd (), + // Width = Dim.Fill (), + // Id = "progressBar", + // BorderStyle = LineStyle.Rounded + //}; + //overlappedView1.Add (progressBar); + + + //View overlappedView2 = CreateOverlappedView (2, 32, 4); + //View overlappedView3 = CreateOverlappedView (3, 34, 6); + + //app.Add (overlappedView1); + //app.Add (overlappedView2); + //app.Add (overlappedView3); + + Timer progressTimer = new Timer (150) + { + AutoReset = true + }; + + progressTimer.Elapsed += (s, e) => + { + tiledProgressBar1.Pulse (); + tiledProgressBar2.Pulse (); + Application.Wakeup (); + }; + + progressTimer.Start (); + Application.Run (app); + progressTimer.Stop (); + app.Dispose (); + Application.Shutdown (); + + return; + } + + private View CreateOverlappedView (int id, Pos x, Pos y) + { + var overlapped = new View + { + X = x, + Y = y, + Height = Dim.Auto (minimumContentDim: 4), + Width = Dim.Auto (minimumContentDim: 14), + Title = $"Overlapped{id} _{GetNextHotKey ()}", + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Id = $"Overlapped{id}", + ShadowStyle = ShadowStyle.Transparent, + BorderStyle = LineStyle.Double, + CanFocus = true, // Can't drag without this? BUGBUG + TabStop = TabBehavior.TabGroup, + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped | ViewArrangement.Resizable + }; + return overlapped; + } + + private View CreateTiledView (int id, Pos x, Pos y) + { + var tiled = new View + { + X = x, + Y = y, + Height = Dim.Auto (minimumContentDim: 8), + Width = Dim.Auto (minimumContentDim: 15), + Title = $"Tiled{id} _{GetNextHotKey ()}", + Id = $"Tiled{id}", + Text = $"Tiled{id}", + BorderStyle = LineStyle.Single, + CanFocus = true, // Can't drag without this? BUGBUG + TabStop = TabBehavior.TabStop, + Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, + ShadowStyle = ShadowStyle.Transparent, + }; + //tiled.Padding.Thickness = new (1); + //tiled.Padding.Diagnostics = ViewDiagnosticFlags.Thickness; + + //tiled.Margin.Thickness = new (1); + + FrameView fv = new () + { + Title = "FrameView", + Width = 15, + Height = 3, + }; + tiled.Add (fv); + + return tiled; + } + + private char GetNextHotKey () { return (char)('A' + _hotkeyCount++); } +} diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index 026a183836..6130c5af57 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -1,9 +1,8 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Reflection; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -13,78 +12,58 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Tests")] [ScenarioCategory ("Controls")] [ScenarioCategory ("Adornments")] +[ScenarioCategory ("Arrangement")] public class AllViewsTester : Scenario { - private readonly List _dimNames = new () { "Auto", "Percent", "Fill", "Absolute" }; - - // TODO: This is missing some - private readonly List _posNames = new () { "Percent", "AnchorEnd", "Center", "Absolute" }; - private ListView _classListView; - private View _curView; - private FrameView _hostPane; - private AdornmentsEditor _adornmentsEditor; - private RadioGroup _hRadioGroup; - private TextField _hText; - private int _hVal; - private FrameView _leftPane; - private FrameView _locationFrame; - - // Settings - private FrameView _settingsPane; - private FrameView _sizeFrame; - private Dictionary _viewClasses; - private RadioGroup _wRadioGroup; - private TextField _wText; - private int _wVal; - private RadioGroup _xRadioGroup; - private TextField _xText; - private int _xVal; - private RadioGroup _yRadioGroup; - private TextField _yText; - private int _yVal; - private RadioGroup _orientation; + private Dictionary? _viewClasses; + private ListView? _classListView; + private AdornmentsEditor? _adornmentsEditor; + + private ArrangementEditor? _arrangementEditor; + + private LayoutEditor? _layoutEditor; + private FrameView? _settingsPane; + private RadioGroup? _orientation; private string _demoText = "This, that, and the other thing."; - private TextView _demoTextView; + private TextView? _demoTextView; + + private FrameView? _hostPane; + private View? _curView; + private EventLog? _eventLog; public override void Main () { // Don't create a sub-win (Scenario.Win); just use Application.Top Application.Init (); - // ConfigurationManager.Apply (); var app = new Window { Title = GetQuitKeyAndName (), - ColorScheme = Colors.ColorSchemes ["TopLevel"] + ColorScheme = Colors.ColorSchemes ["TopLevel"], }; + // Set the BorderStyle we use for all subviews, but disable the app border thickness + app.Border!.LineStyle = LineStyle.Heavy; + app.Border.Thickness = new (0); + _viewClasses = GetAllViewClassesCollection () .OrderBy (t => t.Name) .Select (t => new KeyValuePair (t.Name, t)) .ToDictionary (t => t.Key, t => t.Value); - _leftPane = new () - { - X = 0, - Y = 0, - Width = Dim.Auto (DimAutoStyle.Content), - Height = Dim.Fill (), - CanFocus = true, - ColorScheme = Colors.ColorSchemes ["TopLevel"], - Title = "Classes" - }; - _classListView = new () { + Title = "Classes [_1]", X = 0, Y = 0, Width = Dim.Auto (), Height = Dim.Fill (), AllowsMarking = false, - ColorScheme = Colors.ColorSchemes ["TopLevel"], SelectedItem = 0, - Source = new ListWrapper (new (_viewClasses.Keys.ToList ())) + Source = new ListWrapper (new (_viewClasses.Keys.ToList ())), + SuperViewRendersLineCanvas = true }; + _classListView.Border!.Thickness = new (1); _classListView.SelectedItemChanged += (s, args) => { @@ -101,171 +80,74 @@ public override void Main () _adornmentsEditor.ViewToEdit = _curView; } }; - _leftPane.Add (_classListView); + + _classListView.Accepting += (sender, args) => + { + _curView?.SetFocus (); + args.Cancel = true; + }; _adornmentsEditor = new () { - X = Pos.Right (_leftPane), + Title = "Adornments [_2]", + X = Pos.Right (_classListView) - 1, Y = 0, Width = Dim.Auto (), - Height = Dim.Fill (), - ColorScheme = Colors.ColorSchemes ["TopLevel"], - BorderStyle = LineStyle.Single, - AutoSelectViewToEdit = true, + Height = Dim.Auto (), + AutoSelectViewToEdit = false, AutoSelectAdornments = false, + SuperViewRendersLineCanvas = true, }; + _adornmentsEditor.Border!.Thickness = new (1); + _adornmentsEditor.ExpanderButton!.Orientation = Orientation.Horizontal; + _adornmentsEditor.ExpanderButton.Enabled = false; - var expandButton = new ExpanderButton - { - CanFocus = false, - Orientation = Orientation.Horizontal - }; - _adornmentsEditor.Border.Add (expandButton); - - _settingsPane = new () + _arrangementEditor = new () { - X = Pos.Right (_adornmentsEditor), - Y = 0, // for menu - Width = Dim.Fill (), - Height = Dim.Auto (), - CanFocus = true, - ColorScheme = Colors.ColorSchemes ["TopLevel"], - Title = "Settings" + Title = "Arrangement [_3]", + X = Pos.Right (_classListView) - 1, + Y = Pos.Bottom (_adornmentsEditor) - Pos.Func (() => _adornmentsEditor.Frame.Height == 1 ? 0 : 1), + Width = Dim.Width (_adornmentsEditor), + Height = Dim.Fill (), + AutoSelectViewToEdit = false, + AutoSelectAdornments = false, + SuperViewRendersLineCanvas = true }; + _arrangementEditor.ExpanderButton!.Orientation = Orientation.Horizontal; - string [] radioItems = { "_Percent(x)", "_AnchorEnd", "_Center", "A_bsolute(x)" }; + _arrangementEditor.ExpanderButton.CollapsedChanging += (sender, args) => + { + _adornmentsEditor.ExpanderButton.Collapsed = args.NewValue; + }; + _arrangementEditor.Border!.Thickness = new (1); - _locationFrame = new () + _layoutEditor = new () { - X = 0, + Title = "Layout [_4]", + X = Pos.Right (_arrangementEditor) - 1, Y = 0, + //Width = Dim.Fill (), // set below Height = Dim.Auto (), - Width = Dim.Auto (), - Title = "Location (Pos)", - TabStop = TabBehavior.TabStop, + CanFocus = true, + AutoSelectViewToEdit = false, + AutoSelectAdornments = false, + SuperViewRendersLineCanvas = true }; - _settingsPane.Add (_locationFrame); - - var label = new Label { X = 0, Y = 0, Text = "X:" }; - _locationFrame.Add (label); - _xRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = radioItems }; - _xRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; - _xText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_xVal}" }; - - _xText.Accepting += (s, args) => - { - try - { - _xVal = int.Parse (_xText.Text); - DimPosChanged (_curView); - } - catch - { } - }; - _locationFrame.Add (_xText); - - _locationFrame.Add (_xRadioGroup); - - radioItems = new [] { "P_ercent(y)", "A_nchorEnd", "C_enter", "Absolute(_y)" }; - label = new () { X = Pos.Right (_xRadioGroup) + 1, Y = 0, Text = "Y:" }; - _locationFrame.Add (label); - _yText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_yVal}" }; - - _yText.Accepting += (s, args) => - { - try - { - _yVal = int.Parse (_yText.Text); - DimPosChanged (_curView); - } - catch - { } - }; - _locationFrame.Add (_yText); - _yRadioGroup = new () { X = Pos.X (label), Y = Pos.Bottom (label), RadioLabels = radioItems }; - _yRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; - _locationFrame.Add (_yRadioGroup); - - _sizeFrame = new () + _layoutEditor.Border!.Thickness = new (1); + + _settingsPane = new () { - X = Pos.Right (_locationFrame), - Y = Pos.Y (_locationFrame), + Title = "Settings [_5]", + X = Pos.Right (_adornmentsEditor) - 1, + Y = Pos.Bottom (_layoutEditor) - Pos.Func (() => _layoutEditor.Frame.Height == 1 ? 0 : 1), + Width = Dim.Width (_layoutEditor), Height = Dim.Auto (), - Width = Dim.Auto (), - Title = "Size (Dim)", - TabStop = TabBehavior.TabStop, + CanFocus = true, + SuperViewRendersLineCanvas = true }; + _settingsPane.Border!.Thickness = new (1, 1, 1, 0); - radioItems = new [] { "Auto", "_Percent(width)", "_Fill(width)", "A_bsolute(width)" }; - label = new () { X = 0, Y = 0, Text = "Width:" }; - _sizeFrame.Add (label); - _wRadioGroup = new () { X = 0, Y = Pos.Bottom (label), RadioLabels = radioItems }; - _wRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; - _wText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_wVal}" }; - - _wText.Accepting += (s, args) => - { - try - { - switch (_wRadioGroup.SelectedItem) - { - case 1: - _wVal = Math.Min (int.Parse (_wText.Text), 100); - - break; - case 0: - case 2: - case 3: - _wVal = int.Parse (_wText.Text); - - break; - } - - DimPosChanged (_curView); - } - catch - { } - }; - _sizeFrame.Add (_wText); - _sizeFrame.Add (_wRadioGroup); - - radioItems = new [] { "_Auto", "P_ercent(height)", "F_ill(height)", "Ab_solute(height)" }; - label = new () { X = Pos.Right (_wRadioGroup) + 1, Y = 0, Text = "Height:" }; - _sizeFrame.Add (label); - _hText = new () { X = Pos.Right (label) + 1, Y = 0, Width = 4, Text = $"{_hVal}" }; - - _hText.Accepting += (s, args) => - { - try - { - switch (_hRadioGroup.SelectedItem) - { - case 1: - _hVal = Math.Min (int.Parse (_hText.Text), 100); - - break; - case 0: - case 2: - case 3: - _hVal = int.Parse (_hText.Text); - - break; - } - - DimPosChanged (_curView); - } - catch - { } - }; - _sizeFrame.Add (_hText); - - _hRadioGroup = new () { X = Pos.X (label), Y = Pos.Bottom (label), RadioLabels = radioItems }; - _hRadioGroup.SelectedItemChanged += OnRadioGroupOnSelectedItemChanged; - _sizeFrame.Add (_hRadioGroup); - - _settingsPane.Add (_sizeFrame); - - label = new () { X = 0, Y = Pos.Bottom (_sizeFrame), Text = "_Orientation:" }; + Label label = new () { X = 0, Y = 0, Text = "_Orientation:" }; _orientation = new () { @@ -307,38 +189,73 @@ public override void Main () _settingsPane.Add (label, _demoTextView); + _eventLog = new () + { + // X = Pos.Right(_layoutEditor), + SuperViewRendersLineCanvas = true + }; + _eventLog.Border!.Thickness = new (1); + _eventLog.X = Pos.AnchorEnd () - 1; + _eventLog.Y = 0; + + _eventLog.Height = Dim.Height (_classListView); + + //_eventLog.Width = 30; + + _layoutEditor.Width = Dim.Fill ( + Dim.Func ( + () => + { + if (_eventLog.NeedsLayout) + { + // We have two choices: + // 1) Call Layout explicitly + // 2) Throw LayoutException so Layout tries again + _eventLog.Layout (); + //throw new LayoutException ("_eventLog"); + } + + return _eventLog.Frame.Width; + })); + _hostPane = new () { + Id = "_hostPane", X = Pos.Right (_adornmentsEditor), Y = Pos.Bottom (_settingsPane), - Width = Dim.Fill (), - Height = Dim.Fill (), // + 1 for status bar + Width = Dim.Width (_layoutEditor) - 2, + Height = Dim.Fill (), CanFocus = true, - TabStop = TabBehavior.TabGroup, - ColorScheme = Colors.ColorSchemes ["Dialog"] + TabStop = TabBehavior.TabStop, + ColorScheme = Colors.ColorSchemes ["Base"], + Arrangement = ViewArrangement.LeftResizable | ViewArrangement.BottomResizable | ViewArrangement.RightResizable, + BorderStyle = LineStyle.Double, + SuperViewRendersLineCanvas = true }; + _hostPane.Border!.ColorScheme = app.ColorScheme; + _hostPane.Padding!.Thickness = new (1); + _hostPane.Padding.Diagnostics = ViewDiagnosticFlags.Ruler; + _hostPane.Padding.ColorScheme = app.ColorScheme; - _hostPane.LayoutStarted += (sender, args) => - { - - }; + app.Add (_classListView, _adornmentsEditor, _arrangementEditor, _layoutEditor, _settingsPane, _eventLog, _hostPane); - app.Add (_leftPane, _adornmentsEditor, _settingsPane, _hostPane); - - _classListView.SelectedItem = 0; - _leftPane.SetFocus (); + app.Initialized += App_Initialized; Application.Run (app); app.Dispose (); Application.Shutdown (); } - private void OnRadioGroupOnSelectedItemChanged (object s, SelectedItemChangedArgs selected) { DimPosChanged (_curView); } + private void App_Initialized (object? sender, EventArgs e) + { + _classListView!.SelectedItem = 0; + _classListView.SetFocus (); + } // TODO: Add Command.HotKey handler (pop a message box?) private void CreateCurrentView (Type type) { - Debug.Assert(_curView is null); + Debug.Assert (_curView is null); // If we are to create a generic Type if (type.IsGenericType) @@ -357,7 +274,8 @@ private void CreateCurrentView (Type type) } // Instantiate view - var view = (View)Activator.CreateInstance (type); + var view = (View)Activator.CreateInstance (type)!; + _eventLog!.ViewToLog = view; if (view is IDesignable designable) { @@ -371,20 +289,24 @@ private void CreateCurrentView (Type type) if (view is IOrientation orientatedView) { - _orientation.SelectedItem = (int)orientatedView.Orientation; + _orientation!.SelectedItem = (int)orientatedView.Orientation; _orientation.Enabled = true; } else { - _orientation.Enabled = false; + _orientation!.Enabled = false; } view.Initialized += CurrentView_Initialized; - view.LayoutComplete += CurrentView_LayoutComplete; + view.SubviewsLaidOut += CurrentView_LayoutComplete; + view.Id = "_curView"; _curView = view; - _hostPane.Add (_curView); - // Application.Refresh(); + + _hostPane!.Add (_curView); + _layoutEditor!.ViewToEdit = _curView; + _arrangementEditor!.ViewToEdit = _curView; + _curView.SetNeedsLayout (); } private void DisposeCurrentView () @@ -392,184 +314,62 @@ private void DisposeCurrentView () if (_curView != null) { _curView.Initialized -= CurrentView_Initialized; - _curView.LayoutComplete -= CurrentView_LayoutComplete; - _hostPane.Remove (_curView); + _curView.SubviewsLaidOut -= CurrentView_LayoutComplete; + _hostPane!.Remove (_curView); + _layoutEditor!.ViewToEdit = null; + _arrangementEditor!.ViewToEdit = null; + _curView.Dispose (); _curView = null; } } - private void DimPosChanged (View view) + private static List GetAllViewClassesCollection () { - if (view == null || _updatingSettings) - { - return; - } - - try - { - view.X = _xRadioGroup.SelectedItem switch - { - 0 => Pos.Percent (_xVal), - 1 => Pos.AnchorEnd (), - 2 => Pos.Center (), - 3 => Pos.Absolute (_xVal), - _ => view.X - }; - - view.Y = _yRadioGroup.SelectedItem switch - { - 0 => Pos.Percent (_yVal), - 1 => Pos.AnchorEnd (), - 2 => Pos.Center (), - 3 => Pos.Absolute (_yVal), - _ => view.Y - }; - - view.Width = _wRadioGroup.SelectedItem switch - { - 0 => Dim.Auto (), - 1 => Dim.Percent (_wVal), - 2 => Dim.Fill (_wVal), - 3 => Dim.Absolute (_wVal), - _ => view.Width - }; - - view.Height = _hRadioGroup.SelectedItem switch - { - 0 => Dim.Auto (), - 1 => Dim.Percent (_hVal), - 2 => Dim.Fill (_hVal), - 3 => Dim.Absolute (_hVal), - _ => view.Height - }; - } - catch (Exception e) - { - MessageBox.ErrorQuery ("Exception", e.Message, "Ok"); - } - - if (view.Width is DimAuto) - { - _wText.Text = "Auto"; - _wText.Enabled = false; - } - else - { - _wText.Text = $"{_wVal}"; - _wText.Enabled = true; - } - - if (view.Height is DimAuto) - { - _hText.Text = "Auto"; - _hText.Enabled = false; - } - else - { - _hText.Text = $"{_hVal}"; - _hText.Enabled = true; - } - - UpdateHostTitle (view); - } - - private List GetAllViewClassesCollection () - { - List types = new (); - - foreach (Type type in typeof (View).Assembly.GetTypes () - .Where ( - myType => - myType.IsClass && !myType.IsAbstract && myType.IsPublic && myType.IsSubclassOf (typeof (View)) - )) - { - types.Add (type); - } + List types = typeof (View).Assembly.GetTypes () + .Where ( + myType => myType is { IsClass: true, IsAbstract: false, IsPublic: true } + && myType.IsSubclassOf (typeof (View))) + .ToList (); types.Add (typeof (View)); return types; } - private void CurrentView_LayoutComplete (object sender, LayoutEventArgs args) - { - UpdateSettings (_curView); - UpdateHostTitle (_curView); - } + private void CurrentView_LayoutComplete (object? sender, LayoutEventArgs args) { UpdateHostTitle (_curView); } - private bool _updatingSettings = false; - private void UpdateSettings (View view) - { - _updatingSettings = true; - var x = view.X.ToString (); - var y = view.Y.ToString (); + private void UpdateHostTitle (View? view) { _hostPane!.Title = $"{view!.GetType ().Name} [_0]"; } - try - { - _xRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.First (s => x.Contains (s))); - _yRadioGroup.SelectedItem = _posNames.IndexOf (_posNames.First (s => y.Contains (s))); - } - catch (InvalidOperationException e) + private void CurrentView_Initialized (object? sender, EventArgs e) + { + if (sender is not View view) { - // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet - Debug.WriteLine ($"{e}"); + return; } - _xText.Text = $"{view.Frame.X}"; - _yText.Text = $"{view.Frame.Y}"; - - var w = view.Width.ToString (); - var h = view.Height.ToString (); - _wRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => w.Contains (s))); - _hRadioGroup.SelectedItem = _dimNames.IndexOf (_dimNames.First (s => h.Contains (s))); - - if (view.Width.Has (out _)) + if (!view.Width!.Has (out _) || view.Width is null) { - _wText.Text = "Auto"; - _wText.Enabled = false; - } - else - { - _wText.Text = $"{view.Frame.Width}"; - _wText.Enabled = true; + view.Width = Dim.Fill (); } - if (view.Height.Has (out _)) - { - _hText.Text = "Auto"; - _hText.Enabled = false; - } - else + if (!view.Height!.Has (out _) || view.Height is null) { - _hText.Text = $"{view.Frame.Height}"; - _hText.Enabled = true; + view.Height = Dim.Fill (); } - _updatingSettings = false; + UpdateHostTitle (view); } - private void UpdateHostTitle (View view) { _hostPane.Title = $"_Demo of {view.GetType ().Name}"; } - - private void CurrentView_Initialized (object sender, EventArgs e) + public override List GetDemoKeyStrokes () { - if (sender is not View view) - { - return; - } + var keys = new List (); - if (!view.Width!.Has (out _) || (view.Width is null || view.Frame.Width == 0)) + for (int i = 0; i < GetAllViewClassesCollection ().Count; i++) { - view.Width = Dim.Fill (); + keys.Add (Key.CursorDown); } - if (!view.Height!.Has (out _) || (view.Height is null || view.Frame.Height == 0)) - { - view.Height = Dim.Fill (); - } - - UpdateSettings (view); - - UpdateHostTitle (view); + return keys; } } diff --git a/UICatalog/Scenarios/AnimationScenario.cs b/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs similarity index 63% rename from UICatalog/Scenarios/AnimationScenario.cs rename to UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs index 08add3f0a9..fef9411d31 100644 --- a/UICatalog/Scenarios/AnimationScenario.cs +++ b/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable +using System; +using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; @@ -15,11 +17,11 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Drawing")] public class AnimationScenario : Scenario { - private bool _isDisposed; + private ImageView? _imageView; public override void Main () { - Application.Init(); + Application.Init (); var win = new Window { @@ -30,26 +32,38 @@ public override void Main () Height = Dim.Fill (), }; - var imageView = new ImageView { Width = Dim.Fill (), Height = Dim.Fill () - 2 }; + _imageView = new ImageView { Width = Dim.Fill (), Height = Dim.Fill ()! - 2 }; - win.Add (imageView); + win.Add (_imageView); var lbl = new Label { Y = Pos.AnchorEnd (), Text = "Image by Wikiscient" }; win.Add (lbl); var lbl2 = new Label { - X = Pos.AnchorEnd(), Y = Pos.AnchorEnd (), Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" + X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "https://commons.wikimedia.org/wiki/File:Spinning_globe.gif" }; win.Add (lbl2); + // Start the animation after the window is initialized + win.Initialized += OnWinOnInitialized; + + Application.Run (win); + win.Dispose (); + Application.Shutdown (); + Debug.Assert (!Application.Initialized); + } + + + private void OnWinOnInitialized (object? sender, EventArgs args) + { DirectoryInfo dir; string assemblyLocation = Assembly.GetExecutingAssembly ().Location; if (!string.IsNullOrEmpty (assemblyLocation)) { - dir = new DirectoryInfo (Path.GetDirectoryName (assemblyLocation)); + dir = new DirectoryInfo (Path.GetDirectoryName (assemblyLocation) ?? string.Empty); } else { @@ -57,52 +71,41 @@ public override void Main () } var f = new FileInfo ( - Path.Combine (dir.FullName, "Scenarios", "Spinning_globe_dark_small.gif") + Path.Combine (dir.FullName, "Scenarios/AnimationScenario", "Spinning_globe_dark_small.gif") ); if (!f.Exists) { - MessageBox.ErrorQuery ("Could not find gif", "Could not find " + f.FullName, "Ok"); + Debug.WriteLine ($"Could not find {f.FullName}"); + MessageBox.ErrorQuery ("Could not find gif", $"Could not find\n{f.FullName}", "Ok"); return; } - imageView.SetImage (Image.Load (File.ReadAllBytes (f.FullName))); + _imageView!.SetImage (Image.Load (File.ReadAllBytes (f.FullName))); Task.Run ( () => { - while (!_isDisposed) + while (Application.Initialized) { // When updating from a Thread/Task always use Invoke Application.Invoke ( () => { - imageView.NextFrame (); - imageView.SetNeedsDisplay (); - } - ); + _imageView.NextFrame (); + _imageView.SetNeedsDraw (); + }); Task.Delay (100).Wait (); } - } - ); - - Application.Run (win); - win.Dispose (); - Application.Shutdown (); - } - - protected override void Dispose (bool disposing) - { - _isDisposed = true; - base.Dispose (disposing); + }); } // This is a C# port of https://github.com/andraaspar/bitmap-to-braille by Andraaspar /// Renders an image as unicode Braille. - public class BitmapToBraille + public class BitmapToBraille (int widthPixels, int heightPixels, Func pixelIsLit) { public const int CHAR_HEIGHT = 4; public const int CHAR_WIDTH = 2; @@ -110,16 +113,9 @@ public class BitmapToBraille private const string CHARS = " ⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿"; - public BitmapToBraille (int widthPixels, int heightPixels, Func pixelIsLit) - { - WidthPixels = widthPixels; - HeightPixels = heightPixels; - PixelIsLit = pixelIsLit; - } - - public int HeightPixels { get; } - public Func PixelIsLit { get; } - public int WidthPixels { get; } + public int HeightPixels { get; } = heightPixels; + public Func PixelIsLit { get; } = pixelIsLit; + public int WidthPixels { get; } = widthPixels; public string GenerateImage () { @@ -168,53 +164,58 @@ public string GenerateImage () private class ImageView : View { - private string [] brailleCache; - private int currentFrame; - private int frameCount; - private Image [] fullResImages; - private Image [] matchSizes; - private Rectangle oldSize = Rectangle.Empty; - public void NextFrame () { currentFrame = (currentFrame + 1) % frameCount; } - - public override void OnDrawContent (Rectangle viewport) + private string []? _brailleCache; + private int _currentFrame; + private int _frameCount; + private Image []? _fullResImages; + private Image []? _matchSizes; + private Rectangle _oldSize = Rectangle.Empty; + public void NextFrame () { _currentFrame = (_currentFrame + 1) % _frameCount; } + + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - - if (oldSize != Viewport) + if (_frameCount == 0) + { + return false; + } + if (_oldSize != Viewport) { // Invalidate cached images now size has changed - matchSizes = new Image [frameCount]; - brailleCache = new string [frameCount]; - oldSize = Viewport; + _matchSizes = new Image [_frameCount]; + _brailleCache = new string [_frameCount]; + _oldSize = Viewport; } - Image imgScaled = matchSizes [currentFrame]; - string braille = brailleCache [currentFrame]; + Image? imgScaled = _matchSizes? [_currentFrame]; + string? braille = _brailleCache? [_currentFrame]; if (imgScaled == null) { - Image imgFull = fullResImages [currentFrame]; + Image? imgFull = _fullResImages? [_currentFrame]; // keep aspect ratio int newSize = Math.Min (Viewport.Width, Viewport.Height); // generate one - matchSizes [currentFrame] = imgScaled = imgFull.Clone ( - x => x.Resize ( - newSize * BitmapToBraille.CHAR_HEIGHT, - newSize * BitmapToBraille.CHAR_HEIGHT - ) - ); + if (_matchSizes is { } && imgFull is { }) + { + _matchSizes [_currentFrame] = imgScaled = imgFull.Clone ( + x => x.Resize ( + newSize * BitmapToBraille.CHAR_HEIGHT, + newSize * BitmapToBraille.CHAR_HEIGHT + ) + ); + } } - if (braille == null) + if (braille == null && _brailleCache is { }) { - brailleCache [currentFrame] = braille = GetBraille (matchSizes [currentFrame]); + _brailleCache [_currentFrame] = braille = GetBraille (_matchSizes? [_currentFrame]!); } - string [] lines = braille.Split ('\n'); + string []? lines = braille?.Split ('\n'); - for (var y = 0; y < lines.Length; y++) + for (var y = 0; y < lines!.Length; y++) { string line = lines [y]; @@ -223,24 +224,26 @@ public override void OnDrawContent (Rectangle viewport) AddRune (x, y, (Rune)line [x]); } } + + return true; } internal void SetImage (Image image) { - frameCount = image.Frames.Count; + _frameCount = image.Frames.Count; - fullResImages = new Image [frameCount]; - matchSizes = new Image [frameCount]; - brailleCache = new string [frameCount]; + _fullResImages = new Image [_frameCount]; + _matchSizes = new Image [_frameCount]; + _brailleCache = new string [_frameCount]; - for (var i = 0; i < frameCount - 1; i++) + for (var i = 0; i < _frameCount - 1; i++) { - fullResImages [i] = image.Frames.ExportFrame (0); + _fullResImages [i] = image.Frames.ExportFrame (0); } - fullResImages [frameCount - 1] = image; + _fullResImages [_frameCount - 1] = image; - SetNeedsDisplay (); + SetNeedsDraw (); } private string GetBraille (Image img) diff --git a/UICatalog/Scenarios/Spinning_globe_dark_small.gif b/UICatalog/Scenarios/AnimationScenario/Spinning_globe_dark_small.gif similarity index 100% rename from UICatalog/Scenarios/Spinning_globe_dark_small.gif rename to UICatalog/Scenarios/AnimationScenario/Spinning_globe_dark_small.gif diff --git a/UICatalog/Scenarios/spinning-globe-attribution.txt b/UICatalog/Scenarios/AnimationScenario/spinning-globe-attribution.txt similarity index 100% rename from UICatalog/Scenarios/spinning-globe-attribution.txt rename to UICatalog/Scenarios/AnimationScenario/spinning-globe-attribution.txt diff --git a/UICatalog/Scenarios/Arrangement.cs b/UICatalog/Scenarios/Arrangement.cs index 8c9651bd17..48ed9f225e 100644 --- a/UICatalog/Scenarios/Arrangement.cs +++ b/UICatalog/Scenarios/Arrangement.cs @@ -1,5 +1,4 @@ -using System.Threading; -using System.Timers; +using System.Collections.Generic; using Terminal.Gui; using Timer = System.Timers.Timer; @@ -8,7 +7,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Arrangement", "Arrangement Tester")] [ScenarioCategory ("Mouse and Keyboard")] [ScenarioCategory ("Layout")] -[ScenarioCategory ("Overlapped")] +[ScenarioCategory ("Arrangement")] public class Arrangement : Scenario { private int _hotkeyCount; @@ -29,12 +28,16 @@ public override void Main () Y = 0, AutoSelectViewToEdit = true, TabStop = TabBehavior.NoStop, + ShowViewIdentifier = true }; app.Add (adornmentsEditor); - adornmentsEditor.ExpandButton!.Collapsed = true; - var arrangementEditor = new ArrangementEditor () + adornmentsEditor.ExpanderButton.Orientation = Orientation.Horizontal; + + // adornmentsEditor.ExpanderButton!.Collapsed = true; + + var arrangementEditor = new ArrangementEditor { X = Pos.Right (adornmentsEditor), Y = 0, @@ -66,8 +69,10 @@ public override void Main () View overlappedView1 = CreateOverlappedView (2, 0, 13); overlappedView1.Title = "Movable _& Sizable"; View tiledSubView = CreateTiledView (4, 0, 2); + tiledSubView.Arrangement = ViewArrangement.Fixed; overlappedView1.Add (tiledSubView); tiledSubView = CreateTiledView (5, Pos.Right (tiledSubView), Pos.Top (tiledSubView)); + tiledSubView.Arrangement = ViewArrangement.Fixed; overlappedView1.Add (tiledSubView); ProgressBar progressBar = new () @@ -94,7 +99,7 @@ public override void Main () Application.Wakeup (); - progressBar.SetNeedsDisplay (); + progressBar.SetNeedsDraw (); }; timer.Start (); @@ -231,12 +236,66 @@ private View CreateTiledView (int id, Pos x, Pos y) BorderStyle = LineStyle.Single, CanFocus = true, TabStop = TabBehavior.TabStop, - Arrangement = ViewArrangement.Resizable, -// SuperViewRendersLineCanvas = true + Arrangement = ViewArrangement.Resizable + + // SuperViewRendersLineCanvas = true }; return tiled; } private char GetNextHotKey () { return (char)('A' + _hotkeyCount++); } + + public override List GetDemoKeyStrokes () + { + var keys = new List (); + + // Select view with progress bar + keys.Add ((Key)'&'); + + keys.Add (Application.ArrangeKey); + + for (int i = 0; i < 8; i++) + { + keys.Add (Key.CursorUp); + } + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorRight); + } + + keys.Add (Application.ArrangeKey); + + keys.Add (Key.S); + + keys.Add (Application.ArrangeKey); + + for (int i = 0; i < 10; i++) + { + keys.Add (Key.CursorUp); + } + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorLeft); + } + + keys.Add (Application.ArrangeKey); + + // Select view with progress bar + keys.Add ((Key)'&'); + + keys.Add (Application.ArrangeKey); + + keys.Add (Key.Tab); + + for (int i = 0; i < 10; i++) + { + keys.Add (Key.CursorRight); + keys.Add (Key.CursorDown); + } + + return keys; + } } diff --git a/UICatalog/Scenarios/ArrangementEditor.cs b/UICatalog/Scenarios/ArrangementEditor.cs deleted file mode 100644 index 396c493d60..0000000000 --- a/UICatalog/Scenarios/ArrangementEditor.cs +++ /dev/null @@ -1,240 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Text; -using Terminal.Gui; - -namespace UICatalog.Scenarios; - -/// -/// Provides an editor UI for the Margin, Border, and Padding of a View. -/// -public sealed class ArrangementEditor : View -{ - public ArrangementEditor () - { - Title = "ArrangementEditor"; - - Width = Dim.Auto (DimAutoStyle.Content); - Height = Dim.Auto (DimAutoStyle.Content); - - CanFocus = true; - - TabStop = TabBehavior.TabGroup; - - Initialized += ArrangementEditor_Initialized; - - _arrangementSlider.Options = new List> (); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.Movable.ToString (), - Data = ViewArrangement.Movable - }); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.LeftResizable.ToString (), - Data = ViewArrangement.LeftResizable - }); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.RightResizable.ToString (), - Data = ViewArrangement.RightResizable - }); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.TopResizable.ToString (), - Data = ViewArrangement.TopResizable - }); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.BottomResizable.ToString (), - Data = ViewArrangement.BottomResizable - }); - - _arrangementSlider.Options.Add (new SliderOption - { - Legend = ViewArrangement.Overlapped.ToString (), - Data = ViewArrangement.Overlapped - }); - - Add (_arrangementSlider); - } - - private View? _viewToEdit; - - private Label? _lblView; // Text describing the view being edited - - private Slider _arrangementSlider = new Slider () - { - Orientation = Orientation.Vertical, - UseMinimumSize = true, - Type = SliderType.Multiple, - AllowEmpty = true, - BorderStyle = LineStyle.Dotted, - Title = "_Arrangement", - }; - - /// - /// Gets or sets whether the ArrangementEditor should automatically select the View to edit - /// based on the values of . - /// - public bool AutoSelectViewToEdit { get; set; } - - /// - /// Gets or sets the View that will scope the behavior of . - /// - public View? AutoSelectSuperView { get; set; } - - public View? ViewToEdit - { - get => _viewToEdit; - set - { - if (_viewToEdit == value) - { - return; - } - - _arrangementSlider.OptionsChanged -= ArrangementSliderOnOptionsChanged; - - _viewToEdit = value; - // Set the appropriate options in the slider based on _viewToEdit.Arrangement - if (_viewToEdit is { }) - { - _arrangementSlider.Options.ForEach (option => - { - _arrangementSlider.ChangeOption (_arrangementSlider.Options.IndexOf (option), (_viewToEdit.Arrangement & option.Data) == option.Data); - }); - } - - _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged; - - if (_lblView is { }) - { - _lblView.Text = $"{_viewToEdit?.GetType ().Name}: {_viewToEdit?.Id}" ?? string.Empty; - } - } - } - - - private void NavigationOnFocusedChanged (object? sender, EventArgs e) - { - if (AutoSelectSuperView is null) - { - return; - } - - View? view = Application.Navigation!.GetFocused (); - - if (ApplicationNavigation.IsInHierarchy (this, view)) - { - return; - } - - if (!ApplicationNavigation.IsInHierarchy (AutoSelectSuperView, view)) - { - return; - } - - if (view is { } and not Adornment) - { - ViewToEdit = view; - } - } - - private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) - { - if (e.Flags != MouseFlags.Button1Clicked || !AutoSelectViewToEdit) - { - return; - } - - if ((AutoSelectSuperView is { } && !AutoSelectSuperView.FrameToScreen ().Contains (e.Position)) - || FrameToScreen ().Contains (e.Position)) - { - return; - } - - View? view = e.View; - - if (view is Adornment adornment) - { - view = adornment.Parent; - } - - if (view is { } and not Adornment) - { - ViewToEdit = view; - } - } - - private void ArrangementEditor_Initialized (object? sender, EventArgs e) - { - BorderStyle = LineStyle.Dotted; - - var expandButton = new ExpanderButton - { - Orientation = Orientation.Horizontal - }; - Border.Add (expandButton); - - _lblView = new () - { - X = 0, - Y = 0, - Height = 2 - }; - _lblView.TextFormatter.WordWrap = true; - _lblView.TextFormatter.MultiLine = true; - _lblView.HotKeySpecifier = (Rune)'\uffff'; - _lblView.Width = Dim.Width (_arrangementSlider); - Add (_lblView); - - _arrangementSlider.Y = Pos.Bottom (_lblView); - - _arrangementSlider.OptionsChanged += ArrangementSliderOnOptionsChanged; - - Application.MouseEvent += ApplicationOnMouseEvent; - Application.Navigation!.FocusedChanged += NavigationOnFocusedChanged; - } - - private void ArrangementSliderOnOptionsChanged (object? sender, SliderEventArgs e) - { - if (_viewToEdit is { }) - { - // Set the arrangement based on the selected options - ViewArrangement arrangement = ViewArrangement.Fixed; - foreach (var option in e.Options) - { - arrangement |= option.Value.Data; - } - - _viewToEdit.Arrangement = arrangement; - - if (_viewToEdit.Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - _viewToEdit.ShadowStyle = ShadowStyle.Transparent; - _viewToEdit.ColorScheme = Colors.ColorSchemes ["Toplevel"]; - } - else - { - _viewToEdit.ShadowStyle = ShadowStyle.None; - _viewToEdit.ColorScheme = _viewToEdit!.SuperView!.ColorScheme; - } - - if (_viewToEdit.Arrangement.HasFlag (ViewArrangement.Movable)) - { - _viewToEdit.BorderStyle = LineStyle.Double; - } - else - { - _viewToEdit.BorderStyle = LineStyle.Single; - } - } - } -} diff --git a/UICatalog/Scenarios/Bars.cs b/UICatalog/Scenarios/Bars.cs index b0d2261486..7a4f388b8c 100644 --- a/UICatalog/Scenarios/Bars.cs +++ b/UICatalog/Scenarios/Bars.cs @@ -136,6 +136,7 @@ private void App_Loaded (object sender, EventArgs e) Y = Pos.Bottom (label), }; ConfigureMenu (bar); + bar.Arrangement = ViewArrangement.RightResizable; menuLikeExamples.Add (bar); diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index 33c64d0038..8eb9266a21 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -81,7 +81,7 @@ public override void Main () _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () }; _categoryList.FullRowSelect = true; - + _categoryList.MultiSelect = false; //jumpList.Style.ShowHeaders = false; //jumpList.Style.ShowHorizontalHeaderOverline = false; //jumpList.Style.ShowHorizontalHeaderUnderline = false; @@ -306,9 +306,33 @@ private MenuItem CreateMenuShowWidth () return item; } + public override List GetDemoKeyStrokes () + { + var keys = new List (); + + for (int i = 0; i < 200; i++) + { + keys.Add (Key.CursorDown); + } + + // Category table + keys.Add (Key.Tab.WithShift); + + // Block elements + keys.Add (Key.B); + keys.Add (Key.L); + + keys.Add (Key.Tab); + + for (int i = 0; i < 200; i++) + { + keys.Add (Key.CursorLeft); + } + return keys; + } } -internal class CharMap : View +internal class CharMap : View, IDesignable { private const int COLUMN_WIDTH = 3; @@ -622,7 +646,7 @@ public int SelectedCodePoint } } - SetNeedsDisplay (); + SetNeedsDraw (); SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null)); } } @@ -633,7 +657,7 @@ public bool ShowGlyphWidths set { _rowHeight = value ? 2 : 1; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -649,7 +673,7 @@ public int StartCodePoint _start = value; SelectedCodePoint = value; Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight }; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -657,21 +681,21 @@ public int StartCodePoint private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16; public event EventHandler Hover; - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - if (viewport.Height == 0 || viewport.Width == 0) + if (Viewport.Height == 0 || Viewport.Width == 0) { - return; + return true; } - Clear (); + ClearViewport (); int cursorCol = Cursor.X + Viewport.X - RowLabelWidth - 1; int cursorRow = Cursor.Y + Viewport.Y - 1; - Driver.SetAttribute (GetHotNormalColor ()); + SetAttribute (GetHotNormalColor ()); Move (0, 0); - Driver.AddStr (new (' ', RowLabelWidth + 1)); + AddStr (new (' ', RowLabelWidth + 1)); int firstColumnX = RowLabelWidth - Viewport.X; @@ -683,12 +707,12 @@ public override void OnDrawContent (Rectangle viewport) if (x > RowLabelWidth - 2) { Move (x, 0); - Driver.SetAttribute (GetHotNormalColor ()); - Driver.AddStr (" "); - Driver.SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ()); - Driver.AddStr ($"{hexDigit:x}"); - Driver.SetAttribute (GetHotNormalColor ()); - Driver.AddStr (" "); + SetAttribute (GetHotNormalColor ()); + AddStr (" "); + SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ()); + AddStr ($"{hexDigit:x}"); + SetAttribute (GetHotNormalColor ()); + AddStr (" "); } } @@ -707,7 +731,7 @@ public override void OnDrawContent (Rectangle viewport) } Move (firstColumnX + COLUMN_WIDTH, y); - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); for (var col = 0; col < 16; col++) { @@ -723,7 +747,7 @@ public override void OnDrawContent (Rectangle viewport) // If we're at the cursor position, and we don't have focus, invert the colors. if (row == cursorRow && x == cursorCol && !HasFocus) { - Driver.SetAttribute (GetFocusColor ()); + SetAttribute (GetFocusColor ()); } int scalar = val + col; @@ -741,7 +765,7 @@ public override void OnDrawContent (Rectangle viewport) // Draw the rune if (width > 0) { - Driver.AddRune (rune); + AddRune (rune); } else { @@ -762,11 +786,11 @@ public override void OnDrawContent (Rectangle viewport) if (normal.Length == 1) { - Driver.AddRune (normal [0]); + AddRune ((Rune)normal [0]); } else { - Driver.AddRune (Rune.ReplacementChar); + AddRune (Rune.ReplacementChar); } } } @@ -774,31 +798,33 @@ public override void OnDrawContent (Rectangle viewport) else { // Draw the width of the rune - Driver.SetAttribute (ColorScheme.HotNormal); - Driver.AddStr ($"{width}"); + SetAttribute (ColorScheme.HotNormal); + AddStr ($"{width}"); } // If we're at the cursor position, and we don't have focus, revert the colors to normal if (row == cursorRow && x == cursorCol && !HasFocus) { - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); } } // Draw row label (U+XXXX_) Move (0, y); - Driver.SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal); + SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal); if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0) { - Driver.AddStr ($"U+{val / 16:x5}_ "); + AddStr ($"U+{val / 16:x5}_ "); } else { - Driver.AddStr (new (' ', RowLabelWidth)); + AddStr (new (' ', RowLabelWidth)); } } + + return true; } public override Point? PositionCursor () diff --git a/UICatalog/Scenarios/ClassExplorer.cs b/UICatalog/Scenarios/ClassExplorer.cs index 0bfd4e9fdd..60e88e3e05 100644 --- a/UICatalog/Scenarios/ClassExplorer.cs +++ b/UICatalog/Scenarios/ClassExplorer.cs @@ -193,7 +193,7 @@ private void OnCheckHighlightModelTextOnly () { _treeView.Style.HighlightModelTextOnly = !_treeView.Style.HighlightModelTextOnly; _highlightModelTextOnly.Checked = _treeView.Style.HighlightModelTextOnly; - _treeView.SetNeedsDisplay (); + _treeView.SetNeedsDraw (); } private void Quit () { Application.RequestStop (); } @@ -292,7 +292,7 @@ private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs _textView.Text = ex.Message; } - _textView.SetNeedsDisplay (); + _textView.SetNeedsDraw (); } private enum Showable diff --git a/UICatalog/Scenarios/Clipping.cs b/UICatalog/Scenarios/Clipping.cs index fe0692fc91..3e866023d5 100644 --- a/UICatalog/Scenarios/Clipping.cs +++ b/UICatalog/Scenarios/Clipping.cs @@ -1,4 +1,5 @@ -using Terminal.Gui; +using System.Collections.Generic; +using Terminal.Gui; namespace UICatalog.Scenarios; @@ -82,4 +83,31 @@ public override void Main () win.Dispose (); Application.Shutdown (); } + + public override List GetDemoKeyStrokes () + { + var keys = new List (); + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorDown); + } + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorRight); + } + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorLeft); + } + + for (int i = 0; i < 25; i++) + { + keys.Add (Key.CursorUp); + } + + return keys; + } } diff --git a/UICatalog/Scenarios/CollectionNavigatorTester.cs b/UICatalog/Scenarios/CollectionNavigatorTester.cs index 0ae7a76280..650e5e1801 100644 --- a/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -7,9 +7,9 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ( - "Collection Navigator", - "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator)." - )] + "Collection Navigator", + "Demonstrates keyboard navigation in ListView & TreeView (CollectionNavigator)." + )] [ScenarioCategory ("Controls")] [ScenarioCategory ("ListView")] [ScenarioCategory ("TreeView")] diff --git a/UICatalog/Scenarios/ColorPicker.cs b/UICatalog/Scenarios/ColorPicker.cs index c059319129..cd0789d606 100644 --- a/UICatalog/Scenarios/ColorPicker.cs +++ b/UICatalog/Scenarios/ColorPicker.cs @@ -1,9 +1,10 @@ using System; +using System.Collections.Generic; using Terminal.Gui; namespace UICatalog.Scenarios; -[ScenarioMetadata ("Color Picker", "Color Picker.")] +[ScenarioMetadata ("ColorPicker", "Color Picker.")] [ScenarioCategory ("Colors")] [ScenarioCategory ("Controls")] public class ColorPickers : Scenario @@ -218,7 +219,6 @@ public override void Main () // Set default colors. foregroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Foreground.GetClosestNamedColor16 (); backgroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Background.GetClosestNamedColor16 (); - app.Initialized += (s, e) => app.LayoutSubviews (); Application.Run (app); app.Dispose (); @@ -250,7 +250,7 @@ private void ForegroundColor_ColorChanged (object sender, EventArgs e) /// Update a color label from his ColorPicker. private void UpdateColorLabel (Label label, Color color) { - label.Clear (); + label.ClearViewport (); label.Text = $"{color} ({(int)color}) #{color.R:X2}{color.G:X2}{color.B:X2}"; @@ -271,4 +271,40 @@ private void UpdateDemoLabel () ) }; } + + public override List GetDemoKeyStrokes () + { + var keys = new List (); + + keys.Add (Key.B.WithAlt); + + for (int i = 0; i < 200; i++) + { + keys.Add (Key.CursorRight); + } + + keys.Add (Key.Tab); + keys.Add (Key.Tab); + + for (int i = 0; i < 200; i++) + { + keys.Add (Key.CursorLeft); + } + + keys.Add (Key.Tab); + keys.Add (Key.Tab); + + for (int i = 0; i < 200; i++) + { + keys.Add (Key.CursorLeft); + } + + keys.Add (Key.N.WithAlt); + keys.Add (Key.R.WithAlt); + keys.Add (Key.H.WithAlt); + keys.Add (Key.S.WithAlt); + keys.Add (Key.D1.WithAlt); + + return keys; + } } diff --git a/UICatalog/Scenarios/CombiningMarks.cs b/UICatalog/Scenarios/CombiningMarks.cs index 3b2a8e1964..d351982513 100644 --- a/UICatalog/Scenarios/CombiningMarks.cs +++ b/UICatalog/Scenarios/CombiningMarks.cs @@ -9,24 +9,24 @@ public class CombiningMarks : Scenario public override void Main () { Application.Init (); - var top = new Toplevel { ColorScheme = Colors.ColorSchemes [TopLevelColorScheme] }; + var top = new Toplevel (); - top.DrawContentComplete += (s, e) => + top.DrawComplete += (s, e) => { - Application.Driver?.Move (0, 0); - Application.Driver?.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); - Application.Driver?.Move (0, 2); - Application.Driver?.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr."); - Application.Driver?.Move (0, 3); - Application.Driver?.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr."); - Application.Driver?.Move (0, 4); - Application.Driver?.AddRune ('['); - Application.Driver?.AddRune ('a'); - Application.Driver?.AddRune ('\u0301'); - Application.Driver?.AddRune ('\u0301'); - Application.Driver?.AddRune ('\u0328'); - Application.Driver?.AddRune (']'); - Application.Driver?.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each."); + top.Move (0, 0); + top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616."); + top.Move (0, 2); + top.AddStr ("\u0301\u0301\u0328<- \"\\u301\\u301\\u328]\" using AddStr."); + top.Move (0, 3); + top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u301\\u301\\u328]\" using AddStr."); + top.Move (0, 4); + top.AddRune ('['); + top.AddRune ('a'); + top.AddRune ('\u0301'); + top.AddRune ('\u0301'); + top.AddRune ('\u0328'); + top.AddRune (']'); + top.AddStr ("<- \"[a\\u301\\u301\\u328]\" using AddRune for each."); }; Application.Run (top); diff --git a/UICatalog/Scenarios/ComputedLayout.cs b/UICatalog/Scenarios/ComputedLayout.cs index bdf10337cf..c27eabdb36 100644 --- a/UICatalog/Scenarios/ComputedLayout.cs +++ b/UICatalog/Scenarios/ComputedLayout.cs @@ -50,7 +50,7 @@ public override void Main () Text = vrule }; - app.LayoutComplete += (s, a) => + app.SubviewsLaidOut += (s, a) => { if (horizontalRuler.Viewport.Width == 0 || horizontalRuler.Viewport.Height == 0) { @@ -370,7 +370,6 @@ public override void Main () // The call to app.LayoutSubviews causes the Computed layout to // get updated. anchorButton.Text += "!"; - app.LayoutSubviews (); }; app.Add (anchorButton); @@ -415,10 +414,7 @@ public override void Main () { // This demonstrates how to have a dynamically sized button // Each time the button is clicked the button's text gets longer - // The call to app.LayoutSubviews causes the Computed layout to - // get updated. leftButton.Text += "!"; - app.LayoutSubviews (); }; // show positioning vertically using Pos.AnchorEnd @@ -433,10 +429,7 @@ public override void Main () { // This demonstrates how to have a dynamically sized button // Each time the button is clicked the button's text gets longer - // The call to app.LayoutSubviews causes the Computed layout to - // get updated. centerButton.Text += "!"; - app.LayoutSubviews (); }; // show positioning vertically using another window and Pos.Bottom @@ -451,10 +444,7 @@ public override void Main () { // This demonstrates how to have a dynamically sized button // Each time the button is clicked the button's text gets longer - // The call to app.LayoutSubviews causes the Computed layout to - // get updated. rightButton.Text += "!"; - app.LayoutSubviews (); }; View [] buttons = { leftButton, centerButton, rightButton }; diff --git a/UICatalog/Scenarios/ConfigurationEditor.cs b/UICatalog/Scenarios/ConfigurationEditor.cs index 2c898ac8b8..037acceec6 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -94,7 +94,7 @@ public override void Main () foreach (Tile t in _tileView.Tiles) { t.ContentView.ColorScheme = EditorColorScheme; - t.ContentView.SetNeedsDisplay (); + t.ContentView.SetNeedsDraw (); } ; @@ -154,8 +154,6 @@ private void Open () { _tileView.Tiles.ToArray () [1].ContentView.SetFocus (); } - - Application.Top.LayoutSubviews (); } private void Quit () diff --git a/UICatalog/Scenarios/ContentScrolling.cs b/UICatalog/Scenarios/ContentScrolling.cs index b6986067ec..82f161f327 100644 --- a/UICatalog/Scenarios/ContentScrolling.cs +++ b/UICatalog/Scenarios/ContentScrolling.cs @@ -17,6 +17,7 @@ public class ScrollingDemoView : FrameView { public ScrollingDemoView () { + Id = "ScrollingDemoView"; Width = Dim.Fill (); Height = Dim.Fill (); ColorScheme = Colors.ColorSchemes ["Base"]; @@ -47,7 +48,7 @@ public ScrollingDemoView () // Add a status label to the border that shows Viewport and ContentSize values. Bit of a hack. // TODO: Move to Padding with controls Border.Add (new Label { X = 20 }); - LayoutComplete += VirtualDemoView_LayoutComplete; + ViewportChanged += VirtualDemoView_LayoutComplete; MouseEvent += VirtualDemoView_MouseEvent; } @@ -81,18 +82,14 @@ private void VirtualDemoView_MouseEvent (object sender, MouseEventArgs e) } } - private void VirtualDemoView_LayoutComplete (object sender, LayoutEventArgs e) + private void VirtualDemoView_LayoutComplete (object sender, DrawEventArgs drawEventArgs) { - Label status = Border.Subviews.OfType