Skip to content

Commit

Permalink
Merge pull request #3798 from tig/v2_3761_2886-Draw-and-Layout-Perf
Browse files Browse the repository at this point in the history
Fixes #3761, #2886, #3780, #3485, #3622, #3413, #2995 - Draw  and Layout performance/correctness
  • Loading branch information
tig authored Nov 10, 2024
2 parents 4ccb3fb + d3d3df8 commit dbedeb6
Show file tree
Hide file tree
Showing 285 changed files with 13,479 additions and 7,058 deletions.
2 changes: 1 addition & 1 deletion CommunityToolkitExample/LoginView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void Receive (Message<LoginActions> message)
}
}
SetText();
Application.Refresh ();
Application.LayoutAndDraw ();
}

private void SetText ()
Expand Down
21 changes: 15 additions & 6 deletions Terminal.Gui/Application/Application.Initialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -59,12 +58,12 @@ internal static void InternalInit (
bool calledViaRunT = false
)
{
if (IsInitialized && driver is null)
if (Initialized && driver is null)
{
return;
}

if (IsInitialized)
if (Initialized)
{
throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
}
Expand Down Expand Up @@ -173,7 +172,7 @@ internal static void InternalInit (

SupportedCultures = GetSupportedCultures ();
MainThreadId = Thread.CurrentThread.ManagedThreadId;
bool init = IsInitialized = true;
bool init = Initialized = true;
InitializedChanged?.Invoke (null, new (init));
}

Expand Down Expand Up @@ -215,17 +214,27 @@ public static void Shutdown ()
{
// TODO: Throw an exception if Init hasn't been called.

bool wasInitialized = IsInitialized;
bool wasInitialized = Initialized;
ResetState ();
PrintJsonErrors ();

if (wasInitialized)
{
bool init = IsInitialized;
bool init = Initialized;
InitializedChanged?.Invoke (null, new (in init));
}
}

/// <summary>
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
/// </para>
/// </remarks>
public static bool Initialized { get; internal set; }

/// <summary>
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions Terminal.Gui/Application/Application.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static bool RaiseKeyDownEvent (Key key)
/// <returns><see langword="true"/> if the key was handled.</returns>
public static bool RaiseKeyUpEvent (Key key)
{
if (!IsInitialized)
if (!Initialized)
{
return true;
}
Expand Down Expand Up @@ -200,7 +200,7 @@ internal static void AddApplicationKeyBindings ()
Command.Refresh,
static () =>
{
Refresh ();
LayoutAndDraw ();
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Application/Application.Mouse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
{
if (deepestViewUnderMouse is Adornment adornmentView)
{
deepestViewUnderMouse = adornmentView.Parent!.SuperView;
deepestViewUnderMouse = adornmentView.Parent?.SuperView;
}
else
{
Expand Down
107 changes: 62 additions & 45 deletions Terminal.Gui/Application/Application.Run.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Terminal.Gui;

Expand Down Expand Up @@ -80,27 +81,20 @@ public static RunState Begin (Toplevel toplevel)
{
ArgumentNullException.ThrowIfNull (toplevel);

//#if DEBUG_IDISPOSABLE
// Debug.Assert (!toplevel.WasDisposed);
//#if DEBUG_IDISPOSABLE
// Debug.Assert (!toplevel.WasDisposed);

// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
// {
// Debug.Assert (_cachedRunStateToplevel.WasDisposed);
// }
//#endif
// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
// {
// Debug.Assert (_cachedRunStateToplevel.WasDisposed);
// }
//#endif

// Ensure the mouse is ungrabbed.
MouseGrabView = null;

var rs = new RunState (toplevel);

// View implements ISupportInitializeNotification which is derived from ISupportInitialize
if (!toplevel.IsInitialized)
{
toplevel.BeginInit ();
toplevel.EndInit ();
}

#if DEBUG_IDISPOSABLE
if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
{
Expand Down Expand Up @@ -176,16 +170,26 @@ public static RunState Begin (Toplevel toplevel)
Top.HasFocus = false;
}

// Force leave events for any entered views in the old Top
if (GetLastMousePosition () is { })
{
RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new List<View?> ());
}

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)
Expand All @@ -195,15 +199,16 @@ public static RunState Begin (Toplevel toplevel)

toplevel.OnLoaded ();

Refresh ();

if (PositionCursor ())
{
Driver.UpdateCursor ();
Driver?.UpdateCursor ();
}

NotifyNewRunState?.Invoke (toplevel, new (rs));

// Force an Idle event so that an Iteration (and Refresh) happen.
Application.Invoke (() => { });

return rs;
}

Expand All @@ -225,11 +230,12 @@ internal static bool PositionCursor ()
// If the view is not visible or enabled, don't position the cursor
if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled)
{
Driver!.GetCursorVisibility (out CursorVisibility current);
CursorVisibility current = CursorVisibility.Invisible;
Driver?.GetCursorVisibility (out current);

if (current != CursorVisibility.Invisible)
{
Driver.SetCursorVisibility (CursorVisibility.Invisible);
Driver?.SetCursorVisibility (CursorVisibility.Invisible);
}

return false;
Expand Down Expand Up @@ -326,7 +332,7 @@ internal static bool PositionCursor ()
public static T Run<T> (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null)
where T : Toplevel, new()
{
if (!IsInitialized)
if (!Initialized)
{
// Init() has NOT been called.
InternalInit (driver, null, true);
Expand Down Expand Up @@ -381,7 +387,7 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul
{
ArgumentNullException.ThrowIfNull (view);

if (IsInitialized)
if (Initialized)
{
if (Driver is null)
{
Expand Down Expand Up @@ -452,7 +458,10 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul
/// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a
/// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>.
/// </remarks>
public static object AddTimeout (TimeSpan time, Func<bool> callback) { return MainLoop!.AddTimeout (time, callback); }
public static object? AddTimeout (TimeSpan time, Func<bool> callback)
{
return MainLoop?.AddTimeout (time, callback) ?? null;
}

/// <summary>Removes a previously scheduled timeout</summary>
/// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks>
Expand Down Expand Up @@ -486,20 +495,25 @@ public static void Invoke (Action action)
/// <summary>Wakes up the running application that might be waiting on input.</summary>
public static void Wakeup () { MainLoop?.Wakeup (); }

/// <summary>Triggers a refresh of the entire display.</summary>
public static void Refresh ()
/// <summary>
/// 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 <see cref="View.NeedsLayout"/>) will be laid out.
/// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
/// </summary>
/// <param name="forceDraw">If <see langword="true"/> the entire View hierarchy will be redrawn. The default is <see langword="false"/> and should only be overriden for testing.</param>
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 ();
}

/// <summary>This event is raised on each iteration of the main loop.</summary>
Expand Down Expand Up @@ -534,24 +548,25 @@ public static void RunLoop (RunState state)
return;
}

RunIteration (ref state, ref firstIteration);
firstIteration = RunIteration (ref state, firstIteration);
}

MainLoop!.Running = false;

// Run one last iteration to consume any outstanding input events from Driver
// This is important for remaining OnKeyUp events.
RunIteration (ref state, ref firstIteration);
RunIteration (ref state, firstIteration);
}

/// <summary>Run one application iteration.</summary>
/// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
/// <param name="firstIteration">
/// Set to <see langword="true"/> if this is the first run loop iteration. Upon return, it
/// will be set to <see langword="false"/> if at least one iteration happened.
/// Set to <see langword="true"/> if this is the first run loop iteration.
/// </param>
public static void RunIteration (ref RunState state, ref bool firstIteration)
/// <returns><see langword="false"/> if at least one iteration happened.</returns>
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
Expand All @@ -561,23 +576,25 @@ public static void RunIteration (ref RunState state, ref bool firstIteration)
}

MainLoop.RunIteration ();

Iteration?.Invoke (null, new ());
}

firstIteration = false;

if (Top is null)
{
return;
return firstIteration;
}

Refresh ();
LayoutAndDraw ();

if (PositionCursor ())
{
Driver!.UpdateCursor ();
}

return firstIteration;
}

/// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary>
Expand Down Expand Up @@ -652,7 +669,7 @@ public static void End (RunState runState)
if (TopLevels.Count > 0)
{
Top = TopLevels.Peek ();
Top.SetNeedsDisplay ();
Top.SetNeedsDraw ();
}

if (runState.Toplevel is { HasFocus: true })
Expand All @@ -670,6 +687,6 @@ public static void End (RunState runState)
runState.Toplevel = null;
runState.Dispose ();

Refresh ();
LayoutAndDraw ();
}
}
Loading

0 comments on commit dbedeb6

Please sign in to comment.