Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3761, #2886, #3780, #3485, #3622, #3413, #2995 - Draw and Layout performance/correctness #3798

Merged
merged 126 commits into from
Nov 10, 2024

Conversation

tig
Copy link
Collaborator

@tig tig commented Oct 15, 2024

Fixes

Related

Proposed Changes/Todos

  • Decouple layout from draw
  • Refactor View.LayoutSubviews etc... to make more testable and clearer to use. Introduce View.Layout() as the primary API that builds on View.SetRelativeLayout and View.LayoutSubviews. Move from depending on IsInitialized to NeedsLayout as the signal that layout has been performed.
  • Refactor View.Draw to be more granular and use proper event patterns.
  • Minimize when Draw/OnDrawContent are called
  • Enforce that Clear rarely happens
  • Add View.SetAttribute; make Driver.SetAttribute internal
  • Add non-rectangular clip regions - Improve redraw performance by making Clip be a Region instead of Rectangle #3413
  • Refine non-rectangular clip regions - some edge cases not working like shadowview
  • Undo "fix" to Enabled to enable disabled views to stay disabled even if superview changes; use an event instead of hacky logic.
  • Remove all refs to Driver.Move/AddStr
  • Ensure drivers minimize full repaints
  • Test other drivers
  • Fix LineCanvas and auto-join of lines/Border

Known Issues that will be fixed later

