diff --git a/CommunityToolkitExample/LoginView.cs b/CommunityToolkitExample/LoginView.cs index 8d66f29bb..7f8fbe53d 100644 --- a/CommunityToolkitExample/LoginView.cs +++ b/CommunityToolkitExample/LoginView.cs @@ -59,7 +59,7 @@ internal partial class LoginView : IRecipient> } } 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 02351612f..3d47aeb41 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 @@ public static partial class Application // Initialization (Init/Shutdown) 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 @@ public static partial class Application // Initialization (Init/Shutdown) 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 partial class Application // Initialization (Init/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 c9fa96c63..a883dea8e 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -120,7 +120,7 @@ public static partial class Application // Keyboard handling /// if the key was handled. public static bool RaiseKeyUpEvent (Key key) { - if (!IsInitialized) + if (!Initialized) { return true; } @@ -200,7 +200,7 @@ public static partial class Application // Keyboard handling Command.Refresh, static () => { - Refresh (); + LayoutAndDraw (); return true; } diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index c4477ac85..a944c4c37 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -227,7 +227,7 @@ public static partial class Application // Mouse handling { 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 966fb04c6..898fc935b 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 partial class Application // Run (Begin, Run, End, Stop) { 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 partial class Application // Run (Begin, Run, End, Stop) 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 partial class Application // Run (Begin, Run, End, Stop) 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 @@ public static partial class Application // Run (Begin, Run, End, Stop) // 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 @@ public static partial class Application // Run (Begin, Run, End, Stop) 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 partial class Application // Run (Begin, Run, End, Stop) { ArgumentNullException.ThrowIfNull (view); - if (IsInitialized) + if (Initialized) { if (Driver is null) { @@ -452,7 +458,10 @@ public static partial class Application // Run (Begin, Run, End, Stop) /// 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 partial class Application // Run (Begin, Run, End, Stop) /// 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 partial class Application // Run (Begin, Run, End, Stop) 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 partial class Application // Run (Begin, Run, End, Stop) } MainLoop.RunIteration (); + Iteration?.Invoke (null, new ()); } @@ -568,16 +584,17 @@ public static partial class Application // Run (Begin, Run, End, Stop) 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 partial class Application // Run (Begin, Run, End, Stop) if (TopLevels.Count > 0) { Top = TopLevels.Peek (); - Top.SetNeedsDisplay (); + Top.SetNeedsDraw (); } if (runState.Toplevel is { HasFocus: true }) @@ -670,6 +687,6 @@ public static partial class Application // Run (Begin, Run, End, Stop) runState.Toplevel = null; runState.Dispose (); - Refresh (); + LayoutAndDraw (); } } diff --git a/Terminal.Gui/Application/Application.Screen.cs b/Terminal.Gui/Application/Application.Screen.cs index 087021d1c..c5bf6d6fd 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 partial class Application // Screen related stuff 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 2b7476fbc..d9e6c68d9 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -185,6 +185,8 @@ public static partial class Application 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 @@ public static partial class Application 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 5835957a2..1fe6972ea 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -30,15 +30,6 @@ public class 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 5dccea0a4..f8bf907c7 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -148,7 +148,7 @@ internal static class ClipboardProcessRunner 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 7d6de3834..e1e173001 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 abstract class ConsoleDriver 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 abstract class ConsoleDriver /// 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 @@ public abstract class ConsoleDriver /// 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 @@ public abstract class ConsoleDriver } /// 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 @@ public abstract class ConsoleDriver /// /// /// 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 abstract class ConsoleDriver { 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 abstract class ConsoleDriver { Debug.Assert (runeWidth <= 2); - if (validLocation && Col < Clip.Right) + if (validLocation && Col < clipRect.Right) { lock (Contents!) { @@ -288,7 +300,7 @@ public abstract class ConsoleDriver /// 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 abstract class ConsoleDriver /// 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 abstract class ConsoleDriver } /// 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 abstract class ConsoleDriver _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 abstract class ConsoleDriver /// /// 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 abstract class ConsoleDriver /// /// /// - 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 abstract class ConsoleDriver /// 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 abstract class ConsoleDriver /// 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 abstract class ConsoleDriver 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 abstract class ConsoleDriver /// 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 abstract class ConsoleDriver /// 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 88005861a..446926fef 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 @@ internal class CursesDriver : ConsoleDriver return; } - if (IsValidLocation (col, row)) + if (IsValidLocation (default, col, row)) { Curses.move (row, col); } @@ -101,16 +101,12 @@ internal class CursesDriver : ConsoleDriver { // 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 @@ internal class CursesDriver : ConsoleDriver } } - public override void UpdateScreen () + public override bool UpdateScreen () { + bool updated = false; if (Force16Colors) { for (var row = 0; row < Rows; row++) @@ -297,7 +294,7 @@ internal class CursesDriver : ConsoleDriver || Contents.Length != Rows * Cols || Rows != Console.WindowHeight) { - return; + return updated; } var top = 0; @@ -315,7 +312,7 @@ internal class CursesDriver : ConsoleDriver { if (Console.WindowHeight < 1) { - return; + return updated; } if (!_dirtyLines [row]) @@ -325,7 +322,7 @@ internal class CursesDriver : ConsoleDriver if (!SetCursorPosition (0, row)) { - return; + return updated; } _dirtyLines [row] = false; @@ -338,6 +335,7 @@ internal class CursesDriver : ConsoleDriver for (; col < cols; col++) { + updated = true; if (!Contents [row, col].IsDirty) { if (output.Length > 0) @@ -440,6 +438,8 @@ internal class CursesDriver : ConsoleDriver 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 73c12959f..a5f297ee6 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -101,8 +101,10 @@ public class FakeDriver : ConsoleDriver 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 class FakeDriver : ConsoleDriver continue; } + updated = true; + FakeConsole.CursorTop = row; FakeConsole.CursorLeft = 0; @@ -218,13 +222,9 @@ public class FakeDriver : ConsoleDriver 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 class FakeDriver : ConsoleDriver } // 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 cab7d3e6b..a5afbf258 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 @@ internal class NetDriver : ConsoleDriver 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 @@ internal class NetDriver : ConsoleDriver { if (Console.WindowHeight < 1) { - return; + return updated; } if (!_dirtyLines [row]) @@ -912,9 +907,10 @@ internal class NetDriver : ConsoleDriver if (!SetCursorPosition (0, row)) { - return; + return updated; } + updated = true; _dirtyLines [row] = false; output.Clear (); @@ -1043,6 +1039,8 @@ internal class NetDriver : ConsoleDriver lastCol += outputWidth; outputWidth = 0; } + + return updated; } internal override void End () @@ -1239,12 +1237,12 @@ internal class NetDriver : ConsoleDriver 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 @@ internal class NetDriver : ConsoleDriver } // 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 0b1524542..6c8c14b8c 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1100,13 +1100,6 @@ internal class WindowsDriver : ConsoleDriver 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 @@ internal class WindowsDriver : ConsoleDriver #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 @@ internal class WindowsDriver : ConsoleDriver } _dirtyLines [row] = false; + updated = true; for (var col = 0; col < Cols; col++) { @@ -1380,6 +1375,8 @@ internal class WindowsDriver : ConsoleDriver } WindowsConsole.SmallRect.MakeEmpty (ref _damageRegion); + + return updated; } internal override void End () @@ -1419,6 +1416,7 @@ internal class WindowsDriver : ConsoleDriver 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 class WindowsDriver : ConsoleDriver _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 @@ internal class WindowsDriver : ConsoleDriver { _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 @@ internal class WindowsMainLoop : IMainLoopDriver // 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 @@ internal class WindowsMainLoop : IMainLoopDriver 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 235d657d9..8261b156a 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -1,9 +1,24 @@ #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 { + /// Creates a new instance. + public LineCanvas () + { + // TODO: Refactor ConfigurationManager to not use an event handler for this. + // Instead, have it call a method on any class appropriately attributed + // to update the cached values. See Issue #2871 + 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 @@ -11,7 +26,321 @@ public class LineCanvas : IDisposable /// public FillPair? Fill { get; set; } - private readonly List _lines = []; + private Rectangle _cachedBounds; + + /// + /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is + /// the furthest left/top and Size is defined by the line that extends the furthest right/bottom. + /// + public Rectangle Bounds + { + get + { + if (_cachedBounds.IsEmpty) + { + if (_lines.Count == 0) + { + return _cachedBounds; + } + + Rectangle bounds = _lines [0].Bounds; + + for (var i = 1; i < _lines.Count; i++) + { + bounds = Rectangle.Union (bounds, _lines [i].Bounds); + } + + if (bounds is { Width: 0 } or { Height: 0 }) + { + bounds = bounds with + { + Width = Math.Clamp (bounds.Width, 1, short.MaxValue), + Height = Math.Clamp (bounds.Height, 1, short.MaxValue) + }; + } + + _cachedBounds = bounds; + } + + return _cachedBounds; + } + } + + /// Gets the lines in the canvas. + public IReadOnlyCollection Lines => _lines.AsReadOnly (); + + /// + /// Adds a new long line to the canvas starting at . + /// + /// Use positive for the line to extend Right and negative for Left when + /// is . + /// + /// + /// Use positive for the line to extend Down and negative for Up when + /// is . + /// + /// + /// Starting point. + /// + /// The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for + /// Up/Left. + /// + /// The direction of the line. + /// The style of line to use + /// + public void AddLine ( + Point start, + int length, + Orientation orientation, + LineStyle style, + Attribute? attribute = null + ) + { + _cachedBounds = Rectangle.Empty; + _lines.Add (new (start, length, orientation, style, attribute)); + } + + /// Adds a new line to the canvas + /// + public void AddLine (StraightLine line) + { + _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 () + { + _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 + /// added. + /// + 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 = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) + { + for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) + { + IntersectionDefinition? [] intersects = _lines + .Select (l => l.Intersects (x, y)) + .Where (i => i is { }) + .ToArray (); + + Cell? cell = GetCellForIntersects (Application.Driver, intersects); + + if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false) + { + map.Add (new (x, y), cell); + } + } + } + + return map; + } + + // TODO: Unless there's an obvious use case for this API we should delete it in favor of the + // simpler version that doesn't take an area. + /// + /// 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 rectangle to constrain the search by. + /// A map of the points within the canvas that intersect with . + public Dictionary GetMap (Rectangle inArea) + { + Dictionary map = new (); + + // walk through each pixel of the bitmap + for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++) + { + for (int x = inArea.X; x < inArea.X + inArea.Width; x++) + { + IntersectionDefinition? [] intersects = _lines + .Select (l => l.Intersects (x, y)) + .Where (i => i is { }) + .ToArray (); + + Rune? rune = GetRuneForIntersects (Application.Driver, intersects); + + if (rune is { } && _exclusionRegion?.Contains (x, y) is null or false) + { + map.Add (new (x, y), rune.Value); + } + } + } + + return map; + } + + /// + /// 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 GetMap () { return GetMap (Bounds); } + + /// Merges one line canvas into this one. + /// + public void Merge (LineCanvas lineCanvas) + { + foreach (StraightLine line in lineCanvas._lines) + { + AddLine (line); + } + + if (lineCanvas._exclusionRegion is { }) + { + _exclusionRegion ??= new (); + _exclusionRegion.Union (lineCanvas._exclusionRegion); + } + } + + /// Removes the last line added to the canvas + /// + public StraightLine RemoveLastLine () + { + StraightLine? l = _lines.LastOrDefault (); + + if (l is { }) + { + _lines.Remove (l); + } + + return l!; + } + + /// + /// 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 + /// 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 (Bounds.IsEmpty) + { + return string.Empty; + } + + // Generate the rune map for the entire canvas + Dictionary runeMap = GetMap (); + + // Create the rune canvas + 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 - Bounds.X; + int y = kvp.Key.Y - Bounds.Y; + canvas [y, x] = kvp.Value; + } + + // Convert the canvas to a string + var sb = new StringBuilder (); + + for (var y = 0; y < canvas.GetLength (0); y++) + { + for (var x = 0; x < canvas.GetLength (1); x++) + { + Rune r = canvas [y, x]; + sb.Append (r.Value == 0 ? ' ' : r.ToString ()); + } + + if (y < canvas.GetLength (0) - 1) + { + sb.AppendLine (); + } + } + + return sb.ToString (); + } + + private static bool All (IntersectionDefinition? [] intersects, Orientation orientation) + { + return intersects.All (i => i!.Line.Orientation == orientation); + } + + private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e) + { + foreach (KeyValuePair irr in _runeResolvers) + { + irr.Value.SetGlyphs (); + } + } + + /// + /// Returns true if all requested appear in and there are + /// no additional + /// + /// + /// + /// + private static bool Exactly (HashSet intersects, params IntersectionType [] types) { return intersects.SetEquals (types); } + + private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects) + { + return Fill?.GetAttribute (intersects [0]!.Point) ?? intersects [0]!.Line.Attribute; + } private readonly Dictionary _runeResolvers = new () { @@ -55,287 +384,6 @@ public class LineCanvas : IDisposable // TODO: Add other resolvers }; - private Rectangle _cachedViewport; - - /// Creates a new instance. - public LineCanvas () - { - // TODO: Refactor ConfigurationManager to not use an event handler for this. - // Instead, have it call a method on any class appropriately attributed - // to update the cached values. See Issue #2871 - Applied += ConfigurationManager_Applied; - } - - /// Creates a new instance with the given . - /// Initial lines for the canvas. - public LineCanvas (IEnumerable lines) : this () { _lines = lines.ToList (); } - - /// - /// 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. - /// - public Rectangle Viewport - { - get - { - if (_cachedViewport.IsEmpty) - { - if (_lines.Count == 0) - { - return _cachedViewport; - } - - Rectangle viewport = _lines [0].Viewport; - - for (var i = 1; i < _lines.Count; i++) - { - viewport = Rectangle.Union (viewport, _lines [i].Viewport); - } - - if (viewport is { Width: 0 } or { Height: 0 }) - { - viewport = viewport with - { - Width = Math.Clamp (viewport.Width, 1, short.MaxValue), - Height = Math.Clamp (viewport.Height, 1, short.MaxValue) - }; - } - - _cachedViewport = viewport; - } - - return _cachedViewport; - } - } - - /// 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 . - /// - /// Use positive for the line to extend Right and negative for Left when - /// is . - /// - /// - /// Use positive for the line to extend Down and negative for Up when - /// is . - /// - /// - /// Starting point. - /// - /// The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for - /// Up/Left. - /// - /// The direction of the line. - /// The style of line to use - /// - public void AddLine ( - Point start, - int length, - Orientation orientation, - LineStyle style, - Attribute? attribute = null - ) - { - _cachedViewport = Rectangle.Empty; - _lines.Add (new (start, length, orientation, style, attribute)); - } - - /// Adds a new line to the canvas - /// - public void AddLine (StraightLine line) - { - _cachedViewport = Rectangle.Empty; - _lines.Add (line); - } - - /// Clears all lines from the LineCanvas. - public void Clear () - { - _cachedViewport = Rectangle.Empty; - _lines.Clear (); - } - - /// - /// 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; } - - /// - /// 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. - /// - /// 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 x = Viewport.X; x < Viewport.X + Viewport.Width; x++) - { - IntersectionDefinition? [] intersects = _lines - .Select (l => l.Intersects (x, y)) - .Where (i => i is { }) - .ToArray (); - - Cell? cell = GetCellForIntersects (Application.Driver, intersects); - - if (cell is { }) - { - map.Add (new (x, y), cell); - } - } - } - - return map; - } - - // TODO: Unless there's an obvious use case for this API we should delete it in favor of the - // simpler version that doesn't take an area. - /// - /// 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. - /// - /// A rectangle to constrain the search by. - /// A map of the points within the canvas that intersect with . - public Dictionary GetMap (Rectangle inArea) - { - Dictionary map = new (); - - // walk through each pixel of the bitmap - for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++) - { - for (int x = inArea.X; x < inArea.X + inArea.Width; x++) - { - IntersectionDefinition? [] intersects = _lines - .Select (l => l.Intersects (x, y)) - .Where (i => i is { }) - .ToArray (); - - Rune? rune = GetRuneForIntersects (Application.Driver, intersects); - - if (rune is { }) - { - map.Add (new (x, y), rune.Value); - } - } - } - - return map; - } - - /// - /// 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. - /// - /// A map of all the points within the canvas. - public Dictionary GetMap () { return GetMap (Viewport); } - - /// Merges one line canvas into this one. - /// - public void Merge (LineCanvas lineCanvas) - { - foreach (StraightLine line in lineCanvas._lines) - { - AddLine (line); - } - } - - /// Removes the last line added to the canvas - /// - public StraightLine RemoveLastLine () - { - StraightLine? l = _lines.LastOrDefault (); - - if (l is { }) - { - _lines.Remove (l); - } - - return l!; - } - - /// - /// 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 - /// 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) - { - return string.Empty; - } - - // Generate the rune map for the entire canvas - Dictionary runeMap = GetMap (); - - // Create the rune canvas - Rune [,] canvas = new Rune [Viewport.Height, Viewport.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; - canvas [y, x] = kvp.Value; - } - - // Convert the canvas to a string - var sb = new StringBuilder (); - - for (var y = 0; y < canvas.GetLength (0); y++) - { - for (var x = 0; x < canvas.GetLength (1); x++) - { - Rune r = canvas [y, x]; - sb.Append (r.Value == 0 ? ' ' : r.ToString ()); - } - - if (y < canvas.GetLength (0) - 1) - { - sb.AppendLine (); - } - } - - return sb.ToString (); - } - - private bool All (IntersectionDefinition? [] intersects, Orientation orientation) { return intersects.All (i => i!.Line.Orientation == orientation); } - - private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e) - { - foreach (KeyValuePair irr in _runeResolvers) - { - irr.Value.SetGlyphs (); - } - } - - /// - /// Returns true if all requested appear in and there are - /// no additional - /// - /// - /// - /// - private 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; - } - private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) @@ -677,7 +725,7 @@ public class LineCanvas : IDisposable 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 class LineCanvas : IDisposable _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 000000000..c3ef1d61b --- /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 d2551101d..1eea7f263 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 class Ruler 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 class Ruler 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 fe2ccdc1d..c2b1e034b 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 class 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 d96ab73ea..0950f872b 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 record struct Thickness /// 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 record struct Thickness 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 record struct Thickness 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 @@ public record struct Thickness } } - 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 08813a50f..21bfa3b9e 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 9602776f5..b068b099a 100644 --- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs @@ -42,7 +42,7 @@ public class AppendAutocomplete : AutocompleteBase public override void ClearSuggestions () { base.ClearSuggestions (); - textField.SetNeedsDisplay (); + textField.SetNeedsDraw (); } /// @@ -106,12 +106,12 @@ public class AppendAutocomplete : AutocompleteBase } // 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 @@ public class AppendAutocomplete : AutocompleteBase 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 39bdd7522..2abe2091d 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.PopUp.cs @@ -15,14 +15,16 @@ public abstract partial class PopupAutocomplete 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 4f332bd54..34e058dab 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -120,7 +120,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase if (Visible && Suggestions.Count == 0) { Visible = false; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); return true; } @@ -128,7 +128,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase if (!Visible && Suggestions.Count > 0) { Visible = true; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); Application.UngrabMouse (); return false; @@ -141,7 +141,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase _closed = false; } - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); return false; } @@ -395,11 +395,11 @@ public abstract partial class PopupAutocomplete : AutocompleteBase { 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 @@ public abstract partial class PopupAutocomplete : AutocompleteBase ClearSuggestions (); Visible = false; _closed = true; - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); //RemovePopupFromTop (); } @@ -469,7 +469,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase } EnsureSelectedIdxIsValid (); - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); } /// Moves the selection in the Autocomplete context menu up one @@ -483,7 +483,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase } EnsureSelectedIdxIsValid (); - HostControl?.SetNeedsDisplay (); + HostControl?.SetNeedsDraw (); } /// Render the current selection in the Autocomplete context menu by the mouse reporting. @@ -512,7 +512,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase { 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 5748ae0fb..2938b69bc 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -68,7 +68,10 @@ public class TextFormatter 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 ca90eed76..3b407d575 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 @@ using Attribute = Terminal.Gui.Attribute; /// 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 class Adornment : View /// 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 class Adornment : View #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 class Adornment : View 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 class Adornment : View } /// 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 class Adornment : View /// 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 @@ public class Adornment : View /// 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 class Adornment : View { 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 class Adornment : View /// public override Point ScreenToFrame (in Point location) { - return Parent!.ScreenToFrame (new (location.X - Frame.X, location.Y - Frame.Y)); - } + View? parentOrSuperView = Parent; - /// Does nothing for Adornment - /// - public override bool OnDrawAdornments () { return false; } - - /// Redraws the Adornments that comprise the . - public override void OnDrawContent (Rectangle viewport) - { - if (Thickness == Thickness.Empty) + if (parentOrSuperView is null) { - return; - } + // While there are no real use cases for an Adornment being a subview, we support it for + // testing. E.g. in AllViewsTester. + parentOrSuperView = SuperView; - 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 ()); - - 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); - - if (Subviews.Count > 0) - { - base.OnDrawContent (viewport); - } - - if (Driver is { }) - { - Driver.Clip = prevClip; - } - - ClearLayoutNeeded (); - ClearNeedsDisplay (); + return parentOrSuperView.ScreenToFrame (new (location.X - Frame.X, location.Y - Frame.Y)); } + /// + /// Called when the of the Adornment is to be cleared. + /// + /// to stop further clearing. + protected override bool OnClearingViewport () + { + if (Thickness == Thickness.Empty) + { + return true; + } + + // This just draws/clears the thickness, not the insides. + Thickness.Draw (ViewportToScreen (Viewport), Diagnostics, ToString ()); + + NeedsDraw = true; + + 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 class Adornment : View /// 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; - // } + #endregion View Overrides - // return false; - //} + /// + bool IDesignable.EnableForDesign () + { + // This enables AllViewsTester to show something useful. + Thickness = new (3); + Frame = new (0, 0, 10, 10); + Diagnostics = ViewDiagnosticFlags.Thickness; - ///// - //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 + return true; + } } diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index add7527e6..3a51eb54b 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -64,6 +64,51 @@ public class Border : Adornment 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 class Border : Adornment { base.BeginInit (); + ShowHideDrawIndicator (); #if SUBVIEW_BASED_BORDER if (Parent is { }) { @@ -130,19 +176,11 @@ public class Border : Adornment /// 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 class Border : Adornment // 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 class Border : Adornment _settings = value; - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); } } @@ -254,7 +292,7 @@ public class Border : Adornment ColorScheme = cs; } - Parent?.SetNeedsDisplay (); + Parent?.SetNeedsDraw (); e.Cancel = true; } @@ -266,9 +304,9 @@ public class Border : Adornment { // 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment break; } - Application.Refresh (); return true; } @@ -604,19 +641,14 @@ public class Border : Adornment #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 class Border : Adornment 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 class Border : Adornment } } - 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 class Border : Adornment 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 class Border : Adornment borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } else @@ -740,7 +777,7 @@ public class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -754,7 +791,7 @@ public class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); lc?.AddLine ( @@ -762,7 +799,7 @@ public class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -773,7 +810,7 @@ public class Border : Adornment 2, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add a vert line for ╔╡ @@ -782,7 +819,7 @@ public class Border : Adornment titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add a vert line for ╞ @@ -797,7 +834,7 @@ public class Border : Adornment titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver.GetAttribute () + Driver?.GetAttribute () ); // Add the right hand line for ╞═════╗ @@ -812,7 +849,7 @@ public class Border : Adornment borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } } @@ -826,7 +863,7 @@ public class Border : Adornment sideLineLength, Orientation.Vertical, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } #endif @@ -838,7 +875,7 @@ public class Border : Adornment borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver.GetAttribute () + Driver?.GetAttribute () ); } @@ -849,11 +886,11 @@ public class Border : Adornment 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 class Border : Adornment 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment 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 @@ public class Border : Adornment } } - Application.Refresh (); - return true; }); @@ -1273,8 +1315,6 @@ public class Border : Adornment Parent!.Height = Parent.Height! + 1; } - Application.Refresh (); - return true; }); @@ -1300,8 +1340,6 @@ public class Border : Adornment } } - Application.Refresh (); - return true; }); @@ -1324,8 +1362,6 @@ public class Border : Adornment 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 1f6cc8154..19bf76f37 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 class Margin : Adornment /* 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 class Margin : Adornment } 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 class Margin : Adornment 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); + // This just draws/clears the thickness, not the insides. + if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || base.ColorScheme is { }) + { + Thickness.Draw (screen, Diagnostics, ToString ()); + } if (ShadowStyle != ShadowStyle.None) { - screen = Rectangle.Inflate (screen, -1, -1); + // Don't clear where the shadow goes + screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT); } - // This just draws/clears the thickness, not the insides. - Thickness.Draw (screen, ToString ()); - - if (Subviews.Count > 0) - { - // 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 (); - } - } - } + 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 { }) { - // return style; + Remove (_rightShadow); + _rightShadow.Dispose (); + _rightShadow = null; + } + + if (_bottomShadow is { }) + { + 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 { }) + if (style != ShadowStyle.None) { - _rightShadow.ShadowStyle = style; - } + _rightShadow = new () + { + X = Pos.AnchorEnd (SHADOW_WIDTH), + Y = 0, + Width = SHADOW_WIDTH, + Height = Dim.Fill (), + ShadowStyle = style, + Orientation = Orientation.Vertical + }; - if (_bottomShadow is { }) - { - _bottomShadow.ShadowStyle = style; + _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 class Margin : Adornment 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) { - if (_pressed && e.NewValue == HighlightStyle.None) + return; + } + + 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); - - if (_rightShadow is { }) - { - _rightShadow.Visible = true; - } - - if (_bottomShadow is { }) - { - _bottomShadow.Visible = true; - } - - _pressed = false; - - return; + _rightShadow.Visible = true; } - 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; + _bottomShadow.Visible = true; + } - if (_rightShadow is { }) - { - _rightShadow.Visible = false; - } + _pressed = false; - if (_bottomShadow is { }) - { - _bottomShadow.Visible = false; - } + 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 (_bottomShadow is { }) + { + _bottomShadow.Visible = false; } } } @@ -235,21 +288,27 @@ public class Margin : Adornment { 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 7dbc1cd70..2c7e8d1ab 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 class Padding : Adornment /// 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 @@ public class Padding : Adornment 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 3ca31898c..fa158cbdf 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; - - 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 ())); + return base.GetNormalColor (); } - return base.GetNormalColor (); + var attr = Attribute.Default; + + 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; + } + + /// + 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 @@ internal class ShadowView : View 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 @@ internal class ShadowView : View 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 @@ internal class ShadowView : View 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 @@ internal class ShadowView : View 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 32c07c711..f3cfc7747 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 class DrawEventArgs : EventArgs 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 83d169e2c..849a29e2a 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -22,6 +22,14 @@ using System.Numerics; /// /// /// +/// +/// +/// +/// 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 f159b6476..2d7f04479 100644 --- a/Terminal.Gui/View/Layout/DimAuto.cs +++ b/Terminal.Gui/View/Layout/DimAuto.cs @@ -70,15 +70,15 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt { 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt { 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt { int width = v.Width!.Calculate (0, superviewContentSize, v, dimension); size = v.X.GetAnchor (0) + width; + } else { @@ -237,22 +244,14 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt // 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt // 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt #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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt { 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 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt } #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 c51406f40..5fbc28dc2 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 dac959af0..264122c66 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 000000000..389b5a5ce --- /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 b5233a6dc..6ffe9b8d9 100644 --- a/Terminal.Gui/View/Layout/Pos.cs +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -251,22 +251,22 @@ public abstract record Pos /// 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 abstract record Pos /// /// 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 abstract record Pos /// /// 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 abstract record Pos /// /// 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 197621cf0..d488d0bd8 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 record PosAlign : Pos 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 record PosAlign : Pos 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 record PosAlign : Pos 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 @@ public record PosAlign : Pos 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 @@ public record PosAlign : Pos 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 9156e2230..3b66923ff 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 0bd819ad5..cb7f99c14 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 92748d88b..9e6a3d7c6 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 f3f37d40f..7058fb063 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 @@ public partial class View // Adornments /// /// /// + /// 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 @@ public partial class View // Adornments /// /// /// 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 partial class View // Adornments /// 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 partial class View // Adornments /// 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 partial class View // Adornments 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; - } + protected virtual bool OnBorderStyleChanging (CancelEventArgs e) { return false; } - BorderStyleChanging?.Invoke (this, e); - - 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 partial class View // Adornments { 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 partial class View // Adornments /// 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 partial class View // Adornments /// A thickness that describes the sum of the Adornments' thicknesses. public Thickness GetAdornmentsThickness () { - if (Margin is null) + Thickness result = Thickness.Empty; + + if (Margin is { }) { - return Thickness.Empty; + result += Margin.Thickness; } - return Margin.Thickness + Border.Thickness + Padding.Thickness; + if (Border is { }) + { + result += Border.Thickness; + } + + if (Padding is { }) + { + result += Padding.Thickness; + } + + return result; } - /// Lays out the Adornments of the View. - /// - /// Overriden by to do nothing, as does not have adornments. - /// - internal virtual void LayoutAdornments () + /// Sets the Frame's of the Margin, Border, and Padding. + internal void SetAdornmentFrames () { - if (Margin is null) + if (this is Adornment) { - return; // CreateAdornments () has not been called yet + // Adornments do not have Adornments + return; } - if (Margin.Frame.Size != Frame.Size) + if (Margin 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; + Margin!.Frame = Rectangle.Empty with { Size = Frame.Size }; } - Margin.SetNeedsLayout (); - Margin.SetNeedsDisplay (); - - if (IsInitialized) + if (Border is { } && Margin is { }) { - Margin.LayoutSubviews (); + Border!.Frame = Margin!.Thickness.GetInside (Margin!.Frame); } - Rectangle border = Margin.Thickness.GetInside (Margin.Frame); - - if (border != Border.Frame) + if (Padding is { } && Border is { }) { - Border.SetFrame (border); - Border.X = border.Location.X; - Border.Y = border.Location.Y; - Border.Width = border.Size.Width; - Border.Height = border.Size.Height; - } - - Border.SetNeedsLayout (); - Border.SetNeedsDisplay (); - - if (IsInitialized) - { - Border.LayoutSubviews (); - } - - Rectangle padding = Border.Thickness.GetInside (Border.Frame); - - if (padding != Padding.Frame) - { - Padding.SetFrame (padding); - Padding.X = padding.Location.X; - Padding.Y = padding.Location.Y; - Padding.Width = padding.Size.Width; - Padding.Height = padding.Size.Height; - } - - Padding.SetNeedsLayout (); - Padding.SetNeedsDisplay (); - - if (IsInitialized) - { - 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 aec9e6553..e11b1d389 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 000000000..20e201b66 --- /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 59fdaeb4f..d1bfafc79 100644 --- a/Terminal.Gui/View/View.Content.cs +++ b/Terminal.Gui/View/View.Content.cs @@ -151,10 +151,7 @@ public partial class View if (e.Cancel != true) { - OnResizeNeeded (); - - //SetNeedsLayout (); - //SetNeedsDisplay (); + SetNeedsLayout (); } return e.Cancel; @@ -173,7 +170,6 @@ public partial class View 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); @@ -269,7 +265,7 @@ public partial class View /// /// /// 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 @@ -313,6 +309,8 @@ public partial class View { _viewportLocation = viewport.Location; SetNeedsLayout (); + //SetNeedsDraw(); + //SetSubViewNeedsDraw(); } OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); @@ -328,6 +326,10 @@ public partial class View Size = newSize }; + OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); + + return; + void ApplySettings (ref Rectangle newViewport) { if (!ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth)) diff --git a/Terminal.Gui/View/View.Diagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs index 8145af8fc..8b67e50a8 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 000000000..2996751f9 --- /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 000000000..ba3245cea --- /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 4401d2958..79ab737f7 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -1,90 +1,304 @@ #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 (); - - // The view-relative region that needs to be redrawn. Marked internal for unit tests. - internal Rectangle _needsDisplayRect = Rectangle.Empty; - - /// Gets or sets whether the view needs to be redrawn. - public bool NeedsDisplay - { - get => _needsDisplayRect != Rectangle.Empty; - set - { - if (value) - { - SetNeedsDisplay (); - } - else - { - ClearNeedsDisplay (); - } - } - } - - /// Gets whether any Subviews need to be redrawn. - public bool SubViewNeedsDisplay { get; private set; } - /// - /// 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. + /// Draws the view if it needs to be drawn. /// - public virtual bool SuperViewRendersLineCanvas { get; set; } = false; - - /// 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. + /// + /// 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: . + /// /// - /// Column (viewport-relative). - /// Row (viewport-relative). - /// The Rune. - public void AddRune (int col, int row, Rune rune) + public void Draw () { - if (Move (col, row)) + if (!CanBeVisible (this)) { - Driver.AddRune (rune); + return; + } + + Region? saved = GetClip (); + + // 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) + { + // 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) + { + 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 (); + } + + // 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 (); + + // We're done drawing + DoDrawComplete (); + + // 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) + { + Rectangle borderFrame = FrameToScreen (); + + if (Border is { }) + { + borderFrame = Border.FrameToScreen (); + } + + ExcludeFromClip (borderFrame); } } + #region DrawAdornments + + private void DoDrawBorderAndPaddingSubViews () + { + if (Border?.Subviews is { } && Border.Thickness != Thickness.Empty) + { + // 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 (); + } + + LineCanvas.Exclude (new (subview.FrameToScreen())); + } + + Region? saved = Border?.ClipFrame (); + Border?.DoDrawSubviews (); + SetClip (saved); + } + + if (Padding?.Subviews is { } && Padding.Thickness != Thickness.Empty) + { + foreach (View subview in Padding.Subviews) + { + subview.SetNeedsDraw (); + } + + Region? saved = Padding?.ClipFrame (); + Padding?.DoDrawSubviews (); + SetClip (saved); + } + } + + private void DoDrawBorderAndPadding () + { + if (OnDrawingBorderAndPadding ()) + { + return; + } + + // TODO: add event. + + DrawBorderAndPadding (); + } + + /// + /// Causes and to be drawn. + /// + /// + /// + /// is drawn in a separate pass. + /// + /// + public void DrawBorderAndPadding () + { + // 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) + { + Border?.Draw (); + } + + if (Padding is { } && Padding.Thickness != Thickness.Empty) + { + Padding?.Draw (); + } + + } + + /// + /// 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. + /// + /// to stop further drawing of the Adornments. + protected virtual bool OnDrawingBorderAndPadding () { return false; } + + #endregion DrawAdornments + + #region SetAttribute + + private void DoSetAttribute () + { + if (OnSettingAttribute ()) + { + return; + } + + var args = new CancelEventArgs (); + SettingAttribute?.Invoke (this, args); + + if (args.Cancel) + { + return; + } + + 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 { }) + { + SetAttribute (GetNormalColor ()); + } + } + + #endregion + + #region ClearViewport + + private void DoClearViewport () + { + if (OnClearingViewport ()) + { + return; + } + + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + ClearingViewport?.Invoke (this, dev); + + if (dev.Cancel) + { + return; + } + + if (!NeedsDraw) + { + return; + } + + 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. + /// + /// Rect provides the view-relative rectangle describing the currently visible viewport into the + /// . + /// + /// + public event EventHandler? ClearingViewport; + /// Clears with the normal background. /// /// @@ -97,7 +311,7 @@ public partial class View // Drawing APIs /// the area outside the content to be visually distinct. /// /// - public void Clear () + public void ClearViewport () { if (Driver is null) { @@ -107,175 +321,114 @@ public partial class View // Drawing APIs // Get screen-relative coords Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); - Rectangle prevClip = Driver.Clip; - if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) { Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); toClear = Rectangle.Intersect (toClear, visibleContent); } - Attribute prev = Driver.SetAttribute (GetNormalColor ()); + Attribute prev = SetAttribute (GetNormalColor ()); Driver.FillRect (toClear); - Driver.SetAttribute (prev); - - Driver.Clip = prevClip; + SetAttribute (prev); + SetNeedsDraw (); } - /// 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) + #endregion ClearViewport + + #region DrawText + + private void DoDrawText () { - if (Driver is null) + if (OnDrawingText ()) { return; } - // Get screen-relative coords - Rectangle toClear = ViewportToScreen (rect); + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingText?.Invoke (this, dev); - 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; - } - - /// Sets the 's clip region to . - /// - /// - /// 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. - /// - /// - /// - /// The current screen-relative clip region, which can be then re-applied by setting - /// . - /// - public Rectangle SetClip () - { - if (Driver is null) + if (dev.Cancel) { - return Rectangle.Empty; + return; } - 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 (!NeedsDraw) { - // 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); + return; } - Driver.Clip = clip; - - return previous; + DrawText (); } /// - /// 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 of the View is to be drawn. /// - /// - /// - /// 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 . + protected virtual bool OnDrawingText () { return false; } + + +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved + /// Raised when the of the View is to be drawn. + /// + /// Set to to stop further drawing of + /// . + /// + public event EventHandler? DrawingText; +#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved + + /// + /// Draws the of the View using the . + /// + public void DrawText () { - if (!CanBeVisible (this)) + if (!string.IsNullOrEmpty (TextFormatter.Text)) { - return; + TextFormatter.NeedsFormat = true; } - // 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 (); - } + // TODO: If the output is not in the Viewport, do nothing + var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); - if (!NeedsDisplay && !SubViewNeedsDisplay && !LayoutNeeded) - { - return; - } + TextFormatter?.Draw ( + drawRect, + HasFocus ? GetFocusColor () : GetNormalColor (), + HasFocus ? GetHotFocusColor () : GetHotNormalColor (), + Rectangle.Empty + ); - OnDrawAdornments (); - - if (ColorScheme is { }) - { - //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); - Driver?.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 (); - - // Invoke DrawContentEvent - var dev = new DrawEventArgs (Viewport, Rectangle.Empty); - DrawContent?.Invoke (this, dev); - - if (!dev.Cancel) - { - OnDrawContent (Viewport); - } - - if (Driver is { }) - { - Driver.Clip = prevClip; - } - - OnRenderLineCanvas (); - - // TODO: This is a hack to force the border subviews to draw. - if (Border?.Subviews is { }) - { - foreach (View view in Border.Subviews) - { - view.SetNeedsDisplay (); - view.Draw (); - } - } - - // 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 (); + // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn. + SetSubViewNeedsDraw (); } - /// Event invoked when the content area of the View is to be drawn. + #endregion DrawText + + #region DrawContent + + private void DoDrawContent () + { + if (OnDrawingContent ()) + { + return; + } + + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingContent?.Invoke (this, dev); + + if (dev.Cancel) + { } + + // Do nothing. + } + + /// + /// 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. /// @@ -283,341 +436,137 @@ public partial class View // Drawing APIs /// . /// /// - public event EventHandler? DrawContent; + public event EventHandler? DrawingContent; - /// 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; + #endregion DrawContent - /// 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) + #region DrawSubviews + + private void DoDrawSubviews () { - Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; - Application.Driver?.SetAttribute (normalColor); - - foreach (Rune rune in text.EnumerateRunes ()) + if (OnDrawingSubviews ()) { - if (rune == new Rune (hotkeySpec.Value)) - { - Application.Driver?.SetAttribute (hotColor); - - continue; - } - - Application.Driver?.AddRune (rune); - Application.Driver?.SetAttribute (normalColor); + return; } + + var dev = new DrawEventArgs (Viewport, Rectangle.Empty); + DrawingSubviews?.Invoke (this, dev); + + if (dev.Cancel) + { + return; + } + + if (!SubViewNeedsDraw) + { + return; + } + + DrawSubviews (); } /// - /// Utility function to draw strings that contains a hotkey using a and the "focused" - /// state. + /// Called when the are to be drawn. /// - /// 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 - ); - } - } + /// to stop further drawing of . + protected virtual bool OnDrawingSubviews () { return false; } - /// 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) - { - cs = 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; - - if (cs is null) - { - cs = 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; - - 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; - } - - private Attribute GetColor (Attribute inputAttribute) - { - Attribute attr = inputAttribute; - if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) - { - attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ()); - } - - return attr; - } - - /// Moves the drawing cursor to the specified -relative location in the view. +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved + /// Raised when the are to be 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. - /// /// - /// 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; - } - - // 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. - /// - /// - public virtual bool OnDrawAdornments () - { - if (!IsInitialized) - { - 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; - } + /// + /// 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; - } - - // BUGBUG: this clears way too frequently. Need to optimize this. - if (SuperView is { } || Arrangement.HasFlag (ViewArrangement.Overlapped)) - { - Clear (); - } - - if (!string.IsNullOrEmpty (TextFormatter.Text)) - { - if (TextFormatter is { }) - { - TextFormatter.NeedsFormat = true; - } - } - - // 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 (); + return; } - // TODO: Move drawing of subviews to a separate OnDrawSubviews virtual method - // Draw subviews - // TODO: Implement OnDrawSubviews (cancelable); - if (_subviews is { } && SubViewNeedsDisplay) + // Draw the subviews in reverse order to leverage clipping. + foreach (View view in _subviews.Where (view => view.Visible).Reverse ()) { - IEnumerable subviewsNeedingDraw = _subviews.Where ( - view => view.Visible - && (view.NeedsDisplay - || view.SubViewNeedsDisplay - || view.LayoutNeeded - || view.Arrangement.HasFlag (ViewArrangement.Overlapped) - )); - - foreach (View view in subviewsNeedingDraw) + // TODO: HACK - This enables auto line join to work, but is brute force. + if (view.SuperViewRendersLineCanvas) { - if (view.LayoutNeeded) - { - view.LayoutSubviews (); - } + view.SetNeedsDraw (); + } + view.Draw (); - // 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 (); - } - - view.Draw (); + if (view.SuperViewRendersLineCanvas) + { + LineCanvas.Merge (view.LineCanvas); + view.LineCanvas.Clear (); } } } - /// - /// Called after to enable overrides. - /// - /// - /// The viewport-relative rectangle describing the currently visible viewport into the - /// - /// - public virtual void OnDrawContentComplete (Rectangle viewport) { DrawContentComplete?.Invoke (this, new (viewport, Rectangle.Empty)); } + #endregion DrawSubviews + + #region DrawLineCanvas + + private void DoRenderLineCanvas () + { + if (OnRenderingLineCanvas ()) + { + return; + } + + // TODO: Add event + + RenderLineCanvas (); + } - // TODO: Make this cancelable /// - /// Renders . If is true, only the + /// Called when the is to be rendered. See . + /// + /// 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 (); + + /// + /// 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 partial class View // Drawing APIs LineCanvas.Clear (); } - - if (Subviews.Any (s => s.SuperViewRendersLineCanvas)) - { - foreach (View subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) - { - // Combine the LineCanvas' - LineCanvas.Merge (subview.LineCanvas); - subview.LineCanvas.Clear (); - } - - foreach (KeyValuePair p in LineCanvas.GetCellMap ()) - { - // 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); - } - } - - LineCanvas.Clear (); - } - - return true; } - /// Sets the area of this view needing to be redrawn to . + #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 + { + if (value) + { + SetNeedsDraw (); + } + else + { + ClearNeedsDraw (); + } + } + } + + /// 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; - /// Expands the area of this view needing to be redrawn to include . + 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 . /// /// - /// 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) { - _needsDisplayRect = region; + return; + } + + if (_needsDrawRect.IsEmpty) + { + _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); } - Margin?.SetNeedsDisplay (); - Border?.SetNeedsDisplay (); - Padding?.SetNeedsDisplay (); + // Do not set on Margin - it will be drawn in a separate pass. - SuperView?.SetSubViewNeedsDisplay (); + if (Border is { } && Border.Thickness != Thickness.Empty) + { + Border?.SetNeedsDraw (); + } + + if (Padding is { } && Padding.Thickness != Thickness.Empty) + { + Padding?.SetNeedsDraw (); + } + + SuperView?.SetSubViewNeedsDraw (); + + 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; - Margin?.ClearNeedsDisplay (); - Border?.ClearNeedsDisplay (); - Padding?.ClearNeedsDisplay (); + if (Margin is { } && Margin.Thickness != Thickness.Empty) + { + Margin?.ClearNeedsDraw (); + } + + if (Border is { } && Border.Thickness != Thickness.Empty) + { + Border?.ClearNeedsDraw (); + } + + 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 01cf01beb..5f45236b3 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -47,8 +47,12 @@ public partial class View // SuperView/SubView hierarchy management (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 partial class View // SuperView/SubView hierarchy management (SuperView, if (view.Enabled && !Enabled) { - view._oldEnabled = true; view.Enabled = false; } @@ -85,9 +88,8 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, view.EndInit (); } - CheckDimAuto (); + SetNeedsDraw (); SetNeedsLayout (); - SetNeedsDisplay (); return view; } @@ -126,7 +128,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, { View view = e.SubView; view.IsAdded = true; - view.OnResizeNeeded (); view.Added?.Invoke (this, e); } @@ -150,8 +151,13 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// /// 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 partial class View // SuperView/SubView hierarchy management (SuperView, view._superView = null; SetNeedsLayout (); - SetNeedsDisplay (); + SetNeedsDraw (); foreach (View v in _subviews) { if (v.Frame.IntersectsWith (touched)) { - view.SetNeedsDisplay (); + view.SetNeedsDraw (); } } @@ -339,8 +345,8 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, } // 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 318b5c993..67e18955e 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 @@ public partial class View // Layout APIs /// . /// /// - /// 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; + get + { + if (NeedsLayout) + { + //Debug.WriteLine("Frame_get with _layoutNeeded"); + } + + return _frame ?? Rectangle.Empty; + } set { - if (_frame == value) + // 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) })) { - return; + // 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 (); } - - 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) - { - OnResizeNeeded (); - } - - 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,9 +90,29 @@ public partial class View // Layout APIs // This is the only place where _frame should be set directly. Use Frame = or SetFrame instead. _frame = frame; - OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); + SetAdornmentFrames (); + + SetNeedsDraw (); + SetNeedsLayout (); + + // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK? + OnFrameChanged (in frame); + FrameChanged?.Invoke (this, new (in frame)); + return true; } + /// + /// Called when changes. + /// + /// The new Frame. + protected virtual void OnFrameChanged (in Rectangle frame) { } + + /// + /// Raised when the changes. This event is raised after the has been + /// updated. + /// + public event EventHandler>? FrameChanged; + /// Gets the with a screen-relative location. /// The location and size of the view in screen-relative coordinates. public virtual Rectangle FrameToScreen () @@ -256,7 +132,7 @@ public partial class View // Layout APIs // 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 +170,24 @@ public partial class View // Layout APIs 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 +203,12 @@ public partial class View // Layout APIs /// /// /// 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 +227,7 @@ public partial class View // Layout APIs _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null"); - OnResizeNeeded (); + PosDimSet (); } } @@ -352,12 +246,12 @@ public partial class View // Layout APIs /// /// /// 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 +269,7 @@ public partial class View // Layout APIs } _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null"); - OnResizeNeeded (); + PosDimSet (); } } @@ -390,17 +284,16 @@ public partial class View // Layout APIs /// /// /// 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 +310,12 @@ public partial class View // Layout APIs 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 +334,13 @@ public partial class View // Layout APIs /// . /// /// - /// 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 +357,91 @@ public partial class View // Layout APIs 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 Layout Engine - - /// Fired after the View's method has completed. - /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has - /// otherwise changed. - /// - public event EventHandler? LayoutComplete; - - /// Fired after the View's method has completed. - /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has - /// otherwise changed. - /// - public event EventHandler? LayoutStarted; + #region Core Layout API /// - /// Adjusts given the SuperView's ContentSize (nominally the same as - /// this.SuperView.GetContentSize ()) - /// and the position (, ) and dimension (, and - /// ). + /// 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); + } + } + + return neededLayout; + } + + /// + /// Performs layout of the view and its subviews within the specified content size. + /// + /// + /// + /// 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. + /// + /// + /// + /// If the view could not be laid out (typically because a dependencies was not ready). + public bool Layout (Size contentSize) + { + if (SetRelativeLayout (contentSize)) + { + LayoutSubviews (); + + // Debug.Assert(!NeedsLayout); + return true; + } + + return false; + } + + /// + /// Performs layout of the view and its subviews using the content size of either the or + /// . + /// + /// + /// + /// 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. + /// + /// + /// If the view could not be laid out (typically because dependency was not ready). + public bool Layout () { return Layout (GetContainerSize ()); } + + /// + /// 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 +450,18 @@ public partial class View // Layout APIs /// 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 +469,54 @@ public partial class View // Layout APIs 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 +524,7 @@ public partial class View // Layout APIs 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 +552,7 @@ public partial class View // Layout APIs SetTitleTextFormatterSize (); } - SetNeedsLayout (); - SetNeedsDisplay (); + SuperView?.SetNeedsDraw (); } if (TextFormatter.ConstrainToWidth is null) @@ -605,11 +564,13 @@ public partial class View // Layout APIs { 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 +581,11 @@ public partial class View // Layout APIs /// 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 +593,25 @@ public partial class View // Layout APIs 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 +619,197 @@ public partial class View // Layout APIs CollectAll (this, ref nodes, ref edges); List ordered = TopologicalSort (SuperView!, nodes, edges); + List redo = new (); + foreach (View v in ordered) { - LayoutSubview (v, contentSize); - } - - // 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) - { - foreach ((View from, View to) in edges) + if (!v.Layout (contentSize)) { - LayoutSubview (to, from.GetContentSize ()); + redo.Add (v); } } - LayoutNeeded = false; + var layoutStillNeeded = false; - OnLayoutComplete (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; } - - /// - /// Raises the event. Called from before all sub-views - /// have been laid out. - /// - internal virtual void OnLayoutComplete (LayoutEventArgs args) { LayoutComplete?.Invoke (this, args); } - - /// - /// Raises the event. Called from before any subviews - /// have been laid out. - /// - internal virtual void OnLayoutStarted (LayoutEventArgs args) { LayoutStarted?.Invoke (this, args); } - - /// - /// Called whenever the view needs to be resized. This is called whenever , - /// , , , or changes. - /// - /// - /// - /// Determines the relative bounds of the and its s, and then calls - /// to update the view. - /// - /// - internal void OnResizeNeeded () - { - // TODO: Identify a real-world use-case where this API should be virtual. - // TODO: Until then leave it `internal` and non-virtual - - // 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; - - SetRelativeLayout (superViewContentSize); - - if (IsInitialized) + if (redo.Count > 0) { - LayoutAdornments (); - } - - SetNeedsLayout (); - - // 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)) - { - foreach (Toplevel v in Application.TopLevels) + foreach (View v in ordered) { - if (v.Visible && v != this) + if (!v.Layout (contentSize)) { - v.SetNeedsDisplay (); + layoutStillNeeded = true; } } } + + // If the 'to' is rooted to 'from' it's a special-case. + // 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) + { + // QUESTION: Do we test this with adornments well enough? + to.Layout (from.GetContentSize ()); + } + } + + NeedsLayout = layoutStillNeeded; + + OnSubviewsLaidOut (new (contentSize)); + SubviewsLaidOut?.Invoke (this, new (contentSize)); } - internal bool LayoutNeeded { get; private set; } = true; + /// + /// Called from before any subviews + /// have been laid out. + /// + /// + /// Override to perform tasks when the layout is changing. + /// + protected virtual void OnSubviewLayout (LayoutEventArgs args) { } /// - /// 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. + /// Raised by before any subviews + /// have been laid out. /// - internal void SetNeedsLayout () + /// + /// Subscribe to this event to perform tasks when the layout is changing. + /// + public event EventHandler? SubviewLayout; + + /// + /// Called from after all sub-views + /// have been laid out. + /// + /// + /// 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; + + /// + /// Sets to return , indicating this View and all of it's subviews + /// (including adornments) need to be laid out in the next Application iteration. + /// + /// + /// + /// The will cause to be called on the next + /// so there is normally no reason to call see . + /// + /// + public void SetNeedsLayout () { - if (LayoutNeeded) + NeedsLayout = true; + + if (Margin is { Subviews.Count: > 0 }) + { + Margin.SetNeedsLayout (); + } + + if (Border is { Subviews.Count: > 0 }) + { + Border.SetNeedsLayout (); + } + + if (Padding is { Subviews.Count: > 0 }) + { + Padding.SetNeedsLayout (); + } + + // Use a stack to avoid recursion + Stack stack = new (Subviews); + + while (stack.Count > 0) + { + View current = stack.Pop (); + + if (!current.NeedsLayout) + { + 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 }) + { + current.Padding.SetNeedsLayout (); + } + + foreach (View subview in current.Subviews) + { + stack.Push (subview); + } + } + } + + TextFormatter.NeedsFormat = true; + + if (SuperView is { NeedsLayout: false }) + { + SuperView?.SetNeedsLayout (); + } + + if (SuperView is null) + { + foreach (Toplevel tl in Application.TopLevels) + { + // tl.SetNeedsDraw (); + } + } + + if (this is not Adornment adornment) { return; } - LayoutNeeded = true; - - foreach (View view in Subviews) + if (adornment.Parent is { NeedsLayout: false }) { - view.SetNeedsLayout (); + adornment.Parent?.SetNeedsLayout (); } - - TextFormatter.NeedsFormat = true; - SuperView?.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 +831,7 @@ public partial class View // Layout APIs } /// - /// 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 +842,23 @@ public partial class View // Layout APIs /// 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)); - } + 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); - - 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 +869,12 @@ public partial class View // Layout APIs /// 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 +968,16 @@ public partial class View // Layout APIs { 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 +985,159 @@ public partial class View // Layout APIs 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 +1253,13 @@ public partial class View // Layout APIs 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 a76de77b3..4dbb65e38 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -664,17 +664,20 @@ public partial class View // Mouse APIs 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 e68b3c7d3..cbd2963c4 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -594,7 +594,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // Focus work is done. Notify. RaiseFocusChanged (HasFocus, currentFocusedView, this); - SetNeedsDisplay (); + SetNeedsDraw (); // Post-conditions - prove correctness if (HasFocus == previousValue) @@ -847,21 +847,21 @@ public partial class View // Focus and cross-view navigation management (TabStop 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 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// 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.ScrollBars.cs b/Terminal.Gui/View/View.ScrollBars.cs index c37a527a5..a289885ba 100644 --- a/Terminal.Gui/View/View.ScrollBars.cs +++ b/Terminal.Gui/View/View.ScrollBars.cs @@ -178,19 +178,15 @@ public partial class View else if (viewportSettings.HasFlag (ViewportSettings.AllowNegativeX)) { _horizontalScrollBar.Value.AutoHide = false; - _horizontalScrollBar.Value.ShowScrollIndicator = false; } else if (viewportSettings.HasFlag (ViewportSettings.AllowNegativeY)) { _verticalScrollBar.Value.AutoHide = false; - _verticalScrollBar.Value.ShowScrollIndicator = false; } else if (viewportSettings.HasFlag (ViewportSettings.AllowNegativeLocation)) { _horizontalScrollBar.Value.AutoHide = false; - _horizontalScrollBar.Value.ShowScrollIndicator = false; _verticalScrollBar.Value.AutoHide = false; - _verticalScrollBar.Value.ShowScrollIndicator = false; } else if (viewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth)) { diff --git a/Terminal.Gui/View/View.Text.cs b/Terminal.Gui/View/View.Text.cs index 508637b56..971a8ac83 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 partial class View // Text Property APIs { TextFormatter.PreserveTrailingSpaces = value; TextFormatter.NeedsFormat = true; + SetNeedsLayout (); } } } @@ -58,11 +58,16 @@ public partial class View // Text Property APIs 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 partial class View // Text Property APIs { TextFormatter.Alignment = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); } } @@ -143,7 +148,7 @@ public partial class View // Text Property APIs set { TextFormatter.VerticalAlignment = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -229,9 +234,9 @@ public partial class View // Text Property APIs { 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 706409c5b..24ab96b87 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 . /// @@ -143,6 +145,7 @@ public partial class View : Responder, ISupportInitializeNotification SetupText (); + SetupScrollBars (); } /// @@ -229,7 +232,6 @@ public partial class View : Responder, ISupportInitializeNotification // 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 +244,10 @@ public partial class View : Responder, ISupportInitializeNotification } } + // 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 +257,6 @@ public partial class View : Responder, ISupportInitializeNotification 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 +285,12 @@ public partial class View : Responder, ISupportInitializeNotification } OnEnabledChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); + + if (Border is { }) + { + Border.Enabled = _enabled; + } if (_subviews is null) { @@ -291,18 +299,7 @@ public partial class View : Responder, ISupportInitializeNotification 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 +360,10 @@ public partial class View : Responder, ISupportInitializeNotification OnVisibleChanged (); VisibleChanged?.Invoke (this, EventArgs.Empty); - SetNeedsDisplay (); + SetNeedsLayout (); + SuperView?.SetNeedsLayout (); + SetNeedsDraw (); + SuperView?.SetNeedsDraw (); } } @@ -469,7 +469,7 @@ public partial class View : Responder, ISupportInitializeNotification SetTitleTextFormatterSize (); SetHotKeyFromTitle (); - SetNeedsDisplay (); + SetNeedsDraw (); #if DEBUG if (string.IsNullOrEmpty (Id)) { @@ -531,6 +531,7 @@ public partial class View : Responder, ISupportInitializeNotification DisposeKeyboard (); DisposeAdornments (); + DisposeScrollBars (); for (int i = InternalSubviews.Count - 1; i >= 0; i--) { diff --git a/Terminal.Gui/View/ViewportSettings.cs b/Terminal.Gui/View/ViewportSettings.cs index b0c383865..c3cc51ac0 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. /// must be set for this setting to work (clipping beyond the visible area must be diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 1511cd68a..bece84078 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -31,7 +31,7 @@ public class Bar : View, IOrientation, IDesignable _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 @@ public class Bar : View, IOrientation, IDesignable } } - 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 class Bar : View, IOrientation, IDesignable /// 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 class Bar : View, IOrientation, IDesignable set { _alignmentModes = value; + //SetNeedsDraw (); SetNeedsLayout (); } } @@ -162,7 +168,8 @@ public class Bar : View, IOrientation, IDesignable } } - SetNeedsDisplay (); + //SetNeedsDraw (); + SetNeedsLayout (); } // TODO: Move this to View @@ -185,17 +192,16 @@ public class Bar : View, IOrientation, IDesignable 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 @@ public class Bar : View, IOrientation, IDesignable 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 minKeyWidth = 0; + + List shortcuts = Subviews.Where (s => s is Shortcut && s.Visible).Cast ().ToList (); + + foreach (Shortcut shortcut in shortcuts) + { + // Get the largest width of all KeyView's + minKeyWidth = int.Max (minKeyWidth, shortcut.KeyView.Text.GetColumns ()); + } + + var _maxBarItemWidth = 0; + + for (var index = 0; index < Subviews.Count; index++) + { + 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; + + } + + foreach (var subView in Subviews) + { + subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: _maxBarItemWidth); + } } - - var maxBarItemWidth = 0; - var totalHeight = 0; - - for (var index = 0; index < Subviews.Count; index++) + else { - View barItem = Subviews [index]; - - barItem.ColorScheme = ColorScheme; - - if (!barItem.Visible) + foreach (var subView in Subviews) { - continue; + subView.Width = Dim.Fill(); } - - if (barItem is Shortcut scBarItem) - { - scBarItem.MinimumKeyTextSize = minKeyWidth; - maxBarItemWidth = Math.Max (maxBarItemWidth, scBarItem.Frame.Width); - } - - if (prevBarItem == null) - { - barItem.Y = 0; - } - else - { - // Align the view to the bottom of the previous view - barItem.Y = Pos.Bottom (prevBarItem); - } - - prevBarItem = barItem; - - barItem.X = 0; - totalHeight += barItem.Frame.Height; } - - foreach (View barItem in Subviews) - { - barItem.Width = maxBarItemWidth; - } - - Height = Dim.Auto (DimAutoStyle.Content, totalHeight); - break; } } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index f642f23d9..eaf2d3956 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -191,7 +191,7 @@ public class Button : View, IDesignable _isDefault = value; UpdateTextFormatterText (); - OnResizeNeeded (); + SetNeedsLayout (); } } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index af58ecedd..04156637e 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -153,7 +153,7 @@ public class CheckBox : View _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 581606d17..824b49958 100644 --- a/Terminal.Gui/Views/ColorBar.cs +++ b/Terminal.Gui/Views/ColorBar.cs @@ -78,30 +78,30 @@ internal abstract class ColorBar : View, IColorBar 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 @@ internal abstract class ColorBar : View, IColorBar 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 @@ internal abstract class ColorBar : View, IColorBar } else { - Application.Driver?.SetAttribute (new (color, color)); + SetAttribute (new (color, color)); AddRune (x + xOffset, yOffset, new ('█')); } } @@ -215,7 +215,7 @@ internal abstract class ColorBar : View, IColorBar 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 c1cb72cc2..39a02f351 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -67,8 +67,12 @@ public class ColorPicker16 : View /// 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 class ColorPicker16 : View /// 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 class ColorPicker16 : View /// 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 class ColorPicker16 : View /// 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 class ColorPicker16 : View } /// - 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 class ColorPicker16 : View 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 class ColorPicker16 : View 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 @@ public class ColorPicker16 : View // 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 @@ public class ColorPicker16 : View { 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 @@ public class ColorPicker16 : View 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 a4209ffe4..415fc1acd 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -90,10 +90,7 @@ public partial class ColorPicker : View CreateTextField (); SelectedColor = oldValue; - if (IsInitialized) - { - LayoutSubviews (); - } + SetNeedsLayout (); } /// @@ -102,13 +99,14 @@ public partial class ColorPicker : View 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 @@ public partial class ColorPicker : View { _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 996056068..abfee0fde 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -57,7 +57,7 @@ public class ComboBox : View, IDesignable Initialized += (s, e) => ProcessLayout (); // On resize - LayoutComplete += (sender, a) => ProcessLayout (); + SubviewsLaidOut += (sender, a) => ProcessLayout (); Added += (s, e) => { @@ -73,7 +73,7 @@ public class ComboBox : View, IDesignable } SetNeedsLayout (); - SetNeedsDisplay (); + SetNeedsDraw (); ShowHideList (Text); }; @@ -118,7 +118,7 @@ public class ComboBox : View, IDesignable { _listview.ColorScheme = value; base.ColorScheme = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -198,7 +198,7 @@ public class ComboBox : View, IDesignable if (SuperView is { } && SuperView.Subviews.Contains (this)) { Text = string.Empty; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -294,18 +294,21 @@ public class ComboBox : View, IDesignable 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; } - Driver.SetAttribute (ColorScheme.Focus); - Move (Viewport.Right - 1, 0); - Driver.AddRune (Glyphs.DownArrow); + if (ColorScheme != null) + { + SetAttribute (ColorScheme.Focus); + } + AddRune (Viewport.Right - 1, 0, Glyphs.DownArrow); + + return true; } @@ -504,11 +507,13 @@ public class ComboBox : View, IDesignable } 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 @@ public class ComboBox : View, IDesignable _listview.SetSource (_searchSet); _listview.ResumeSuspendCollectionChangedEvent (); - _listview.Clear (); + _listview.ClearViewport (); _listview.Height = CalculateHeight (); SuperView?.MoveSubviewToStart (this); } @@ -872,7 +877,7 @@ public class ComboBox : View, IDesignable if (isMousePositionValid) { _highlighted = Math.Min (TopItem + me.Position.Y, Source.Count); - SetNeedsDisplay (); + SetNeedsDraw (); } _isFocusing = false; @@ -883,10 +888,10 @@ public class ComboBox : View, IDesignable 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 class ComboBox : View, IDesignable if (newcolor != current) { - Driver.SetAttribute (newcolor); + SetAttribute (newcolor); current = newcolor; } @@ -926,7 +931,7 @@ public class ComboBox : View, IDesignable { for (var c = 0; c < f.Width; c++) { - Driver.AddRune ((Rune)' '); + AddRune (0, row, (Rune)' '); } } else @@ -937,24 +942,26 @@ public class ComboBox : View, IDesignable 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 aca0c086e..2962464ec 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -200,8 +200,9 @@ public class DatePicker : View ShowHeaders = true, ShowHorizontalBottomline = true, ShowVerticalCellLines = true, - ExpandLastColumn = true - } + ExpandLastColumn = true, + }, + MultiSelect = false }; _dateField = new DateField (DateTime.Now) @@ -286,6 +287,9 @@ public class DatePicker : View 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 44cf5a168..259638baf 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -89,13 +89,13 @@ public class Dialog : Window /// 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 2ded19418..b80dcacab 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 @@ public class FileDialog : Dialog 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 @@ public class FileDialog : Dialog 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 @@ public class FileDialog : Dialog 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 @@ public class FileDialog : Dialog _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 @@ public class FileDialog : Dialog Add (_tbFind); Add (_spinnerView); - Add(_btnOk); - Add(_btnCancel); + Add (_btnOk); + Add (_btnCancel); } /// @@ -368,10 +382,8 @@ public class FileDialog : Dialog } /// - 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 class FileDialog : Dialog 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 class FileDialog : Dialog 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 class FileDialog : Dialog _btnOk.X = Pos.Right (_btnCancel) + 1; MoveSubviewTowardsStart (_btnCancel); } - LayoutSubviews (); + + SetNeedsDraw (); + SetNeedsLayout (); } /// @@ -634,7 +650,7 @@ public class FileDialog : Dialog if (!IsCompatibleWithOpenMode (f.FullName, out string reason)) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -661,7 +677,7 @@ public class FileDialog : Dialog if (reason is { }) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); } return; @@ -827,7 +843,11 @@ public class FileDialog : Dialog } Canceled = false; - Application.RequestStop (); + + if (Modal) + { + Application.RequestStop (); + } } private string GetBackButtonText () { return Glyphs.LeftArrow + "-"; } @@ -1115,7 +1135,7 @@ public class FileDialog : Dialog _tableView.RowOffset = 0; _tableView.SelectedRow = 0; - SetNeedsDisplay (); + SetNeedsDraw (); UpdateNavigationVisibility (); } finally @@ -1400,7 +1420,7 @@ public class FileDialog : Dialog if (reason is { }) { _feedback = reason; - SetNeedsDisplay (); + SetNeedsDraw (); } return false; @@ -1588,9 +1608,16 @@ public class FileDialog : Dialog 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 7dbc8836e..02b67c974 100644 --- a/Terminal.Gui/Views/GraphView/Annotations.cs +++ b/Terminal.Gui/Views/GraphView/Annotations.cs @@ -127,6 +127,17 @@ public class LegendAnnotation : View, IAnnotation /// 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 class LegendAnnotation : View, IAnnotation if (BorderStyle != LineStyle.None) { - OnDrawAdornments (); - OnRenderLineCanvas (); + DrawBorderAndPadding (); + RenderLineCanvas (); } var linesDrawn = 0; @@ -149,7 +160,7 @@ public class LegendAnnotation : View, IAnnotation { 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 d810957dd..93dcc1992 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 class GraphView : View } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { if (CellSize.X == 0 || CellSize.Y == 0) { @@ -212,13 +212,13 @@ public class GraphView : View 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 class GraphView : View // 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 class GraphView : View { a.Render (this); } + return true; } /// Scrolls the graph down 1 page. @@ -296,7 +297,7 @@ public class GraphView : View 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 class GraphView : View 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 f7c02e174..194bfeac2 100644 --- a/Terminal.Gui/Views/GraphView/Series.cs +++ b/Terminal.Gui/Views/GraphView/Series.cs @@ -33,7 +33,7 @@ public class ScatterSeries : ISeries { 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 @@ public class BarSeries : ISeries 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 8c614062b..cc0ba4df5 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -104,7 +104,7 @@ public class HexView : View, IDesignable KeyBindings.Remove (Key.Space); KeyBindings.Remove (Key.Enter); - LayoutComplete += HexView_LayoutComplete; + SubviewsLaidOut += HexView_LayoutComplete; } /// Initializes a class. @@ -198,7 +198,8 @@ public class HexView : View, IDesignable Address = 0; } - SetNeedsDisplay (); + SetNeedsLayout (); + SetNeedsDraw (); } } @@ -270,7 +271,8 @@ public class HexView : View, IDesignable } _addressWidth = value; - SetNeedsDisplay (); + SetNeedsDraw (); + SetNeedsLayout (); } } @@ -291,7 +293,7 @@ public class HexView : View, IDesignable _displayStart = value; } - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -317,7 +319,7 @@ public class HexView : View, IDesignable } _edits = new (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -413,35 +415,35 @@ public class HexView : View, IDesignable } } - 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 class HexView : View, IDesignable 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 class HexView : View, IDesignable 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 class HexView : View, IDesignable 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 @@ public class HexView : View, IDesignable /// protected void RaisePositionChanged () { + SetNeedsDraw (); + HexViewEventArgs args = new (Address, Position, BytesPerLine); OnPositionChanged (args); PositionChanged?.Invoke (this, args); @@ -598,6 +604,11 @@ public class HexView : View, IDesignable return false; } + if (keyEvent.IsAlt) + { + return false; + } + if (_leftSideHasFocus) { int value; @@ -775,7 +786,7 @@ public class HexView : View, IDesignable if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + bytes); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -793,7 +804,7 @@ public class HexView : View, IDesignable if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (Address); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -807,7 +818,7 @@ public class HexView : View, IDesignable { // 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 @@ public class HexView : View, IDesignable private bool MoveHome () { DisplayStart = 0; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -844,7 +855,7 @@ public class HexView : View, IDesignable if (Address - 1 < DisplayStart) { SetDisplayStart (_displayStart - BytesPerLine); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -881,7 +892,7 @@ public class HexView : View, IDesignable if (Address >= DisplayStart + BytesPerLine * Viewport.Height) { SetDisplayStart (DisplayStart + BytesPerLine); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -906,7 +917,7 @@ public class HexView : View, IDesignable private bool MoveLeftStart () { Address = Address / BytesPerLine * BytesPerLine; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -923,7 +934,7 @@ public class HexView : View, IDesignable if (Address < DisplayStart) { SetDisplayStart (DisplayStart - bytes); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -943,7 +954,7 @@ public class HexView : View, IDesignable 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 730d5a1aa..82b7499e1 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -64,39 +64,40 @@ public class Line : View, IOrientation } /// - 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 4191ed213..ccb65d3fd 100644 --- a/Terminal.Gui/Views/LineView.cs +++ b/Terminal.Gui/Views/LineView.cs @@ -54,12 +54,10 @@ public class LineView : View 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 class LineView : View 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 925bc203c..87f9db948 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 @@ public interface IListDataSource : IDisposable /// /// /// 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 class ListView : View, IDesignable // 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 class ListView : View, IDesignable // 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 class ListView : View, IDesignable set { _allowsMarking = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -254,7 +272,7 @@ public class ListView : View, IDesignable } } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -282,7 +300,7 @@ public class ListView : View, IDesignable } Viewport = Viewport with { X = value }; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -313,7 +331,7 @@ public class ListView : View, IDesignable /// 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 class ListView : View, IDesignable 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 @@ public class ListView : View, IDesignable SelectedItem = Source.Count - 1; } - SetNeedsDisplay (); + SetNeedsDraw (); OnCollectionChanged (e); } @@ -446,11 +465,11 @@ public class ListView : View, IDesignable 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 class ListView : View, IDesignable if (UnmarkAllButSelected ()) { Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); - SetNeedsDisplay (); + SetNeedsDraw (); return Source.IsMarked (SelectedItem); } @@ -537,7 +556,7 @@ public class ListView : View, IDesignable } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); if (me.Flags == MouseFlags.Button1DoubleClicked) { @@ -564,7 +583,7 @@ public class ListView : View, IDesignable // 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 class ListView : View, IDesignable } 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 class ListView : View, IDesignable } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -631,7 +650,7 @@ public class ListView : View, IDesignable _selected = 0; Viewport = Viewport with { Y = _selected }; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -670,7 +689,7 @@ public class ListView : View, IDesignable } OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -692,7 +711,7 @@ public class ListView : View, IDesignable _selected = n; Viewport = Viewport with { Y = _selected }; OnSelectedChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); } return true; @@ -715,7 +734,7 @@ public class ListView : View, IDesignable // 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 class ListView : View, IDesignable } 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 class ListView : View, IDesignable if (newcolor != current) { - Driver.SetAttribute (newcolor); + SetAttribute (newcolor); current = newcolor; } @@ -780,7 +797,7 @@ public class ListView : View, IDesignable { for (var c = 0; c < f.Width; c++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } } else @@ -791,21 +808,22 @@ public class ListView : View, IDesignable 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 @@ public class ListView : View, IDesignable { SelectedItem = (int)newItem; EnsureSelectedItemVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -900,20 +918,20 @@ public class ListView : View, IDesignable /// 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 @@ public class ListWrapper : IListDataSource, IDisposable /// public void Render ( ListView container, - ConsoleDriver driver, bool marked, int item, int col, @@ -1113,17 +1130,17 @@ public class ListWrapper : IListDataSource, IDisposable 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 @@ public class ListWrapper : IListDataSource, IDisposable 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 a8c50755b..fdfab4d98 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -70,7 +70,18 @@ internal sealed class Menu : View { 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 @@ internal sealed class Menu : View { if (Application.Top is { }) { - Application.Top.DrawContentComplete += Current_DrawContentComplete; + Application.Top.DrawComplete += Top_DrawComplete; Application.Top.SizeChanging += Current_TerminalResized; } @@ -279,7 +290,7 @@ internal sealed class Menu : View if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); return true; @@ -382,19 +393,26 @@ internal sealed class Menu : View 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 (); - OnDrawAdornments (); - OnRenderLineCanvas (); + // 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 (); + + SetAttribute (GetNormalColor ()); for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) { @@ -410,7 +428,7 @@ internal sealed class Menu : View MenuItem? item = _barItems.Children [i]; - Driver.SetAttribute ( + SetAttribute ( // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract item is null ? GetNormalColor () : i == _currentChild ? GetFocusColor () : GetNormalColor () @@ -427,7 +445,7 @@ internal sealed class Menu : View 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 @@ internal sealed class Menu : View } } - 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 @@ internal sealed class Menu : View Application.UngrabMouse (); _host.CloseAllMenus (); Application.Driver!.ClearContents (); - Application.Refresh (); + Application.LayoutAndDraw (); _host.Run (action); } @@ -702,7 +710,7 @@ internal sealed class Menu : View } while (_barItems?.Children? [_currentChild] is null || disabled); - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); if (!_host.UseSubMenusSingleFrame) @@ -783,7 +791,7 @@ internal sealed class Menu : View } while (_barItems.Children [_currentChild] is null || disabled); - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); if (!_host.UseSubMenusSingleFrame) @@ -800,12 +808,12 @@ internal sealed class Menu : View { 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 @@ internal sealed class Menu : View if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); return me.Handled = true; @@ -935,7 +943,7 @@ internal sealed class Menu : View } else { - SetNeedsDisplay (); + SetNeedsDraw (); SetParentSetNeedsDisplay (); } @@ -948,7 +956,7 @@ internal sealed class Menu : View 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 a15d1e463..7bba8b714 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -244,7 +244,7 @@ public class MenuBar : View, IDesignable if (value && UseKeysUpDownAsKeysLeftRight) { _useKeysUpDownAsKeysLeftRight = false; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -297,12 +297,8 @@ public class MenuBar : View, IDesignable 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 class MenuBar : View, IDesignable } //PositionCursor (); + return true; } /// Virtual method that will invoke the . @@ -381,7 +378,7 @@ public class MenuBar : View, IDesignable mi = parent?.Children?.Length > 0 ? parent.Children [_openMenu!._currentChild] : null; } } - + MenuOpened?.Invoke (this, new (parent, mi)); } @@ -411,7 +408,7 @@ public class MenuBar : View, IDesignable } _selected = 0; - SetNeedsDisplay (); + SetNeedsDraw (); _previousFocused = (SuperView is null ? Application.Top?.Focused : SuperView.Focused)!; OpenMenu (_selected); @@ -478,7 +475,7 @@ public class MenuBar : View, IDesignable } OpenMenu (idx, sIdx, subMenu); - SetNeedsDisplay (); + SetNeedsDraw (); } internal void CleanUp () @@ -500,7 +497,7 @@ public class MenuBar : View, IDesignable _lastFocused.SetFocus (); } - SetNeedsDisplay (); + SetNeedsDraw (); if (Application.MouseGrabView is { } && Application.MouseGrabView is MenuBar && Application.MouseGrabView != this) { @@ -593,7 +590,7 @@ public class MenuBar : View, IDesignable Application.Top?.Remove (_openMenu); } - SetNeedsDisplay (); + SetNeedsDraw (); if (_previousFocused is Menu && _openMenu is { } && _previousFocused.ToString () != OpenCurrentMenu!.ToString ()) { @@ -655,7 +652,7 @@ public class MenuBar : View, IDesignable case true: _selectedSub = -1; - SetNeedsDisplay (); + SetNeedsDraw (); RemoveAllOpensSubMenus (); OpenCurrentMenu!._previousSubFocused!.SetFocus (); _openSubMenu = null; @@ -773,7 +770,7 @@ public class MenuBar : View, IDesignable return; } - SetNeedsDisplay (); + SetNeedsDraw (); if (UseKeysUpDownAsKeysLeftRight) { @@ -861,6 +858,7 @@ public class MenuBar : View, IDesignable if (Application.Top is { }) { Application.Top.Add (_openMenu); + // _openMenu.SetRelativeLayout (Application.Screen.Size); } else { @@ -991,7 +989,7 @@ public class MenuBar : View, IDesignable { _selectedSub--; RemoveSubMenu (_selectedSub, ignoreUseSubMenusSingleFrame); - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -1119,7 +1117,7 @@ public class MenuBar : View, IDesignable Application.UngrabMouse (); CloseAllMenus (); - Application.Refresh (); + Application.LayoutAndDraw (); _openedByAltKey = true; return Run (item.Action); @@ -1138,7 +1136,7 @@ public class MenuBar : View, IDesignable LastFocused?.SetFocus (); } - SetNeedsDisplay (); + SetNeedsDraw (); } private Point GetLocationOffset () @@ -1167,14 +1165,14 @@ public class MenuBar : View, IDesignable } 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 @@ public class MenuBar : View, IDesignable } } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1324,7 +1322,7 @@ public class MenuBar : View, IDesignable if (value && UseSubMenusSingleFrame) { UseSubMenusSingleFrame = false; - SetNeedsDisplay (); + SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/MenuBarv2.cs b/Terminal.Gui/Views/MenuBarv2.cs index d4c598dfe..7eb470968 100644 --- a/Terminal.Gui/Views/MenuBarv2.cs +++ b/Terminal.Gui/Views/MenuBarv2.cs @@ -22,7 +22,7 @@ public class MenuBarv2 : Bar 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 e8d371e22..609fb962c 100644 --- a/Terminal.Gui/Views/Menuv2.cs +++ b/Terminal.Gui/Views/Menuv2.cs @@ -46,7 +46,8 @@ public class Menuv2 : Bar // 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 @@ public class Menuv2 : Bar } } - base.OnLayoutStarted (args); + base.OnSubviewLayout (args); } /// diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index 4e73d87c5..5d3d602ce 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -64,7 +64,7 @@ public class NumericUpDown : View where T : notnull 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 class NumericUpDown : View where T : notnull 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 class NumericUpDown : View where T : notnull 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 class NumericUpDown : View where T : notnull /// 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 c2021d954..e86d99d2c 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 class ProgressBar : View, IDesignable { _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 class ProgressBar : View, IDesignable 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 class ProgressBar : View, IDesignable 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 class ProgressBar : View, IDesignable } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - Driver.SetAttribute (GetHotNormalColor ()); + SetAttribute (GetHotNormalColor ()); Move (0, 0); @@ -149,13 +141,13 @@ public class ProgressBar : View, IDesignable { 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 class ProgressBar : View, IDesignable 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 class ProgressBar : View, IDesignable } else if (_activityPos [0] >= Viewport.Width) { - if (_bidirectionalMarquee) + if (BidirectionalMarquee) { for (var i = 0; i < _activityPos.Length; i++) { @@ -250,7 +244,7 @@ public class ProgressBar : View, IDesignable } } - SetNeedsDisplay (); + SetNeedsDraw (); } private void PopulateActivityPos () @@ -263,29 +257,13 @@ public class ProgressBar : View, IDesignable } } - 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 f929d0ce5..ac2867a3a 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -52,9 +52,9 @@ public class RadioGroup : View, IDesignable, IOrientation // 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 class RadioGroup : View, IDesignable, IOrientation SetupKeyBindings (); - LayoutStarted += RadioGroup_LayoutStarted; + SubviewLayout += RadioGroup_LayoutStarted; HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; @@ -354,17 +354,15 @@ public class RadioGroup : View, IDesignable, IOrientation 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 class RadioGroup : View, IDesignable, IOrientation } 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 class RadioGroup : View, IDesignable, IOrientation if (j == hotPos && i == Cursor) { - Application.Driver?.SetAttribute ( + SetAttribute ( HasFocus ? ColorScheme!.HotFocus : GetHotNormalColor () @@ -403,11 +401,11 @@ public class RadioGroup : View, IDesignable, IOrientation } 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 class RadioGroup : View, IDesignable, IOrientation if (i == Cursor) { - Application.Driver?.SetAttribute ( + SetAttribute ( HasFocus ? ColorScheme!.HotFocus : GetHotNormalColor () @@ -425,12 +423,12 @@ public class RadioGroup : View, IDesignable, IOrientation } 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 class RadioGroup : View, IDesignable, IOrientation DrawHotString (rl, HasFocus && i == Cursor); } } + return true; } #region IOrientation @@ -524,7 +523,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (Cursor + 1 < _radioLabels.Count) { Cursor++; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -552,7 +551,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (Cursor > 0) { Cursor--; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } diff --git a/Terminal.Gui/Views/Scroll/Scroll.cs b/Terminal.Gui/Views/Scroll/Scroll.cs index 803fb035b..e37b4c8df 100644 --- a/Terminal.Gui/Views/Scroll/Scroll.cs +++ b/Terminal.Gui/Views/Scroll/Scroll.cs @@ -1,6 +1,7 @@ #nullable enable using System.ComponentModel; +using System.Drawing; namespace Terminal.Gui; @@ -16,137 +17,99 @@ namespace Terminal.Gui; /// By default, this view cannot be focused and does not support keyboard. /// /// -public class Scroll : View +public class Scroll : View, IOrientation, IDesignable { + internal readonly ScrollSlider _slider; + /// public Scroll () { _slider = new (); Add (_slider); + _slider.FrameChanged += OnSliderOnFrameChanged; - WantContinuousButtonPressed = true; CanFocus = false; - Orientation = Orientation.Vertical; - Width = Dim.Auto (DimAutoStyle.Content, 1); - Height = Dim.Auto (DimAutoStyle.Content, 1); + _orientationHelper = new (this); // Do not use object initializer! + _orientationHelper.Orientation = Orientation.Vertical; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + + // This sets the width/height etc... + OnOrientationChanged (Orientation); } - internal readonly ScrollSlider _slider; - private Orientation _orientation; - private int _position; - private int _size; - private bool _keepContentInAllViewport; + + /// + protected override void OnSubviewLayout (LayoutEventArgs args) + { + if (ViewportDimension < 1) + { + _slider.Size = 1; + + return; + } + _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size)), 1, ViewportDimension); + } + + #region IOrientation members + private readonly OrientationHelper _orientationHelper; /// - public override void EndInit () - { - base.EndInit (); - - AdjustScroll (); - } - - /// Get or sets if the view-port is kept in all visible area of this - public bool KeepContentInAllViewport - { - get => _keepContentInAllViewport; - set - { - if (_keepContentInAllViewport != value) - { - _keepContentInAllViewport = value; - var pos = 0; - - if (value - && Orientation == Orientation.Horizontal - && _position + (SuperViewAsScrollBar is { } ? SuperViewAsScrollBar.GetContentSize ().Width : GetContentSize ().Width) > Size) - { - pos = Size - (SuperViewAsScrollBar is { } ? SuperViewAsScrollBar.GetContentSize ().Width : GetContentSize ().Width); - } - - if (value - && Orientation == Orientation.Vertical - && _position + (SuperViewAsScrollBar is { } ? SuperViewAsScrollBar.GetContentSize ().Height : GetContentSize ().Height) > Size) - { - pos = _size - (SuperViewAsScrollBar is { } ? SuperViewAsScrollBar.GetContentSize ().Height : GetContentSize ().Height); - } - - if (pos != 0) - { - Position = pos; - } - - SetNeedsDisplay (); - AdjustScroll (); - } - } - } - - /// - /// Gets or sets if the Scroll is oriented vertically or horizontally. - /// public Orientation Orientation { - get => _orientation; - set - { - _orientation = value; - AdjustScroll (); - } + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } - /// - /// Gets or sets the position of the start of the Scroll slider, relative to . - /// - public int Position + /// + public event EventHandler>? OrientationChanging; + + /// + public event EventHandler>? OrientationChanged; + + /// + public void OnOrientationChanged (Orientation newOrientation) { - get => _position; - set + TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom; + TextAlignment = Alignment.Center; + VerticalTextAlignment = Alignment.Center; + + X = 0; + Y = 0; + + if (Orientation == Orientation.Vertical) { - if (value == _position || value < 0) - { - return; - } - - if (SuperViewAsScrollBar is { IsInitialized: false }) - { - // Ensures a more exactly calculation - SetRelativeLayout (SuperViewAsScrollBar.Frame.Size); - } - - int pos = SetPosition (value); - - if (pos == _position) - { - return; - } - - CancelEventArgs args = OnPositionChanging (_position, pos); - - if (args.Cancel) - { - return; - } - - _position = pos; - - AdjustScroll (); - - OnPositionChanged (_position); + Width = 1; + Height = Dim.Fill (); } + else + { + Width = Dim.Fill (); + Height = 1; + } + + _slider.Orientation = newOrientation; } - /// Raised when the has changed. - public event EventHandler>? PositionChanged; + #endregion /// - /// Raised when the is changing. Set to - /// to prevent the position from being changed. + /// Gets or sets whether the Scroll will show the percentage the slider + /// takes up within the . /// - public event EventHandler>? PositionChanging; + public bool ShowPercent + { + get => _slider.ShowPercent; + set => _slider.ShowPercent = value; + } + + private int ViewportDimension => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width; + + private int _size; /// - /// Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through. + /// Gets or sets the total size of the content that can be scrolled. /// public int Size { @@ -160,116 +123,123 @@ public class Scroll : View _size = value; OnSizeChanged (_size); - AdjustScroll (); + SizeChanged?.Invoke (this, new (in _size)); } } + /// Called when has changed. + protected virtual void OnSizeChanged (int size) { } + /// Raised when has changed. public event EventHandler>? SizeChanged; - /// - protected override bool OnMouseEvent (MouseEventArgs mouseEvent) + private int _position; + + private void OnSliderOnFrameChanged (object? sender, EventArgs args) { - int location = Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X; - int barSize = BarSize; + if (ViewportDimension == 0) + { + return; + } + int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X; + double pos = ((double)ViewportDimension * ViewportDimension / (Size)) * framePos; + RaisePositionChangeEvents (_position, (int)pos); + } - (int start, int end) sliderPos = _orientation == Orientation.Vertical - ? new (_slider.Frame.Y, _slider.Frame.Bottom - 1) - : new (_slider.Frame.X, _slider.Frame.Right - 1); + /// + /// Gets or sets the position of the start of the Scroll slider, relative to . + /// + public int Position + { + get => _position; + set => RaisePositionChangeEvents (_position, value); + } - if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && location < sliderPos.start) + private void RaisePositionChangeEvents (int current, int value) + { + if (OnPositionChanging (current, value)) { - int distance = sliderPos.start - location; - int scrollAmount = (int)((double)distance / barSize * (Size - barSize)); - Position = Math.Max (Position - scrollAmount, 0); - } - else if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && location > sliderPos.end) - { - Position = Math.Min (Position + barSize, Size - barSize + (KeepContentInAllViewport ? 0 : barSize)); - } - else if ((mouseEvent.Flags == MouseFlags.WheeledDown && Orientation == Orientation.Vertical) - || (mouseEvent.Flags == MouseFlags.WheeledRight && Orientation == Orientation.Horizontal)) - { - Position = Math.Min (Position + 1, Size - barSize + (KeepContentInAllViewport ? 0 : barSize)); - } - else if ((mouseEvent.Flags == MouseFlags.WheeledUp && Orientation == Orientation.Vertical) - || (mouseEvent.Flags == MouseFlags.WheeledLeft && Orientation == Orientation.Horizontal)) - { - Position = Math.Max (Position - 1, 0); - } - else if (mouseEvent.Flags == MouseFlags.Button1Clicked) - { - if (_slider.Frame.Contains (mouseEvent.Position)) - { - //return _slider.OnMouseEvent (mouseEvent); - } + _slider.Position = current; + return; } + CancelEventArgs args = new (ref current, ref value); + PositionChanging?.Invoke (this, args); + + if (args.Cancel) + { + _slider.Position = current; + return; + } + + // This sets the slider position and clamps the value + _slider.Position = value; + + if (_slider.Position == _position) + { + return; + } + + _position = value; + + OnPositionChanged (_position); + PositionChanged?.Invoke (this, new (in value)); + } + + /// + /// Called when is changing. Return true to cancel the change. + /// + protected virtual bool OnPositionChanging (int currentPos, int newPos) + { return false; } - /// Virtual method called when has changed. Raises . - protected virtual void OnPositionChanged (int position) { PositionChanged?.Invoke (this, new (in position)); } - /// - /// Virtual method called when is changing. Raises , which is - /// cancelable. + /// Raised when the is changing. Set to + /// to prevent the position from being changed. /// - protected virtual CancelEventArgs OnPositionChanging (int currentPos, int newPos) - { - CancelEventArgs args = new (ref currentPos, ref newPos); - PositionChanging?.Invoke (this, args); + public event EventHandler>? PositionChanging; - return args; - } + /// Called when has changed. + protected virtual void OnPositionChanged (int position) { } - /// Called when has changed. Raises . - protected void OnSizeChanged (int size) { SizeChanged?.Invoke (this, new (in size)); } + /// Raised when the has changed. + public event EventHandler>? PositionChanged; - internal void AdjustScroll () - { - if (SuperViewAsScrollBar is { }) - { - X = Orientation == Orientation.Vertical ? 0 : 1; - Y = Orientation == Orientation.Vertical ? 1 : 0; - Width = Orientation == Orientation.Vertical ? Dim.Fill () : Dim.Fill (1); - Height = Orientation == Orientation.Vertical ? Dim.Fill (1) : Dim.Fill (); - } - - _slider.AdjustSlider (); - SetScrollText (); - } /// - internal override void OnLayoutStarted (LayoutEventArgs args) + protected override bool OnClearingViewport () { - AdjustScroll (); + FillRect (Viewport, Glyphs.Stipple); + + return true; } - internal ScrollBar? SuperViewAsScrollBar => SuperView as ScrollBar; - private int BarSize => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width; - - private int SetPosition (int position) + /// + public bool EnableForDesign () { - int barSize = BarSize; + OrientationChanged += (sender, args) => + { + if (args.CurrentValue == Orientation.Vertical) + { + Width = 1; + Height = Dim.Fill (); + } + else + { + Width = Dim.Fill (); + Height = 1; + } + }; - if (position + barSize > Size + (KeepContentInAllViewport ? 0 : barSize) - (SuperViewAsScrollBar is { } ? 2 : 0)) - { - return KeepContentInAllViewport ? Math.Max (Size - barSize - (SuperViewAsScrollBar is { } ? 2 : 0), 0) : Math.Max (Size - 1, 0); - } + Width = 1; + Height = Dim.Fill (); + Size = 1000; + Position = 10; - return position; - } - private void SetScrollText () - { - TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom; - // QUESTION: Should these Glyphs be configurable via CM? - Text = string.Concat ( - Enumerable.Repeat ( - Glyphs.Stipple.ToString (), - GetContentSize ().Width * GetContentSize ().Height)); + return true; } } diff --git a/Terminal.Gui/Views/Scroll/ScrollBar.cs b/Terminal.Gui/Views/Scroll/ScrollBar.cs index 788665fb5..e97d1f727 100644 --- a/Terminal.Gui/Views/Scroll/ScrollBar.cs +++ b/Terminal.Gui/Views/Scroll/ScrollBar.cs @@ -6,50 +6,116 @@ namespace Terminal.Gui; /// /// Provides a visual indicator that content can be scrolled. ScrollBars consist of two buttons, one each for scrolling -/// forward or backwards, a Scroll that can be clicked to scroll large amounts, and a ScrollSlider that can be dragged +/// forward or backwards, a that can be dragged /// to scroll continuously. ScrollBars can be oriented either horizontally or vertically and support the user dragging /// and clicking with the mouse to scroll. /// /// /// -/// indicates the current location between zero and . +/// indicates the number of rows or columns the Scroll has moved from 0. /// -/// If the scrollbar is larger than three cells, arrow indicators are drawn. /// -public class ScrollBar : View +public class ScrollBar : View, IOrientation, IDesignable { + private readonly Scroll _scroll; + private readonly Button _decreaseButton; + private readonly Button _increaseButton; + /// public ScrollBar () { - _scroll = new (); - _decrease = new () - { - NoDecorations = true, - NoPadding = true, - }; - _increase = new () - { - NoDecorations = true, - NoPadding = true, - }; - Add (_scroll, _decrease, _increase); - CanFocus = false; - Orientation = Orientation.Vertical; - Width = Dim.Auto (DimAutoStyle.Content, 1); - Height = Dim.Auto (DimAutoStyle.Content, 1); - _scroll.PositionChanging += Scroll_PositionChanging; - _scroll.PositionChanged += Scroll_PositionChanged; - _scroll.SizeChanged += _scroll_SizeChanged; + _scroll = new (); + _scroll.PositionChanging += OnScrollOnPositionChanging; + _scroll.PositionChanged += OnScrollOnPositionChanged; + _scroll.SizeChanged += OnScrollOnSizeChanged; + + _decreaseButton = new () + { + CanFocus = false, + NoDecorations = true, + NoPadding = true, + ShadowStyle = ShadowStyle.None, + WantContinuousButtonPressed = true + }; + _decreaseButton.Accepting += OnDecreaseButtonOnAccept; + + _increaseButton = new () + { + CanFocus = false, + NoDecorations = true, + NoPadding = true, + ShadowStyle = ShadowStyle.None, + WantContinuousButtonPressed = true + }; + _increaseButton.Accepting += OnIncreaseButtonOnAccept; + Add (_decreaseButton, _scroll, _increaseButton); + + _orientationHelper = new (this); // Do not use object initializer! + _orientationHelper.Orientation = Orientation.Vertical; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + + // This sets the width/height etc... + OnOrientationChanged (Orientation); + + return; + + void OnDecreaseButtonOnAccept (object? s, CommandEventArgs e) + { + _scroll.Position--; + e.Cancel = true; + } + + void OnIncreaseButtonOnAccept (object? s, CommandEventArgs e) + { + _scroll.Position++; + e.Cancel = true; + } } - private readonly Scroll _scroll; - private readonly Button _decrease; - private readonly Button _increase; + #region IOrientation members + + private readonly OrientationHelper _orientationHelper; + + /// + public Orientation Orientation + { + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; + } + + /// + public event EventHandler>? OrientationChanging; + + /// + public event EventHandler>? OrientationChanged; + + /// + public void OnOrientationChanged (Orientation newOrientation) + { + TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom; + TextAlignment = Alignment.Center; + VerticalTextAlignment = Alignment.Center; + + if (Orientation == Orientation.Vertical) + { + Width = 1; + Height = Dim.Fill (); + } + else + { + Width = Dim.Fill (); + Height = 1; + } + + _scroll.Orientation = newOrientation; + } + + #endregion private bool _autoHide = true; - private bool _showScrollIndicator = true; /// /// Gets or sets whether will be set to if the dimension of the @@ -63,27 +129,55 @@ public class ScrollBar : View if (_autoHide != value) { _autoHide = value; - AdjustAll (); + + if (!AutoHide) + { + Visible = true; + } + + SetNeedsLayout (); } } } + /// + protected override void OnFrameChanged (in Rectangle frame) { ShowHide (); } + + private void ShowHide () + { + if (!AutoHide || !IsInitialized) + { + return; + } + + if (Orientation == Orientation.Vertical) + { + Visible = Frame.Height - (_decreaseButton.Frame.Height + _increaseButton.Frame.Height) < Size; + } + else + { + Visible = Frame.Width - (_decreaseButton.Frame.Width + _increaseButton.Frame.Width) < Size; + } + } + + /// + /// Gets or sets whether the Scroll will show the percentage the slider + /// takes up within the . + /// + public bool ShowPercent + { + get => _scroll.ShowPercent; + set => _scroll.ShowPercent = value; + } + + /// Get or sets if the view-port is kept in all visible area of this . public bool KeepContentInAllViewport { - get => _scroll.KeepContentInAllViewport; - set => _scroll.KeepContentInAllViewport = value; - } - - /// Gets or sets if a scrollbar is vertical or horizontal. - public Orientation Orientation - { - get => _scroll.Orientation; - set - { - Resize (value); - _scroll.Orientation = value; - } + //get => _scroll.KeepContentInAllViewport; + //set => _scroll.KeepContentInAllViewport = value; + get; + set; } /// Gets or sets the position, relative to , to set the scrollbar at. @@ -91,13 +185,12 @@ public class ScrollBar : View public int Position { get => _scroll.Position; - set - { - _scroll.Position = value; - AdjustAll (); - } + set => _scroll.Position = value; } + private void OnScrollOnPositionChanged (object? sender, EventArgs e) { PositionChanged?.Invoke (this, e); } + private void OnScrollOnPositionChanging (object? sender, CancelEventArgs e) { PositionChanging?.Invoke (this, e); } + /// Raised when the has changed. public event EventHandler>? PositionChanged; @@ -107,149 +200,87 @@ public class ScrollBar : View /// public event EventHandler>? PositionChanging; - /// Gets or sets the visibility for the vertical or horizontal scroll indicator. - /// true if show vertical or horizontal scroll indicator; otherwise, false. - public bool ShowScrollIndicator - { - get => Visible; - set - { - if (value == _showScrollIndicator) - { - return; - } - - _showScrollIndicator = value; - - if (IsInitialized) - { - SetNeedsLayout (); - - if (value) - { - Visible = true; - } - else - { - Visible = false; - Position = 0; - } - - AdjustAll (); - } - } - } - /// /// Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through. /// public int Size { get => _scroll.Size; - set - { - _scroll.Size = value; - AdjustAll (); - } + set => _scroll.Size = value; } /// Raised when has changed. public event EventHandler>? SizeChanged; - /// - /// - internal override void OnLayoutStarted (LayoutEventArgs args) + private void OnScrollOnSizeChanged (object? sender, EventArgs e) { - AdjustAll (); + ShowHide (); + SizeChanged?.Invoke (this, e); } - private void _scroll_SizeChanged (object? sender, EventArgs e) { SizeChanged?.Invoke (this, e); } + /// + protected override void OnSubviewLayout (LayoutEventArgs args) { PositionSubviews (); } - private void AdjustAll () + private void PositionSubviews () { - CheckVisibility (); - _scroll.AdjustScroll (); - if (Orientation == Orientation.Vertical) { - _decrease.Y = 0; - _decrease.X = 0; - _decrease.Width = Dim.Fill (); - _decrease.Height = 1; - _decrease.Text = Glyphs.DownArrow.ToString (); - _increase.Y = Pos.Bottom (_scroll); - _increase.X = 0; - _increase.Width = Dim.Fill (); - _increase.Height = 1; - _increase.Text = Glyphs.UpArrow.ToString (); + _decreaseButton.Y = 0; + _decreaseButton.X = 0; + _decreaseButton.Width = Dim.Fill (); + _decreaseButton.Height = 1; + _decreaseButton.Title = Glyphs.UpArrow.ToString (); + _increaseButton.Y = Pos.Bottom (_scroll); + _increaseButton.X = 0; + _increaseButton.Width = Dim.Fill (); + _increaseButton.Height = 1; + _increaseButton.Title = Glyphs.DownArrow.ToString (); + _scroll.X = 0; + _scroll.Y = Pos.Bottom (_decreaseButton); + _scroll.Height = Dim.Fill (1); + _scroll.Width = Dim.Fill (); } else { - _decrease.Y = 0; - _decrease.X = 0; - _decrease.Width = 1; - _decrease.Height = Dim.Fill (); - _decrease.Text = Glyphs.LeftArrow.ToString (); - _increase.Y = 0; - _increase.X = Pos.Right (_scroll); - _increase.Width = 1; - _increase.Height = Dim.Fill (); - _increase.Text = Glyphs.RightArrow.ToString (); + _decreaseButton.Y = 0; + _decreaseButton.X = 0; + _decreaseButton.Width = 1; + _decreaseButton.Height = Dim.Fill (); + _decreaseButton.Title = Glyphs.LeftArrow.ToString (); + _increaseButton.Y = 0; + _increaseButton.X = Pos.Right (_scroll); + _increaseButton.Width = 1; + _increaseButton.Height = Dim.Fill (); + _increaseButton.Title = Glyphs.RightArrow.ToString (); + _scroll.Y = 0; + _scroll.X = Pos.Bottom (_decreaseButton); + _scroll.Width = Dim.Fill (1); + _scroll.Height = Dim.Fill (); } } - private bool CheckVisibility () + /// + public bool EnableForDesign () { - if (!AutoHide) - { - if (Visible != _showScrollIndicator) - { - Visible = _showScrollIndicator; - SetNeedsDisplay (); - } - - return _showScrollIndicator; - } - - int barSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width; - - if (barSize == 0 || barSize >= Size) - { - if (Visible) - { - Visible = false; - SetNeedsDisplay (); - - return false; - } - } - else - { - if (!Visible) - { - Visible = true; - SetNeedsDisplay (); - } - } + OrientationChanged += (sender, args) => + { + if (args.CurrentValue == Orientation.Vertical) + { + Width = 1; + Height = Dim.Fill (); + } + else + { + Width = Dim.Fill (); + Height = 1; + } + }; + Width = 1; + Height = Dim.Fill (); + Size = 200; + Position = 10; + //ShowPercent = true; return true; } - - private void Resize (Orientation orientation) - { - switch (orientation) - { - case Orientation.Horizontal: - - break; - case Orientation.Vertical: - break; - default: - throw new ArgumentOutOfRangeException (nameof (orientation), orientation, null); - } - } - - private void Scroll_PositionChanged (object? sender, EventArgs e) { PositionChanged?.Invoke (this, e); } - - private void Scroll_PositionChanging (object? sender, CancelEventArgs e) { PositionChanging?.Invoke (this, e); } } diff --git a/Terminal.Gui/Views/Scroll/ScrollSlider.cs b/Terminal.Gui/Views/Scroll/ScrollSlider.cs index 96c561e23..759734413 100644 --- a/Terminal.Gui/Views/Scroll/ScrollSlider.cs +++ b/Terminal.Gui/Views/Scroll/ScrollSlider.cs @@ -1,249 +1,293 @@ #nullable enable -using System.ComponentModel; +using System.Diagnostics; namespace Terminal.Gui; -internal class ScrollSlider : View +/// +/// The ScrollSlider can be dragged with the mouse, constrained by the size of the Viewport of it's superview. The ScrollSlider can be +/// oriented either vertically or horizontally. +/// +/// +/// +/// If is set, it will be displayed centered within the slider. Set +/// to automatically have the Text +/// be show what percent the slider is to the Superview's Viewport size. +/// +/// +/// Used to represent the proportion of the visible content to the Viewport in a . +/// +/// +public class ScrollSlider : View, IOrientation, IDesignable { + /// + /// Initializes a new instance. + /// public ScrollSlider () { Id = "scrollSlider"; - Width = Dim.Auto (DimAutoStyle.Content); - Height = Dim.Auto (DimAutoStyle.Content); WantMousePositionReports = true; + + _orientationHelper = new (this); // Do not use object initializer! + _orientationHelper.Orientation = Orientation.Vertical; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + + OnOrientationChanged (Orientation); + + HighlightStyle = HighlightStyle.Hover; + + // Default size is 1 + Size = 1; } - private int _lastLocation = -1; - private ColorScheme? _savedColorScheme; + #region IOrientation members + private readonly OrientationHelper _orientationHelper; - public void AdjustSlider () + /// + public Orientation Orientation { - if (!IsInitialized) - { - return; - } - - (int Location, int Dimension) sliderLocationAndDimension = GetSliderLocationDimensionFromPosition (); - X = SuperViewAsScroll.Orientation == Orientation.Vertical ? 0 : sliderLocationAndDimension.Location; - Y = SuperViewAsScroll.Orientation == Orientation.Vertical ? sliderLocationAndDimension.Location : 0; - - SetContentSize ( - new ( - SuperViewAsScroll.Orientation == Orientation.Vertical - ? SuperViewAsScroll.GetContentSize ().Width - : sliderLocationAndDimension.Dimension, - SuperViewAsScroll.Orientation == Orientation.Vertical - ? sliderLocationAndDimension.Dimension - : SuperViewAsScroll.GetContentSize ().Height - )); - SetSliderText (); + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } /// - public override Attribute GetNormalColor () + public event EventHandler>? OrientationChanging; + + /// + public event EventHandler>? OrientationChanged; + + /// + public void OnOrientationChanged (Orientation newOrientation) { - if (_savedColorScheme is null) + TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom; + TextAlignment = Alignment.Center; + VerticalTextAlignment = Alignment.Center; + + // Reset Position to 0 when changing orientation + X = 0; + Y = 0; + + // Reset Size to 1 when changing orientation + if (Orientation == Orientation.Vertical) { - ColorScheme = new () { Normal = new (SuperViewAsScroll.ColorScheme.HotNormal.Foreground, SuperViewAsScroll.ColorScheme.HotNormal.Foreground) }; + Width = Dim.Fill (); + Height = 1; } else { - ColorScheme = new () { Normal = new (SuperViewAsScroll.ColorScheme.Normal.Foreground, SuperViewAsScroll.ColorScheme.Normal.Foreground) }; + Width = 1; + Height = Dim.Fill (); } + } - return base.GetNormalColor (); + #endregion + + /// + protected override bool OnClearingViewport () + { + FillRect (Viewport, Glyphs.ContinuousMeterSegment); + + return true; + } + + /// + /// Gets or sets whether the ScrollSlider will set to show the percentage the slider + /// takes up within the 's Viewport. + /// + public bool ShowPercent { get; set; } + + /// + /// Gets or sets the size of the ScrollSlider. This is a helper that simply gets or sets the Width or Height depending on the + /// . The size will be constrained such that the ScrollSlider will not go outside the Viewport of + /// the . The size will never be less than 1. + /// + /// + /// + /// The dimension of the ScrollSlider that is perpendicular to the will be set to + /// + /// + public int Size + { + get + { + if (Orientation == Orientation.Vertical) + { + return Frame.Height; + } + else + { + return Frame.Width; + } + } + set + { + if (Orientation == Orientation.Vertical) + { + Width = Dim.Fill (); + int viewport = Math.Max (1, SuperView?.Viewport.Height ?? 1); + Height = Math.Clamp (value, 1, viewport); + } + else + { + int viewport = Math.Max (1, SuperView?.Viewport.Width ?? 1); + Width = Math.Clamp (value, 1, viewport); + Height = Dim.Fill (); + } + } + } + + /// + /// Gets or sets the position of the ScrollSlider. This is a helper that simply gets or sets the X or Y depending on the + /// . The position will be constrained such that the ScrollSlider will not go outside the Viewport of + /// the . + /// + public int Position + { + get + { + if (Orientation == Orientation.Vertical) + { + return Frame.Y; + } + else + { + return Frame.X; + } + } + set + { + if (Orientation == Orientation.Vertical) + { + int viewport = Math.Max (1, SuperView?.Viewport.Height ?? 1); + Y = Math.Clamp (value, 0, viewport - Frame.Height); + } + else + { + int viewport = Math.Max (1, SuperView?.Viewport.Width ?? 1); + X = Math.Clamp (value, 0, viewport - Frame.Width); + } + } } /// - /// - protected override bool OnMouseEnter (CancelEventArgs eventArgs) + protected override bool OnDrawingText () { - _savedColorScheme ??= SuperViewAsScroll.ColorScheme; - - ColorScheme = new () + if (!ShowPercent) { - Normal = new (_savedColorScheme.HotNormal.Foreground, _savedColorScheme.HotNormal.Foreground), - Focus = new (_savedColorScheme.Focus.Foreground, _savedColorScheme.Focus.Foreground), - HotNormal = new (_savedColorScheme.Normal.Foreground, _savedColorScheme.Normal.Foreground), - HotFocus = new (_savedColorScheme.HotFocus.Foreground, _savedColorScheme.HotFocus.Foreground), - Disabled = new (_savedColorScheme.Disabled.Foreground, _savedColorScheme.Disabled.Foreground) - }; - return true; + return false; + } + + if (Orientation == Orientation.Vertical) + { + Text = $"{(int)Math.Round ((double)Viewport.Height / SuperView!.GetContentSize ().Height * 100)}%"; + } + else + { + Text = $"{(int)Math.Round ((double)Viewport.Width / SuperView!.GetContentSize ().Width * 100)}%"; + } + + return false; } + /// + public override Attribute GetNormalColor () { return base.GetHotNormalColor (); } + + ///// + private int _lastLocation = -1; + /// protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { - int location = SuperViewAsScroll.Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X; - int offset = _lastLocation > -1 ? location - _lastLocation : 0; - int barSize = SuperViewAsScroll.Orientation == Orientation.Vertical ? SuperViewAsScroll.Viewport.Height : SuperViewAsScroll.Viewport.Width; - - if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1) + if (SuperView is null) { - if (Application.MouseGrabView != this) + return false; + } + + int location = Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X; + int offset = _lastLocation > -1 ? location - _lastLocation : 0; + int superViewDimension = Orientation == Orientation.Vertical ? SuperView!.Viewport.Height : SuperView!.Viewport.Width; + + if (mouseEvent.IsPressed || mouseEvent.IsReleased) + { + if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1) { - Application.GrabMouse (this); - _lastLocation = location; + if (Application.MouseGrabView != this) + { + Application.GrabMouse (this); + _lastLocation = location; + } + } + else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + { + if (Orientation == Orientation.Vertical) + { + Y = Frame.Y + offset < 0 + ? 0 + : Frame.Y + offset + Frame.Height > superViewDimension + ? Math.Max (superViewDimension - Frame.Height, 0) + : Frame.Y + offset; + } + else + { + X = Frame.X + offset < 0 + ? 0 + : Frame.X + offset + Frame.Width > superViewDimension + ? Math.Max (superViewDimension - Frame.Width, 0) + : Frame.X + offset; + } + } + else if (mouseEvent.Flags == MouseFlags.Button1Released) + { + _lastLocation = -1; + + if (Application.MouseGrabView == this) + { + Application.UngrabMouse (); + } } } - else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + + if (mouseEvent.IsWheel) { - if (SuperViewAsScroll.Orientation == Orientation.Vertical) + if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown) || mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight)) + { + offset = 1; + } + else if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown) || mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft)) + { + offset = -1; + } + + if (Orientation == Orientation.Vertical) { Y = Frame.Y + offset < 0 ? 0 - : Frame.Y + offset + Frame.Height > barSize - ? Math.Max (barSize - Frame.Height, 0) + : Frame.Y + offset + Frame.Height > superViewDimension + ? Math.Max (superViewDimension - Frame.Height, 0) : Frame.Y + offset; - - SuperViewAsScroll.Position = GetPositionFromSliderLocation (Frame.Y); } else { X = Frame.X + offset < 0 ? 0 - : Frame.X + offset + Frame.Width > barSize - ? Math.Max (barSize - Frame.Width, 0) + : Frame.X + offset + Frame.Width > superViewDimension + ? Math.Max (superViewDimension - Frame.Width, 0) : Frame.X + offset; - - SuperViewAsScroll.Position = GetPositionFromSliderLocation (Frame.X); } } - else if (mouseEvent.Flags == MouseFlags.Button1Released) - { - _lastLocation = -1; - - if (Application.MouseGrabView == this) - { - Application.UngrabMouse (); - } - } - else if ((mouseEvent.Flags == MouseFlags.WheeledDown && SuperViewAsScroll.Orientation == Orientation.Vertical) - || (mouseEvent.Flags == MouseFlags.WheeledRight && SuperViewAsScroll.Orientation == Orientation.Horizontal)) - { - SuperViewAsScroll.Position = Math.Min ( - SuperViewAsScroll.Position + 1, - SuperViewAsScroll.KeepContentInAllViewport ? SuperViewAsScroll.Size - barSize : SuperViewAsScroll.Size - 1); - } - else if ((mouseEvent.Flags == MouseFlags.WheeledUp && SuperViewAsScroll.Orientation == Orientation.Vertical) - || (mouseEvent.Flags == MouseFlags.WheeledLeft && SuperViewAsScroll.Orientation == Orientation.Horizontal)) - { - SuperViewAsScroll.Position = Math.Max (SuperViewAsScroll.Position - 1, 0); - } - else if (mouseEvent.Flags != MouseFlags.ReportMousePosition) - { - return base.OnMouseEvent (mouseEvent); - } return true; } - /// /// - protected override void OnMouseLeave () + public bool EnableForDesign () { - if (_savedColorScheme is { } /*&& !mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)*/) - { - ColorScheme = _savedColorScheme; - _savedColorScheme = null; - } + Orientation = Orientation.Vertical; + Width = 1; + Height = 10; + ShowPercent = true; + + return true; } - - internal int GetPositionFromSliderLocation (int location) - { - if (SuperViewAsScroll.GetContentSize ().Height == 0 || SuperViewAsScroll.GetContentSize ().Width == 0) - { - return 0; - } - - int scrollSize = SuperViewAsScroll.Orientation == Orientation.Vertical - ? SuperViewAsScroll.GetContentSize ().Height - : SuperViewAsScroll.GetContentSize ().Width; - - // Ensure the Position is valid if the slider is at end - // We use Frame here instead of ContentSize because even if the slider has a margin or border, Frame indicates the actual size - if ((SuperViewAsScroll.Orientation == Orientation.Vertical && location + Frame.Height >= scrollSize) - || (SuperViewAsScroll.Orientation == Orientation.Horizontal && location + Frame.Width >= scrollSize)) - { - return SuperViewAsScroll.Size - scrollSize + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize); - } - - return (int)Math.Min ( - Math.Round ( - (double)(location * (SuperViewAsScroll.Size + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize)) - + location) - / scrollSize), - SuperViewAsScroll.Size - scrollSize + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize)); - } - - internal (int Location, int Dimension) GetSliderLocationDimensionFromPosition () - { - if (SuperViewAsScroll.GetContentSize ().Height == 0 || SuperViewAsScroll.GetContentSize ().Width == 0) - { - return new (0, 0); - } - - int scrollSize = SuperViewAsScroll.Orientation == Orientation.Vertical - ? SuperViewAsScroll.GetContentSize ().Height - : SuperViewAsScroll.GetContentSize ().Width; - int location; - int dimension; - - if (SuperViewAsScroll.Size > 0) - { - dimension = (int)Math.Min ( - Math.Max ( - Math.Ceiling ( - (double)scrollSize - * scrollSize - / (SuperViewAsScroll.Size + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize))), - 1), - scrollSize); - - // Ensure the Position is valid - if (SuperViewAsScroll.Position > 0 - && SuperViewAsScroll.Position + scrollSize > SuperViewAsScroll.Size + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize)) - { - SuperViewAsScroll.Position = SuperViewAsScroll.KeepContentInAllViewport ? SuperViewAsScroll.Size - scrollSize : SuperViewAsScroll.Size - 1; - } - - location = (int)Math.Min ( - Math.Round ( - (double)SuperViewAsScroll.Position - * scrollSize - / (SuperViewAsScroll.Size + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize))), - scrollSize - dimension); - - if (SuperViewAsScroll.Position == SuperViewAsScroll.Size - scrollSize + (SuperViewAsScroll.KeepContentInAllViewport ? 0 : scrollSize) - && location + dimension < scrollSize) - { - location = scrollSize - dimension; - } - } - else - { - location = 0; - dimension = scrollSize; - } - - return new (location, dimension); - } - - // TODO: I think you should create a new `internal` view named "ScrollSlider" with an `Orientation` property. It should inherit from View and override GetNormalColor and the mouse events - // that can be moved within it's Superview, constrained to move only horizontally or vertically depending on Orientation. - // This will really simplify a lot of this. - - private void SetSliderText () - { - TextDirection = SuperViewAsScroll.Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom; - - // QUESTION: Should these Glyphs be configurable via CM? - Text = string.Concat ( - Enumerable.Repeat ( - Glyphs.ContinuousMeterSegment.ToString (), - SuperViewAsScroll.GetContentSize ().Width * SuperViewAsScroll.GetContentSize ().Height)); - } - - private Scroll SuperViewAsScroll => (SuperView as Scroll)!; } diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs index f6e8da296..8873e5ba7 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 class ScrollBarView : View if (_autoHideScrollBars != value) { _autoHideScrollBars = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -259,7 +261,7 @@ public class ScrollBarView : View { SetRelativeLayout (SuperView?.Frame.Size ?? Host.Frame.Size); ShowHideScrollBars (false); - SetNeedsDisplay (); + SetNeedsLayout (); } } } @@ -444,7 +446,7 @@ public class ScrollBarView : View 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 class ScrollBarView : View 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 class ScrollBarView : View { if (Viewport.Bottom < Viewport.Height - 1) { - return; + return true; } int row = Viewport.Height - 1; @@ -661,6 +663,8 @@ public class ScrollBarView : View Driver.AddRune (Glyphs.RightArrow); } } + + return false; } @@ -761,11 +765,11 @@ public class ScrollBarView : View 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 @@ public class ScrollBarView : View _contentBottomRightCorner.Width = 1; _contentBottomRightCorner.Height = 1; _contentBottomRightCorner.MouseClick += ContentBottomRightCorner_MouseClick; - _contentBottomRightCorner.DrawContent += ContentBottomRightCorner_DrawContent; + _contentBottomRightCorner.DrawingContent += ContentBottomRightCorner_DrawContent; } } @@ -899,7 +903,7 @@ public class ScrollBarView : View if (newPosition < 0) { _position = 0; - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -924,7 +928,7 @@ public class ScrollBarView : View } 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 35d4409e5..aeae539ad 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -191,7 +191,7 @@ public class ScrollView : View _horizontal.AutoHideScrollBars = value; } - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -228,7 +228,7 @@ public class ScrollView : View // _contentView.Frame = new Rectangle (_contentOffset, value); // _vertical.Size = GetContentSize ().Height; // _horizontal.Size = GetContentSize ().Width; - // SetNeedsDisplay (); + // SetNeedsDraw (); // } // } //} @@ -372,19 +372,23 @@ public class ScrollView : 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 class ScrollView : View return view; } - SetNeedsDisplay (); + SetNeedsDraw (); View container = view?.SuperView; if (container == this) @@ -580,18 +584,24 @@ public class ScrollView : View { 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 @@ public class ScrollView : View { _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 f23ec23e3..b7b860df0 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -96,6 +96,12 @@ public class Shortcut : View, IOrientation, IDesignable 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 class Shortcut : View, IOrientation, IDesignable 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; + SubviewLayout += OnLayoutStarted; - 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 (); - - SetColors (); - } - - // 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,100 +195,83 @@ public class Shortcut : View, IOrientation, IDesignable 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 (); } + // Force Width to DimAuto to calculate natural width and then set it back + private void ForceCalculateNaturalWidth () + { + // 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 () { - if (Orientation == Orientation.Vertical) - { - return new (1, 0, 1, 0); - } - 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; + } + + // Frame.Width is smaller than the natural width. Reduce width of HelpView. + _maxHelpWidth = int.Max (0, GetContentSize ().Width - CommandView.Frame.Width - KeyView.Frame.Width); + + if (_maxHelpWidth < 3) + { + Thickness t = GetMarginThickness (); + + 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; + + case 2: + + // Scrunch just the right margin + HelpView.Margin.Thickness = new (t.Right, t.Top, t.Left - 1, t.Bottom); + + break; + } } 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); - - break; - - case 2: - // Scrunch just the right margin - Thickness t = GetMarginThickness (); - HelpView.Margin.Thickness = new (t.Right, t.Top, t.Left - 1, t.Bottom); - - break; - - default: - // Default margin - HelpView.Margin.Thickness = GetMarginThickness (); - - break; - } - - 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 (); - } + // Reset to default + HelpView.Margin.Thickness = GetMarginThickness (); } } @@ -387,14 +355,7 @@ public class Shortcut : View, IOrientation, IDesignable /// . /// /// - /// - /// 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 class Shortcut : View, IOrientation, IDesignable Title = _commandView.Text; _commandView.Selecting += CommandViewOnSelecting; - _commandView.Accepting += CommandViewOnAccepted; - SetCommandViewDefaultLayout (); - SetHelpViewDefaultLayout (); - SetKeyViewDefaultLayout (); - ShowHide (); + //ShowHide (); UpdateKeyBindings (Key.Empty); return; @@ -536,10 +493,17 @@ public class Shortcut : View, IOrientation, IDesignable 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 @@ public class Shortcut : View, IOrientation, IDesignable #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 class Shortcut : View, IOrientation, IDesignable /// 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 class Shortcut : View, IOrientation, IDesignable _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 class Shortcut : View, IOrientation, IDesignable 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 @@ public class Shortcut : View, IOrientation, IDesignable } 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 @@ public class Shortcut : View, IOrientation, IDesignable }; 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 class Shortcut : View, IOrientation, IDesignable return true; } - /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index 9cbc785d1..ff37d513c 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -47,7 +47,7 @@ public class Slider : View, IOrientation _options = options ?? new List> (); - _orientationHelper = new (this); + _orientationHelper = new (this); // Do not use object initializer! _orientationHelper.Orientation = _config._sliderOrientation = orientation; _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); @@ -59,7 +59,7 @@ public class Slider : View, IOrientation // 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 class Slider : View, IOrientation // Todo: Custom logic to preserve options. _setOptions.Clear (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -353,7 +353,7 @@ public class Slider : View, IOrientation 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 @@ public class Slider : View, IOrientation #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 class Slider : View, IOrientation { 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 @@ public class Slider : View, IOrientation private void DrawSlider () { // TODO: be more surgical on clear - Clear (); + ClearViewport (); // Attributes @@ -848,8 +850,8 @@ public class Slider : View, IOrientation 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 @@ public class Slider : View, IOrientation // 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 @@ public class Slider : View, IOrientation } else { - Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); + SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < _config._startSpacing; i++) { @@ -940,7 +942,7 @@ public class Slider : View, IOrientation } // 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 @@ public class Slider : View, IOrientation 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 @@ public class Slider : View, IOrientation // 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 @@ public class Slider : View, IOrientation } else { - Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); + SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < remaining; i++) { @@ -1056,8 +1058,8 @@ public class Slider : View, IOrientation 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 @@ public class Slider : View, IOrientation } // Legend - Driver?.SetAttribute (isOptionSet ? setAttr : normalAttr); + SetAttribute (isOptionSet ? setAttr : normalAttr); foreach (Rune c in text.EnumerateRunes ()) { @@ -1247,7 +1249,7 @@ public class Slider : View, IOrientation } // Option Right Spacing of Option - Driver?.SetAttribute (spaceAttr); + SetAttribute (spaceAttr); if (isTextVertical) { @@ -1309,7 +1311,7 @@ public class Slider : View, IOrientation Application.GrabMouse (this); } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1343,7 +1345,7 @@ public class Slider : View, IOrientation } } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1377,7 +1379,7 @@ public class Slider : View, IOrientation } } - SetNeedsDisplay (); + SetNeedsDraw (); mouseEvent.Handled = true; diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs b/Terminal.Gui/Views/SpinnerView/SpinnerStyle.cs index 09d435893..fac2005c9 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 474ef57f5..a16f88a26 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 class SpinnerView : View /// 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 class SpinnerView : View /// 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 class SpinnerView : View _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 @@ public class SpinnerView : View TimeSpan.FromMilliseconds (SpinDelay), () => { - Application.Invoke (AdvanceAnimation); + Application.Invoke (() => AdvanceAnimation()); return true; } @@ -289,4 +314,12 @@ public class SpinnerView : View 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 975d2c7ad..84914ab55 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -26,7 +26,7 @@ public class StatusBar : Bar, IDesignable 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 eb42a59b9..b683b04b6 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 class Tab : View 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/TabView.cs b/Terminal.Gui/Views/TabView.cs index d605b6fa0..99d90bec2 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 class TabView : View () => { TabScrollOffset = 0; - SelectedTab = Tabs.FirstOrDefault (); + SelectedTab = Tabs.FirstOrDefault ()!; return true; } @@ -59,7 +58,7 @@ public class TabView : View () => { TabScrollOffset = Tabs.Count - 1; - SelectedTab = Tabs.LastOrDefault (); + SelectedTab = Tabs.LastOrDefault ()!; return true; } @@ -69,7 +68,7 @@ public class TabView : View Command.PageDown, () => { - TabScrollOffset += _tabLocations.Length; + TabScrollOffset += _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -80,7 +79,7 @@ public class TabView : View Command.PageUp, () => { - TabScrollOffset -= _tabLocations.Length; + TabScrollOffset -= _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -104,19 +103,20 @@ public class TabView : View /// 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 class TabView : View _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 class TabView : View public int TabScrollOffset { get => _tabScrollOffset; - set => _tabScrollOffset = EnsureValidScrollOffsets (value); + set + { + _tabScrollOffset = EnsureValidScrollOffsets (value); + SetNeedsLayout (); + } } /// Adds the given to . @@ -188,13 +210,13 @@ public class TabView : View 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 class TabView : View 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 class TabView : View // 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 class TabView : View /// 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 (Tabs.Any ()) + if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this) { - Rectangle savedClip = SetClip (); - _tabsBar.OnDrawContent (viewport); - _contentView.SetNeedsDisplay (); - _contentView.Draw (); - Driver.Clip = savedClip; + SelectedTab?.SetFocus (); + + return; } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); } /// - public override void OnDrawContentComplete (Rectangle viewport) { _tabsBar.OnDrawContentComplete (viewport); } + protected override bool OnDrawingContent () + { + if (Tabs.Any ()) + { + // Region savedClip = SetClip (); + _tabsBar.Draw (); + _contentView.SetNeedsDraw (); + _contentView.Draw (); + + //if (Driver is { }) + //{ + // Driver.Clip = savedClip; + //} + } + + 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 class TabView : View } 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 class TabView : View if (Tabs.Count == 1 || SelectedTab is null) { SelectedTab = Tabs.ElementAt (0); - SetNeedsDisplay (); return SelectedTab is { }; } @@ -360,8 +390,6 @@ public class TabView : View if (currentIdx == -1) { SelectedTab = Tabs.ElementAt (0); - SetNeedsDisplay (); - return true; } @@ -373,7 +401,6 @@ public class TabView : View } SelectedTab = _tabs [newIdx]; - SetNeedsDisplay (); EnsureSelectedTabIsVisible (); @@ -384,7 +411,7 @@ public class TabView : View /// 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 @@ public class TabView : View 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 @@ public class TabView : View 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 @@ public class TabView : View // 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 @@ public class TabView : View { foreach (TabToRender tabToRender in _tabLocations) { - tabToRender.Tab.MouseClick -= Tab_MouseClick; + tabToRender.Tab.MouseClick -= Tab_MouseClick!; tabToRender.Tab.Visible = false; } @@ -554,7 +581,7 @@ public class TabView : View Visible = false, Text = Glyphs.RightArrow.ToString () }; - _rightScrollIndicator.MouseClick += _host.Tab_MouseClick; + _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; _leftScrollIndicator = new View { @@ -564,14 +591,14 @@ public class TabView : View 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 @@ public class TabView : View { _host.SwitchTabBy (scrollIndicatorHit); - SetNeedsDisplay (); + SetNeedsLayout (); return true; } @@ -619,7 +646,7 @@ public class TabView : View if (hit is { }) { _host.SelectedTab = hit; - SetNeedsDisplay (); + SetNeedsLayout (); return true; } @@ -628,20 +655,37 @@ public class TabView : View return false; } - public override void OnDrawContent (Rectangle viewport) + /// + protected override bool OnClearingViewport () + { + // clear any old text + ClearViewport (); + + return true; + } + + protected override bool OnDrawingContent () { _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - // clear any old text - Clear (); - RenderTabLine (); RenderUnderline (); - Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + + SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); + + return true; } - public override void OnDrawContentComplete (Rectangle viewport) + /// + protected override bool OnDrawingSubviews () + { + // RenderTabLine (); + + return false; + } + + protected override void OnDrawComplete () { if (_host._tabLocations is null) { @@ -1171,7 +1215,9 @@ public class TabView : View } tab.LineCanvas.Merge (lc); - tab.OnRenderLineCanvas (); + tab.RenderLineCanvas (); + + // RenderUnderline (); } } @@ -1188,21 +1234,15 @@ public class TabView : View /// 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 @@ public class TabView : View 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 @@ public class TabView : View 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 @@ public class TabView : View ColorScheme.HotNormal ); - tab.OnRenderLineCanvas (); + tab.DrawBorderAndPadding (); - Driver.SetAttribute (GetNormalColor ()); + + SetAttribute (GetNormalColor ()); } } @@ -1294,7 +1336,7 @@ public class TabView : View { 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 @@ public class TabView : View } } - 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 class TabView : View 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 abbb2c704..c3c47a64c 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -147,7 +147,7 @@ public abstract class CheckBoxTableSourceWrapperBase : ITableSource } e.Cancel = true; - tableView.SetNeedsDisplay (); + tableView.SetNeedsDraw (); } private void TableView_MouseClick (object sender, MouseEventArgs e) @@ -171,7 +171,7 @@ public abstract class CheckBoxTableSourceWrapperBase : ITableSource // 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 @@ public abstract class CheckBoxTableSourceWrapperBase : ITableSource } e.Handled = true; - tableView.SetNeedsDisplay (); + tableView.SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/TableView/ListTableSource.cs b/Terminal.Gui/Views/TableView/ListTableSource.cs index 1286e2660..6a117698f 100644 --- a/Terminal.Gui/Views/TableView/ListTableSource.cs +++ b/Terminal.Gui/Views/TableView/ListTableSource.cs @@ -38,7 +38,7 @@ public class ListTableSource : ITableSource 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 a33903d0b..468b851f6 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 @@ public delegate ColorScheme RowColorGetterDelegate (RowColorGetterArgs args); /// 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 class TableView : View //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 class TableView : View 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 class TableView : View /// 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 class TableView : View /// (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 class TableView : View /// 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 @@ public class TableView : View 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 @@ public class TableView : View { ColumnOffset--; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + SetNeedsDraw (); } if (scrollRightPoint != null @@ -875,7 +890,7 @@ public class TableView : View { ColumnOffset++; EnsureValidScrollOffsets (); - SetNeedsDisplay (); + SetNeedsDraw (); } Point? hit = ScreenToCell (boundsX, boundsY); @@ -910,10 +925,8 @@ public class TableView : View } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { - base.OnDrawContent (viewport); - Move (0, 0); scrollRightPoint = null; @@ -922,7 +935,7 @@ public class TableView : View // 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 class TableView : View RenderRow (line, rowToRender, columnsToRender); } + + return true; } /// @@ -1226,12 +1241,12 @@ public class TableView : View /// 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 class TableView : View EnsureSelectedCellIsVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Invokes the event @@ -1280,20 +1295,20 @@ public class TableView : View 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 @@ public class TableView : View private void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) { Move (col, row); - d.AddRune (ch); + d?.AddRune (ch); } /// @@ -1505,8 +1520,12 @@ public class TableView : View /// 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 @@ public class TableView : View SelectedRow = match; EnsureValidSelection (); EnsureSelectedCellIsVisible (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1728,7 +1747,7 @@ public class TableView : View 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 @@ public class TableView : View } } - AddRuneAt (Driver, c, row, rune); + if (Driver is { }) + { + AddRuneAt (Driver, c, row, rune); + } } } @@ -1885,19 +1907,22 @@ public class TableView : View //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 @@ public class TableView : View 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 @@ public class TableView : View // 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 @@ public class TableView : View 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 @@ public class TableView : View 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 class TableView : View /// 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 c3458b32d..9125c0c95 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -162,7 +162,7 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T if (e.Handled) { _tree.InvalidateLineMap (); - _tableView.SetNeedsDisplay (); + _tableView.SetNeedsDraw (); } } @@ -197,7 +197,7 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T if (e.Handled) { _tree.InvalidateLineMap (); - _tableView.SetNeedsDisplay (); + _tableView.SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 0b08e1f04..c12a16bd9 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -562,7 +562,7 @@ public class TextField : View } Adjust (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -585,7 +585,7 @@ public class TextField : View _selectedText = null; _start = 0; SelectedLength = 0; - SetNeedsDisplay (); + SetNeedsDraw (); } /// Allows clearing the items updating the original text. @@ -627,7 +627,7 @@ public class TextField : View _selectedStart = 0; MoveEndExtend (); DeleteCharLeft (false); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Deletes the character to the left. @@ -909,7 +909,7 @@ public class TextField : View ShowContextMenu (); } - //SetNeedsDisplay (); + //SetNeedsDraw (); return true; @@ -931,14 +931,14 @@ public class TextField : View } /// - 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 class TextField : View 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 class TextField : View } 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 class TextField : View } } - Driver.SetAttribute (GetFocusColor ()); + SetAttribute (GetFocusColor ()); for (int i = col; i < width; i++) { - Driver.AddRune ((Rune)' '); + Driver?.AddRune ((Rune)' '); } PositionCursor (); @@ -1010,6 +1010,8 @@ public class TextField : View DrawAutocomplete (); _isDrawing = false; + + return true; } /// @@ -1093,7 +1095,7 @@ public class TextField : View _cursorPosition = Math.Min (selStart + cbTxt.GetRuneCount (), _text.Count); ClearAllSelection (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -1142,7 +1144,7 @@ public class TextField : View _selectedStart = 0; MoveEndExtend (); - SetNeedsDisplay (); + SetNeedsDraw (); } ///// @@ -1191,7 +1193,7 @@ public class TextField : View //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 @@ public class TextField : View if (need) { - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -1711,7 +1713,7 @@ public class TextField : View _selectedText = null; } - SetNeedsDisplay (); + SetNeedsDraw (); } else if (SelectedLength > 0 || _selectedText is { }) { @@ -1767,7 +1769,7 @@ public class TextField : View } var color = new Attribute (CaptionColor, GetNormalColor ().Background); - Driver.SetAttribute (color); + SetAttribute (color); Move (0, 0); string render = Caption; @@ -1777,7 +1779,7 @@ public class TextField : View render = render [..Viewport.Width]; } - Driver.AddStr (render); + Driver?.AddStr (render); } private void SetClipboard (IEnumerable text) @@ -1791,7 +1793,7 @@ public class TextField : View 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 289855c30..e1560c077 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -526,7 +526,7 @@ namespace Terminal.Gui _provider.Text = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -544,7 +544,7 @@ namespace Terminal.Gui _cursorPosition = c; SetFocus (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -553,14 +553,14 @@ namespace Terminal.Gui } /// - 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 @@ namespace Terminal.Gui 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 @@ namespace Terminal.Gui _cursorPosition = _provider.CursorLeft (_cursorPosition); _provider.Delete (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -671,7 +673,7 @@ namespace Terminal.Gui int current = _cursorPosition; _cursorPosition = _provider.CursorLeft (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return current != _cursorPosition; } @@ -687,7 +689,7 @@ namespace Terminal.Gui int current = _cursorPosition; _cursorPosition = _provider.CursorRight (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return current != _cursorPosition; } @@ -702,7 +704,7 @@ namespace Terminal.Gui } _provider.Delete (_cursorPosition); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -712,7 +714,7 @@ namespace Terminal.Gui private bool EndKeyHandler () { _cursorPosition = _provider.CursorEnd (); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -743,7 +745,7 @@ namespace Terminal.Gui 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 132f907a3..2ac3cf098 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1901,7 +1901,7 @@ public class TextView : View Added += TextView_Added!; - LayoutComplete += TextView_LayoutComplete; + SubviewsLaidOut += TextView_LayoutComplete; // Things this view knows how to do @@ -2457,7 +2457,7 @@ public class TextView : View AllowsTab = false; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2489,7 +2489,7 @@ public class TextView : View _tabWidth = 0; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2522,7 +2522,7 @@ public class TextView : View 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 class TextView : View _model.LoadString (Text); } - SetNeedsDisplay (); + SetNeedsDraw (); } else if (_multiline && _savedHeight is { }) { Height = _savedHeight; - SetNeedsDisplay (); + SetNeedsDraw (); } KeyBindings.Remove (Key.Enter); @@ -2629,7 +2629,7 @@ public class TextView : View { _isReadOnly = value; - SetNeedsDisplay (); + SetNeedsDraw (); WrapTextModel (); Adjust (); } @@ -2683,7 +2683,7 @@ public class TextView : View _selectionStartColumn = value < 0 ? 0 : value > line.Count ? line.Count : value; IsSelecting = true; - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } } @@ -2697,7 +2697,7 @@ public class TextView : View _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 class TextView : View AllowsTab = true; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2747,7 +2747,7 @@ public class TextView : View } OnTextChanged (); - SetNeedsDisplay (); + SetNeedsDraw (); _historyText.Clear (_model.GetAllLines ()); } @@ -2795,7 +2795,7 @@ public class TextView : View _model = _wrapManager.Model; } - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -2809,7 +2809,7 @@ public class TextView : View SetWrapModel (); bool res = _model.CloseFile (); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); return res; @@ -2978,7 +2978,7 @@ public class TextView : View _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 class TextView : View InsertText (key); - if (NeedsDisplay) + if (NeedsDraw) { Adjust (); } @@ -3225,7 +3225,7 @@ public class TextView : View finally { UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -3243,7 +3243,7 @@ public class TextView : View _model.LoadStream (stream); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -3255,7 +3255,7 @@ public class TextView : View _model.LoadCells (cells, ColorScheme?.Focus); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); InheritsPreviousAttribute = true; } @@ -3269,7 +3269,7 @@ public class TextView : View _model.LoadListCells (cellsList, ColorScheme?.Focus); _historyText.Clear (_model.GetAllLines ()); ResetPosition (); - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -3325,7 +3325,7 @@ public class TextView : View } else { - SetNeedsDisplay (); + SetNeedsDraw (); } _lastWasKill = false; @@ -3534,7 +3534,7 @@ public class TextView : View _leftColumn = 0; TrackColumn (); PositionCursor (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -3550,7 +3550,7 @@ public class TextView : View } /// - public override void OnDrawContent (Rectangle viewport) + protected override bool OnDrawingContent () { _isDrawing = true; @@ -3616,7 +3616,7 @@ public class TextView : View 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 class TextView : View 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 class TextView : View HistoryText.LineStatus.Replaced ); - SetNeedsDisplay (); + SetNeedsDraw (); OnContentsChanged (); } else @@ -3778,7 +3780,7 @@ public class TextView : View ); } - SetNeedsDisplay (); + SetNeedsDraw (); } UpdateWrapModel (); @@ -3801,8 +3803,8 @@ public class TextView : View // 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 class TextView : View _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0); } - SetNeedsDisplay (); + SetNeedsDraw (); } /// Select all text. @@ -3933,7 +3935,7 @@ public class TextView : View _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 class TextView : View /// /// 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 @@ public class TextView : View 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 @@ public class TextView : View 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 @@ public class TextView : View { 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 @@ public class TextView : View /// /// 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 @@ public class TextView : View /// 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 @@ public class TextView : View _wrapNeeded = false; } - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -4266,14 +4268,14 @@ public class TextView : View 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 @@ public class TextView : View UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); } private void ClearSelectedRegion () @@ -4363,13 +4365,13 @@ public class TextView : View 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 @@ public class TextView : View ); CurrentColumn = prevCount; - SetNeedsDisplay (); + SetNeedsDraw (); } UpdateWrapModel (); @@ -4460,7 +4462,7 @@ public class TextView : View _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 @@ public class TextView : View _wrapNeeded = true; } - DoSetNeedsDisplay ( + DoSetNeedsDraw ( new ( CurrentColumn - _leftColumn, CurrentRow - _topRow, @@ -4496,7 +4498,7 @@ public class TextView : View private void DoNeededAction () { - if (NeedsDisplay) + if (NeedsDraw) { Adjust (); } @@ -4506,17 +4508,17 @@ public class TextView : View } } - 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 @@ public class TextView : View 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 @@ public class TextView : View 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 @@ public class TextView : View if (CurrentColumn >= _leftColumn + Viewport.Width) { _leftColumn++; - SetNeedsDisplay (); + SetNeedsDraw (); } } else @@ -5063,7 +5065,7 @@ public class TextView : View 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 @@ public class TextView : View 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 @@ public class TextView : View UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); DoNeededAction (); } @@ -5299,7 +5301,7 @@ public class TextView : View UpdateWrapModel (); - DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); + DoSetNeedsDraw (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height)); DoNeededAction (); } @@ -5352,7 +5354,7 @@ public class TextView : View if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5395,7 +5397,7 @@ public class TextView : View if (CurrentRow < _topRow) { _topRow--; - SetNeedsDisplay (); + SetNeedsDraw (); } List currentLine = GetCurrentLine (); @@ -5433,7 +5435,7 @@ public class TextView : View _topRow = CurrentRow >= _model.Count ? CurrentRow - nPageDnShift : _topRow + nPageDnShift; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5459,7 +5461,7 @@ public class TextView : View if (CurrentRow < _topRow) { _topRow = _topRow - nPageUpShift < 0 ? 0 : _topRow - nPageUpShift; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5487,7 +5489,7 @@ public class TextView : View if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - SetNeedsDisplay (); + SetNeedsDraw (); } else { @@ -5510,7 +5512,7 @@ public class TextView : View { if (_leftColumn > 0) { - SetNeedsDisplay (); + SetNeedsDraw (); } CurrentColumn = 0; @@ -5552,7 +5554,7 @@ public class TextView : View if (CurrentRow < _topRow) { _topRow--; - SetNeedsDisplay (); + SetNeedsDraw (); } TrackColumn (); @@ -5684,7 +5686,7 @@ public class TextView : View ); } - SetNeedsDisplay (); + SetNeedsDraw (); UpdateWrapModel (); } @@ -6184,12 +6186,12 @@ public class TextView : View CurrentRow++; - var fullNeedsDisplay = false; + var fullNeedsDraw = false; if (CurrentRow >= _topRow + Viewport.Height) { _topRow++; - fullNeedsDisplay = true; + fullNeedsDraw = true; } CurrentColumn = 0; @@ -6202,19 +6204,19 @@ public class TextView : View 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 @@ public class TextView : View else { UpdateWrapModel (); - SetNeedsDisplay (); + SetNeedsDraw (); Adjust (); } @@ -6355,15 +6357,15 @@ public class TextView : View 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 @@ public class TextView : View _selectionStartColumn = nStartCol; _wrapNeeded = true; - SetNeedsDisplay (); + SetNeedsDraw (); } if (_currentCaller is { }) @@ -6558,7 +6560,7 @@ public class TextView : View CurrentColumn = nCol; _selectionStartRow = nStartRow; _selectionStartColumn = nStartCol; - SetNeedsDisplay (); + SetNeedsDraw (); } } } diff --git a/Terminal.Gui/Views/Tile.cs b/Terminal.Gui/Views/Tile.cs index 89149f0ec..f93c55a6b 100644 --- a/Terminal.Gui/Views/Tile.cs +++ b/Terminal.Gui/Views/Tile.cs @@ -31,7 +31,7 @@ public class 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 60370b2cb..94f4c0f85 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 class TileView : View { 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 class TileView : View get => _orientation; set { + if (_orientation == value) + { + return; + } + _orientation = value; - if (IsInitialized) - { - LayoutSubviews (); - } + 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 class TileView : View /// /// 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 class TileView : View /// 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 class TileView : View /// 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 class TileView : View // 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 class TileView : View } } - SetNeedsDisplay (); - - if (IsInitialized) - { - LayoutSubviews (); - } + SetNeedsDraw (); + SetNeedsLayout (); return toReturn; } @@ -155,44 +171,24 @@ public class TileView : View /// 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; } + + /// + /// + protected override bool OnRenderingLineCanvas () { return false; } /// - public override void OnDrawContent (Rectangle viewport) + protected override void OnDrawComplete () { - Driver.SetAttribute (ColorScheme.Normal); - - Clear (); - - base.OnDrawContent (viewport); + if (ColorScheme is { }) + { + SetAttribute (ColorScheme.Normal); + } var lc = new LineCanvas (); @@ -207,14 +203,14 @@ public class TileView : View 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 +219,7 @@ public class TileView : View 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 +243,10 @@ public class TileView : View } } - Driver.SetAttribute (ColorScheme.Normal); + if (ColorScheme is { }) + { + SetAttribute (ColorScheme.Normal); + } foreach (KeyValuePair p in lc.GetMap (Viewport)) { @@ -282,6 +281,8 @@ public class TileView : View AddRune (renderAt.X + i, renderAt.Y, (Rune)title [i]); } } + + return; } //// BUGBUG: Why is this not handled by a key binding??? @@ -292,7 +293,7 @@ public class TileView : View if (key.KeyCode == ToggleResizable) { - foreach (TileViewLineView l in _splitterLines) + foreach (TileViewLineView l in _splitterLines!) { bool iniBefore = l.IsInitialized; l.IsInitialized = false; @@ -319,8 +320,8 @@ public class TileView : View /// public void RebuildForTileCount (int count) { - _tiles = new List (); - _splitterDistances = new List (); + _tiles = new (); + _splitterDistances = new (); if (_splitterLines is { }) { @@ -330,13 +331,13 @@ public class TileView : View } } - _splitterLines = new List (); + _splitterLines = new (); RemoveAll (); foreach (Tile tile in _tiles) { - tile.ContentView.Dispose (); + tile.ContentView?.Dispose (); tile.ContentView = null; } @@ -361,15 +362,14 @@ public class TileView : View 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 (); + + // BUGBUG: This should not be needed: + tile.TitleChanged += (s, e) => SetNeedsLayout (); } - if (IsInitialized) - { - LayoutSubviews (); - } + SetNeedsLayout (); } /// @@ -378,7 +378,7 @@ public class TileView : View /// /// /// - public Tile RemoveTile (int idx) + public Tile? RemoveTile (int idx) { Tile [] oldTiles = Tiles.ToArray (); @@ -391,14 +391,14 @@ public class TileView : View 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 +406,6 @@ public class TileView : View Add (_tiles [i].ContentView); } - SetNeedsDisplay (); - LayoutSubviews (); - return removed; } @@ -439,15 +436,20 @@ public class TileView : View 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 +471,10 @@ public class TileView : View { // 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 +489,7 @@ public class TileView : View }; // 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 +501,16 @@ public class TileView : View 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 +524,14 @@ public class TileView : View 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 +558,14 @@ public class TileView : View 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 +581,7 @@ public class TileView : View { if (sub.Title.Length > 0) { - titles.Add (new TileTitleToRender (v, sub, depth)); + titles.Add (new (v, sub, depth)); } } } @@ -599,20 +601,20 @@ public class TileView : View 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 +631,19 @@ public class TileView : View 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 +667,7 @@ public class TileView : View 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 +726,7 @@ public class TileView : View } // 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 +742,7 @@ public class TileView : View } // 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 +776,7 @@ public class TileView : View return; } - for (var i = 0; i < _splitterLines.Count; i++) + for (var i = 0; i < _splitterLines!.Count; i++) { TileViewLineView line = _splitterLines [i]; @@ -791,19 +793,19 @@ public class TileView : View 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 +814,27 @@ public class TileView : View 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 +842,8 @@ public class TileView : View } 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 +851,22 @@ public class TileView : View /// 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 +877,7 @@ public class TileView : View { public Point? moveRuneRenderLocation; - private Pos dragOrignalPos; + private Pos? dragOrignalPos; private Point? dragPosition; public TileViewLineView (TileView parent, int idx) @@ -883,13 +887,13 @@ public class TileView : View 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 +960,7 @@ public class TileView : View moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Viewport.Height - 2, mouseEvent.Position.Y))); } - Parent.SetNeedsDisplay (); + Parent.SetNeedsLayout (); return true; } @@ -969,7 +973,7 @@ public class TileView : View //Driver.UncookMouse (); FinalisePosition ( - dragOrignalPos, + dragOrignalPos!, Orientation == Orientation.Horizontal ? Y : X ); dragPosition = null; @@ -979,11 +983,14 @@ public class TileView : View 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 +1022,7 @@ public class TileView : View 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 +1043,10 @@ public class TileView : View /// private bool FinalisePosition (Pos oldValue, Pos newValue) { + SetNeedsDraw (); + + SetNeedsLayout (); + if (oldValue is PosPercent) { if (Orientation == Orientation.Horizontal) @@ -1078,10 +1089,10 @@ public class TileView : View 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 +1100,4 @@ public class TileView : View } /// 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 ecc94a7be..932f67ddb 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -106,7 +106,7 @@ public class TimeField : TextField SetText (Text); ReadOnly = ro; - SetNeedsDisplay (); + SetNeedsDraw (); } } diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index ddee6b041..7861fcfd0 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -235,7 +235,7 @@ public partial class Toplevel : View return; } - var layoutSubviews = false; + //var layoutSubviews = false; var maxWidth = 0; if (superView.Margin is { } && superView == top.SuperView) @@ -251,36 +251,26 @@ public partial class Toplevel : View 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 2b9f637d8..e2d220ace 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -237,7 +237,7 @@ internal class Branch where T : class }; tree.OnDrawLine (e); - if (!e.Handled) + if (!e.Handled && driver != null) { foreach (Cell cell in cells) { @@ -246,7 +246,7 @@ internal class Branch where T : class } } - 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 4e09eba2b..728a411bd 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 class TreeView : 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 class TreeView : View, ITreeView where T : class { KeyBindings.ReplaceKey (ObjectActivationKey, value); objectActivationKey = value; - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -369,7 +388,7 @@ public class TreeView : View, ITreeView where T : class /// 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 class TreeView : View, ITreeView where T : class 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 class TreeView : View, ITreeView where T : class set { scrollOffsetVertical = Math.Max (0, value); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -435,7 +454,7 @@ public class TreeView : View, ITreeView where T : class multiSelectedRegions.Clear (); roots = new Dictionary> (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -471,7 +490,7 @@ public class TreeView : View, ITreeView where T : class { roots.Add (o, new Branch (this, null, o)); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -494,7 +513,7 @@ public class TreeView : View, ITreeView where T : class if (objectsAdded) { InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -562,7 +581,7 @@ public class TreeView : View, ITreeView where T : class } } - SetNeedsDisplay (); + SetNeedsDraw (); } /// Moves the selection to the last child in the currently selected level. @@ -594,7 +613,7 @@ public class TreeView : View, ITreeView where T : class { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -636,7 +655,7 @@ public class TreeView : View, ITreeView where T : class { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -697,7 +716,7 @@ public class TreeView : View, ITreeView where T : class } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -753,7 +772,7 @@ public class TreeView : View, ITreeView where T : class ObjectToBranch (toExpand)?.Expand (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Expands the supplied object and all child objects. @@ -767,7 +786,7 @@ public class TreeView : View, ITreeView where T : class ObjectToBranch (toExpand)?.ExpandAll (); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -782,7 +801,7 @@ public class TreeView : View, ITreeView where T : class } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -911,7 +930,7 @@ public class TreeView : View, ITreeView where T : class /// /// 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 class TreeView : View, ITreeView where T : class 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 class TreeView : View, ITreeView where T : class ScrollOffsetVertical = Math.Max (0, map.Count - Viewport.Height + 1); SelectedObject = map.LastOrDefault ()?.Model; - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -969,7 +988,7 @@ public class TreeView : View, ITreeView where T : class ScrollOffsetVertical = 0; SelectedObject = roots.Keys.FirstOrDefault (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// Clears any cached results of the tree state. @@ -1022,7 +1041,7 @@ public class TreeView : View, ITreeView where T : class if (me.Flags == MouseFlags.WheeledRight) { ScrollOffsetHorizontal++; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1030,7 +1049,7 @@ public class TreeView : View, ITreeView where T : class if (me.Flags == MouseFlags.WheeledLeft) { ScrollOffsetHorizontal--; - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1079,7 +1098,7 @@ public class TreeView : View, ITreeView where T : class multiSelectedRegions.Clear (); } - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1098,7 +1117,7 @@ public class TreeView : View, ITreeView where T : class // 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 @@ public class TreeView : View, ITreeView where T : class 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 class TreeView : View, ITreeView where T : class { // 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 @@ public class TreeView : View, ITreeView where T : class { SelectedObject = map.ElementAt ((int)newIndex).Model; EnsureVisible (selectedObject); - SetNeedsDisplay (); + SetNeedsDraw (); return true; } @@ -1241,7 +1262,7 @@ public class TreeView : View, ITreeView where T : class } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -1262,7 +1283,7 @@ public class TreeView : View, ITreeView where T : class { branch.Refresh (startAtTop); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1276,7 +1297,7 @@ public class TreeView : View, ITreeView where T : class { roots.Remove (o); InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); if (Equals (SelectedObject, o)) { @@ -1291,7 +1312,7 @@ public class TreeView : View, ITreeView where T : class if (ScrollOffsetVertical <= ContentHeight - 2) { ScrollOffsetVertical++; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1301,7 +1322,7 @@ public class TreeView : View, ITreeView where T : class if (scrollOffsetVertical > 0) { ScrollOffsetVertical--; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -1323,7 +1344,7 @@ public class TreeView : View, ITreeView where T : class } multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map)); - SetNeedsDisplay (); + SetNeedsDraw (); OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } @@ -1368,7 +1389,7 @@ public class TreeView : View, ITreeView where T : class } InvalidateLineMap (); - SetNeedsDisplay (); + SetNeedsDraw (); } /// @@ -1396,7 +1417,7 @@ public class TreeView : View, ITreeView where T : class { SelectedObject = parent; AdjustSelection (0); - SetNeedsDisplay (); + SetNeedsDraw (); } } } @@ -1529,7 +1550,7 @@ public class TreeView : View, ITreeView where T : class { SelectedObject = map.ElementAt (idxCur).Model; EnsureVisible (map.ElementAt (idxCur).Model); - SetNeedsDisplay (); + SetNeedsDraw (); return; } @@ -1593,4 +1614,5 @@ internal class TreeSelection where T : class 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 ba9b19092..907c1b5dc 100644 --- a/Terminal.Gui/Views/TreeViewTextFilter.cs +++ b/Terminal.Gui/Views/TreeViewTextFilter.cs @@ -51,6 +51,6 @@ public class TreeViewTextFilter : ITreeViewFilter where T : class 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 b9bc49bd5..254aece7a 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -546,9 +546,6 @@ public class Wizard : Dialog 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 eacb7c9df..e3d94f6b3 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 class WizardStep : View // if (helpTextView.TopRow != scrollBar.Position) { // scrollBar.Position = helpTextView.TopRow; // } - // helpTextView.SetNeedsDisplay (); + // helpTextView.SetNeedsDraw (); //}; //scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { @@ -81,7 +81,7 @@ public class WizardStep : View // if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) { // scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn; // } - // helpTextView.SetNeedsDisplay (); + // helpTextView.SetNeedsDraw (); //}; //scrollBar.VisibleChanged += (s,e) => { @@ -130,7 +130,7 @@ public class WizardStep : View { _helpTextView.Text = value; ShowHide (); - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -158,7 +158,7 @@ public class WizardStep : 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 219c41cbe..835e9828b 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 000000000..050fcf5c4 --- /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 94aa4f91a..54e8f521d 100644 --- a/UICatalog/KeyBindingsDialog.cs +++ b/UICatalog/KeyBindingsDialog.cs @@ -103,7 +103,7 @@ internal class KeyBindingsDialog : Dialog _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 041a5c22d..7ed43499a 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": { + "All Views Tester": { "commandName": "Project", - "commandLineArgs": "MenuBar" + "commandLineArgs": "\"All Views Tester\" -b" }, - "ListView & ComboBox": { + "Charmap": { "commandName": "Project", - "commandLineArgs": "\"ListView & ComboBox\"" - }, - "Frames Demo": { - "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 f0a03f5ce..1d62c4ad7 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 class Scenario : IDisposable /// 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 @@ public class Scenario : IDisposable 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 @@ public class Scenario : IDisposable 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 (); - } + public virtual List GetDemoKeyStrokes () => new List (); - /// 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; - - /// 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 000000000..342974405 --- /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 000000000..0e1ef4b16 --- /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 45889f3e8..d7e181eae 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -111,7 +111,7 @@ public class ASCIICustomButtonTest : Scenario 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 d4d54b819..55feb28ec 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 class Adornments : Scenario Window app = new () { - Title = GetQuitKeyAndName () + Title = GetQuitKeyAndName (), + BorderStyle = LineStyle.None }; var editor = new AdornmentsEditor @@ -34,9 +36,8 @@ public class Adornments : Scenario 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 class Adornments : Scenario 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 class Adornments : Scenario 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 class Adornments : Scenario 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 d50714c5f..000000000 --- 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 000000000..840886702 --- /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 026a18383..d982b2b89 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" }; + private Dictionary? _viewClasses; + private ListView? _classListView; + private AdornmentsEditor? _adornmentsEditor; - // 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; + private ArrangementEditor? _arrangementEditor; - // 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 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 class AllViewsTester : Scenario _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 + _arrangementEditor = new () { - CanFocus = false, - Orientation = Orientation.Horizontal + 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 }; - _adornmentsEditor.Border.Add (expandButton); + _arrangementEditor.ExpanderButton!.Orientation = Orientation.Horizontal; + + _arrangementEditor.ExpanderButton.CollapsedChanging += (sender, args) => + { + _adornmentsEditor.ExpanderButton.Collapsed = args.NewValue; + }; + _arrangementEditor.Border!.Thickness = new (1); + + _layoutEditor = new () + { + Title = "Layout [_4]", + X = Pos.Right (_arrangementEditor) - 1, + Y = 0, + //Width = Dim.Fill (), // set below + Height = Dim.Auto (), + CanFocus = true, + AutoSelectViewToEdit = false, + AutoSelectAdornments = false, + SuperViewRendersLineCanvas = true + }; + _layoutEditor.Border!.Thickness = new (1); _settingsPane = new () { - X = Pos.Right (_adornmentsEditor), - Y = 0, // for menu - Width = Dim.Fill (), + 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 (), CanFocus = true, - ColorScheme = Colors.ColorSchemes ["TopLevel"], - Title = "Settings" + SuperViewRendersLineCanvas = true }; + _settingsPane.Border!.Thickness = new (1, 1, 1, 0); - string [] radioItems = { "_Percent(x)", "_AnchorEnd", "_Center", "A_bsolute(x)" }; - - _locationFrame = new () - { - X = 0, - Y = 0, - Height = Dim.Auto (), - Width = Dim.Auto (), - Title = "Location (Pos)", - TabStop = TabBehavior.TabStop, - }; - _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 () - { - X = Pos.Right (_locationFrame), - Y = Pos.Y (_locationFrame), - Height = Dim.Auto (), - Width = Dim.Auto (), - Title = "Size (Dim)", - TabStop = TabBehavior.TabStop, - }; - - 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 class AllViewsTester : Scenario _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 @@ public class AllViewsTester : Scenario } // 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,25 @@ public class AllViewsTester : Scenario 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(); + _curView = view; + + _hostPane!.Add (_curView); + _layoutEditor!.ViewToEdit = _curView; + _arrangementEditor!.ViewToEdit = _curView; + _curView.SetNeedsLayout (); } private void DisposeCurrentView () @@ -392,184 +315,62 @@ public class AllViewsTester : Scenario 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) - { - // This is a hack to work around the fact that the Pos enum doesn't have an "Align" value yet - Debug.WriteLine ($"{e}"); - } - - _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 _)) - { - _wText.Text = "Auto"; - _wText.Enabled = false; - } - else - { - _wText.Text = $"{view.Frame.Width}"; - _wText.Enabled = true; - } - - if (view.Height.Has (out _)) - { - _hText.Text = "Auto"; - _hText.Enabled = false; - } - else - { - _hText.Text = $"{view.Frame.Height}"; - _hText.Enabled = true; - } - - _updatingSettings = false; - } - - private void UpdateHostTitle (View view) { _hostPane.Title = $"_Demo of {view.GetType ().Name}"; } - - private void CurrentView_Initialized (object sender, EventArgs e) + private void CurrentView_Initialized (object? sender, EventArgs e) { if (sender is not View view) { return; } - if (!view.Width!.Has (out _) || (view.Width is null || view.Frame.Width == 0)) + if (view.Width == Dim.Absolute(0) || view.Width is null) { view.Width = Dim.Fill (); } - if (!view.Height!.Has (out _) || (view.Height is null || view.Frame.Height == 0)) + if (view.Height == Dim.Absolute (0) || view.Height is null) { view.Height = Dim.Fill (); } - UpdateSettings (view); - UpdateHostTitle (view); } + + public override List GetDemoKeyStrokes () + { + var keys = new List (); + + for (int i = 0; i < GetAllViewClassesCollection ().Count; i++) + { + keys.Add (Key.CursorDown); + } + + 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 08add3f0a..fef9411d3 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 class AnimationScenario : Scenario 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 class AnimationScenario : Scenario } 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 AnimationScenario : Scenario 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 class AnimationScenario : Scenario 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; } + 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) + 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 class AnimationScenario : Scenario 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 8c9651bd1..48ed9f225 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 class Arrangement : Scenario 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 class Arrangement : Scenario 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 class Arrangement : Scenario Application.Wakeup (); - progressBar.SetNeedsDisplay (); + progressBar.SetNeedsDraw (); }; timer.Start (); @@ -231,12 +236,66 @@ public class Arrangement : Scenario 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 396c493d6..000000000 --- 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 b0d226148..7a4f388b8 100644 --- a/UICatalog/Scenarios/Bars.cs +++ b/UICatalog/Scenarios/Bars.cs @@ -136,6 +136,7 @@ public class Bars : Scenario 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 3fd4a3a23..0890aa911 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; using System.Linq; using System.Net.Http; @@ -47,7 +46,7 @@ public class CharacterMap : Scenario { X = 0, Y = 0, - Width = Dim.Fill (), + Width = Dim.Fill (Dim.Func (() => _categoryList.Frame.Width)), Height = Dim.Fill () }; top.Add (_charMap); @@ -80,6 +79,7 @@ public class CharacterMap : Scenario _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; @@ -136,9 +136,6 @@ public class CharacterMap : Scenario top.Add (_categoryList); - // TODO: Replace this with Dim.Auto when that's ready - _categoryList.Initialized += _categoryList_Initialized; - var menu = new MenuBar { Menus = @@ -247,8 +244,6 @@ public class CharacterMap : Scenario } } - private void _categoryList_Initialized (object sender, EventArgs e) { _charMap.Width = Dim.Fill () - _categoryList.Width; } - private EnumerableTableSource CreateCategoryTable (int sortByColumn, bool descending) { Func orderBy; @@ -303,9 +298,35 @@ public class CharacterMap : Scenario return item; } + + public override List GetDemoKeyStrokes () + { + List keys = new List (); + + for (var 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 (var 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; @@ -471,9 +492,9 @@ internal class CharMap : View AutoHide = false, X = RowLabelWidth + 1, Y = Pos.AnchorEnd (), + Orientation = Orientation.Horizontal, Width = Dim.Fill (1), - Size = COLUMN_WIDTH * 15, - Orientation = Orientation.Horizontal + Size = COLUMN_WIDTH * 15 }; ScrollBar vScrollBar = new () @@ -482,31 +503,46 @@ internal class CharMap : View X = Pos.AnchorEnd (), Y = 1, // Header Height = Dim.Fill (Dim.Func (() => Padding.Thickness.Bottom)), - Orientation = Orientation.Vertical, - Size = GetContentSize ().Height, + Size = GetContentSize ().Height }; - vScrollBar.PositionChanged += (sender, args) => { Viewport = Viewport with { Y = args.CurrentValue }; }; Padding.Add (vScrollBar, hScrollBar); - hScrollBar.PositionChanged += (sender, args) => { Viewport = Viewport with { X = args.CurrentValue }; }; + + vScrollBar.PositionChanged += (sender, args) => + { + if (Viewport.Height > 0) + { + Viewport = Viewport with { Y = args.CurrentValue }; + } + }; + + hScrollBar.PositionChanged += (sender, args) => + { + if (Viewport.Width > 0) + { + Viewport = Viewport with { X = args.CurrentValue }; + } + }; ViewportChanged += (sender, args) => - { - if (Viewport.Width < GetContentSize ().Width) - { - Padding.Thickness = Padding.Thickness with { Bottom = 1 }; - } - else - { - Padding.Thickness = Padding.Thickness with { Bottom = 0 }; - } + { + if (Viewport.Width < GetContentSize ().Width) + { + Padding.Thickness = Padding.Thickness with { Bottom = 1 }; + hScrollBar.Visible = true; + } + else + { + Padding.Thickness = Padding.Thickness with { Bottom = 0 }; + hScrollBar.Visible = false; + } - hScrollBar.Size = COLUMN_WIDTH * 15; - hScrollBar.Position = Viewport.X; + hScrollBar.Size = COLUMN_WIDTH * 15; + hScrollBar.Position = Viewport.X; - vScrollBar.Size = GetContentSize ().Height; - vScrollBar.Position = Viewport.Y; - }; + vScrollBar.Size = GetContentSize ().Height; + vScrollBar.Position = Viewport.Y; + }; } private void Handle_MouseEvent (object sender, MouseEventArgs e) @@ -604,7 +640,7 @@ internal class CharMap : View } } - SetNeedsDisplay (); + SetNeedsDraw (); SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null)); } } @@ -615,7 +651,7 @@ internal class CharMap : View set { _rowHeight = value ? 2 : 1; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -631,7 +667,7 @@ internal class CharMap : View _start = value; SelectedCodePoint = value; Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight }; - SetNeedsDisplay (); + SetNeedsDraw (); } } @@ -639,21 +675,21 @@ internal class CharMap : View 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; @@ -665,12 +701,12 @@ internal class CharMap : View 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 (" "); } } @@ -689,7 +725,7 @@ internal class CharMap : View } Move (firstColumnX + COLUMN_WIDTH, y); - Driver.SetAttribute (GetNormalColor ()); + SetAttribute (GetNormalColor ()); for (var col = 0; col < 16; col++) { @@ -705,7 +741,7 @@ internal class CharMap : View // 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; @@ -723,7 +759,7 @@ internal class CharMap : View // Draw the rune if (width > 0) { - Driver.AddRune (rune); + AddRune (rune); } else { @@ -744,11 +780,11 @@ internal class CharMap : View if (normal.Length == 1) { - Driver.AddRune (normal [0]); + AddRune ((Rune)normal [0]); } else { - Driver.AddRune (Rune.ReplacementChar); + AddRune (Rune.ReplacementChar); } } } @@ -756,31 +792,33 @@ internal class CharMap : View 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 () @@ -984,7 +1022,7 @@ internal class CharMap : View document.RootElement, new JsonSerializerOptions - { WriteIndented = true } + { WriteIndented = true } ); } @@ -997,16 +1035,16 @@ internal class CharMap : View var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] }; copyGlyph.Accepting += (s, a) => - { - CopyGlyph (); - dlg.RequestStop (); - }; + { + CopyGlyph (); + dlg.RequestStop (); + }; copyCP.Accepting += (s, a) => - { - CopyCodePoint (); - dlg.RequestStop (); - }; + { + CopyCodePoint (); + dlg.RequestStop (); + }; cancel.Accepting += (s, a) => dlg.RequestStop (); var rune = (Rune)SelectedCodePoint; diff --git a/UICatalog/Scenarios/ClassExplorer.cs b/UICatalog/Scenarios/ClassExplorer.cs index 0bfd4e9fd..60e88e3e0 100644 --- a/UICatalog/Scenarios/ClassExplorer.cs +++ b/UICatalog/Scenarios/ClassExplorer.cs @@ -193,7 +193,7 @@ public class ClassExplorer : Scenario { _treeView.Style.HighlightModelTextOnly = !_treeView.Style.HighlightModelTextOnly; _highlightModelTextOnly.Checked = _treeView.Style.HighlightModelTextOnly; - _treeView.SetNeedsDisplay (); + _treeView.SetNeedsDraw (); } private void Quit () { Application.RequestStop (); } @@ -292,7 +292,7 @@ public class ClassExplorer : Scenario _textView.Text = ex.Message; } - _textView.SetNeedsDisplay (); + _textView.SetNeedsDraw (); } private enum Showable diff --git a/UICatalog/Scenarios/Clipping.cs b/UICatalog/Scenarios/Clipping.cs index fe0692fc9..3e866023d 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 class Clipping : Scenario 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 0ae7a7628..650e5e180 100644 --- a/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -7,9 +7,9 @@ using Terminal.Gui; 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 c05931912..cd0789d60 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 class ColorPickers : Scenario // 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 @@ public class ColorPickers : Scenario /// 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 @@ public class ColorPickers : Scenario ) }; } + + 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 3b2a8e196..d35198251 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 bdf10337c..c27eabdb3 100644 --- a/UICatalog/Scenarios/ComputedLayout.cs +++ b/UICatalog/Scenarios/ComputedLayout.cs @@ -50,7 +50,7 @@ public class ComputedLayout : Scenario Text = vrule }; - app.LayoutComplete += (s, a) => + app.SubviewsLaidOut += (s, a) => { if (horizontalRuler.Viewport.Width == 0 || horizontalRuler.Viewport.Height == 0) { @@ -370,7 +370,6 @@ public class ComputedLayout : Scenario // The call to app.LayoutSubviews causes the Computed layout to // get updated. anchorButton.Text += "!"; - app.LayoutSubviews (); }; app.Add (anchorButton); @@ -415,10 +414,7 @@ public class ComputedLayout : Scenario { // 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 class ComputedLayout : Scenario { // 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 class ComputedLayout : Scenario { // 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 2c898ac8b..037acceec 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -94,7 +94,7 @@ public class ConfigurationEditor : Scenario foreach (Tile t in _tileView.Tiles) { t.ContentView.ColorScheme = EditorColorScheme; - t.ContentView.SetNeedsDisplay (); + t.ContentView.SetNeedsDraw (); } ; @@ -154,8 +154,6 @@ public class ConfigurationEditor : Scenario { _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 d700dcac5..1c5775541 100644 --- a/UICatalog/Scenarios/ContentScrolling.cs +++ b/UICatalog/Scenarios/ContentScrolling.cs @@ -17,6 +17,7 @@ public class ContentScrolling : Scenario { public ScrollingDemoView () { + Id = "ScrollingDemoView"; Width = Dim.Fill (); Height = Dim.Fill (); ColorScheme = Colors.ColorSchemes ["Base"]; @@ -47,7 +48,7 @@ public class ContentScrolling : Scenario // 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 @@ public class ContentScrolling : Scenario } } - private void VirtualDemoView_LayoutComplete (object sender, LayoutEventArgs e) + private void VirtualDemoView_LayoutComplete (object sender, DrawEventArgs drawEventArgs) { - Label status = Border.Subviews.OfType