Force rerender on visibility change

This commit is contained in:
2023-08-12 09:09:17 +02:00
parent 1fde0df2d6
commit 16bdc1ed40
20 changed files with 292 additions and 147 deletions

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TerminalUI\TerminalUI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace TerminalUI.DependencyInjection;
public static class TerminalUiServiceCollectionExtensions
{
public static IServiceCollection AddTerminalUi(this IServiceCollection collection)
{
collection.TryAddSingleton<IFocusManager, FocusManager>();
collection.TryAddSingleton<IRenderEngine, RenderEngine>();
collection.TryAddSingleton<IApplicationContext, ApplicationContext>();
collection.TryAddSingleton<IEventLoop, EventLoop>();
return collection;
}
}

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
@@ -6,15 +7,23 @@ namespace TerminalUI;
public class ApplicationContext : IApplicationContext
{
public required IConsoleDriver ConsoleDriver { get; init; }
public required IFocusManager FocusManager { get; init; }
public ILoggerFactory? LoggerFactory { get; init; }
public IEventLoop EventLoop { get; init; }
private readonly Lazy<IConsoleDriver> _consoleDriver;
private readonly Lazy<IFocusManager> _focusManager;
private readonly Lazy<ILoggerFactory?> _loggerFactory;
private readonly Lazy<IRenderEngine> _renderEngine;
public IConsoleDriver ConsoleDriver => _consoleDriver.Value;
public IFocusManager FocusManager => _focusManager.Value;
public ILoggerFactory? LoggerFactory => _loggerFactory.Value;
public IRenderEngine RenderEngine => _renderEngine.Value;
public bool IsRunning { get; set; }
public char EmptyCharacter { get; init; } = ' ';
public ApplicationContext()
public ApplicationContext(IServiceProvider serviceProvider)
{
EventLoop = new EventLoop(this);
_consoleDriver = new Lazy<IConsoleDriver>(serviceProvider.GetRequiredService<IConsoleDriver>);
_focusManager = new Lazy<IFocusManager>(serviceProvider.GetRequiredService<IFocusManager>);
_loggerFactory = new Lazy<ILoggerFactory?>(serviceProvider.GetService<ILoggerFactory?>);
_renderEngine = new Lazy<IRenderEngine>(serviceProvider.GetRequiredService<IRenderEngine>);
}
}

View File

@@ -7,7 +7,6 @@ namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
{
private readonly ObservableCollection<IView> _children = new();
private readonly Dictionary<IView, bool> _visibilities = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; }
@@ -27,7 +26,7 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
}
}
ApplicationContext?.EventLoop.RequestRerender();
ApplicationContext?.RenderEngine.RequestRerender(this);
}
};
@@ -43,18 +42,6 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
};
}
protected void SaveVisibilities()
{
_visibilities.Clear();
foreach (var child in _children)
{
_visibilities[child] = child.IsVisible;
}
}
protected bool? GetLastVisibility(IView view)
=> _visibilities.TryGetValue(view, out var visibility) ? visibility : null;
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);

View File