Pull Request checklist:

  • I've named my PR in the form of "Fixes #issue. Terse description."
  • My code follows the style guidelines of Terminal.Gui - if you use Visual Studio, hit CTRL-K-D to automatically reformat your files before committing.
  • My code follows the Terminal.Gui library design guidelines
  • I ran dotnet test before commit
  • I have made corresponding changes to the API documentation (using /// style comments)
  • My changes generate no new warnings
  • I have checked my code and corrected any poor grammar or misspellings
  • I conducted basic QA to assure all features are working

@tig
Copy link
Collaborator Author

tig commented Oct 18, 2024

@BDisp, @tznind, @dodexahedron or anyone else who'd like to comment...

As I've worked this PR, I've learned a bunch of things, and gotten clarity on other issues that have bugged me for a long time.

Before I go further, I want to share my thoughts in writing. This will solidify my thinking and enable y'all to challenge and debate it.

Some of the changes I've made, and want to continue to make are significant. While they don't fundamentally change the TG programming model, they do require changes to how devs will develop against it. Some will make porting from v1 more challenging.

Layout vs Draw

First, TG has always been confused about the relationship between Layout and Draw. This confusion got worse and worse over time. The addition of View.AutoSize made it even worse. As did more sophisticated use-cases including overlapped Views, multi-threading, and Adornments. The state of v2_develop is horrific in this regard. Hence #3761 etc... that lead to this PR.

The codebase is full of convoluted code that attempts to ensure things get laid out then drawn. Property _set methods call a combination of SetNeedsDisplay, SetLayoutNeeded, and LayoutSubviews. The default OnDrawContent forces layout because it can't assume layout has already happened. The mainloop uses brute-force-overkill to force layouts and draws and ends up calling Consoledriver.Refresh way too frequently. Plus View.Clear is called constantly. Some Views (TabView) re-layout elements DURING draw!

We have to fix this at a fundamental level or the library will continue to get bogged down.

Assertions:

  • Layout and Draw must be decoupled - Layout code should not call draw code. Draw code should not call layout code.
  • Layout happens first; then Draw - This sounds obvious, but there are multiple places where the library or built-in Views either blindly do the opposite or attempt to do layout mid-draw.

Asynchronous Layout vs Explicit/Synchronous

Way back, the only place that called SetRelativeLayout and LayoutSubviews was the mainloop. MG's design for gui-cswas these methods were to be called each mainloop iteration. They were internal for this reason. Overtime, though, folks started using them more and more from within View implementations or app code.

The names of these methods leads to confusion as well.

Assertion:

  • Layout is Asynchronous - In the vast majority of cases developers should not call SetRelativeLayout or LayoutSuviews directly, but should just signal layout is needed with SetLayoutNeeded(). We will introduce a new API (which is really just a refactor of the currently private LayoutSubview(): public bool Layout (Size contentSize) that first calls SetRelativeLayout and then LayoutSubviews. Devs can use this in the rare cases where an immediate layout is required.

Great, but what about cases where a dev DOES want immediate layout? Should it be implicit (as it is today) or explicit?

Option 1

  • Frame, X, Y, Width, and Height are non-deterministic if IsLayoutNeeded is true - This is a BIG one. It means this code will fail on the 2nd assert:
  View v = new ();
  Assert.Equal (0, v.Frame.Height);
  v.Height = 1;
  Assert.Equal (1, v.Frame.Height);

This will not fail:

  View v = new ();
  Assert.Equal (0, v.Frame.Height);
  v.Height = 1;
  Assert.Equal (0, v.Frame.Height);
  v.Layout ();  
  Assert.Equal (1, v.Frame.Height);

Option 2

  • Frame, X, Y, Width, and Height are non-deterministic if IsLayoutNeeded is true
  • Setting X, Y, Width, and Height to a Pos/DimAbsolute will call Layout() (causing IsLayoutNeeded to be false) IFF the other 3 are also Pos/DimAbsolute.
  • Setting Frame, will cause will call Layout()(causingIsLayoutNeededto befalse`).

The downside of this is that devs may be confused:

  View v = new ();
  Assert.Equal (0, v.Frame.Height);
  v.Height = 1;
  Assert.False (v.IsLayoutNeeded());
  Assert.Equal (1, v.Frame.Height); // works because `1` is `Dim.Absoulte(1)`

  v.Width = Dim.Percent(50);
  Assert.True (v.IsLayoutNeeded());
  Assert.Equal (0, v.Frame.Width); // still 0

The reality is the above code is really only valid in UNIT TESTS. No dev will ever do this IRL.

IOW, if we don't call SetLayout in the Pos/DimAbsoulte cases, all unit tests will need to explicitly call Layout which is a PITA.

In this PR I started with Option 1. It felt pure. But it means I have to touch 100s of Unit tests that simply are using absolute values and add calls to v.Layout(). So now I'm going with Option 2.

.... More later. This is enough for now...

@BDisp
Copy link
Collaborator

BDisp commented Oct 18, 2024

As I remember the layout was always done before drawn. When the drawn was called alone and IsLayoutNeeded() == true the layout was being performed first. I also agree with the option 2. The #3761 really needs to be fixed. Good work.

@tznind
Copy link
Collaborator

tznind commented Oct 19, 2024 via email

@tig
Copy link
Collaborator Author

tig commented Nov 7, 2024

Huge progress on auto-join borders and lines. I've been upgrading AllViewsTester as a hard-core test case...

oB2IpR5 1

Things that are not quite right. I plan on fixing these in later PRs.

  • Titles get messed up - This will be fixed once Border starts actually using subviews for the border lines and title.
  • I have a hack in place right now that causes all views in a view heirarchy that have SuperViewRendersLineCanvas set to redraw anytime any of them changes. This negates the perf work I've done when SuperViewRendersLineCanvas is enabled.

@tig tig mentioned this pull request Nov 7, 2024
@tig tig changed the title Fixes #3761, #2886 - Draw and Layout performance issues Fixes #3761, #2886, #3780, #3485, #3622, #3413, #2995 - Draw and Layout performance/correctness Nov 7, 2024
@tig tig requested a review from BDisp November 7, 2024 23:17
@tig
Copy link
Collaborator Author

tig commented Nov 7, 2024

@tznind, @BDisp, @dodexahedron - This is as ready as it's gonna get. Please read the first post to see all that I've done.

I'd like to get this merged tomorrow.

Copy link
Collaborator

@BDisp BDisp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only remining the TabView but I think it will be fixed with the #3808.

Please can you change the file TabMouseEventArgs.cs as below? Thanks!

using System.ComponentModel;

namespace Terminal.Gui;

/// <summary>Describes a mouse event over a specific <see cref="Tab"/> in a <see cref="TabView"/>.</summary>
public class TabMouseEventArgs : HandledEventArgs
{
    /// <summary>Creates a new instance of the <see cref="TabMouseEventArgs"/> class.</summary>
    /// <param name="tab"><see cref="Tab"/> that the mouse was over when the event occurred.</param>
    /// <param name="mouseEvent">The mouse activity being reported</param>
    public TabMouseEventArgs (Tab tab, MouseEventArgs mouseEvent)
    {
        Tab = tab;
        MouseEvent = mouseEvent;
    }

    /// <summary>
    ///     Gets the actual mouse event.  Use <see cref="HandledEventArgs.Handled"/> to cancel this event and perform custom
    ///     behavior (e.g. show a context menu).
    /// </summary>
    public MouseEventArgs MouseEvent { get; }

    /// <summary>Gets the <see cref="Tab"/> (if any) that the mouse was over when the <see cref="MouseEvent"/> occurred.</summary>
    /// <remarks>This will be null if the click is after last tab or before first.</remarks>
    public Tab Tab { get; }
}

Terminal.Gui/Views/TileView.cs Outdated Show resolved Hide resolved
Terminal.Gui/Views/TileView.cs Outdated Show resolved Hide resolved
@tig tig merged commit dbedeb6 into gui-cs:v2_develop Nov 10, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants