Repository: gui-cs/Terminal.Gui

Issue #2491: Get rid of `Toplevel` - Introduce `Runnable` and `Overlapped` instead
Created: 2023-04-04T04:07:47Z
This PR is for a major rehaul of how Terminal.Gui deals with non-Tiled view arrangements and "runnable" views.
@BDisp, @tznind, @dodexahedron, and @migueldeicaza - PLEASE READ AND TURN ON YOUR BRAINSTORM MODE
We have a chance to get the design of how all this works in v2 right. We should approach this throwing away existing notions of how Terminal.Gui has always worked in these regards. This does not mean we WILL throw all those things away, but that we should not constrain our thinking at this point.
I've written the below in the spirit of generating debate and ideas. I'm highly motivated to start working on this as I've been noodling on it for years. But my thinking is still very far from clear.
We will need a new "Multiple overlapped, MDI-like views" example (see #3636).
Related Issues & PRs

#2994
#2537
#3636
#3634
#3627
#3735
#3406

Background
In v2, we have the following layout abilities:

Arrangement - Includes View.Arrangement and code in Border.cs for determining how a view (via Border) can be moved and sized. Right now only ViewArrangement.Fixed and ViewArrangement.Movable have any implementation.
Tiled - By default, there is no 'z-order` to the Subviews of a View. And in mose use-cases, subviews do not overlap with each other (the exception being when it's done intentionally to create some effect). As a result, the default layout for most TUI apps is "tiled".
Modal - A modal view is one that is run as an "application" via Application.Run(Toplevel) where Modal == true. Dialog, Messagebox, and Wizard are the prototypical examples. When run this way, there IS a z-order but it is highly-constrained: the modal view has a z-order of 1 and everything else is at 0.
Runnable - Today, Overlapped and Runnable are intrinsically linked. A runnable view is one where Application.Run(Toplevel) is called.  Each Runnable view where (Modal == false) has it's own RunState and is, effectively, a self-contained "application". Application.Run() non-preemptively dispatches events (screen, keyboard, mouse , Timers, and Idle handlers) to the associated Toplevel. It is possible for such a Toplevel to create a thread and call Application.Run(someotherToplevel) on that thread, enabling pre-emptive user-interface multitasking (BackgroundWorkerCollection does this).
Overlapped - A form of layout where SubViews of a View are visually arranged such that their Frames overlap. In Overlap view arrangements there is a Z-axis (Z-order) in addition to the X and Y dimension. The Z-order indicates which Views are shown above other views. Today, all Overlapped views are Runnable and have Modal = true.  There are a few Scenarios (BackgroundWorkerCollection) that utilize this to provide multiple "windows" that have a z-ordering and are NOT tiled. This is enabled by a bunch of crufty code in Toplevel (mostly in ToplevelOverlapped.cs) and a few other places. While there is a psuedo-z-order provided by Toplevel._toplevels, which is a Stack<Toplevel>, the API is confused and bug-ridden (e.g. not ALL Toplevels are added to this stack, both Toplevel.Current and Toplevel.Top exist...along with whatever _toplevels.TryPeek() returns).

Assertions (these are up for challenge/debate!)

Application should have public Stack<IRunnable> Runnables where user-input events go only to Runnables.TryPeek(), but all get timer, idle, and screen events. This is effectively how the Win32 non-pre-emptive multitasking system works.
There's no need for Overlapped views to need to be Runnable (just as in Win32, any Window could either tile or be overlapped).
Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z), and exclusive key/mouse input.
If a developer has a need for pre-emptive UI multi-tasking, they still can spin up a thread and call Application.Run(someView). someView can be Overlapped or tiled. There's really no reason subviewA.X = Pos.Bottom(someView) or someView.Width = Dim.Width (subviewA) can't work.
Runnable should be via an interface IRunnable
Overlapped uses ViewArrangement - We should add ViewArrangement.Overlapped. This is already done in #3627.
Overlapped views do not need a "close" command. Visible = false is sufficient as a default behavior for a user action that "closes" an Overlapped view (e.g.  the equivalent of Alt-F4 for Windows or the (coming soon) close button in Border). If an app really wants an Overlapped to be Disposed it can subscribe to VisibleChanging/VisibleChanged events (TBI).
Related to the above point, a Modal does need a "close" command. This is already provided by Command.QuitToplevel, which should be renamed Command.QuitRunnable.
Key Bindings current in TopLevel for subview should be Application scoped. This is already done in #3627.