@@ -2,14 +2,17 @@
using Microsoft.Extensions.Logging;
using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.Traits;
using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls;
public class Grid<T> : ChildContainerView<T>
public class Grid<T> : ChildContainerView<T>, IVisibilityChangeHandler
{
private List<RowDefinition> _rowDefinitions = new() {RowDefinition.Star(1)};
private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)};
private List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new();
private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>();
private delegate void WithSizes(RenderContext renderContext, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights);
@@ -150,6 +153,13 @@ public class Grid<T> : ChildContainerView<T>
new Option<Size>(size, true),
(context, columnWidths, rowHeights) =>
{
IReadOnlyList<IView> forceRerenderChildren;
lock (_forceRerenderChildrenLock)
{
forceRerenderChildren = _forceRerenderChildren;
_forceRerenderChildren.Clear();
}
context = new RenderContext(
context.ConsoleDriver,
context.ForceRerender,
@@ -169,7 +179,8 @@ public class Grid<T> : ChildContainerView<T>
rowHeights,
viewsByPosition,
column,
row
row,
forceRerenderChildren
);
}
}
@@ -177,29 +188,17 @@ public class Grid<T> : ChildContainerView<T>
return true;
});
private void RenderViewsByPosition(
RenderContext context,
private void RenderViewsByPosition(RenderContext context,
Position gridPosition,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowHeights,
IReadOnlyDictionary<(int, int), List<IView>> viewsByPosition,
int column,
int row)
int row,
IReadOnlyList<IView> forceRerenderChildren)
{
if (!viewsByPosition.TryGetValue((column, row), out var children)) return;
var anyChangedVisibility = false;
foreach (var child in children)
{
var lastVisibility = GetLastVisibility(child);
if (lastVisibility is { } b && b != child.IsVisible)
{
anyChangedVisibility = true;
break;
}
}
var width = columnWidths[column];
var height = rowHeights[row];
var renderSize = new Size(width, height);
@@ -212,7 +211,7 @@ public class Grid<T> : ChildContainerView<T>
row
);
var needsRerender = anyChangedVisibility;
var needsRerender = children.Any(forceRerenderChildren.Contains);
if (needsRerender)
{
context = new RenderContext(
@@ -232,7 +231,7 @@ public class Grid<T> : ChildContainerView<T>
{
needsRerender = true;
context = new RenderContext(
context.ConsoleDriver,
context.ConsoleDriver,
true,
context.Foreground,
context.Background
@@ -483,4 +482,20 @@ public class Grid<T> : ChildContainerView<T>
ColumnDefinitions = columnDefinitions;
}
public void ChildVisibilityChanged(IView child)
{
var viewToForceRerender = child;
while (viewToForceRerender.VisualParent != null && viewToForceRerender.VisualParent != this)
{
viewToForceRerender = viewToForceRerender.VisualParent;
}
if (viewToForceRerender.VisualParent != this) return;
lock (_forceRerenderChildrenLock)
{
_forceRerenderChildren.Add(viewToForceRerender);
}
}
}

View File

@@ -27,6 +27,7 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; }
RenderMethod RenderMethod { get; set; }
IView? VisualParent { get; set; }
event Action<IView> Disposed;
Size GetRequestedSize();

View File

@@ -102,14 +102,14 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
{
((INotifyCollectionChanged) observableDeclarative).CollectionChanged +=
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
(_, _) => ApplicationContext?.RenderEngine.RequestRerender(this);
_getItems = () => observableDeclarative;
}
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
{
((INotifyCollectionChanged) readOnlyObservableDeclarative).CollectionChanged +=
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
(_, _) => ApplicationContext?.RenderEngine.RequestRerender(this);
_getItems = () => readOnlyObservableDeclarative;
}

View File

@@ -124,7 +124,7 @@ public partial class TextBox<T> : View<T>, IFocusable
HandleKeyInputInternal(keyEventArgs);
if (keyEventArgs.Handled)
{
ApplicationContext?.EventLoop.RequestRerender();
ApplicationContext?.RenderEngine.RequestRerender(this);
}
}

View File

