Skip to content

Commit edc94a0

Browse files
committed
Eliminate per-frame allocations in rendering pipeline, fix missing markup colors, add gradient backgrounds
- Replace LINQ chains with pooled lists and manual loops on all hot rendering paths (Renderer, RenderCoordinator, ScrollablePanelControl, WindowRenderer) - Pool StringBuilders in TreeControl and ConsoleBuffer to avoid per-frame allocations - Add StringBuilder overload to TerminalRawMode.WriteStdout using GetChunks()/Encoder.Convert() to eliminate large ToString() allocation on every frame - Fix lock bug in ConsoleBuffer: lock(obj ?? new object()) created new lock each call - Fix ScrollablePanelBuilder defaulting to Color.Black instead of null, blocking gradient inheritance - Add 7 missing colors to ColorTable (gold1, gold3, coral, magenta1, dodgerblue2, springgreen2, chartreuse2) - Add Color.Coral static property - Add gradient background to MarkupSyntax demo window - Wrap DataGrid right panel in ScrollablePanel - Update MARKUP_SYNTAX.md with all registered color names - Add DemoApp screenshot to EXAMPLES.md
1 parent c7f68c3 commit edc94a0

16 files changed

Lines changed: 330 additions & 201 deletions

File tree

Examples/DemoApp/DemoWindows/DataGridWindow.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,17 @@ public static Window Create(ConsoleWindowSystem ws)
334334
.StickyBottom()
335335
.Build();
336336

337+
// Right panel: scrollable info + feature table
338+
var infoPanel = Controls.ScrollablePanel()
339+
.AddControl(infoMarkup)
340+
.AddControl(featureTable)
341+
.WithVerticalAlignment(VerticalAlignment.Fill)
342+
.Build();
343+
337344
// Layout: left = data grid, right = info panel
338345
var grid = Controls.HorizontalGrid()
339346
.Column(col => col.Add(dataGrid))
340-
.Column(col => col.Width(38).Add(infoMarkup).Add(featureTable))
347+
.Column(col => col.Width(38).Add(infoPanel))
341348
.WithSplitterAfter(0)
342349
.WithAlignment(HorizontalAlignment.Stretch)
343350
.WithVerticalAlignment(VerticalAlignment.Fill)

Examples/DemoApp/DemoWindows/MarkupSyntaxWindow.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using SharpConsoleUI;
22
using SharpConsoleUI.Builders;
33
using SharpConsoleUI.Controls;
4+
using SharpConsoleUI.Helpers;
5+
using SharpConsoleUI.Rendering;
46

57
namespace DemoApp.DemoWindows;
68

@@ -113,10 +115,16 @@ public static Window Create(ConsoleWindowSystem ws)
113115
.WithVerticalAlignment(SharpConsoleUI.Layout.VerticalAlignment.Fill)
114116
.Build();
115117

