19 KiB
V2 Spec for View Refactor - WORK IN PROGRESS
IMPORTANT: I am critical of the existing codebase below. Do not take any of this personally. It is about the code, not the amazing people who wrote the code.
ALSO IMPORTANT: I've written this to encourage and drive DEBATE. My style is to "Have strong opinions, weakly held." If you read something here you don't understand or don't agree with, SAY SO. Tell me why. Take a stand.
This covers my thinking on how we will refactor View and the classes in the View hierarchy (including Responder). It does not cover Text formatting which will be covered in another spec.
- TrueColor support will be covered separately.
- ConsoleDriver refactor.
Goals
- Refactor View to have "real" Bounds where the Location part can be non-zero
- Enable a real "margin", "border", and "padding" thickness can be implemented that matches how these concepts work in HTML
- Leverage LineCanvas to draw borders and auto-join borders. Remove the need for
TileVeiwandSplitViewclasses. - Reduce 20/30% of the existing View, Toplevel, Window, and FrameView can code.
- Make porting apps to use the new architecture relatively easy, but result in less code in apps.
- Make it easier to add new Views and View-like classes.
Terminal.Gui v2 View-related Lexicon & Taxonomy
-
Responder - A class that can handle user input. Implemented in the
Responderbase class.- In v2 we will move more mouse/keyboard base-logic out of
ViewandWindowand intoResponder.
- In v2 we will move more mouse/keyboard base-logic out of
-
View - A base class for implementing higher-level visual/interactive Terminal.Gui elements. Implemented in the
Viewbase class, which is aResponderand hosts severalFrames.- In v2 we will move all logic for rendering out of
Toplevel,FrameView, andWindowintoView.
- In v2 we will move all logic for rendering out of
-
SubView - A View that is contained in another view and will be rendered as part of the containing view's ContentArea. SubViews are added to another view via the
View.Addmethod. A View may only be a SubView of a single View. -
SuperView - The View that is a container for SubViews. Referring to the View another View was added to as SubView.
-
Child View - A view that is held by another view in a parent/child relationship, but is NOT a SubView. Examples of this are the submenus of
MenuBar. -
Parent View - A view that holds a reference to another view in a parent/child relationship, but is NOT a SuperView of the child.
-
Thickness - A class describing a rectangle where each of the four sides can have a width. Valid width values are >= 0. The inner area of a Thickness is the sum of the widths of the four sides minus the size of the rectangle. The
Thicknessclass has aDrawmethod that clears the rectangle. -
Frame Class - A
Frameis a special form ofViewthat appears outside of a normalView's content area. Examples ofFrames areMargin,Border, andPadding. TheFrameclass is derived fromViewand uses aThicknessto hold the rectangle. -
Frame - The
Rectthat defines the location and size of theViewincluding all of the margin, border, adornments, padding, and content area. The coordinates are relative to the SuperView of the View (or, in the case ofApplication.Top,ConsoleDriver.Row == 0; ConsoleDriver.Col == 0). The Frame's location and size are controlled by the.X,.Y,.Height, and.Widthproperties of the View.- In v2,
View.Frame.Sizeis the size of theView'sContentAreaplus theThicknessof theView'sMargin,Border, andPadding.
- In v2,
-
Margin - The
Framethat separates a View from other SubViews of the same SuperView. The Margin is not part of the View's content and is not clipped by the View'sClipArea. By defaultMarginis{0,0,0,0}.Margincan be used instead of (or with)Dim.Posto position a View relative to another View. Eg.view.X = Pos.Right (otherView) + 1; view.Y = Pos.Bottom (otherView) + 1;is equivalent to
otherView.Margin.Thickness = new Thickness (0, 0, 1, 1); view.X = Pos.Right (otherView); view.Y = Pos.Bottom (otherView);- QUESTION: Will it be possible to have a negative Margin? If so, will that allow us to have "magic borderframe connections" as I've demonstrated in my TileViewExperiment? Or, should the magic happen when a View's dimensions overlap with another, independent of the Margin?
-
Title - Text that is displayed for the View that describes the View to users. Typically the Title is displayed at the top-left, overlaying the Border. The title is not part of the View's content and is not clipped by the View's
ClipArea. -
Text - Text that is rendered by the view within the view's content area, using
TextFormatter.Textis part of the View's content and is clipped by the View'sClipArea. -
Border (currently
BorderFrameuntil the oldBordercan be removed) - TheFramewhere a visual border (drawn using line-drawing glyphs) and the Title are drawn. The Border expands inward; in other words ifBorder.Thickness.Top == 2the border & title will take up the first row and the second row will be filled with spaces. The Border is not part of the View's content and is not clipped by the View'sClipArea. -
Adornments (NOT IMPLEMENTED YET; May replace
BorderFrame)- TheFramebetween theBorderandPadding. Adornments are not part of the View's content and are not clipped by the View'sClipArea. Examples of Adornments:- A
TitleBarrenders the View'sTitleand a horizontal line defining the top of the View. Adds thickness to the top of Adornments. - One or more
LineViews that render the View's border (NOTE: The magic ofLineCanvaslets us automatically have the right joins for these andTitleBar!). - A
Vertical Scrollbaradds thickness toAdornments.Right(or.Leftwhen right-to-left language support is added). - A
Horizontal Scrollbaradds thickness toAdornments.Bottomwhen enabled. - A
MenuBaradds thickness toAdornments.Top(NOTE: This is a change from v1 wheresubview.Y = 1is required). - A
StatusBaradds thickness otAdornments.Bottomand is rendered at the bottom ofPadding. - NOTE: The use of
View.Addin v1 to add adornments to Views is the cause of much code complexity. Changing the API such thatView.Addis ONLY for subviews and adding aView.Adornments.AddAPI for menu, StatusBar, scroll bar... will enable us to significantly simplify the codebase.
- A
-
Padding - The
Frameinside of an element that offsets theContentfrom the Border. (NOTE: in v1Paddingis OUTSIDE of theBorder). Padding is{0, 0, 0, 0}by default. Padding is not part of the View's content and is not clipped by the View'sClipArea. -
VisibleArea - (NOT IMPLEMENTED YET) Means the area inside of the Margin + Border (Title) + Padding.
VisibleArea.Locationis always{0, 0}.VisibleArea.Sizeis theView.Frame.Sizeshrunk by Margin + Border + Padding. -
ContentArea - (NOT IMPLEMENTED YET; currently
Bounds) TheRectthat describes the location and size of the View's content, relative toVisibleArea. IfContentArea.Locationis negative, anything drawn there will be clipped and any subview positioned in the negative area will cause (optional) scrollbars to appear (making the Thickness of Padding thicker on the appropriate sides). IfContentArea.Sizeis changed such that the dimensions fall outside ofFrame.Size shrunk by Margin + Border +Padding`, drawing will be clipped and (optional) scrollbars will appear.- QUESTION: Can we just have one
ContentAreaproperty that is theRectthat describes the location and size of the View's content, relative toFrame? If so, we can removeVisibleAreaandBoundsand just haveContentAreaandFrame? The key to answering this is all wrapped up in scrolling and clipping.
- QUESTION: Can we just have one
-
Bounds - Synomous with VisibleArea. (Debate: Do we rename
BoundstoVisbleAreain v2?) -
ClipArea - The currently visible portion of the Content. This is defined as a
Rectin coordinates relative to ContentArea (NOT VisibleArea) (e.g.ClipArea {X = 0, Y = 0} == ContentArea {X = 0, Y = 0}). In v2 we will NOT pass thisRectis passedView.Redrawand instead just haveRedrawuseBounds.- QUESTION: Do we need
ClipAreaat all? Can we just haveRedrawuseBounds?
- QUESTION: Do we need
-
Modal - Modal - The term used when describing a
Viewthat was created using theApplication.Run(view)orApplication.Run<T>APIs. When a View is running as a modal, user input is restricted to just that View untilApplication.Runexits. AModalView has its ownRunState.- In v1, classes derived from
Dialogwere originally thought to only work modally. However,Wizardproved that aDialog-based class can also work non-modally. - In v2, we will simplify the
Dialogclass, and let any class be run viaApplicaiton.Run. TheModalproperty will be set byApplication.Runso the class can detect it is running modally if it needs to.
- In v1, classes derived from
-
TopLevel - The v1 term used to describe a view that can have a MenuBar and/or StatusBar. In v2, we will delete the
TopLevelclass and ensure ANY View can have a menu bar and/or status bar (viaAdornments).- NOTE: There will still be an
Application.Topwhich is theViewthat is the root of theApplication's view hierarchy.
- NOTE: There will still be an
-
Window - A View that, by default, has a
Borderand aTitle.- QUESTION: Why can't this just be a property on
View(e.g.View.Border = true)? Why do we need aWindowclass at all in v2?
- QUESTION: Why can't this just be a property on
-
Tile, Tiled, Tiling (NOT IMPLEMENTED YET) - Refer to a form of
ComputedLayoutwhere SubViews of aVieware visually arranged such that they abut each other and do not overlap. In a Tiled view arrangement, Z-ordering only comes into play when a developer intentionally causes views to be aligned such that they overlap. Borders that are drawn between the SubViews can optionally support resizing the SubViews (negating the need forTileView). -
Overlap, Overlapped, Overlapping (NOT IMPLEMENTED YET) - Refers to a form of
ComputedLayoutwhere 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.
Focus
- Focus is a concept that is used to describe which Responder is currently receiving user input.
- QUESTION: Since
Frames areViewsin v2, theFrameis aResponderthat receives user input. This raises the question of how a user can use the keyboard to navigate betweenFrames andViews within aFrame(and theFrame'sParent's subviews).
View classes to be nuked
- PanelView (done)
- FrameView (almost done)
- TileVeiw
- TopLevel?
- Window?
LineViewcan be reimplemented usingLineCanvas?ButtonandLabelcan be merged.StatusBarandMenuBarcould be combined. If not, then at least made consistent (e.g. in how hotkeys are specified).ComboBoxcan be replaced byMenuBar
What's wrong with the View and the View-class hierarchy in v1?
-
Frame,Bounds, andClipRectare confusing and not consistently applied...BoundsisRectbut is used to describe aSize(e.g.Bounds.Sizeis the size of theView's content area). It literally is implemented as a property that returnsnew Rect(0, 0, Width, Height). Throughtout the codebaseboundsis used for things that have non-zeroSize(and actually descibe either the cliprect or the Frame).- The restrictive nature of how
Boundsis defined led to the hackyFrameViewandWindowclasses with an embeddedContentViewin order to draw a border around the content.- The only reason FrameView exists is because the original architecture didn't support offsetting
View.Boundssuch that a border could be drawn and the interior content would clip correctly. Thus Miguel (or someone) built FrameView with nestedContentViewthat was atnew Rect(+1, +1, -2, -2). Borderwas added later, but couldn't be retrofitted intoViewsuch that ifView.Border ~= nulljust worked likeFrameView.- Thus devs are forced to use the clunky
FrameViewinstead of just settingView.Border.
- The only reason FrameView exists is because the original architecture didn't support offsetting
Borderhas a bunch of confusing concepts that don't match other systems (esp the Web/HTML)Marginon the web means the space between elements -Borderdoesn't have a margin property, but does has the confusingDrawMarginFrameproperty.Borderon the web means the space where a border is drawn. The current implementaiton confuses the termFrameandBorder.BorderThicknessis provided.Paddingon the web means the padding inside of an element between theBorderandContent. In the current implementationPaddingis actually OUTSIDE of theBorder. This means it's not possible for a view to offset internally by simply changingBounds.Contenton the web means the area inside of the Margin + Border + Padding.Viewdoes not currently have a concept of this (butFrameViewandWindowdo via the embeddedContentViews.Borderhas aTitleproperty. So doesWindowandFrameView. This is duplicate code.- It is not possible for a class derived from View to override the drawing of the "Border" (frame, title, padding, etc...). Multiple devs have asked to be able to have the border frame to be drawn with a different color than
View.ColorScheme. The API should explicitly enable devs to override the drawing ofBorderindependently of theView.Drawmethod. See howWM_NCDRAWworks in Windows (Draw non-client). It should be easy to do this from within aViewsub-class (e.g. overrideOnDrawBorder) and externally (e.g.DrawBorder += () => ....
-
AutoSizemostly works, but only because of heroic special-casing logic all over the place by @bdisp. This should be massively simplified.FrameViewis superfluous and should be removed from the hierarchy (instead devs should just be able to manipulateView.Border(or similar) to achieve whatFrameViewprovides). The internalFrameView.ContentViewis a bug-farm and un-needed ifView.Borderworked correctly. -
TopLevelis currently built around several concepts that are muddled:- Views that host a Menu and StatusBar. It is not clear why this is and if it's needed as a concept.
- Views that can be run via
Application.Run<TopLevel>(need a separateRunState). 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 byWizard, 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
Windowtoday. It should be possilbe to enable moving of any View (e.g.View.CanMove = true).
-
The
MdiContainerstuff is complex, perhaps overly so, and is not actually used by anyone outside of the project. It's also mis-named because Terminal.Gui doesn't actually support "documents" nor does it have a full "MDI" system like Windows (did). 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
LineCanvasandTileViewcombined with @tig's experiments show it could be done in a great way for both modal (overlapping) and tiled Views. -
DrawFrameandDrawTitleare implemented inConsoleDriverand can be replaced by a combination ofLineCanvasandBorder. -
Colors -
- As noted above each of Margin, Border, Padding, and Content should support independent colors.
- Many View sub-classes bastardize the existing ColorSchemes to get look/feel that works (e.g.
TextViewandWizard). Separately we should revamp ColorSchemes to enable more scenarios. - TrueColor support is needed and should be the default.
-
Responderis supposed to be where all common, non-visual-related, code goes. We should ensure this is the case. -
Viewshould have default support for scroll bars. e.g. assume in the new worldView.ContentBoundsis the clip area (defined byVIew.FrameminusMargin+Border+Padding) then if any view is added withView.Addthat has Frame coordinates outside ofContentBoundsthe appropriate scroll bars show up automatgically (optionally of course). Without any code, scrolling just works. -
We have many requests to support non-full-screen apps. We need to ensure the
Viewclass 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 thatFrameis "screen relative" is broken. Although we COULD just define "screen" as "the area that bounds the Terminal.GUI app.".
Design
Responder("Responder base class implemented by objects that want to participate on keyboard and mouse input.") remains mostly unchanged, with minor changes:- Methods that take
Viewparameters (e.g.OnEnter) change to takeResponder(bad OO design). - Nuke
IsOverriden(bad OO design) - Move
View.DatatoResponder(primitive) - Move
CommandandKeyBindingstuff fromView. - Move the generic mouse and keyboard stuff from
View(e.g.WantMousePositionReports)
- Methods that take
Example of creating Adornments
// ends up looking just like the v1 default Window with a menu & status bar
// and a vertical scrollbar. In v2 the Window class would do all of this automatically.
var top = new TitleBar() {
X = 0, Y = 0,
Width = Dim.Fill(),
Height = 1
LineStyle = LineStyle.Single
};
var left = new LineView() {
X = 0, Y = 0,
Width = 1,
Height = Dim.Fill(),
LineStyle = LineStyle.Single
};
var right = new LineView() {
X = Pos.AnchorEnd(), Y = 0,
Width = 1,
Height = Dim.Fill(),
LineStyle = LineStyle.Single
};
var bottom = new LineView() {
X = 0, Y = Pos.AnchorEnd(),
Width = Dim.Fill(),
Height = 1,
LineStyle = LineStyle.Single
};
var menu = new MenuBar() {
X = Pos.Right(left), Y = Pos.Bottom(top)
};
var status = new StatusBar () {
X = Pos.Right(left), Y = Pos.Top(bottom)
};
var vscroll = new ScrollBarView () {
X = Pos.Left(right),
Y = Dim.Fill(2) // for menu & status bar
};
Adornments.Add(titleBar);
Adornments.Add(left);
Adornments.Add(right);
Adornments.Add(bottom);
Adornments.Add(vscroll);
var treeView = new TreeView () {
X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill()
};
Add (treeView);