@@ -30,7 +30,8 @@ public abstract partial class View<T> : IView<T>
[Notify] private string? _name;
[Notify] private IApplicationContext? _applicationContext;
[Notify] private bool _attached;
[Notify] private IView? _visualParent;
protected ObservableCollection<IView> VisualChildren { get; } = new();
public List<object> Extensions { get; } = new();
@@ -90,7 +91,7 @@ public abstract partial class View<T> : IView<T>
)
)
{
ApplicationContext?.EventLoop.RequestRerender();
ApplicationContext?.RenderEngine.RequestRerender(this);
}
if (e.PropertyName == nameof(Attached))
@@ -100,13 +101,17 @@ public abstract partial class View<T> : IView<T>
visualChild.Attached = Attached;
}
}
else if(e.PropertyName == nameof(ApplicationContext))
else if (e.PropertyName == nameof(ApplicationContext))
{
foreach (var visualChild in VisualChildren)
{
visualChild.ApplicationContext = ApplicationContext;
}
}
else if (e.PropertyName == nameof(IsVisible))
{
ApplicationContext?.RenderEngine.VisibilityChanged(this);
}
}
protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size);
@@ -221,7 +226,7 @@ public abstract partial class View<T> : IView<T>
Size size)
{
var contentString = new string(content, size.Width);
for (var i = 0; i < size.Height; i++)
{
var currentPosition = position with {Y = position.Y + i};
@@ -234,7 +239,7 @@ public abstract partial class View<T> : IView<T>
protected void SetColorsForDriver(RenderContext renderContext)
{
var driver = renderContext.ConsoleDriver;
var foreground = Foreground ?? renderContext.Foreground;
var background = Background ?? renderContext.Background;
if (foreground is not null)
@@ -264,12 +269,8 @@ public abstract partial class View<T> : IView<T>
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
{
child.DataContext = DataContext;
CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, T>(this, child, d => d);
AddDisposable(mapper);
child.AddDisposable(mapper);
SetupNewChild(child, mapper);
return child;
}
@@ -278,20 +279,21 @@ public abstract partial class View<T> : IView<T>
where TChild : IView<TDataContext>
{
child.DataContext = dataContextMapper(DataContext);
CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper);
AddDisposable(mapper);
child.AddDisposable(mapper);
SetupNewChild(child, mapper);
return child;
}
private void CopyCommonPropertiesToNewChild(IView child)
private void SetupNewChild(IView child, IDisposable dataContextmapper)
{
child.ApplicationContext = ApplicationContext;
child.Attached = Attached;
child.VisualParent = this;
VisualChildren.Add(child);
AddDisposable(dataContextmapper);
child.AddDisposable(dataContextmapper);
}
public virtual void RemoveChild<TDataContext>(IView<TDataContext> child)

View File

@@ -1,86 +1,32 @@
using TerminalUI.Controls;
using TerminalUI.Models;
namespace TerminalUI;
namespace TerminalUI;
public class EventLoop : IEventLoop
{
private readonly IApplicationContext _applicationContext;
private readonly object _lock = new();
private readonly List<IView> _viewsToRender = new();
private bool _rerenderRequested;
private bool _lastCursorVisible;
private readonly List<Action> _permanentQueue = new();
public EventLoop(IApplicationContext applicationContext)
{
_applicationContext = applicationContext;
}
public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action);
public void Run()
{
_applicationContext.IsRunning = true;
_rerenderRequested = true;
while (_applicationContext.IsRunning)
{
Render();
ProcessQueues();
Thread.Sleep(10);
}
}
public void RequestRerender()
private void ProcessQueues()
{
lock (_lock)
foreach (var action in _permanentQueue)
{
_rerenderRequested = true;
}
}
public void Render()
{
List<IView> viewsToRender;
lock (_lock)
{
if (!_rerenderRequested) return;
_rerenderRequested = false;
viewsToRender = _viewsToRender.ToList();
}
var driver = _applicationContext.ConsoleDriver;
var size = driver.GetWindowSize();
var renderContext = new RenderContext(
driver,
false,
null,
null
);
foreach (var view in viewsToRender)
{
view.Attached = true;
view.GetRequestedSize();
view.Render(renderContext, new Position(0, 0), size);
}
if (_applicationContext.FocusManager.Focused is { } focused)
{
focused.SetCursorPosition(driver);
if (!_lastCursorVisible)
{
driver.SetCursorVisible(true);
_lastCursorVisible = true;
}
}
else if (_lastCursorVisible)
{
driver.SetCursorVisible(false);
_lastCursorVisible = false;
}
}
public void AddViewToRender(IView view)
{
lock (_lock)
{
_viewsToRender.Add(view);
action();
}
}
}

View File

@@ -6,10 +6,10 @@ namespace TerminalUI;
public interface IApplicationContext
{
IEventLoop EventLoop { get; init; }
IRenderEngine RenderEngine { get; }
bool IsRunning { get; set; }
IConsoleDriver ConsoleDriver { get; init; }
ILoggerFactory? LoggerFactory { get; init; }
char EmptyCharacter { get; init; }
IFocusManager FocusManager { get; init; }
IConsoleDriver ConsoleDriver { get; }
ILoggerFactory? LoggerFactory { get; }
char EmptyCharacter { get; }
IFocusManager FocusManager { get; }
}

View File

@@ -1,11 +1,7 @@
using TerminalUI.Controls;
namespace TerminalUI;
namespace TerminalUI;
public interface IEventLoop
{
void Render();
void AddViewToRender(IView view);
void Run();
void RequestRerender();
void AddToPermanentQueue(Action action);
}

View File

@@ -0,0 +1,11 @@
using TerminalUI.Controls;
namespace TerminalUI;
public interface IRenderEngine
{
void RequestRerender(IView view);
void VisibilityChanged(IView view);
void AddViewToPermanentRenderGroup(IView view);
void Run();
}

View File

@@ -0,0 +1,141 @@
using TerminalUI.Controls;
using TerminalUI.Models;
using TerminalUI.Traits;
namespace TerminalUI;
public class RenderEngine : IRenderEngine
{
private readonly IApplicationContext _applicationContext;
private readonly IEventLoop _eventLoop;
private readonly object _lock = new();
private readonly List<IView> _permanentViewsToRender = new();
private readonly List<IView> _forcedTemporaryViewsToRender = new();
private bool _rerenderRequested = true;
private bool _lastCursorVisible;
public RenderEngine(IApplicationContext applicationContext, IEventLoop eventLoop)
{
_applicationContext = applicationContext;
_eventLoop = eventLoop;
_eventLoop.AddToPermanentQueue(Render);
}
public void RequestRerender(IView view) => RequestRerender();
public void VisibilityChanged(IView view)
{
IVisibilityChangeHandler? visibilityChangeHandler = null;
var parent = view.VisualParent;
while (parent?.VisualParent != null)
{
if (parent is IVisibilityChangeHandler v)
{
visibilityChangeHandler = v;
break;
}
parent = parent.VisualParent;
}
if (visibilityChangeHandler is null)
{
AddViewToForcedTemporaryRenderGroup(parent ?? view);
}
else
{
visibilityChangeHandler.ChildVisibilityChanged(view);
}
}
public void Run() => _eventLoop.Run();
public void RequestRerender()
{
lock (_lock)
{
_rerenderRequested = true;
}
}
private void Render()
{
List<IView> permanentViewsToRender;
List<IView> forcedTemporaryViewsToRender;
lock (_lock)
{
if (!_rerenderRequested) return;
_rerenderRequested = false;
permanentViewsToRender = _permanentViewsToRender.ToList();
forcedTemporaryViewsToRender = _forcedTemporaryViewsToRender.ToList();
}
var driver = _applicationContext.ConsoleDriver;
var initialPosition = new Position(0, 0);
var size = driver.GetWindowSize();
RenderViews(
forcedTemporaryViewsToRender,
new RenderContext(
driver,
true,
null,
null
),
initialPosition,
size);
RenderViews(
permanentViewsToRender,
new RenderContext(
driver,
false,
null,
null
),
initialPosition,
size);
if (_applicationContext.FocusManager.Focused is { } focused)
{
focused.SetCursorPosition(driver);
if (!_lastCursorVisible)
{
driver.SetCursorVisible(true);
_lastCursorVisible = true;
}
}
else if (_lastCursorVisible)
{
driver.SetCursorVisible(false);
_lastCursorVisible = false;
}
}
private void RenderViews(List<IView> views, RenderContext renderContext, Position position, Size size)
{
foreach (var view in views)
{
view.Attached = true;
view.GetRequestedSize();
view.Render(renderContext, position, size);
}
}
public void AddViewToPermanentRenderGroup(IView view)
{
lock (_lock)
{
_permanentViewsToRender.Add(view);
}
}
public void AddViewToForcedTemporaryRenderGroup(IView view)
{
lock (_lock)
{
_forcedTemporaryViewsToRender.Add(view);
}
}
}

View File

@@ -0,0 +1,8 @@
using TerminalUI.Controls;
namespace TerminalUI.Traits;
public interface IVisibilityChangeHandler
{
void ChildVisibilityChanged(IView child);
}