Fixes #1276. Added TextDirection constructor to View and Label and improving AutoSize. (#1277)

* Added AutoSize. CalcRect calculates vertical. Fixes Draw bug.

* Added TextDirection constructor. Ensuring set frame on Pos/Dim absolute. Improves AutoSize.

* Added a scenario for the AutoSize and TextDirection.

* Added more unit tests.

* Fixing some broken scenarios.

* Fixes others TextDirection that was not working.
This commit is contained in:
BDisp
2021-05-02 19:21:08 +01:00
committed by GitHub
parent dbd36d9d27
commit 8dabd16975
10 changed files with 322 additions and 73 deletions

View File

@@ -141,6 +141,14 @@ namespace Terminal.Gui {
}
}
/// <summary>
/// Used by <see cref="Text"/> to resize the view's <see cref="View.Bounds"/> with the <see cref="Size"/>.
/// Setting <see cref="AutoSize"/> to true only work if the <see cref="View.Width"/> and <see cref="View.Height"/> are null or
/// <see cref="LayoutStyle.Absolute"/> values and doesn't work with <see cref="LayoutStyle.Computed"/> layout,
/// to avoid breaking the <see cref="Pos"/> and <see cref="Dim"/> settings.
/// </summary>
public bool AutoSize { get; set; }
// TODO: Add Vertical Text Alignment
/// <summary>
/// Controls the horizontal text-alignment property.
@@ -306,8 +314,14 @@ namespace Terminal.Gui {
if (IsVerticalDirection (textDirection)) {
lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > 1);
if (!AutoSize && lines.Count > Size.Width) {
lines.RemoveRange (Size.Width, lines.Count - Size.Width);
}
} else {
lines = Format (shown_text, Size.Width, textAlignment == TextAlignment.Justified, Size.Height > 1);
if (!AutoSize && lines.Count > Size.Height) {
lines.RemoveRange (Size.Height, lines.Count - Size.Height);
}
}
NeedsFormat = false;
@@ -675,40 +689,77 @@ namespace Terminal.Gui {
/// <param name="x">The x location of the rectangle</param>
/// <param name="y">The y location of the rectangle</param>
/// <param name="text">The text to measure</param>
/// <param name="direction">The text direction.</param>
/// <returns></returns>
public static Rect CalcRect (int x, int y, ustring text)
public static Rect CalcRect (int x, int y, ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
{
if (ustring.IsNullOrEmpty (text)) {
return new Rect (new Point (x, y), Size.Empty);
}
int mw = 0;
int ml = 1;
int w, h;
int cols = 0;
foreach (var rune in text) {
if (rune == '\n') {
ml++;
if (cols > mw) {
mw = cols;
}
cols = 0;
} else {
if (rune != '\r') {
cols++;
var rw = Rune.ColumnWidth (rune);
if (rw > 0) {
rw--;
if (IsHorizontalDirection (direction)) {
int mw = 0;
int ml = 1;
int cols = 0;
foreach (var rune in text) {
if (rune == '\n') {
ml++;
if (cols > mw) {
mw = cols;
}
cols = 0;
} else {
if (rune != '\r') {
cols++;
var rw = Rune.ColumnWidth (rune);
if (rw > 0) {
rw--;
}
cols += rw;
}
cols += rw;
}
}
}
if (cols > mw) {
mw = cols;
if (cols > mw) {
mw = cols;
}
w = mw;
h = ml;
} else {
int vw = 0;
int vh = 0;
int rows = 0;
foreach (var rune in text) {
if (rune == '\n') {
vw++;
if (rows > vh) {
vh = rows;
}
rows = 0;
} else {
if (rune != '\r') {
rows++;
var rw = Rune.ColumnWidth (rune);
if (rw < 0) {
rw++;
}
if (rw > vw) {
vw = rw;
}
}
}
}
if (rows > vh) {
vh = rows;
}
w = vw;
h = vh;
}
return new Rect (x, y, mw, ml);
return new Rect (x, y, w, h);
}
/// <summary>
@@ -932,6 +983,7 @@ namespace Terminal.Gui {
var current = start;
for (var idx = start; idx < start + size; idx++) {
if (idx < 0) {
current++;
continue;
}
var rune = (Rune)' ';

View File

@@ -478,6 +478,10 @@ namespace Terminal.Gui {
x = value;
SetNeedsLayout ();
if (x is Pos.PosAbsolute) {
frame = new Rect (x.Anchor (0), frame.Y, frame.Width, frame.Height);
}
textFormatter.Size = frame.Size;
SetNeedsDisplay (frame);
}
}
@@ -498,6 +502,10 @@ namespace Terminal.Gui {
y = value;
SetNeedsLayout ();
if (y is Pos.PosAbsolute) {
frame = new Rect (frame.X, y.Anchor (0), frame.Width, frame.Height);
}
textFormatter.Size = frame.Size;
SetNeedsDisplay (frame);
}
}
@@ -520,6 +528,10 @@ namespace Terminal.Gui {
width = value;
SetNeedsLayout ();
if (width is Dim.DimAbsolute) {
frame = new Rect (frame.X, frame.Y, width.Anchor (0), frame.Height);
}
textFormatter.Size = frame.Size;
SetNeedsDisplay (frame);
}
}
@@ -538,6 +550,10 @@ namespace Terminal.Gui {
height = value;
SetNeedsLayout ();
if (height is Dim.DimAbsolute) {
frame = new Rect (frame.X, frame.Y, frame.Width, height.Anchor (0));
}
textFormatter.Size = frame.Size;
SetNeedsDisplay (frame);
}
}
@@ -572,7 +588,7 @@ namespace Terminal.Gui {
/// </remarks>
public View (Rect frame)
{
Initialize (ustring.Empty, frame, LayoutStyle.Absolute);
Initialize (ustring.Empty, frame, LayoutStyle.Absolute, TextDirection.LeftRight_TopBottom);
}
/// <summary>
@@ -593,7 +609,7 @@ namespace Terminal.Gui {
/// Use <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> properties to dynamically control the size and location of the view.
/// </para>
/// </remarks>
public View () : this (text: string.Empty) { }
public View () : this (text: string.Empty, direction: TextDirection.LeftRight_TopBottom) { }
/// <summary>
/// Initializes a new instance of <see cref="View"/> using <see cref="LayoutStyle.Absolute"/> layout.
@@ -647,15 +663,17 @@ namespace Terminal.Gui {
/// </para>
/// </remarks>
/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
public View (ustring text)
/// <param name="direction">The text direction.</param>
public View (ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
{
Initialize (text, Rect.Empty);
Initialize (text, Rect.Empty, LayoutStyle.Computed, direction);
}
void Initialize (ustring text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed)
void Initialize (ustring text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed,
TextDirection direction = TextDirection.LeftRight_TopBottom)
{
textFormatter = new TextFormatter ();
Text = text;
TextDirection = direction;
shortcutHelper = new ShortcutHelper ();
@@ -666,7 +684,7 @@ namespace Terminal.Gui {
// BUGBUG: CalcRect doesn't account for line wrapping
Rect r;
if (rect.IsEmpty) {
r = TextFormatter.CalcRect (0, 0, text);
r = TextFormatter.CalcRect (0, 0, text, direction);
} else {
r = rect;
}
@@ -676,6 +694,8 @@ namespace Terminal.Gui {
Height = r.Height;
Frame = r;
Text = text;
}
/// <summary>
@@ -1969,6 +1989,9 @@ namespace Terminal.Gui {
set {
textFormatter.Text = value;
ResizeView (autoSize);
if (textFormatter.Size != Bounds.Size) {
textFormatter.Size = Bounds.Size;
}
SetNeedsLayout ();
SetNeedsDisplay ();
}
@@ -1984,8 +2007,10 @@ namespace Terminal.Gui {
get => autoSize;
set {
var v = ResizeView (value);
textFormatter.AutoSize = v;
if (autoSize != v) {
autoSize = v;
textFormatter.NeedsFormat = true;
SetNeedsLayout ();
SetNeedsDisplay ();
}
@@ -2023,8 +2048,17 @@ namespace Terminal.Gui {
public virtual TextDirection TextDirection {
get => textFormatter.Direction;
set {
textFormatter.Direction = value;
SetNeedsDisplay ();
if (textFormatter.Direction != value) {
textFormatter.Direction = value;
if (IsInitialized && AutoSize) {
ResizeView (true);
} else if (IsInitialized) {
var b = new Rect (Bounds.X, Bounds.Y, Bounds.Height, Bounds.Width);
SetWidthHeight (b);
}
textFormatter.Size = Bounds.Size;
SetNeedsDisplay ();
}
}
}
@@ -2045,27 +2079,38 @@ namespace Terminal.Gui {
bool ResizeView (bool autoSize)
{
var aSize = autoSize;
if (textFormatter.Size != Bounds.Size && (((width == null || width is Dim.DimAbsolute) && (Bounds.Width == 0
|| autoSize && Bounds.Width != textFormatter.Size.Width))
|| ((height == null || height is Dim.DimAbsolute) && (Bounds.Height == 0
|| autoSize && Bounds.Height != textFormatter.Size.Height)))) {
Bounds = new Rect (Bounds.X, Bounds.Y, textFormatter.Size.Width, textFormatter.Size.Height);
if (width == null) {
width = Bounds.Width;
} else if (width is Dim.DimAbsolute) {
width = Math.Max (Bounds.Width, height.Anchor (Bounds.Width));
} else {
aSize = false;
}
if (height == null) {
height = Bounds.Height;
} else if (height is Dim.DimAbsolute) {
height = Math.Max (Bounds.Height, height.Anchor (Bounds.Height));
} else {
aSize = false;
}
if (!autoSize) {
return false;
}
var aSize = autoSize;
Rect nBounds = TextFormatter.CalcRect (Bounds.X, Bounds.Y, Text, textFormatter.Direction);
if ((textFormatter.Size != Bounds.Size || textFormatter.Size != nBounds.Size)
&& (((width == null || width is Dim.DimAbsolute) && (Bounds.Width == 0
|| autoSize && Bounds.Width != nBounds.Width))
|| ((height == null || height is Dim.DimAbsolute) && (Bounds.Height == 0
|| autoSize && Bounds.Height != nBounds.Height)))) {
aSize = SetWidthHeight (nBounds);
}
return aSize;
}
bool SetWidthHeight (Rect nBounds)
{
bool aSize;
var canSizeW = SetWidth (nBounds.Width, out int rW);
var canSizeH = SetHeight (nBounds.Height, out int rH);
if (canSizeW && canSizeH) {
aSize = true;
Bounds = nBounds;
width = rW;
height = rH;
textFormatter.Size = Bounds.Size;
} else {
aSize = false;
}
return aSize;
}

View File

@@ -13,7 +13,8 @@ using NStack;
namespace Terminal.Gui {
/// <summary>
/// The Label <see cref="View"/> displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap.
/// The Label <see cref="View"/> displays a string at a given position and supports multiple lines separated by newline characters.
/// Multi-line Labels support word wrap.
/// </summary>
/// <remarks>
/// The <see cref="Label"/> view is functionality identical to <see cref="View"/> and is included for API backwards compatibility.
@@ -44,6 +45,12 @@ namespace Terminal.Gui {
{
}
/// <inheritdoc/>
public Label (ustring text, TextDirection direction)
: base (text, direction)
{
}
/// <summary>
/// Clicked <see cref="Action"/>, raised when the user clicks the primary mouse button within the Bounds of this <see cref="View"/>
/// or if the user presses the action key while this view is focused. (TODO: IsDefault)
@@ -84,7 +91,6 @@ namespace Terminal.Gui {
if (MouseEvent (mouseEvent))
return true;
if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
if (!HasFocus && SuperView != null) {
SetFocus ();

View File

@@ -0,0 +1,65 @@
using Terminal.Gui;
namespace UICatalog {
[ScenarioMetadata (Name: "AutoSize and Direction Text", Description: "Demonstrates the text auto-size and direction manipulation.")]
[ScenarioCategory ("Text")]
class AutoSizeAndDirectionText : Scenario {
public override void Setup ()
{
var text = "Hello World";
var color = Colors.Dialog;
var labelH = new Label (text, TextDirection.LeftRight_TopBottom) {
X = 1,
Y = 1,
ColorScheme = color
};
Win.Add (labelH);
var labelV = new Label (text, TextDirection.TopBottom_LeftRight) {
X = 70,
Y = 1,
ColorScheme = color
};
Win.Add (labelV);
var editText = new TextView () {
X = Pos.Center (),
Y = Pos.Center (),
Width = 20,
Height = 5,
ColorScheme = color,
Text = text
};
editText.SetFocus ();
Win.Add (editText);
var ckbDirection = new CheckBox ("Toggle Direction") {
X = Pos.Center (),
Y = Pos.Center () + 3
};
ckbDirection.Toggled += (_) => {
if (labelH.TextDirection == TextDirection.LeftRight_TopBottom) {
labelH.TextDirection = TextDirection.TopBottom_LeftRight;
labelV.TextDirection = TextDirection.LeftRight_TopBottom;
} else {
labelH.TextDirection = TextDirection.LeftRight_TopBottom;
labelV.TextDirection = TextDirection.TopBottom_LeftRight;
}
};
Win.Add (ckbDirection);
var ckbAutoSize = new CheckBox ("Auto Size") {
X = Pos.Center (),
Y = Pos.Center () + 5
};
ckbAutoSize.Toggled += (_) => labelH.AutoSize = labelV.AutoSize = ckbAutoSize.Checked;
Win.Add (ckbAutoSize);
Win.KeyUp += (_) =>
labelH.Text = labelV.Text = text = editText.Text.ToString ();
}
}
}

View File

@@ -13,9 +13,9 @@ namespace UICatalog {
public override void Setup ()
{
var frame = new FrameView ("Dialog Options") {
X = Pos.Center(),
X = Pos.Center (),
Y = 1,
Width = Dim.Percent(75),
Width = Dim.Percent (75),
Height = 10
};
Win.Add (frame);
@@ -72,7 +72,7 @@ namespace UICatalog {
var titleEdit = new TextField ("Title") {
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Width = Dim.Fill(),
Width = Dim.Fill (),
Height = 1
};
frame.Add (titleEdit);
@@ -93,8 +93,13 @@ namespace UICatalog {
};
frame.Add (numButtonsEdit);
frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit)
+ Dim.Height (numButtonsEdit) + 2;
void Top_Loaded ()
{
frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit)
+ Dim.Height (numButtonsEdit) + 2;
Top.Loaded -= Top_Loaded;
}
Top.Loaded += Top_Loaded;
label = new Label ("Button Pressed:") {
X = Pos.Center (),
@@ -113,7 +118,7 @@ namespace UICatalog {
//var btnText = new [] { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" };
var showDialogButton = new Button ("Show Dialog") {
X = Pos.Center(),
X = Pos.Center (),
Y = Pos.Bottom (frame) + 2,
IsDefault = true,
};
@@ -129,7 +134,7 @@ namespace UICatalog {
var buttonId = i;
//var button = new Button (btnText [buttonId % 10],
// is_default: buttonId == 0);
var button = new Button (NumberToWords.Convert(buttonId),
var button = new Button (NumberToWords.Convert (buttonId),
is_default: buttonId == 0);
button.Clicked += () => {
clicked = buttonId;

View File

@@ -87,9 +87,9 @@ namespace UICatalog {
var labelKeypress = new Label ("") {
X = Pos.Left (edit),
Y = Pos.Top (keyPressedLabel),
Width = 20,
TextAlignment = Terminal.Gui.TextAlignment.Centered,
ColorScheme = Colors.Error,
AutoSize = true
};
Win.Add (labelKeypress);

View File

@@ -140,8 +140,13 @@ namespace UICatalog {
};
frame.Add (styleRadioGroup);
frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (messageEdit)
void Top_Loaded ()
{
frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (messageEdit)
+ Dim.Height (numButtonsEdit) + Dim.Height(defaultButtonEdit) + Dim.Height (styleRadioGroup) + 2;
Top.Loaded -= Top_Loaded;
}
Top.Loaded += Top_Loaded;
label = new Label ("Button Pressed:") {
X = Pos.Center (),

View File

@@ -129,6 +129,7 @@ namespace UICatalog {
X = 0,
Y = 0,
Width = Dim.Fill (), // FIXED: I don't think this should be needed; DimFill() should respect container's frame. X does.
Height = 2,
ColorScheme = Colors.Error
};
scrollView.Add (horizontalRuler);
@@ -143,12 +144,13 @@ namespace UICatalog {
};
scrollView.Add (verticalRuler);
void Top_Loaded() {
void Top_Loaded ()
{
horizontalRuler.Text = rule.Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)] +
"\n" + "| ".Repeat ((int)Math.Ceiling ((double)(horizontalRuler.Bounds.Width) / (double)rule.Length)) [0..(horizontalRuler.Bounds.Width)];
verticalRuler.Text = vrule.Repeat ((int)Math.Ceiling ((double)(verticalRuler.Bounds.Height * 2) / (double)rule.Length)) [0..(verticalRuler.Bounds.Height * 2)];
Top.Loaded -= Top_Loaded;
};
}
Top.Loaded += Top_Loaded;
var pressMeButton = new Button ("Press me!") {
@@ -204,8 +206,8 @@ namespace UICatalog {
scrollView.Add (anchorButton);
var hCheckBox = new CheckBox ("Horizontal Scrollbar", scrollView.ShowHorizontalScrollIndicator) {
X = Pos.X(scrollView),
Y = Pos.Bottom(scrollView) + 1,
X = Pos.X (scrollView),
Y = Pos.Bottom (scrollView) + 1,
};
Win.Add (hCheckBox);
@@ -273,7 +275,7 @@ namespace UICatalog {
int count = 0;
var mousePos = new Label ("Mouse: ");
mousePos.X = Pos.Right(scrollView) + 1;
mousePos.X = Pos.Right (scrollView) + 1;
mousePos.Y = Pos.AnchorEnd (1);
mousePos.Width = 50;
Application.RootMouseEvent += delegate (MouseEvent me) {

View File

@@ -29,7 +29,6 @@ namespace Terminal.Gui.Types {
action = () => new Rect (1, 2, -3, -4);
ex = Assert.Throws<ArgumentException> (action);
Assert.Equal ("Width must be greater or equal to 0.", ex.Message);
}
[Fact]
@@ -110,5 +109,43 @@ namespace Terminal.Gui.Types {
rect2 = new Rect (-1, 2, 3, 4);
Assert.NotEqual (rect1, rect2);
}
[Fact]
public void Positive_X_Y_Positions ()
{
var rect = new Rect (10, 5, 100, 50);
int yCount = 0, xCount = 0, yxCount = 0;
for (int line = rect.Y; line < rect.Y + rect.Height; line++) {
yCount++;
xCount = 0;
for (int col = rect.X; col < rect.X + rect.Width; col++) {
xCount++;
yxCount++;
}
}
Assert.Equal (yCount, rect.Height);
Assert.Equal (xCount, rect.Width);
Assert.Equal (yxCount, rect.Height * rect.Width);
}
[Fact]
public void Negative_X_Y_Positions ()
{
var rect = new Rect (-10, -5, 100, 50);
int yCount = 0, xCount = 0, yxCount = 0;
for (int line = rect.Y; line < rect.Y + rect.Height; line++) {
yCount++;
xCount = 0;
for (int col = rect.X; col < rect.X + rect.Width; col++) {
xCount++;
yxCount++;
}
}
Assert.Equal (yCount, rect.Height);
Assert.Equal (xCount, rect.Width);
Assert.Equal (yxCount, rect.Height * rect.Width);
}
}
}

View File

@@ -37,6 +37,7 @@ namespace Terminal.Gui.Views {
Assert.False (r.WantMousePositionReports);
Assert.Null (r.SuperView);
Assert.Null (r.MostFocused);
Assert.Equal (TextDirection.LeftRight_TopBottom, r.TextDirection);
// Empty Rect
r = new View (Rect.Empty);
@@ -60,6 +61,7 @@ namespace Terminal.Gui.Views {
Assert.False (r.WantMousePositionReports);
Assert.Null (r.SuperView);
Assert.Null (r.MostFocused);
Assert.Equal (TextDirection.LeftRight_TopBottom, r.TextDirection);
// Rect with values
r = new View (new Rect (1, 2, 3, 4));
@@ -83,6 +85,32 @@ namespace Terminal.Gui.Views {
Assert.False (r.WantMousePositionReports);
Assert.Null (r.SuperView);
Assert.Null (r.MostFocused);
Assert.Equal (TextDirection.LeftRight_TopBottom, r.TextDirection);
// Initializes a view with a vertical direction
r = new View ("Vertical View", TextDirection.TopBottom_LeftRight);
Assert.NotNull (r);
Assert.Equal (LayoutStyle.Computed, r.LayoutStyle);
Assert.Equal ("View()({X=0,Y=0,Width=1,Height=13})", r.ToString ());
Assert.False (r.CanFocus);
Assert.False (r.HasFocus);
Assert.Equal (new Rect (0, 0, 1, 13), r.Bounds);
Assert.Equal (new Rect (0, 0, 1, 13), r.Frame);
Assert.Null (r.Focused);
Assert.Null (r.ColorScheme);
Assert.NotNull (r.Width); // All view Dim are initialized now,
Assert.NotNull (r.Height); // avoiding Dim errors.
Assert.NotNull (r.X); // All view Pos are initialized now,
Assert.NotNull (r.Y); // avoiding Pos errors.
Assert.False (r.IsCurrentTop);
Assert.Empty (r.Id);
Assert.Empty (r.Subviews);
Assert.False (r.WantContinuousButtonPressed);
Assert.False (r.WantMousePositionReports);
Assert.Null (r.SuperView);
Assert.Null (r.MostFocused);
Assert.Equal (TextDirection.TopBottom_LeftRight, r.TextDirection);
}
[Fact]
@@ -1020,7 +1048,7 @@ namespace Terminal.Gui.Views {
}
[Fact]
public void View_Difference_Between_An_Object_Initializer_And_A_Constructor ()
public void View_With_No_Difference_Between_An_Object_Initializer_And_A_Constructor ()
{
// Object Initializer
var view = new View () {
@@ -1033,8 +1061,10 @@ namespace Terminal.Gui.Views {
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.True (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
view.LayoutSubviews ();
@@ -1073,8 +1103,10 @@ namespace Terminal.Gui.Views {
Assert.Equal (2, view.Y);
Assert.Equal (3, view.Width);
Assert.Equal (4, view.Height);
Assert.True (view.Frame.IsEmpty);
Assert.True (view.Bounds.IsEmpty);
Assert.False (view.Frame.IsEmpty);
Assert.Equal (new Rect (1, 2, 3, 4), view.Frame);
Assert.False (view.Bounds.IsEmpty);
Assert.Equal (new Rect (0, 0, 3, 4), view.Bounds);
}
[Fact]