Skip to content

Commit dbedeb6

Browse files
authored
Merge pull request #3798 from tig/v2_3761_2886-Draw-and-Layout-Perf
Fixes #3761, #2886, #3780, #3485, #3622, #3413, #2995 - Draw and Layout performance/correctness
2 parents 4ccb3fb + d3d3df8 commit dbedeb6

File tree

285 files changed

+13479
-7058
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

285 files changed

+13479
-7058
lines changed

CommunityToolkitExample/LoginView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public void Receive (Message<LoginActions> message)
5959
}
6060
}
6161
SetText();
62-
Application.Refresh ();
62+
Application.LayoutAndDraw ();
6363
}
6464

6565
private void SetText ()

Terminal.Gui/Application/Application.Initialization.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public static partial class Application // Initialization (Init/Shutdown)
3939
[RequiresDynamicCode ("AOT")]
4040
public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); }
4141

42-
internal static bool IsInitialized { get; set; }
4342
internal static int MainThreadId { get; set; } = -1;
4443

4544
// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
@@ -59,12 +58,12 @@ internal static void InternalInit (
5958
bool calledViaRunT = false
6059
)
6160
{
62-
if (IsInitialized && driver is null)
61+
if (Initialized && driver is null)
6362
{
6463
return;
6564
}
6665

67-
if (IsInitialized)
66+
if (Initialized)
6867
{
6968
throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
7069
}
@@ -173,7 +172,7 @@ internal static void InternalInit (
173172

174173
SupportedCultures = GetSupportedCultures ();
175174
MainThreadId = Thread.CurrentThread.ManagedThreadId;
176-
bool init = IsInitialized = true;
175+
bool init = Initialized = true;
177176
InitializedChanged?.Invoke (null, new (init));
178177
}
179178

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

218-
bool wasInitialized = IsInitialized;
217+
bool wasInitialized = Initialized;
219218
ResetState ();
220219
PrintJsonErrors ();
221220

222221
if (wasInitialized)
223222
{
224-
bool init = IsInitialized;
223+
bool init = Initialized;
225224
InitializedChanged?.Invoke (null, new (in init));
226225
}
227226
}
228227

228+
/// <summary>
229+
/// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>.
230+
/// </summary>
231+
/// <remarks>
232+
/// <para>
233+
/// The <see cref="InitializedChanged"/> event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
234+
/// </para>
235+
/// </remarks>
236+
public static bool Initialized { get; internal set; }
237+
229238
/// <summary>
230239
/// This event is raised after the <see cref="Init"/> and <see cref="Shutdown"/> methods have been called.
231240
/// </summary>

