Skip to content

Commit

Permalink
Merge pull request #3323 from tig/v2_3269_Bounds-ContentArea
Browse files Browse the repository at this point in the history
Fixes #3169. `Bounds` -> `Viewport`: Content Scrolling in `View`
  • Loading branch information
tig authored Apr 16, 2024
2 parents 980fed4 + 8d2c64a commit 56922b4
Show file tree
Hide file tree
Showing 136 changed files with 5,552 additions and 3,039 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
![Terminal.Gui](https://socialify.git.ci/gui-cs/Terminal.GuiV2Docs/image?description=1&font=Rokkitt&forks=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Fgui-cs%2FTerminal.Gui%2Fdevelop%2Fdocfx%2Fimages%2Flogo.png&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Auto)
![.NET Core](https://github.com/gui-cs/Terminal.GuiV2Docs/workflows/.NET%20Core/badge.svg?branch=develop)
![Code scanning - action](https://github.com/gui-cs/Terminal.GuiV2Docs/workflows/Code%20scanning%20-%20action/badge.svg)
![Terminal.Gui](https://socialify.git.ci/gui-cs/Terminal.Gui/image?description=1&font=Rokkitt&forks=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Fgui-cs%2FTerminal.Gui%2Fdevelop%2Fdocfx%2Fimages%2Flogo.png&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Auto)
![.NET Core](https://github.com/gui-cs/Terminal.Gui/workflows/.NET%20Core/badge.svg?branch=develop)
![Code scanning - action](https://github.com/gui-cs/Terminal.Gui/workflows/Code%20scanning%20-%20action/badge.svg)
[![Version](https://img.shields.io/nuget/v/Terminal.Gui.svg)](https://www.nuget.org/packages/Terminal.Gui)
![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27/raw/code-coverage.json)
[![Downloads](https://img.shields.io/nuget/dt/Terminal.Gui)](https://www.nuget.org/packages/Terminal.Gui)
Expand Down Expand Up @@ -31,8 +31,10 @@ dotnet run

## Documentation

* [Documentation Home](https://gui-cs.github.io/Terminal.GuiV2Docs)
* [Getting Started](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/getting-started.html)
* [What's new in v2](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/newinv2.html)
* [API Documentation](https://gui-cs.github.io/Terminal.GuiV2Docs/api/Terminal.Gui.html)
* [Documentation Home](https://gui-cs.github.io/Terminal.GuiV2Docs)

## Showcase & Examples

Expand Down
2 changes: 1 addition & 1 deletion ReactiveExample/ReactiveExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageReference Include="ReactiveUI" Version="19.5.41" />
<PackageReference Include="ReactiveUI" Version="19.6.1" />
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.3.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
Expand Down
72 changes: 51 additions & 21 deletions Terminal.Gui/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ internal static void ResetState ()
#if DEBUG_IDISPOSABLE

// Don't dispose the toplevels. It's up to caller dispose them
Debug.Assert (t.WasDisposed);
//Debug.Assert (t.WasDisposed);
#endif
}

Expand All @@ -99,6 +99,7 @@ internal static void ResetState ()
// Don't dispose the Top. It's up to caller dispose it
if (Top is { })
{

Debug.Assert (Top.WasDisposed);

// If End wasn't called _cachedRunStateToplevel may be null
Expand Down Expand Up @@ -132,7 +133,7 @@ internal static void ResetState ()

// Don't reset ForceDriver; it needs to be set before Init is called.
//ForceDriver = string.Empty;
Force16Colors = false;
//Force16Colors = false;
_forceFakeConsole = false;

// Run State stuff
Expand Down Expand Up @@ -525,7 +526,10 @@ public static RunState Begin (Toplevel toplevel)
MoveCurrent (Current);
}

toplevel.SetRelativeLayout (Driver.Bounds);
//if (Toplevel.LayoutStyle == LayoutStyle.Computed) {
toplevel.SetRelativeLayout (Driver.Screen.Size);

//}

// BUGBUG: This call is likely not needed.
toplevel.LayoutSubviews ();
Expand Down Expand Up @@ -638,7 +642,7 @@ public static T Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver
public static void Run (Toplevel view, Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null)
{
ArgumentNullException.ThrowIfNull (view);

if (_initialized)
{
if (Driver is null)
Expand Down Expand Up @@ -878,7 +882,7 @@ public static void RunIteration (ref RunState state, ref bool firstIteration)
}
else
{
Driver.UpdateCursor ();
//Driver.UpdateCursor ();
}

if (state.Toplevel != Top && !state.Toplevel.Modal && (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded))
Expand Down Expand Up @@ -1307,7 +1311,7 @@ public static bool OnSizeChanging (SizeChangedEventArgs args)

foreach (Toplevel t in _topLevels)
{
t.SetRelativeLayout (Rectangle.Empty with { Size = args.Size });
t.SetRelativeLayout (args.Size);
t.LayoutSubviews ();
t.PositionToplevels ();
t.OnSizeChanging (new (args.Size));
Expand Down Expand Up @@ -1437,23 +1441,22 @@ private static void OnUnGrabbedMouse (View view)
/// <remarks>
/// <para>
/// Use this event to receive mouse events in screen coordinates. Use <see cref="MouseEvent"/> to
/// receive mouse events relative to a <see cref="View"/>'s bounds.
/// receive mouse events relative to a <see cref="View.Viewport"/>.
/// </para>
/// <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
/// </remarks>
public static event EventHandler<MouseEvent> MouseEvent;
public static event EventHandler<MouseEvent>? MouseEvent;

/// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
/// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
/// <param name="a">The mouse event with coordinates relative to the screen.</param>
/// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
internal static void OnMouseEvent (MouseEvent mouseEvent)
{
if (IsMouseDisabled)
{
return;
}

// TODO: In PR #3273, FindDeepestView will return adornments. Update logic below to fix adornment mouse handling
var view = View.FindDeepestView (Current, mouseEvent.X, mouseEvent.Y);

if (view is { })
Expand All @@ -1472,18 +1475,18 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
{
// If the mouse is grabbed, send the event to the view that grabbed it.
// The coordinates are relative to the Bounds of the view that grabbed the mouse.
Point boundsLoc = MouseGrabView.ScreenToBounds (mouseEvent.X, mouseEvent.Y);
Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.X, mouseEvent.Y);

var viewRelativeMouseEvent = new MouseEvent
{
X = boundsLoc.X,
Y = boundsLoc.Y,
X = frameLoc.X,
Y = frameLoc.Y,
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = MouseGrabView
};

if (MouseGrabView.Bounds.Contains (viewRelativeMouseEvent.X, viewRelativeMouseEvent.Y) is false)
if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.X, viewRelativeMouseEvent.Y) is false)
{
// The mouse has moved outside the bounds of the view that grabbed the mouse
_mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
Expand Down Expand Up @@ -1546,14 +1549,14 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
View = view
};
}
else if (view.BoundsToScreen (view.Bounds).Contains (mouseEvent.X, mouseEvent.Y))
else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.X, mouseEvent.Y))
{
Point boundsPoint = view.ScreenToBounds (mouseEvent.X, mouseEvent.Y);
Point viewportLocation = view.ScreenToViewport (mouseEvent.X, mouseEvent.Y);

me = new ()
{
X = boundsPoint.X,
Y = boundsPoint.Y,
X = viewportLocation.X,
Y = viewportLocation.Y,
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = view
Expand Down Expand Up @@ -1586,10 +1589,37 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)

//Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");

if (view.NewMouseEvent (me) == false)
while (view.NewMouseEvent (me) != true)
{
// Should we bubble up the event, if it is not handled?
//return;
if (MouseGrabView is { })
{
break;
}

if (view is Adornment adornmentView)
{
view = adornmentView.Parent.SuperView;
}
else
{
view = view.SuperView;
}

if (view is null)
{
break;
}

Point boundsPoint = view.ScreenToViewport (mouseEvent.X, mouseEvent.Y);

me = new ()
{
X = boundsPoint.X,
Y = boundsPoint.Y,
Flags = mouseEvent.Flags,
ScreenPosition = new (mouseEvent.X, mouseEvent.Y),
View = view
};
}

BringOverlappedTopToFront ();
Expand Down
93 changes: 55 additions & 38 deletions Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,31 @@ public abstract class ConsoleDriver
// This is in addition to the dirty flag on each cell.
internal bool [] _dirtyLines;

/// <summary>Gets the dimensions of the terminal.</summary>
public Rectangle Bounds => new (0, 0, Cols, Rows);
// QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application?
/// <summary>Gets the location and size of the terminal screen.</summary>
public Rectangle Screen => new (0, 0, Cols, Rows);

private Rectangle _clip;

/// <summary>
/// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
/// to.
/// </summary>
/// <value>The rectangle describing the bounds of <see cref="Clip"/>.</value>
public Rectangle Clip { get; set; }
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
public Rectangle Clip
{
get => _clip;
set
{
if (_clip == value)
{
return;
}

// Don't ever let Clip be bigger than Screen
_clip = Rectangle.Intersect (Screen, value);
}
}

/// <summary>Get the operating system clipboard.</summary>
public IClipboard Clipboard { get; internal set; }
Expand Down Expand Up @@ -278,67 +294,63 @@ public void AddStr (string str)

for (var i = 0; i < runes.Count; i++)
{
//if (runes [i].IsCombiningMark()) {

// // Attempt to normalize
// string combined = runes [i-1] + runes [i].ToString();

// // Normalize to Form C (Canonical Composition)
// string normalized = combined.Normalize (NormalizationForm.FormC);

// runes [i-]
//}
AddRune (runes [i]);
}
}

/// <summary>Clears the <see cref="Contents"/> of the driver.</summary>
public void ClearContents ()
{
// TODO: This method is really "Clear Contents" now and should not be abstract (or virtual)
Contents = new Cell [Rows, Cols];
//CONCURRENCY: Unsynchronized access to Clip isn't safe.
Clip = new (0, 0, Cols, Rows);
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
Clip = Screen;
_dirtyLines = new bool [Rows];

lock (Contents)
{
// Can raise an exception while is still resizing.
try
for (var row = 0; row < Rows; row++)
{
for (var row = 0; row < Rows; row++)
for (var c = 0; c < Cols; c++)
{
for (var c = 0; c < Cols; c++)
Contents [row, c] = new Cell
{
Contents [row, c] = new Cell
{
Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), IsDirty = true
};
_dirtyLines [row] = true;
}
Rune = (Rune)' ',
Attribute = new Attribute (Color.White, Color.Black),
IsDirty = true
};
_dirtyLines [row] = true;
}
}
catch (IndexOutOfRangeException)
{ }
}
}

/// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary>
/// <returns><see langword="true"/> upon success</returns>
public abstract bool EnsureCursorVisibility ();

// TODO: Move FillRect to ./Drawing
/// <summary>Fills the specified rectangle with the specified rune.</summary>
/// <param name="rect"></param>
/// <param name="rune"></param>
/// <summary>Fills the specified rectangle with the specified rune, using <see cref="CurrentAttribute"/></summary>
/// <remarks>
/// The value of <see cref="Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn.
/// </remarks>
/// <param name="rect">The Screen-relative rectangle.</param>
/// <param name="rune">The Rune used to fill the rectangle</param>
public void FillRect (Rectangle rect, Rune rune = default)
{
for (int r = rect.Y; r < rect.Y + rect.Height; r++)
rect = Rectangle.Intersect (rect, Clip);
lock (Contents)
{
for (int c = rect.X; c < rect.X + rect.Width; c++)
for (int r = rect.Y; r < rect.Y + rect.Height; r++)
{
Application.Driver.Move (c, r);
Application.Driver.AddRune (rune == default (Rune) ? new Rune (' ') : rune);
for (int c = rect.X; c < rect.X + rect.Width; c++)
{
Contents [r, c] = new Cell
{
Rune = (rune != default ? rune : (Rune)' '),
Attribute = CurrentAttribute, IsDirty = true
};
_dirtyLines [r] = true;
}
}
}
}
Expand Down Expand Up @@ -372,10 +384,13 @@ public void FillRect (Rectangle rect, Rune rune = default)
/// <param name="col">The column.</param>
/// <param name="row">The row.</param>
/// <returns>
/// <see langword="false"/> if the coordinate is outside of the screen bounds or outside of <see cref="Clip"/>.
/// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
/// <see langword="true"/> otherwise.
/// </returns>
public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); }
public bool IsValidLocation (int col, int row)
{
return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row);
}

/// <summary>
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
Expand Down Expand Up @@ -437,6 +452,7 @@ public virtual void Move (int col, int row)
/// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary>
public virtual bool SupportsTrueColor => true;

// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application.
/// <summary>
/// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors.
/// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
Expand Down Expand Up @@ -466,6 +482,7 @@ public Attribute CurrentAttribute
get => _currentAttribute;
set
{
// TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
if (Application.Driver is { })
{
_currentAttribute = new Attribute (value.Foreground, value.Background);
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/ConsoleDrivers/NetDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,7 @@ public override void UpdateCursor ()
{
EnsureCursorVisibility ();

if (Col >= 0 && Col < Cols && Row >= 0 && Row < Rows)
if (Col >= 0 && Col < Cols && Row >= 0 && Row <= Rows)
{
SetCursorPosition (Col, Row);
SetWindowPosition (0, Row);
Expand Down
Loading

0 comments on commit 56922b4

Please sign in to comment.