118+
var gradient = ColorGradient.FromColors(
119+
new Color(45, 15, 70),
120+
new Color(15, 50, 80),
121+
new Color(60, 20, 75));
122+
116123
return new WindowBuilder(ws)
117124
.WithTitle("Markup Syntax")
118125
.WithSize(85, 35)
119126
.Centered()
127+
.WithBackgroundGradient(gradient, GradientDirection.Vertical)
120128
.AddControl(content)
121129
.OnKeyPressed((s, e) =>
122130
{

SharpConsoleUI/Builders/ScrollablePanelBuilder.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public sealed class ScrollablePanelBuilder : IControlBuilder<ScrollablePanelCont
3838
private string? _name;
3939
private object? _tag;
4040
private StickyPosition _stickyPosition = StickyPosition.None;
41-
private Color _backgroundColor = Color.Black;
41+
private Color? _backgroundColor;
4242
private Color _foregroundColor = Color.White;
4343

4444
/// <summary>
@@ -375,10 +375,15 @@ public ScrollablePanelControl Build()
375375
Name = _name,
376376
Tag = _tag,
377377
StickyPosition = _stickyPosition,
378-
BackgroundColor = _backgroundColor,
379378
ForegroundColor = _foregroundColor
380379
};
381380

381+
// Set background only if explicitly specified (null = transparent, inherits gradient)
382+
if (_backgroundColor.HasValue)
383+
{
384+
control.BackgroundColor = _backgroundColor.Value;
385+
}
386+
382387
// Add all children
383388
foreach (var child in _children)
384389
{

SharpConsoleUI/Color.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ private Color(byte r, byte g, byte b, bool isDefault)
158158
public static Color Chartreuse2 => new(118, 238, 0);
159159
public static Color Gold1 => new(255, 215, 0);
160160
public static Color Gold3 => new(205, 173, 0);
161+
public static Color Coral => new(255, 127, 80);
161162

162163
#endregion
163164

SharpConsoleUI/ColorTable.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ internal static class ColorTable
115115
["darkslategray3"] = Color.DarkSlateGray3,
116116
["cyan1"] = Color.Cyan1,
117117
["orange3"] = Color.Orange3,
118+
["magenta1"] = Color.Magenta1,
119+
["dodgerblue2"] = Color.DodgerBlue2,
120+
["springgreen2"] = Color.SpringGreen2,
121+
["chartreuse2"] = Color.Chartreuse2,
122+
["gold1"] = Color.Gold1,
123+
["gold3"] = Color.Gold3,
124+
["coral"] = Color.Coral,
118125

119126
// Grey scale
120127
["grey0"] = Color.Grey0,

SharpConsoleUI/Controls/ScrollablePanelControl/ScrollablePanelControl.Rendering.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutR
100100
fixedHeight += node.DesiredSize.Height;
101101
}
102102
}
103-
int fillCount = paintSnapshot.Count(c => c.Visible && c.VerticalAlignment == VerticalAlignment.Fill);
103+
int fillCount = 0;
104+
foreach (var c in paintSnapshot)
105+
{
106+
if (c.Visible && c.VerticalAlignment == VerticalAlignment.Fill)
107+
fillCount++;
108+
}
104109
int perFillHeight = (_viewportHeight > 0 && fillCount > 0)
105110
? Math.Max(0, (_viewportHeight - fixedHeight) / fillCount) : _viewportHeight;
106111

@@ -262,14 +267,19 @@ private int CalculateContentHeight(int viewportWidth, int maxHeight = 0)
262267

263268
List<IWindowControl> calcSnapshot;
264269
lock (_childrenLock) { calcSnapshot = new List<IWindowControl>(_children); }
265-
var visible = calcSnapshot.Where(c => c.Visible).ToList();
266270

267271
// Two-pass measurement: fixed children first, then Fill children get remaining space.
268272
// Pass 1: measure non-Fill children to determine fixed height.
269273
int fixedHeight = 0;
270-
foreach (var child in visible)
274+
int fillCount = 0;
275+
foreach (var child in calcSnapshot)
271276
{
272-
if (child.VerticalAlignment == VerticalAlignment.Fill) continue;
277+
if (!child.Visible) continue;
278+
if (child.VerticalAlignment == VerticalAlignment.Fill)
279+
{
280+
fillCount++;
281+
continue;
282+
}
273283
var childNode = LayoutNodeFactory.CreateSubtree(child);
274284
childNode.IsVisible = true;
275285
var constraints = new LayoutConstraints(1, availableWidth, 1, int.MaxValue);
@@ -278,15 +288,14 @@ private int CalculateContentHeight(int viewportWidth, int maxHeight = 0)
278288
}
279289

280290
// Pass 2: measure Fill children with remaining space.
281-
int fillCount = visible.Count(c => c.VerticalAlignment == VerticalAlignment.Fill);
282291
int remainingHeight = (maxH < int.MaxValue) ? Math.Max(0, maxH - fixedHeight) : int.MaxValue;
283292
int perFillHeight = (fillCount > 0 && remainingHeight < int.MaxValue)
284293
? Math.Max(0, remainingHeight / fillCount) : int.MaxValue;
285294

