//
// Label.cs: Label control
//
// Authors:
// Miguel de Icaza (miguel@gnome.org)
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using NStack;
namespace Terminal.Gui {
///
/// The Label displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap.
///
public class Label : View {
List lines = new List ();
bool recalcPending = true;
ustring text;
TextAlignment textAlignment;
static Rect CalcRect (int x, int y, ustring s)
{
int mw = 0;
int ml = 1;
int cols = 0;
foreach (var rune in s) {
if (rune == '\n') {
ml++;
if (cols > mw)
mw = cols;
cols = 0;
} else
cols++;
}
if (cols > mw)
mw = cols;
return new Rect (x, y, mw, ml);
}
///
/// Initializes a new instance of using layout.
///
///
///
/// The will be created at the given
/// coordinates with the given string. The size ( will be
/// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
///
///
/// No line wrapping is provided.
///
///
/// column to locate the Label.
/// row to locate the Label.
/// text to initialize the property with.
public Label (int x, int y, ustring text) : this (CalcRect (x, y, text), text)
{
}
///
/// Initializes a new instance of using layout.
///
///
///
/// The will be created at the given
/// coordinates with the given string. The initial size ( will be
/// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
///
///
/// If rect.Height is greater than one, word wrapping is provided.
///
///
/// Location.
/// text to initialize the property with.
public Label (Rect rect, ustring text) : base (rect)
{
this.text = text;
}
///
/// Initializes a new instance of using layout.
///
///
///
/// The will be created using
/// coordinates with the given string. The initial size ( will be
/// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
///
///
/// If Height is greater than one, word wrapping is provided.
///
///
/// text to initialize the property with.
public Label (ustring text) : base ()
{
this.text = text;
var r = CalcRect (0, 0, text);
Width = r.Width;
Height = r.Height;
}
///
/// Initializes a new instance of using layout.
///
///
///
/// The will be created using
/// coordinates. The initial size ( will be
/// adjusted to fit the contents of , including newlines ('\n') for multiple lines.
///
///
/// If Height is greater than one, word wrapping is provided.
///
///
public Label () : this (text: string.Empty) { }
static char [] whitespace = new char [] { ' ', '\t' };
static ustring ClipAndJustify (ustring str, int width, TextAlignment talign)
{
// Get rid of any '\r' added by Windows
str = str.Replace ("\r", ustring.Empty);
int slen = str.RuneCount;
if (slen > width) {
var uints = str.ToRunes (width);
var runes = new Rune [uints.Length];
for (int i = 0; i < uints.Length; i++)
runes [i] = uints [i];
return ustring.Make (runes);
} else {
if (talign == TextAlignment.Justified) {
// TODO: ustring needs this
var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries);
int textCount = words.Sum (arg => arg.Length);
var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
var s = new System.Text.StringBuilder ();
//s.Append ($"tc={textCount} sp={spaces},x={extras} - ");
for (int w = 0; w < words.Length; w++) {
var x = words [w];
s.Append (x);
if (w + 1 < words.Length)
for (int i = 0; i < spaces; i++)
s.Append (' ');
if (extras > 0) {
//s.Append ('_');
extras--;
}
}
return ustring.Make (s.ToString ());
}
return str;
}
}
void Recalc ()
{
recalcPending = false;
Recalc (text, lines, Frame.Width, textAlignment, Bounds.Height > 1);
}
static ustring ReplaceNonPrintables (ustring str)
{
var runes = new List ();
foreach (var r in str.ToRunes ()) {
if (r < 0x20) {
runes.Add (new Rune (r + 0x2400)); // U+25A1 □ WHITE SQUARE
} else {
runes.Add (r);
}
}
return ustring.Make (runes); ;
}
static List WordWrap (ustring text, int margin)
{
int start = 0, end;
var lines = new List ();
text = ReplaceNonPrintables (text);
while ((end = start + margin) < text.Length) {
while (text [end] != ' ' && end > start)
end -= 1;
if (end == start)
end = start + margin;
lines.Add (text [start, end]);
start = end + 1;
}
if (start < text.Length)
lines.Add (text.Substring (start));
return lines;
}
static void Recalc (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap)
{
lineResult.Clear ();
if (wordWrap == false) {
textStr = ReplaceNonPrintables (textStr);
lineResult.Add (ClipAndJustify (textStr, width, talign));
return;
}
int textLen = textStr.Length;
int lp = 0;
for (int i = 0; i < textLen; i++) {
Rune c = textStr [i];
if (c == '\n') {
var wrappedLines = WordWrap (textStr [lp, i], width);
foreach (var line in wrappedLines) {
lineResult.Add (ClipAndJustify (line, width, talign));
}
if (wrappedLines.Count == 0) {
lineResult.Add (ustring.Empty);
}
lp = i + 1;
}
}
foreach (var line in WordWrap (textStr [lp, textLen], width)) {
lineResult.Add (ClipAndJustify (line, width, talign));
}
}
///
public override void LayoutSubviews ()
{
recalcPending = true;
}
///
public override void Redraw (Rect bounds)
{
if (recalcPending)
Recalc ();
if (TextColor != -1)
Driver.SetAttribute (TextColor);
else
Driver.SetAttribute (ColorScheme.Normal);
Clear ();
for (int line = 0; line < lines.Count; line++) {
if (line < bounds.Top || line >= bounds.Bottom)
continue;
var str = lines [line];
int x;
switch (textAlignment) {
case TextAlignment.Left:
x = 0;
break;
case TextAlignment.Justified:
x = Bounds.Left;
break;
case TextAlignment.Right:
x = Bounds.Right - str.Length;
break;
case TextAlignment.Centered:
x = Bounds.Left + (Bounds.Width - str.Length) / 2;
break;
default:
throw new ArgumentOutOfRangeException ();
}
Move (x, line);
Driver.AddStr (str);
}
}
///
/// Computes the number of lines needed to render the specified text by the view
///
/// Number of lines.
/// Text, may contain newlines.
/// The width for the text.
public static int MeasureLines (ustring text, int width)
{
var result = new List ();
Recalc (text, result, width, TextAlignment.Left, true);
return result.Count;
}
///
/// Computes the max width of a line or multilines needed to render by the Label control
///
/// Max width of lines.
/// Text, may contain newlines.
/// The width for the text.
public static int MaxWidth (ustring text, int width)
{
var result = new List ();
Recalc (text, result, width, TextAlignment.Left, true);
return result.Max (s => s.RuneCount);
}
///
/// Computes the max height of a line or multilines needed to render by the Label control
///
/// Max height of lines.
/// Text, may contain newlines.
/// The width for the text.
public static int MaxHeight (ustring text, int width)
{
var result = new List ();
Recalc (text, result, width, TextAlignment.Left, true);
return result.Count;
}
///
/// The text displayed by the .
///
public virtual ustring Text {
get => text;
set {
text = value;
recalcPending = true;
SetNeedsDisplay ();
}
}
///
/// Controls the text-alignment property of the label, changing it will redisplay the .
///
/// The text alignment.
public TextAlignment TextAlignment {
get => textAlignment;
set {
textAlignment = value;
SetNeedsDisplay ();
}
}
Attribute textColor = -1;
///
/// The color used for the .
///
public Attribute TextColor {
get => textColor;
set {
textColor = value;
SetNeedsDisplay ();
}
}
}
}