FilePreview use ASCII only, force rerender when item disappears

This commit is contained in:
2023-08-19 12:46:26 +02:00
parent 1b60af389b
commit c9a92f4923
15 changed files with 170 additions and 113 deletions

View File

@@ -79,9 +79,11 @@ public class ItemPreviews
.Setup(t => t.Bind( .Setup(t => t.Bind(
t, t,
dc => dc.TextContent, dc => dc.TextContent,
t => t.Text)), t => t.Text,
fallbackValue: string.Empty)),
new TextBlock<IElementPreviewViewModel> new TextBlock<IElementPreviewViewModel>
{ {
Margin = "0 1 0 0",
Extensions = {new GridPositionExtension(0, 1)} Extensions = {new GridPositionExtension(0, 1)}
}.Setup(t => t.Bind( }.Setup(t => t.Bind(
t, t,

View File

@@ -2,7 +2,6 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.ExpressionTrackers;
using TerminalUI.Traits; using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;

View File

@@ -6,7 +6,11 @@ namespace TerminalUI.ConsoleDrivers;
public class DotnetDriver : IConsoleDriver public class DotnetDriver : IConsoleDriver
{ {
protected bool CheckThreadId;
public bool SupportsAnsiEscapeSequence { get; protected set; } public bool SupportsAnsiEscapeSequence { get; protected set; }
public int ThreadId { get; set; }
public void EnterRestrictedMode() => CheckThreadId = true;
public virtual bool Init() public virtual bool Init()
{ {
@@ -25,10 +29,31 @@ public class DotnetDriver : IConsoleDriver
return new(x, y); return new(x, y);
} }
public void Write(string text) => Console.Out.Write(text); public void Write(string text)
public void Write(ReadOnlySpan<char> text) => Console.Out.Write(text); {
CheckThread();
Console.Out.Write(text);
}
public void Write(char text) => Console.Out.Write(text); public void Write(ReadOnlySpan<char> text)
{
CheckThread();
Console.Out.Write(text);
}
public void Write(char text)
{
CheckThread();
Console.Out.Write(text);
}
private void CheckThread()
{
if (CheckThreadId && ThreadId != Thread.CurrentThread.ManagedThreadId)
{
throw new InvalidOperationException("Cannot write to console from another thread");
}
}
public virtual void Dispose() => Console.Clear(); public virtual void Dispose() => Console.Clear();

View File

@@ -6,6 +6,7 @@ namespace TerminalUI.ConsoleDrivers;
public interface IConsoleDriver public interface IConsoleDriver
{ {
bool SupportsAnsiEscapeSequence { get; } bool SupportsAnsiEscapeSequence { get; }
int ThreadId { get; set; }
bool Init(); bool Init();
void Dispose(); void Dispose();
void SetCursorPosition(Position position); void SetCursorPosition(Position position);
@@ -22,4 +23,5 @@ public interface IConsoleDriver
void SetBackgroundColor(IColor background); void SetBackgroundColor(IColor background);
Size GetWindowSize(); Size GetWindowSize();
void Clear(); void Clear();
void EnterRestrictedMode();
} }

View File

@@ -28,6 +28,7 @@ public sealed class XTermDriver : DotnetDriver
public override void Dispose() public override void Dispose()
{ {
CheckThreadId = false;
Write("\x1b[?1047l"); Write("\x1b[?1047l");
SetCursorPosition(_initialCursorPosition); SetCursorPosition(_initialCursorPosition);
} }

View File

@@ -1,6 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;

View File

@@ -96,37 +96,6 @@ public sealed partial class StackPanel<T> : ChildCollectionView<StackPanel<T>, T
: childSize.Width; : childSize.Width;
} }
if (Orientation == Orientation.Horizontal)
{
var leftWidth = size.Width - delta;
Span<char> text = stackalloc char[leftWidth];
text.Fill(ApplicationContext!.EmptyCharacter);
SetColorsForDriver(renderContext);
RenderText(
text,
renderContext,
position with {X = position.X + delta},
size with {Width = leftWidth},
!neededRerender
);
}
else
{
var leftHeight = size.Height - delta;
Span<char> text = stackalloc char[size.Width];
text.Fill(ApplicationContext!.EmptyCharacter);
SetColorsForDriver(renderContext);
RenderText(
text,
renderContext,
position with {Y = position.Y + delta},
size with {Height = leftHeight},
!neededRerender
);
}
return neededRerender; return neededRerender;
} }

View File

@@ -21,7 +21,6 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
private RenderState? _lastRenderState; private RenderState? _lastRenderState;
private string[]? _textLines; private string[]? _textLines;
private bool _placeholderRenderDone;
[Notify] private string? _text = string.Empty; [Notify] private string? _text = string.Empty;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left; [Notify] private TextAlignment _textAlignment = TextAlignment.Left;
@@ -64,30 +63,22 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
_lastRenderState = renderState; _lastRenderState = renderState;
if (_textLines is null) var textLines = _textLines;
var textStartIndex = _textStartIndex;
if (textLines is null)
{ {
if (!_placeholderRenderDone)
{
_placeholderRenderDone = true;
RenderEmpty(renderContext, position, size, skipRender);
return true;
}
return false; return false;
} }
_placeholderRenderDone = false;
SetStyleColor(renderContext, foreground, background, _textFormat); SetStyleColor(renderContext, foreground, background, _textFormat);
var textLines = _textLines; if (textStartIndex < textLines.Length)
if (_textStartIndex < _textLines.Length)
{ {
textLines = _textLines[_textStartIndex..]; textLines = textLines[textStartIndex..];
} }
else else
{ {
_textStartIndex = _textLines.Length - size.Height; _textStartIndex = textLines.Length - size.Height;
} }
RenderText(textLines, renderContext, position, size, skipRender, TransformText); RenderText(textLines, renderContext, position, size, skipRender, TransformText);
@@ -99,7 +90,7 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
=> TextAlignment switch => TextAlignment switch
{ {
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text), TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
_ => string.Format($"{{0,{-size.Width}}}", text) _ => text
}; };
private bool NeedsRerender(RenderState renderState) private bool NeedsRerender(RenderState renderState)

View File

@@ -103,11 +103,8 @@ public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDispl
var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderStatus); var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderStatus);
_lastRenderState = renderStatus; _lastRenderState = renderStatus;
var driver = renderContext.ConsoleDriver;
SetStyleColor(renderContext, foreground, background); SetStyleColor(renderContext, foreground, background);
RenderEmpty(renderContext, position, size, skipRender);
if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar)) if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar))
{ {
for (var i = 0; i < _textLines.Count; i++) for (var i = 0; i < _textLines.Count; i++)

View File

@@ -132,7 +132,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
} }
else if (e.PropertyName == nameof(IsVisible)) else if (e.PropertyName == nameof(IsVisible))
{ {
ApplicationContext?.RenderEngine.VisibilityChanged(this); ApplicationContext?.RenderEngine.VisibilityChanged(this, IsVisible);
} }
} }
@@ -218,7 +218,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
} }
} }
private void UpdateCells(bool[,] renderContextUpdatedCells, Position position, int sizeWidth, int sizeHeight) private static void UpdateCells(bool[,] renderContextUpdatedCells, Position position, int sizeWidth, int sizeHeight)
{ {
for (var x = 0; x < sizeWidth; x++) for (var x = 0; x < sizeWidth; x++)
{ {
@@ -235,14 +235,12 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
Position position, Position position,
Size size, Size size,
bool updateCellsOnly, bool updateCellsOnly,
TextTransformer? textTransformer = null) TextTransformer? textTransformer = null,
bool useAsciiOnly = true)
{ {
UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height);
if (updateCellsOnly) return;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
for (var i = 0; i < textLines.Count; i++) var end = int.Min(textLines.Count, size.Height);
for (var i = 0; i < end; i++)
{ {
var currentPosition = position with {Y = position.Y + i}; var currentPosition = position with {Y = position.Y + i};
var text = textLines[i]; var text = textLines[i];
@@ -252,15 +250,32 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
text = textTransformer(text, currentPosition, size); text = textTransformer(text, currentPosition, size);
} }
if (useAsciiOnly)
{
RenderTextAsciiOnly(
text,
renderContext,
currentPosition,
size.Width,
updateCellsOnly);
}
else
{
if (text.Length > size.Width) if (text.Length > size.Width)
{ {
text = text[..size.Width]; text = text[..size.Width];
} }
else if (text.Length < size.Width)
foreach (var c in text)
{ {
text = text.PadRight(size.Width); if (char.IsControl(c))
throw new Exception("Control character");
} }
if (updateCellsOnly) continue;
UpdateCells(renderContext.UpdatedCells, currentPosition, text.Length, 1);
try try
{ {
driver.SetCursorPosition(currentPosition); driver.SetCursorPosition(currentPosition);
@@ -271,6 +286,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
} }
} }
} }
}
protected void RenderText( protected void RenderText(
in ReadOnlySpan<char> text, in ReadOnlySpan<char> text,
@@ -279,10 +295,6 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
Size size, Size size,
bool updateCellsOnly) bool updateCellsOnly)
{ {
UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height);
if (updateCellsOnly) return;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
{ {
@@ -294,6 +306,9 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
finalText = finalText[..size.Width]; finalText = finalText[..size.Width];
} }
UpdateCells(renderContext.UpdatedCells, currentPosition, finalText.Length, 1);
if (updateCellsOnly) continue;
driver.SetCursorPosition(currentPosition); driver.SetCursorPosition(currentPosition);
driver.Write(finalText); driver.Write(finalText);
} }
@@ -311,7 +326,8 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
if (updateCellsOnly) return; if (updateCellsOnly) return;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
var contentString = new string(content, size.Width); Span<char> contentString = stackalloc char[size.Width];
contentString.Fill(content);
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
{ {
@@ -322,6 +338,37 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
} }
} }
private void RenderTextAsciiOnly(
ReadOnlySpan<char> text,
in RenderContext renderContext,
Position position,
int width,
bool updateCellsOnly)
{
Span<char> finalText = stackalloc char[width];
var finalTextPosition = 0;
for (var i = 0; i < text.Length && finalTextPosition < width; i++)
{
var c = text[i];
if (c < 32 || c > 255) continue;
finalText[finalTextPosition] = c;
finalTextPosition++;
}
for (var i = 0; i < finalTextPosition; i++)
{
renderContext.UpdatedCells[position.X + i, position.Y] = true;
}
if (updateCellsOnly) return;
var driver = renderContext.ConsoleDriver;
driver.SetCursorPosition(position);
driver.Write(finalText[..finalTextPosition]);
}
protected void SetStyleColor( protected void SetStyleColor(
in RenderContext renderContext, in RenderContext renderContext,
IColor? foreground = null, IColor? foreground = null,

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
namespace TerminalUI; namespace TerminalUI;
@@ -7,8 +6,11 @@ public class EventLoop : IEventLoop
{ {
private readonly IApplicationContext _applicationContext; private readonly IApplicationContext _applicationContext;
private readonly ILogger<EventLoop> _logger; private readonly ILogger<EventLoop> _logger;
private readonly List<Action> _initializers = new();
private readonly List<Action> _permanentQueue = new(); private readonly List<Action> _permanentQueue = new();
public int ThreadId { get; set; } = -1;
public EventLoop( public EventLoop(
IApplicationContext applicationContext, IApplicationContext applicationContext,
ILogger<EventLoop> logger) ILogger<EventLoop> logger)
@@ -18,15 +20,22 @@ public class EventLoop : IEventLoop
} }
public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action); public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action);
public void AddInitializer(Action action) => _initializers.Add(action);
public void Run() public void Run()
{ {
_applicationContext.IsRunning = true; _applicationContext.IsRunning = true;
ThreadId = Thread.CurrentThread.ManagedThreadId;
foreach (var initializer in _initializers)
{
initializer();
}
while (_applicationContext.IsRunning) while (_applicationContext.IsRunning)
{ {
ProcessQueues(); ProcessQueues();
Thread.Sleep(10); Thread.Sleep(10);
} }
ThreadId = -1;
} }
private void ProcessQueues() private void ProcessQueues()