286295
int totalHeight = fixedHeight;
287-
foreach (var child in visible)
296+
foreach (var child in calcSnapshot)
288297
{
289-
if (child.VerticalAlignment != VerticalAlignment.Fill) continue;
298+
if (!child.Visible || child.VerticalAlignment != VerticalAlignment.Fill) continue;
290299
var childNode = LayoutNodeFactory.CreateSubtree(child);
291300
childNode.IsVisible = true;
292301
var constraints = new LayoutConstraints(1, availableWidth, 1, perFillHeight);
@@ -301,8 +310,16 @@ private int CalculateContentWidth()
301310
{
302311
List<IWindowControl> snapshot;
303312
lock (_childrenLock) { snapshot = new List<IWindowControl>(_children); }
304-
var visibleChildren = snapshot.Where(c => c.Visible).ToList();
305-
return visibleChildren.Any() ? visibleChildren.Max(c => c.GetLogicalContentSize().Width) : 0;
313+
int maxWidth = 0;
314+
foreach (var c in snapshot)
315+
{
316+
if (c.Visible)
317+
{
318+
int w = c.GetLogicalContentSize().Width;
319+
if (w > maxWidth) maxWidth = w;
320+
}
321+
}
322+
return maxWidth;
306323
}
307324

308325
#endregion

SharpConsoleUI/Controls/TreeControl/TreeControl.Rendering.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
using SharpConsoleUI.Helpers;
1010
using SharpConsoleUI.Layout;
11-
using System.Text;
1211

1312
namespace SharpConsoleUI.Controls
1413
{
@@ -22,25 +21,25 @@ private string BuildTreePrefix(int depth, bool[] ancestorIsLast, (string cross,
2221
if (depth == 0)
2322
return "";
2423

25-
StringBuilder prefix = new StringBuilder();
24+
_prefixBuilder.Clear();
2625

2726
// For each ancestor level, draw a vertical continuation line if that ancestor
2827
// still has siblings below it, or spaces if it was the last child.
2928
for (int i = 0; i < depth - 1; i++)
3029
{
31-
prefix.Append(ancestorIsLast[i] ? " " : guides.vertical);
32-
prefix.Append(_indent);
30+
_prefixBuilder.Append(ancestorIsLast[i] ? " " : guides.vertical);
31+
_prefixBuilder.Append(_indent);
3332
}
3433

3534
// Add appropriate connector for the current node
3635
bool isLast = ancestorIsLast[depth - 1];
3736
string connector = isLast ? guides.corner : guides.tee;
3837

39-
prefix.Append(connector);
40-
prefix.Append(guides.horizontal);
41-
prefix.Append(" ");
38+
_prefixBuilder.Append(connector);
39+
_prefixBuilder.Append(guides.horizontal);
40+
_prefixBuilder.Append(" ");
4241

43-
return prefix.ToString();
42+
return _prefixBuilder.ToString();
4443
}
4544

4645
private (string cross, string corner, string tee, string vertical, string horizontal) GetGuideChars()

SharpConsoleUI/Controls/TreeControl/TreeControl.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public partial class TreeControl : BaseControl, IInteractiveControl, IFocusableC
5252
// Performance: Cache for expensive text measurement operations
5353
private readonly TextMeasurementCache _textMeasurementCache = new(Parsing.MarkupParser.StripLength);
5454

55+
// Pooled StringBuilder to avoid per-node allocations during tree prefix building
56+
private readonly System.Text.StringBuilder _prefixBuilder = new();
57+
5558
// Read-only helpers
5659
private int CurrentSelectedIndex => _selectedIndex;
5760
private int CurrentScrollOffset => _scrollOffset;

0 commit comments

Comments
 (0)