Terminal.Gui/Application/Application.Keyboard.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public static bool RaiseKeyDownEvent (Key key)
120120
/// <returns><see langword="true"/> if the key was handled.</returns>
121121
public static bool RaiseKeyUpEvent (Key key)
122122
{
123-
if (!IsInitialized)
123+
if (!Initialized)
124124
{
125125
return true;
126126
}
@@ -200,7 +200,7 @@ internal static void AddApplicationKeyBindings ()
200200
Command.Refresh,
201201
static () =>
202202
{
203-
Refresh ();
203+
LayoutAndDraw ();
204204

205205
return true;
206206
}

Terminal.Gui/Application/Application.Mouse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
227227
{
228228
if (deepestViewUnderMouse is Adornment adornmentView)
229229
{
230-
deepestViewUnderMouse = adornmentView.Parent!.SuperView;
230+
deepestViewUnderMouse = adornmentView.Parent?.SuperView;
231231
}
232232
else
233233
{

Terminal.Gui/Application/Application.Run.cs

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#nullable enable
22
using System.Diagnostics;
33
using System.Diagnostics.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
45

56
namespace Terminal.Gui;
67

@@ -80,27 +81,20 @@ public static RunState Begin (Toplevel toplevel)
8081
{
8182
ArgumentNullException.ThrowIfNull (toplevel);
8283

83-
//#if DEBUG_IDISPOSABLE
84-
// Debug.Assert (!toplevel.WasDisposed);
84+
//#if DEBUG_IDISPOSABLE
85+
// Debug.Assert (!toplevel.WasDisposed);
8586

86-
// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
87-
// {
88-
// Debug.Assert (_cachedRunStateToplevel.WasDisposed);
89-
// }
90-
//#endif
87+
// if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
88+
// {
89+
// Debug.Assert (_cachedRunStateToplevel.WasDisposed);
90+
// }
91+
//#endif
9192

9293
// Ensure the mouse is ungrabbed.
9394
MouseGrabView = null;
9495

9596
var rs = new RunState (toplevel);
9697

97-
// View implements ISupportInitializeNotification which is derived from ISupportInitialize
98-
if (!toplevel.IsInitialized)
99-
{
100-
toplevel.BeginInit ();
101-
toplevel.EndInit ();
102-
}
103-
10498
#if DEBUG_IDISPOSABLE
10599
if (Top is { } && toplevel != Top && !TopLevels.Contains (Top))
106100
{
@@ -176,16 +170,26 @@ public static RunState Begin (Toplevel toplevel)
176170
Top.HasFocus = false;
177171
}
178172

173+
// Force leave events for any entered views in the old Top
174+
if (GetLastMousePosition () is { })
175+
{
176+
RaiseMouseEnterLeaveEvents (GetLastMousePosition ()!.Value, new List<View?> ());
177+
}
178+
179179
Top?.OnDeactivate (toplevel);
180-
Toplevel previousCurrent = Top!;
180+
Toplevel previousTop = Top!;
181181

182182
Top = toplevel;
183-
Top.OnActivate (previousCurrent);
183+
Top.OnActivate (previousTop);
184184
}
185185
}
186186

187-
toplevel.SetRelativeLayout (Driver!.Screen.Size);
188-
toplevel.LayoutSubviews ();
187+
// View implements ISupportInitializeNotification which is derived from ISupportInitialize
188+
if (!toplevel.IsInitialized)
189+
{
190+
toplevel.BeginInit ();
191+
toplevel.EndInit (); // Calls Layout
192+
}
189193

190194
// Try to set initial focus to any TabStop
191195
if (!toplevel.HasFocus)
@@ -195,15 +199,16 @@ public static RunState Begin (Toplevel toplevel)
195199

196200
toplevel.OnLoaded ();
197201

198-
Refresh ();
199-
200202
if (PositionCursor ())
201203
{
202-
Driver.UpdateCursor ();
204+
Driver?.UpdateCursor ();
203205
}
204206

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

209+
// Force an Idle event so that an Iteration (and Refresh) happen.
210+
Application.Invoke (() => { });
211+
207212
return rs;
208213
}
209214

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

230236
if (current != CursorVisibility.Invisible)
231237
{
232-
Driver.SetCursorVisibility (CursorVisibility.Invisible);
238+
Driver?.SetCursorVisibility (CursorVisibility.Invisible);
233239
}
234240

235241
return false;
@@ -326,7 +332,7 @@ internal static bool PositionCursor ()
326332
public static T Run<T> (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null)
327333
where T : Toplevel, new()
328334
{
329-
if (!IsInitialized)
335+
if (!Initialized)
330336
{
331337
// Init() has NOT been called.
332338
InternalInit (driver, null, true);
@@ -381,7 +387,7 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul
381387
{
382388
ArgumentNullException.ThrowIfNull (view);
383389

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

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

489-
/// <summary>Triggers a refresh of the entire display.</summary>
490-
public static void Refresh ()
498+
/// <summary>
499+
/// 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.
500+
/// Only Views that need to be drawn (see <see cref="View.NeedsDraw"/>) will be drawn.
501+
/// </summary>
502+
/// <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>
503+
public static void LayoutAndDraw (bool forceDraw = false)
491504
{
492-
foreach (Toplevel tl in TopLevels.Reverse ())
493-
{
494-
if (tl.LayoutNeeded)
495-
{
496-
tl.LayoutSubviews ();
497-
}
505+
bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size);
498506

499-
tl.Draw ();
507+
if (forceDraw)
508+
{
509+
Driver?.ClearContents ();
500510
}
501511

502-
Driver!.Refresh ();
512+
View.SetClipToScreen ();
513+
View.Draw (TopLevels, neededLayout || forceDraw);
514+
View.SetClipToScreen ();
515+
516+
Driver?.Refresh ();
503517
}
504518

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

537-
RunIteration (ref state, ref firstIteration);
551+
firstIteration = RunIteration (ref state, firstIteration);
538552
}
539553

540554
MainLoop!.Running = false;
541555

542556
// Run one last iteration to consume any outstanding input events from Driver
543557
// This is important for remaining OnKeyUp events.
544-
RunIteration (ref state, ref firstIteration);
558+
RunIteration (ref state, firstIteration);
545559
}
546560

547561
/// <summary>Run one application iteration.</summary>
548562
/// <param name="state">The state returned by <see cref="Begin(Toplevel)"/>.</param>
549563
/// <param name="firstIteration">
550-
/// Set to <see langword="true"/> if this is the first run loop iteration. Upon return, it
551-
/// will be set to <see langword="false"/> if at least one iteration happened.
564+
/// Set to <see langword="true"/> if this is the first run loop iteration.
552565
/// </param>
553-
public static void RunIteration (ref RunState state, ref bool firstIteration)
566+
/// <returns><see langword="false"/> if at least one iteration happened.</returns>
567+
public static bool RunIteration (ref RunState state, bool firstIteration = false)
554568
{
569+
// If the driver has events pending do an iteration of the driver MainLoop
555570
if (MainLoop!.Running && MainLoop.EventsPending ())
556571
{
557572
// Notify Toplevel it's ready
@@ -561,23 +576,25 @@ public static void RunIteration (ref RunState state, ref bool firstIteration)
561576
}
562577

563578
MainLoop.RunIteration ();
579+
564580
Iteration?.Invoke (null, new ());
565581
}
566582

567583
firstIteration = false;
568584

569585
if (Top is null)
570586
{
571-
return;
587+
return firstIteration;
572588
}
573589

574-
Refresh ();
590+
LayoutAndDraw ();
575591

576592
if (PositionCursor ())
577593
{
578594
Driver!.UpdateCursor ();
579595
}
580596

597+
return firstIteration;
581598
}
582599

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

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

673-
Refresh ();
690+
LayoutAndDraw ();
674691
}
675692
}

0 commit comments

Comments
 (0)