View File

@@ -4,4 +4,6 @@ public interface IEventLoop
{ {
void Run(); void Run();
void AddToPermanentQueue(Action action); void AddToPermanentQueue(Action action);
void AddInitializer(Action action);
int ThreadId { get; set; }
} }

View File

@@ -5,7 +5,7 @@ namespace TerminalUI;
public interface IRenderEngine public interface IRenderEngine
{ {
void RequestRerender(IView view); void RequestRerender(IView view);
void VisibilityChanged(IView view); void VisibilityChanged(IView view, bool newVisibility);
void AddViewToPermanentRenderGroup(IView view); void AddViewToPermanentRenderGroup(IView view);
void Run(); void Run();
} }

View File

@@ -1,5 +1,4 @@
using TerminalUI.ConsoleDrivers; using TerminalUI.Controls;
using TerminalUI.Controls;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.TextFormat; using TerminalUI.TextFormat;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -15,9 +14,11 @@ public class RenderEngine : IRenderEngine
private readonly List<IView> _forcedTemporaryViewsToRender = new(); private readonly List<IView> _forcedTemporaryViewsToRender = new();
private bool _rerenderRequested = true; private bool _rerenderRequested = true;
private bool _lastCursorVisible; private bool _lastCursorVisible;
private bool _forceRerenderAll;
private bool[,]? _updatedCells; private bool[,]? _updatedCells;
private bool[,]? _filledCells; private bool[,]? _filledCells;
private bool[,]? _lastFilledCells; private bool[,]? _lastFilledCells;
private DateTime _renderRequestDetected;
public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop)
{ {
@@ -25,12 +26,23 @@ public class RenderEngine : IRenderEngine
_eventLoop = eventLoop; _eventLoop = eventLoop;
_eventLoop.AddToPermanentQueue(Render); _eventLoop.AddToPermanentQueue(Render);
_eventLoop.AddInitializer(() =>
{
_applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId;
_applicationContext.ConsoleDriver.EnterRestrictedMode();
});
} }
public void RequestRerender(IView view) => RequestRerender(); public void RequestRerender(IView view) => RequestRerender();
public void VisibilityChanged(IView view) public void VisibilityChanged(IView view, bool newVisibility)
{ {
if (!newVisibility)
{
_forceRerenderAll = true;
return;
}
IVisibilityChangeHandler? visibilityChangeHandler = null; IVisibilityChangeHandler? visibilityChangeHandler = null;
var parent = view.VisualParent; var parent = view.VisualParent;
while (parent?.VisualParent != null) while (parent?.VisualParent != null)
@@ -46,13 +58,12 @@ public class RenderEngine : IRenderEngine
if (visibilityChangeHandler is null) if (visibilityChangeHandler is null)
{ {
AddViewToForcedTemporaryRenderGroup(parent ?? view); //AddViewToForcedTemporaryRenderGroup(parent ?? view);
return;
} }
else
{
visibilityChangeHandler.ChildVisibilityChanged(view); visibilityChangeHandler.ChildVisibilityChanged(view);
} }
}
public void Run() => _eventLoop.Run(); public void Run() => _eventLoop.Run();
@@ -68,6 +79,7 @@ public class RenderEngine : IRenderEngine
{ {
List<IView> permanentViewsToRender; List<IView> permanentViewsToRender;
List<IView> forcedTemporaryViewsToRender; List<IView> forcedTemporaryViewsToRender;
bool forceRerenderAll;
lock (_lock) lock (_lock)
{ {
if (!_rerenderRequested) return; if (!_rerenderRequested) return;
@@ -75,6 +87,9 @@ public class RenderEngine : IRenderEngine
permanentViewsToRender = _permanentViewsToRender.ToList(); permanentViewsToRender = _permanentViewsToRender.ToList();
forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList(); forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList();
_forcedTemporaryViewsToRender.Clear(); _forcedTemporaryViewsToRender.Clear();
forceRerenderAll = _forceRerenderAll;
_forceRerenderAll = false;
} }
var driver = _applicationContext.ConsoleDriver; var driver = _applicationContext.ConsoleDriver;
@@ -93,6 +108,8 @@ public class RenderEngine : IRenderEngine
ClearArray2D(_updatedCells); ClearArray2D(_updatedCells);
} }
if (!forceRerenderAll)
{
RenderViews( RenderViews(
forcedTemporaryViewsToRender, forcedTemporaryViewsToRender,
new RenderContext( new RenderContext(
@@ -106,12 +123,13 @@ public class RenderEngine : IRenderEngine
), ),
initialPosition, initialPosition,
size); size);
}
RenderViews( RenderViews(
permanentViewsToRender, permanentViewsToRender,
new RenderContext( new RenderContext(
driver, driver,
false, forceRerenderAll,
null, null,
null, null,
new RenderStatistics(), new RenderStatistics(),

View File

@@ -24,8 +24,4 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="KeyHandling\" />
</ItemGroup>
</Project> </Project>