Keyboard Nav
See https://github.com/tig/Terminal.Gui/blob/8e70e2ae8faafab7cb8305ec6dbbd552c7bf3a43/docfx/docs/navigation.md
Requirements

 Enable Runnable as 1st class concept, not tightly coupled with Overlapped, with a well-designed, clean, API and implementation.
 Enable Overlapped as 1st class concept, not tightly coupled with Runnable,  with a well-designed, clean, API and implementation.
 Any View should be able to be used with Application.Run'd without it having to be a subclass of Toplevel
 MenuBar and StatusBar should not be coupled with Toplevel, which prevents non-Toplevel-dervived views from hosting them. See #2488
 Make it possible to move and resize any View with the mouse and keyboard. Partially addressed in ##2488. Full Issue: #2537
 We have many requests to support non-full-screen apps. See #272

Justification / Issues with current systems (very old text; ignore if you want)
Overlapped is partially supported in v1 using Toplevel views. However, the v1 design is lacking:

MenuBar and StatusBar are tied to Toplevel, preventing non-Toplevel views from hosting them.
Only TopLevel views can be run via Application.Run<view> (need a separate RunState). It is not clear why ANY VIEW can't be run this way, but it seems to be a limitation of the current implementation.
Views that can be used as a pop-up (modal) (e.g. Dialog). As proven by Wizard, it is possible to build a View that works well both ways. But it's way too hard to do this today.
Views that can be moved by the user must inherit from Window today. It should be possible to enable the moving and resizing of any View (e.g. View.CanMove = true).
The MdiContainer stuff is complex, perhaps overly so. It's also misnamed because Terminal.Gui doesn't support "documents" nor does it have a full "MDI" system like Windows (used to). It seems to represent features useful in overlapping Views, but it is super confusing on how this works, and the naming doesn't help. This all can be refactored to support specific scenarios and thus be simplified.
There is no facility for users' resizing of Views. @tznind's awesome work on LineCanvas and TileView combined with @tig's experiments show it could be done in a great way for both modal (overlapping) and tiled Views.
We have many requests to support non-full-screen apps. We need to ensure the View class hierarchy supports this in a simple, understandable way. In a world with non-full-screen (where screen is defined as the visible terminal view) apps, the idea that Frame is "screen relative" is broken. Although we COULD just define "screen" as "the area that bounds the Terminal.GUI app."
The MdiContainer/Overlapped stuff is complex, perhaps overly so. It's also misnamed as Terminal.Gui doesn't support "documents" nor does it have a full "MDI" system like Windows (used to). It seems to represent features useful in overlapping Views, but it is super confusing on how this works, and the naming doesn't help. This all can be refactored to support specific scenarios and thus be simplified.

Duplicate issue #1906: Enhance `Modal = false` for all `Toplevel` views (pulling from Wizard)
Created: 2022-07-27T14:28:49Z
This issue is to track the fact that Wizard, which is derived from Dialog, implements some slick behavior that enables it to be used as a non-Toplevel (when Modal = false). The fix is to move the logic in Wizard that deals with Modal  == false into TopLevel.
See also #124 and #1849.
From @GaikwadPratik in #1849:

@tig,
One more question on this, just for my understanding. Why can't Wizard control(I have not used Dialog but I am sure it would be same) be like other controls which can be used in any other views like Frame or Window? Why does it have to be either TopLevel(modal) or can be added only in Top view?
P.S. This question is to just get an understanding.

Ideally, there'd be no difference. However, for some reason way back when Miguel first wrote the code, the behavior of Views derived from Toplevel was fairly hard-coded to assume they were created by calling Application.Run(topLevel) vs. being run as a sub-view of another Toplevel.
These behaviors include:

How the border and frame are rendered
The colors used
The fact that Toplevel has Ready and Loaded events and View doesn't

What I did in creating Wizard (after you convinced me it should work embedded as well) was to smooth over these behaviors, leveraging the fact that Toplevel had a Modal property. Originally Modal was intended to ONLY change the behavior of keyboard events.
In a future PR, I plan on moving the logic I put in Wizard down the hierarchy, preferably all the way to Toplevel. This will enable Window and Dialog to also work either way.
However, there's a risk of breaking changes by doing this, so I choose not to do it just to get Wizard done.

Closing comment (2024-01-19T16:19:18Z): Dupe of

#2491