diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs index 0ca33d4..5f9c20a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/ItemPreviews.cs @@ -79,9 +79,11 @@ public class ItemPreviews .Setup(t => t.Bind( t, dc => dc.TextContent, - t => t.Text)), + t => t.Text, + fallbackValue: string.Empty)), new TextBlock { + Margin = "0 1 0 0", Extensions = {new GridPositionExtension(0, 1)} }.Setup(t => t.Bind( t, diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 2628ad9..142688b 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -2,7 +2,6 @@ using System.Linq.Expressions; using System.Reflection; using TerminalUI.Controls; -using TerminalUI.ExpressionTrackers; using TerminalUI.Traits; namespace TerminalUI; diff --git a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs index 4440cbd..1e37b67 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/DotnetDriver.cs @@ -6,7 +6,11 @@ namespace TerminalUI.ConsoleDrivers; public class DotnetDriver : IConsoleDriver { + protected bool CheckThreadId; public bool SupportsAnsiEscapeSequence { get; protected set; } + public int ThreadId { get; set; } + + public void EnterRestrictedMode() => CheckThreadId = true; public virtual bool Init() { @@ -25,10 +29,31 @@ public class DotnetDriver : IConsoleDriver return new(x, y); } - public void Write(string text) => Console.Out.Write(text); - public void Write(ReadOnlySpan text) => Console.Out.Write(text); + public void Write(string text) + { + CheckThread(); + Console.Out.Write(text); + } - public void Write(char text) => Console.Out.Write(text); + public void Write(ReadOnlySpan 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(); @@ -40,7 +65,7 @@ public class DotnetDriver : IConsoleDriver public virtual void SetForegroundColor(IColor foreground) { if (foreground == SpecialColor.None) return; - + if (foreground is not ConsoleColor consoleColor) throw new NotSupportedException(); Console.ForegroundColor = consoleColor.Color; } @@ -48,7 +73,7 @@ public class DotnetDriver : IConsoleDriver public virtual void SetBackgroundColor(IColor background) { if (background == SpecialColor.None) return; - + if (background is not ConsoleColor consoleColor) throw new NotSupportedException(); Console.BackgroundColor = consoleColor.Color; } diff --git a/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs index 39619f0..72c8bad 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/IConsoleDriver.cs @@ -6,6 +6,7 @@ namespace TerminalUI.ConsoleDrivers; public interface IConsoleDriver { bool SupportsAnsiEscapeSequence { get; } + int ThreadId { get; set; } bool Init(); void Dispose(); void SetCursorPosition(Position position); @@ -22,4 +23,5 @@ public interface IConsoleDriver void SetBackgroundColor(IColor background); Size GetWindowSize(); void Clear(); + void EnterRestrictedMode(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs b/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs index e555c9d..6697cea 100644 --- a/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs +++ b/src/Library/TerminalUI/ConsoleDrivers/XTermDriver.cs @@ -28,6 +28,7 @@ public sealed class XTermDriver : DotnetDriver public override void Dispose() { + CheckThreadId = false; Write("\x1b[?1047l"); SetCursorPosition(_initialCursorPosition); } diff --git a/src/Library/TerminalUI/Controls/ChildCollectionView.cs b/src/Library/TerminalUI/Controls/ChildCollectionView.cs index 1e67a8b..3647908 100644 --- a/src/Library/TerminalUI/Controls/ChildCollectionView.cs +++ b/src/Library/TerminalUI/Controls/ChildCollectionView.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; namespace TerminalUI.Controls; diff --git a/src/Library/TerminalUI/Controls/StackPanel.cs b/src/Library/TerminalUI/Controls/StackPanel.cs index 67e234b..8632e9e 100644 --- a/src/Library/TerminalUI/Controls/StackPanel.cs +++ b/src/Library/TerminalUI/Controls/StackPanel.cs @@ -96,37 +96,6 @@ public sealed partial class StackPanel : ChildCollectionView, T : childSize.Width; } - if (Orientation == Orientation.Horizontal) - { - var leftWidth = size.Width - delta; - Span 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 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; } diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index 99527ca..51246f2 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -21,7 +21,6 @@ public sealed partial class TextBlock : View, T>, IDisplayView private RenderState? _lastRenderState; private string[]? _textLines; - private bool _placeholderRenderDone; [Notify] private string? _text = string.Empty; [Notify] private TextAlignment _textAlignment = TextAlignment.Left; @@ -64,30 +63,22 @@ public sealed partial class TextBlock : View, T>, IDisplayView _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; } - _placeholderRenderDone = false; - SetStyleColor(renderContext, foreground, background, _textFormat); - var textLines = _textLines; - if (_textStartIndex < _textLines.Length) + if (textStartIndex < textLines.Length) { - textLines = _textLines[_textStartIndex..]; + textLines = textLines[textStartIndex..]; } else { - _textStartIndex = _textLines.Length - size.Height; + _textStartIndex = textLines.Length - size.Height; } RenderText(textLines, renderContext, position, size, skipRender, TransformText); @@ -99,7 +90,7 @@ public sealed partial class TextBlock : View, T>, IDisplayView => TextAlignment switch { TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text), - _ => string.Format($"{{0,{-size.Width}}}", text) + _ => text }; private bool NeedsRerender(RenderState renderState) diff --git a/src/Library/TerminalUI/Controls/TextBox.cs b/src/Library/TerminalUI/Controls/TextBox.cs index 03fdf35..9a6b9f8 100644 --- a/src/Library/TerminalUI/Controls/TextBox.cs +++ b/src/Library/TerminalUI/Controls/TextBox.cs @@ -103,11 +103,8 @@ public sealed partial class TextBox : View, T>, IFocusable, IDispl var skipRender = !renderContext.ForceRerender && !NeedsRerender(renderStatus); _lastRenderState = renderStatus; - var driver = renderContext.ConsoleDriver; SetStyleColor(renderContext, foreground, background); - RenderEmpty(renderContext, position, size, skipRender); - if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar)) { for (var i = 0; i < _textLines.Count; i++) diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 4a637dd..79d394c 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -132,7 +132,7 @@ public abstract partial class View : IView where TConcrete : Vi } else if (e.PropertyName == nameof(IsVisible)) { - ApplicationContext?.RenderEngine.VisibilityChanged(this); + ApplicationContext?.RenderEngine.VisibilityChanged(this, IsVisible); } } @@ -218,7 +218,7 @@ public abstract partial class View : IView 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++) { @@ -235,14 +235,12 @@ public abstract partial class View : IView where TConcrete : Vi Position position, Size size, 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; - 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 text = textLines[i]; @@ -252,22 +250,40 @@ public abstract partial class View : IView where TConcrete : Vi text = textTransformer(text, currentPosition, size); } - if (text.Length > size.Width) + if (useAsciiOnly) { - text = text[..size.Width]; + RenderTextAsciiOnly( + text, + renderContext, + currentPosition, + size.Width, + updateCellsOnly); } - else if (text.Length < size.Width) + else { - text = text.PadRight(size.Width); - } + if (text.Length > size.Width) + { + text = text[..size.Width]; + } - try - { - driver.SetCursorPosition(currentPosition); - driver.Write(text); - } - catch - { + + foreach (var c in text) + { + if (char.IsControl(c)) + throw new Exception("Control character"); + } + + if (updateCellsOnly) continue; + UpdateCells(renderContext.UpdatedCells, currentPosition, text.Length, 1); + + try + { + driver.SetCursorPosition(currentPosition); + driver.Write(text); + } + catch + { + } } } } @@ -279,10 +295,6 @@ public abstract partial class View : IView where TConcrete : Vi Size size, bool updateCellsOnly) { - UpdateCells(renderContext.UpdatedCells, position, size.Width, size.Height); - - if (updateCellsOnly) return; - var driver = renderContext.ConsoleDriver; for (var i = 0; i < size.Height; i++) { @@ -294,6 +306,9 @@ public abstract partial class View : IView where TConcrete : Vi finalText = finalText[..size.Width]; } + UpdateCells(renderContext.UpdatedCells, currentPosition, finalText.Length, 1); + if (updateCellsOnly) continue; + driver.SetCursorPosition(currentPosition); driver.Write(finalText); } @@ -311,7 +326,8 @@ public abstract partial class View : IView where TConcrete : Vi if (updateCellsOnly) return; var driver = renderContext.ConsoleDriver; - var contentString = new string(content, size.Width); + Span contentString = stackalloc char[size.Width]; + contentString.Fill(content); for (var i = 0; i < size.Height; i++) { @@ -322,6 +338,37 @@ public abstract partial class View : IView where TConcrete : Vi } } + private void RenderTextAsciiOnly( + ReadOnlySpan text, + in RenderContext renderContext, + Position position, + int width, + bool updateCellsOnly) + { + Span 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( in RenderContext renderContext, IColor? foreground = null, diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index 79c8e34..cfb900f 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; namespace TerminalUI; @@ -7,8 +6,11 @@ public class EventLoop : IEventLoop { private readonly IApplicationContext _applicationContext; private readonly ILogger _logger; + private readonly List _initializers = new(); private readonly List _permanentQueue = new(); + public int ThreadId { get; set; } = -1; + public EventLoop( IApplicationContext applicationContext, ILogger logger) @@ -18,15 +20,22 @@ public class EventLoop : IEventLoop } public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action); + public void AddInitializer(Action action) => _initializers.Add(action); public void Run() { _applicationContext.IsRunning = true; + ThreadId = Thread.CurrentThread.ManagedThreadId; + foreach (var initializer in _initializers) + { + initializer(); + } while (_applicationContext.IsRunning) { ProcessQueues(); Thread.Sleep(10); } + ThreadId = -1; } private void ProcessQueues() @@ -35,7 +44,7 @@ public class EventLoop : IEventLoop { /*try {*/ - action(); + action(); /*} catch (Exception e) { diff --git a/src/Library/TerminalUI/IEventLoop.cs b/src/Library/TerminalUI/IEventLoop.cs index be875e7..0ea07d5 100644 --- a/src/Library/TerminalUI/IEventLoop.cs +++ b/src/Library/TerminalUI/IEventLoop.cs @@ -4,4 +4,6 @@ public interface IEventLoop { void Run(); void AddToPermanentQueue(Action action); + void AddInitializer(Action action); + int ThreadId { get; set; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IRenderEngine.cs b/src/Library/TerminalUI/IRenderEngine.cs index fa2f396..a1dc0a8 100644 --- a/src/Library/TerminalUI/IRenderEngine.cs +++ b/src/Library/TerminalUI/IRenderEngine.cs @@ -5,7 +5,7 @@ namespace TerminalUI; public interface IRenderEngine { void RequestRerender(IView view); - void VisibilityChanged(IView view); + void VisibilityChanged(IView view, bool newVisibility); void AddViewToPermanentRenderGroup(IView view); void Run(); } \ No newline at end of file diff --git a/src/Library/TerminalUI/RenderEngine.cs b/src/Library/TerminalUI/RenderEngine.cs index 1472332..52e16b8 100644 --- a/src/Library/TerminalUI/RenderEngine.cs +++ b/src/Library/TerminalUI/RenderEngine.cs @@ -1,5 +1,4 @@ -using TerminalUI.ConsoleDrivers; -using TerminalUI.Controls; +using TerminalUI.Controls; using TerminalUI.Models; using TerminalUI.TextFormat; using TerminalUI.Traits; @@ -15,9 +14,11 @@ public class RenderEngine : IRenderEngine private readonly List _forcedTemporaryViewsToRender = new(); private bool _rerenderRequested = true; private bool _lastCursorVisible; + private bool _forceRerenderAll; private bool[,]? _updatedCells; private bool[,]? _filledCells; private bool[,]? _lastFilledCells; + private DateTime _renderRequestDetected; public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop) { @@ -25,12 +26,23 @@ public class RenderEngine : IRenderEngine _eventLoop = eventLoop; _eventLoop.AddToPermanentQueue(Render); + _eventLoop.AddInitializer(() => + { + _applicationContext.ConsoleDriver.ThreadId = _eventLoop.ThreadId; + _applicationContext.ConsoleDriver.EnterRestrictedMode(); + }); } 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; var parent = view.VisualParent; while (parent?.VisualParent != null) @@ -46,12 +58,11 @@ public class RenderEngine : IRenderEngine if (visibilityChangeHandler is null) { - AddViewToForcedTemporaryRenderGroup(parent ?? view); - } - else - { - visibilityChangeHandler.ChildVisibilityChanged(view); + //AddViewToForcedTemporaryRenderGroup(parent ?? view); + return; } + + visibilityChangeHandler.ChildVisibilityChanged(view); } public void Run() => _eventLoop.Run(); @@ -68,6 +79,7 @@ public class RenderEngine : IRenderEngine { List permanentViewsToRender; List forcedTemporaryViewsToRender; + bool forceRerenderAll; lock (_lock) { if (!_rerenderRequested) return; @@ -75,6 +87,9 @@ public class RenderEngine : IRenderEngine permanentViewsToRender = _permanentViewsToRender.ToList(); forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList(); _forcedTemporaryViewsToRender.Clear(); + + forceRerenderAll = _forceRerenderAll; + _forceRerenderAll = false; } var driver = _applicationContext.ConsoleDriver; @@ -93,25 +108,28 @@ public class RenderEngine : IRenderEngine ClearArray2D(_updatedCells); } - RenderViews( - forcedTemporaryViewsToRender, - new RenderContext( - driver, - true, - null, - null, - new RenderStatistics(), - new TextFormatContext(driver.SupportsAnsiEscapeSequence), - _updatedCells - ), - initialPosition, - size); + if (!forceRerenderAll) + { + RenderViews( + forcedTemporaryViewsToRender, + new RenderContext( + driver, + true, + null, + null, + new RenderStatistics(), + new TextFormatContext(driver.SupportsAnsiEscapeSequence), + _updatedCells + ), + initialPosition, + size); + } RenderViews( permanentViewsToRender, new RenderContext( driver, - false, + forceRerenderAll, null, null, new RenderStatistics(), diff --git a/src/Library/TerminalUI/TerminalUI.csproj b/src/Library/TerminalUI/TerminalUI.csproj index eafc8b2..ad83fe9 100644 --- a/src/Library/TerminalUI/TerminalUI.csproj +++ b/src/Library/TerminalUI/TerminalUI.csproj @@ -24,8 +24,4 @@ - - - -