[!IMPORTANT] This document is a work in progress and does not represent the final design or even the current implementation.
See end for list of issues this design addresses.
More GUI than Command Line. The concept of a cursor on the command line of a terminal is intrinsically tied to enabling the user to know where keyboard import is going to impact text editing. TUI apps have many more modalities than text editing where the keyboard is used (e.g. scrolling through a ColorPicker). Terminal.Gui's cursor system is biased towards the broader TUI experiences.
Be Consistent With the User's Platform - Users get to choose the platform they run Terminal.Gui apps on and the cursor should behave in a way consistent with the terminal.
view.HasFocus == true), and there is only one View in a focused hierarchy that is the most-focused; the one receiving keyboard input. See Navigation for a deep-dive.ListView the Cursor and Selection (SelectedItem) are the same, but the Cursor is not visible. In a TextView with text selected, the Cursor is at either the start or end of the Selection. A `TableView' supports mutliple things being selected at once.OutputBuffer.Col and OutputBuffer.Row that indicates where the next AddRune() or AddStr() call will write. This is NOT the same as the visible terminal cursor and should never be used for cursor positioning.Application.CursorStyle.this.CursorPosition.this.CursorPostion to null.Enabled == trueVisible == trueCanFocus == truethis == SuperView.MostFocusedConsoleDriver supports Cursor Styles other than Default, they should be supported per-application (NOT View).Application, not View.Driver. API. Only Application and the View base class should call ConsoleDriver APIs; before we ship v2, all ConsoleDriver APIs will be made internal.View Focus ChangesIt doesn't make sense the every View instance has it's own notion of MostFocused. The current implemention is overly complicated and fragile because the concept of "MostFocused" is handled by View. There can be only ONE "most focused" view in an application. MostFocused should be a property on Application.
View.MostFocusedApplication.MostFocusedView (see Application below)view._hasFocus = and change them to use SetHasFocus (today, anyplace that sets _hasFocus is a BUG!!).SetFocus/SetHasFocus etc... such that if the focus is changed to a different view heirarchy, Application.MostFocusedView gets set appropriately.MORE THOUGHT REQUIRED HERE - There be dragons given how Runnable has OnEnter/OnLeave overrrides. The above needs more study, but is directioally correct.
View Cursor Changespublic Point? CursorPosition
private Point? _cursorPosition!HasValue the cursor is not visibleHasValue the cursor is visible at the Point.value != _cursorPosition, call OnCursorPositionChanged()public event EventHandler<LocaitonChangedEventArgs>? CursorPositionChangedinternal void OnCursorPositionChanged(LocationChangedEventArgs a)
CursorPositionChangedConsoleDriversRemove Refresh and have UpdateScreen and UpdateCursor be called separately. The fact that Refresh in all drivers currently calls both is a source of flicker.
Remove the xxxCursorVisibility APIs and replace with:
internal int CursorStyle {get; internal set; }private int _cursorStyleOnCursorStyleChanged()internal abstract void OnCursorStyleChanged()Called by base whenever the cursor style changes, but ONLY if value != _cursorStyle.
Add internal virtual (int Id, string StyleName) [] GetCursorStyles()
Returns an array of styles supported by the driver, NOT including Invisible.
The first item in array is always "Default".
Base implementation returns { 0, "Default" }
CursesDriver and WindowsDriver will need to implement overrides.
Add internal Point? CursorPosition {get; internal set; }
Backed with private Point? _cursorPosition
If !HasValue the cursor is not visible
If HasValue the cursor is visible at the Point.
On set, calls OnCursorPositionChanged ONLY if value != _cursorPosition.
Add internal abstract void OnCursorPositionChanged()
Called by base whenever the cursor position changes.
Depending on the value of CursorPosition:
!HasValue the cursor is not visible - does whatever is needed to make the cursor invisible.HasValue the cursor is visible at the CursorPosition - does whatever is needed to make the cursor visible (using CursorStyle).Make sure the drivers only make the cursor visible (or leave it visible) when CursorPosition changes!
ApplicationAdd internal static View FocusedView {get; private set;}
private static _focusedViewvalue != _focusedView
_focusedView.CursorPositionChangedvalue.CursorPositionChanged += CursorPositionChanged_focusedView = valueUpdateCursorAdd internal bool CursorPositionChanged (object sender, LocationChangedEventArgs a)
Called when:
FocusedView
FocusedView.Visible/Enable changes)CursorPositionCursorStyle has changedDoes:
FocusedView is {} and FocusedView.CursorPosition is visible (e.g. w/in FocusedView.SuperView.Viewport)
Driver.CursorPosition = ToScreen(FocusedView.CursorPosition)Driver.CursorPosition = nullAdd public static int CursorStyle {get; internal set; }
value != _cursorStyleConsoleDriver.CursorStyle = _cursorStyleUpdateCursorAdd public (int Id, string StyleName) [] GetCursorStyles()
ConsoleDriver.GetCursorStyles()Driver.Row/Col, which are changed via Move serves two purposes that confuse each other:a) Where the next AddRune will put the next rune (the "Draw Cursor")
b) The current "Cursor Location" (the visible terminal cursor)
These are completely separate concepts that were conflated in the original design.
The Draw Cursor (OutputBuffer.Col/OutputBuffer.Row) tracks where drawing operations will write characters. Every call to Move() during view drawing updates these values. By the end of drawing, they point to wherever the last AddRune() or AddStr() call left them - typically the bottom-right of the last drawn element.
The Terminal Cursor is the visible cursor indicator in the terminal that shows the user where their input will go. This should ONLY be positioned based on View.PositionCursor() for the focused view.
The conflation of these two concepts caused the cursor to be positioned at arbitrary "Draw Cursor" locations (wherever drawing happened to finish) instead of where the application actually wanted it. Any code that tried to use Driver.Col/Driver.Row for cursor positioning was fundamentally broken.
In OutputBase.Write(IOutputBuffer): Removed the cursor visibility save/restore pattern that was causing flickering.
Previous (Broken) Code:
CursorVisibility? savedVisibility = _cachedCursorVisibility;
SetCursorVisibility (CursorVisibility.Invisible); // Hide while drawing
// ... draw everything ...
SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); // PROBLEM: Restores stale visibility!
_cachedCursorVisibility = savedVisibility;
The problem: After drawing, cursor visibility was restored to savedVisibility, which was whatever was set previously. This was often wrong:
null from PositionCursor()), it would get shown anywayFixed Code:
// Hide cursor while writing to prevent flickering
// Note: ApplicationMainLoop.SetCursor() is responsible for positioning and
// showing the cursor after drawing is complete
SetCursorVisibility (CursorVisibility.Invisible);
// ... draw everything ...
// DO NOT restore cursor visibility here - let ApplicationMainLoop.SetCursor() handle it
Now OutputBase.Write() only hides the cursor during drawing. The responsibility for showing the cursor at the correct location with the correct visibility is left entirely to ApplicationMainLoop.SetCursor(), which:
View.PositionCursor() on the focused viewThis separation of concerns eliminates the flickering and ensures the cursor is only shown when and where the application actually wants it.
Any future cursor system design MUST maintain this separation:
Move(), AddRune(), AddStr()) should NEVER affect the visible terminal cursorOutputBuffer.Col and OutputBuffer.Row are internal state for drawing and should not be exposed for cursor positioningMainloop.Iteration).Derived from above, the current design means we need to call View.PositionCursor every iteration. For some views this is a low-cost operation. For others it involves a lot of math.
This is just stupid.
Potential optimization: Cache the last cursor position and only call PositionCursor() when:
SetNeedsDraw())Related to the above, we need constantly Show/Hide the cursor every iteration. This causes ridiculous cursor flicker.
FIXED 2025-01-13: The root cause was OutputBase.Write() restoring stale cursor visibility after drawing. See fix details above.
View.PositionCursor is poorly spec'd and confusing to implement correctlyShould a view call base.PositionCursor? If so, before or after doing stuff?
OnEnter actually makes no senseFirst, leaving it up to views to do this is fragile.
Second, when a View gets focus is but one of many places where cursor visibilty should be updated.