Skip to content

Master Issue: Get rid of Toplevel - Introduce Runnable and Overlapped instead #2491

@tig

Description

@tig

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

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 Partially Adresses #2491. Refactors how Focus works #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 Partially Adresses #2491. Refactors how Focus works #3627.

Keyboard Nav

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

Requirements

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

✅ Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions