using Amphora;
using Annulus;
using Azimuth;
using Clave;
using Glass.Editing;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace Glass.Testbed.TestCases.Text
{
///
/// A TextBox over a text of a billion chars that exists only between Generate and Clear. It is built in memory a
/// chunk per frame, so nothing is stored on disk and the window stays alive while it fills.
///
public class HugeTextTestCase : TestCase
{
private const int TargetChars = 1000000000;
private const int ChunkChars = 1000000;
private const int ProgressEveryChunks = 8;
private const int BoxWidth = 600;
private const int BoxLines = 16;
// A chunk ends on the first word past ChunkChars, which no word and separator together can stretch past this.
private const int ChunkSlack = 64;
private static readonly string[] Words =
{
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "and", "then", "keeps", "running",
"through", "tall", "grass", "toward", "a", "river", "that", "bends", "around", "old", "stone", "bridge",
"where", "children", "sometimes", "gather", "to", "watch", "boats", "drift", "slowly", "downstream",
"carrying", "nothing", "but", "light", "reflected", "from", "windows", "of", "houses", "built",
"centuries", "before", "anyone", "thought", "of", "counting", "characters",
};
[Flags]
private enum CopyRefusedAnswer
{
Ok = 1,
}
private static readonly TextMaterialStyle ButtonTextStyle =
Widgets.MenuLabelStyle with { HorizontalAlignment = Alignment.Horizontal.Center };
private readonly StringBuilder chunkText = new StringBuilder(ChunkChars + ChunkSlack);
private readonly char[] chunkChars = new char[ChunkChars + ChunkSlack];
private readonly Stopwatch generationWatch = new Stopwatch();
private TextBox box;
private MessageBox copyRefusedBox;
private Label readout;
private bool generating;
private bool wholeLines;
private bool awaitingCount;
private int chunksMade;
private static long MegabytesHeld()
{
return GC.GetTotalMemory(forceFullCollection: false) / 1048576;
}
private void Show(string text)
{
readout.Text = text;
}
private string Describe(string what)
{
return string.Format(CultureInfo.InvariantCulture, "{0:N0} chars {1}, {2:N0} MB held",
box.Buffer.Length, what, MegabytesHeld());
}
private void Clear()
{
generating = false;
awaitingCount = false;
box.Buffer.Replace(0, box.Buffer.Length, "");
GC.Collect();
Show("Empty. " + Describe("in the box"));
}
private void Start(bool inLines)
{
Clear();
wholeLines = inLines;
chunksMade = 0;
generating = true;
generationWatch.Restart();
}
// Words picked by position alone, so the same press always builds the same text.
private void BuildChunk(int number)
{
chunkText.Clear();
chunkText.Append("[chunk ").Append(number.ToString(CultureInfo.InvariantCulture)).Append("] ");
int word = number * 7919;
while (chunkText.Length < ChunkChars)
{
chunkText.Append(Words[(word * 31 + word / 7) % Words.Length]);
if (wholeLines && word % 120 == 119)
{
chunkText.Append(".\n\n");
}
else if (wholeLines && word % 12 == 11)
{
chunkText.Append(".\n");
}
else if (word % 12 == 11)
{
chunkText.Append(". ");
}
else
{
chunkText.Append(' ');
}
++word;
}
}
// The caret stays where it was, because an insert would carry it to the end and the next frame would follow it
// there, counting every line in one go.
private void AppendChunk()
{
BuildChunk(chunksMade);
chunkText.CopyTo(0, chunkChars, 0, chunkText.Length);
int caret = box.Buffer.Caret;
box.Buffer.Replace(box.Buffer.Length, 0, new StringView(chunkChars, 0, chunkText.Length));
box.Buffer.MoveTo(caret, extend: false);
++chunksMade;
}
// The box only says a copy was refused. What to do about it is the application's, which here is a message.
private void CopyRefused(TextBox refusedBox, int length)
{
copyRefusedBox.Message = string.Format(CultureInfo.InvariantCulture,
"The clipboard couldn't take {0:N0} chars. Nothing was copied or cut.", length);
copyRefusedBox.Open();
}
private void AddButton(ref Rect row, string text, Action pressed)
{
Vector size = Widgets.ButtonLabel(text).PreferredSize(Constraints.Unbounded);
Button button = new Label(Widgets.ButtonLabelStyle, text).FillParent(
new Button(ButtonBehavior.Default)
{
DebugName = text,
ButtonMaterial = UiPacksheetMaterials.Button,
Bounds = row.NewCol(size.X, margin: Margin),
});
button.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___)
{
pressed();
};
Add(button);
}
protected override void Populate()
{
AddCaption("Builds a text of a billion chars in memory, a million a frame. Nothing is written to disk.");
AddCaption("The text lives until Clear, or until you leave this case. It takes about 2.6 GB.");
AddCaption("The caret stays at the start, so lines are counted a frame at a time as it fills.");
AddCaption("A jump or drag far into uncounted text counts the gap in one go.");
AddCaption("The box keeps no undo history, so editing it never copies the text.");
AddCaption("Ctrl+A then Ctrl+C copies the text into the clipboard a piece at a time, needing 2 GB for it.");
AddCaption("Ctrl+V reads the clipboard a piece at a time, and moves what it read in as whole pages.");
AddCaption("A copy the clipboard refuses opens a message box, through the box's OnCopyRefused.");
readout = new Label(Widgets.CaptionStyle, "Empty.");
readout.Bounds = ContentAggregator.NewRow(readout.PreferredSize(Constraints.Unbounded).Y);
Add(readout);
Scalar buttonHeight = Widgets.ButtonLabel("Generate").PreferredSize(Constraints.Unbounded).Y;
Rect buttons = ContentAggregator.NewRow(buttonHeight);
AddButton(ref buttons, "Generate one line", () => Start(inLines: false));
AddButton(ref buttons, "Generate many lines", () => Start(inLines: true));
AddButton(ref buttons, "Clear", Clear);
box = new TextBox(new TextBoxBehavior { Multiline = true })
{
DebugName = "huge text",
TextBoxMaterial = Widgets.TextBox,
};
Scalar barWidth = Widgets.TextBox.ScrollBar.Value.PreferredThickness(Orientation.Vertical);
Scalar boxHeight = Widgets.FieldTextStyle.SingleLineHeight * BoxLines;
box.Bounds = Rect.FromPositionAndSize(Vector.Zero,
Widgets.TextBox.Background.BoundsSizeFor(new Vector(BoxWidth + barWidth, boxHeight)));
box.Buffer.UndoBudget = 0;
Rect row = ContentAggregator.NewRow(box.Bounds.Height);
box.Bounds = box.Bounds.WithPosition(row.NewCol(box.Bounds.Width).TopLeft);
Add(box);
copyRefusedBox = new MessageBox(CopyRefusedAnswer.Ok, DialogBehavior.Default)
{
DefaultAnswer = CopyRefusedAnswer.Ok,
CancelAnswer = CopyRefusedAnswer.Ok,
MessageBoxMaterial = new MessageBoxMaterial
{
Background = UiPacksheetMaterials.Panel,
TextStyle = Widgets.MenuLabelStyle,
Spacing = Margin,
}
.WithButton(CopyRefusedAnswer.Ok, UiPacksheetMaterials.Button.Framing(
new TextMaterial { Text = "OK", Style = ButtonTextStyle })),
};
Add(copyRefusedBox);
box.OnCopyRefused += CopyRefused;
}
// The text is the biggest thing the testbed holds, so it goes when the case is left.
protected override void Teardown()
{
Clear();
}
public override void Update(Duration elapsed)
{
if (generating)
{
AppendChunk();
if (box.Buffer.Length >= TargetChars)
{
generating = false;
awaitingCount = true;
Show(Describe(string.Format(CultureInfo.InvariantCulture, "built in {0:F1} s, counting lines",
generationWatch.Elapsed.TotalSeconds)));
}
else if (chunksMade % ProgressEveryChunks == 0)
{
Show(Describe("built so far"));
}
}
else if (awaitingCount && box.AllLinesCounted)
{
awaitingCount = false;
Show(Describe("built, and every line is counted"));
}
}
}
}