New controls, main view

This commit is contained in:
2023-08-09 20:40:54 +02:00
parent d549733b71
commit 7dcca6363b
41 changed files with 668 additions and 234 deletions

View File

@@ -12,7 +12,7 @@ public interface IAppState
IDeclarativeProperty<ITabViewModel?> SelectedTab { get; } IDeclarativeProperty<ITabViewModel?> SelectedTab { get; }
IObservable<string?> SearchText { get; } IObservable<string?> SearchText { get; }
IDeclarativeProperty<ViewMode> ViewMode { get; } IDeclarativeProperty<ViewMode> ViewMode { get; }
DeclarativeProperty<string?> RapidTravelText { get; } IDeclarativeProperty<string?> RapidTravelText { get; }
IDeclarativeProperty<string?> RapidTravelTextDebounced { get; } IDeclarativeProperty<string?> RapidTravelTextDebounced { get; }
IDeclarativeProperty<string?> ContainerStatus { get; } IDeclarativeProperty<string?> ContainerStatus { get; }
List<KeyConfig> PreviousKeys { get; } List<KeyConfig> PreviousKeys { get; }
@@ -24,4 +24,5 @@ public interface IAppState
void SetSearchText(string? searchText); void SetSearchText(string? searchText);
Task SwitchViewModeAsync(ViewMode newViewMode); Task SwitchViewModeAsync(ViewMode newViewMode);
Task SetSelectedTabAsync(ITabViewModel tabToSelect); Task SetSelectedTabAsync(ITabViewModel tabToSelect);
Task SetRapidTravelTextAsync(string? text);
} }

View File

@@ -75,7 +75,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
if (_appState.RapidTravelText.Value!.Length > 0) if (_appState.RapidTravelText.Value!.Length > 0)
{ {
args.Handled = true; args.Handled = true;
await _appState.RapidTravelText.SetValue( await _appState.SetRapidTravelTextAsync(
_appState.RapidTravelText.Value![..^1] _appState.RapidTravelText.Value![..^1]
); );
} }
@@ -83,7 +83,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
else if (keyString.Length == 1) else if (keyString.Length == 1)
{ {
args.Handled = true; args.Handled = true;
await _appState.RapidTravelText.SetValue( await _appState.SetRapidTravelTextAsync(
_appState.RapidTravelText.Value + keyString.ToLower() _appState.RapidTravelText.Value + keyString.ToLower()
); );
} }

View File

@@ -251,7 +251,7 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
if (_currentSelectedItem?.Value is not IContainerViewModel containerViewModel || containerViewModel.Container is null) if (_currentSelectedItem?.Value is not IContainerViewModel containerViewModel || containerViewModel.Container is null)
return; return;
await _appState.RapidTravelText.SetValue(""); await _appState.SetRapidTravelTextAsync("");
if (_selectedTab?.Tab is { } tab) if (_selectedTab?.Tab is { } tab)
{ {
await tab.SetCurrentLocation(containerViewModel.Container); await tab.SetCurrentLocation(containerViewModel.Container);
@@ -260,13 +260,13 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
private async Task GoUp() private async Task GoUp()
{ {
if (_currentLocation?.Value?.Parent is not AbsolutePath parentPath || if (_currentLocation?.Value?.Parent is not { } parentPath ||
await parentPath.ResolveAsyncSafe() is not IContainer newContainer) await parentPath.ResolveAsyncSafe() is not IContainer newContainer)
{ {
return; return;
} }
await _appState.RapidTravelText.SetValue(""); await _appState.SetRapidTravelTextAsync("");
if (_selectedTab?.Tab is { } tab) if (_selectedTab?.Tab is { } tab)
{ {
await tab.SetCurrentLocation(newContainer); await tab.SetCurrentLocation(newContainer);

View File

@@ -18,6 +18,7 @@ public abstract partial class AppStateBase : IAppState
private readonly DeclarativeProperty<ITabViewModel?> _selectedTab = new(); private readonly DeclarativeProperty<ITabViewModel?> _selectedTab = new();
private readonly DeclarativeProperty<ViewMode> _viewMode = new(Models.Enums.ViewMode.Default); private readonly DeclarativeProperty<ViewMode> _viewMode = new(Models.Enums.ViewMode.Default);
private readonly ObservableCollection<ITabViewModel> _tabs = new(); private readonly ObservableCollection<ITabViewModel> _tabs = new();
private readonly DeclarativeProperty<string?> _rapidTravelText;
public IDeclarativeProperty<ViewMode> ViewMode { get; } public IDeclarativeProperty<ViewMode> ViewMode { get; }
@@ -25,7 +26,7 @@ public abstract partial class AppStateBase : IAppState
public IObservable<string?> SearchText { get; } public IObservable<string?> SearchText { get; }
public IDeclarativeProperty<ITabViewModel?> SelectedTab { get; } public IDeclarativeProperty<ITabViewModel?> SelectedTab { get; }
public DeclarativeProperty<string?> RapidTravelText { get; } public IDeclarativeProperty<string?> RapidTravelText { get; }
public IDeclarativeProperty<string?> RapidTravelTextDebounced { get; } public IDeclarativeProperty<string?> RapidTravelTextDebounced { get; }
public IDeclarativeProperty<string?> ContainerStatus { get; } public IDeclarativeProperty<string?> ContainerStatus { get; }
@@ -35,7 +36,8 @@ public abstract partial class AppStateBase : IAppState
protected AppStateBase() protected AppStateBase()
{ {
RapidTravelText = new(""); _rapidTravelText = new ("");
RapidTravelText = _rapidTravelText.DistinctUntilChanged();
RapidTravelTextDebounced = RapidTravelText RapidTravelTextDebounced = RapidTravelText
.Debounce(v => .Debounce(v =>
string.IsNullOrEmpty(v) string.IsNullOrEmpty(v)
@@ -83,6 +85,7 @@ public abstract partial class AppStateBase : IAppState
public async Task SwitchViewModeAsync(ViewMode newViewMode) => await _viewMode.SetValue(newViewMode); public async Task SwitchViewModeAsync(ViewMode newViewMode) => await _viewMode.SetValue(newViewMode);
public async Task SetSelectedTabAsync(ITabViewModel tabToSelect) => await _selectedTab.SetValue(tabToSelect); public async Task SetSelectedTabAsync(ITabViewModel tabToSelect) => await _selectedTab.SetValue(tabToSelect);
public async Task SetRapidTravelTextAsync(string? text) => await _rapidTravelText.SetValue(text);
private ITabViewModel? GetSelectedTab(IEnumerable<ITabViewModel> tabs, ITabViewModel? expectedSelectedTab) private ITabViewModel? GetSelectedTab(IEnumerable<ITabViewModel> tabs, ITabViewModel? expectedSelectedTab)
{ {

View File

@@ -1,11 +1,9 @@
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Models; using FileTime.Core.Models;
using MvvmGen;
namespace FileTime.App.Core.ViewModels; namespace FileTime.App.Core.ViewModels;
[ViewModel(GenerateConstructor = false)]
public partial class ContainerViewModel : ItemViewModel, IContainerViewModel public partial class ContainerViewModel : ItemViewModel, IContainerViewModel
{ {
public IContainer? Container => BaseItem as IContainer; public IContainer? Container => BaseItem as IContainer;

View File

@@ -2,11 +2,9 @@ using DeclarativeProperty;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Models; using FileTime.Core.Models;
using MvvmGen;
namespace FileTime.App.Core.ViewModels; namespace FileTime.App.Core.ViewModels;
[ViewModel(GenerateConstructor = false)]
public partial class ElementViewModel : ItemViewModel, IElementViewModel public partial class ElementViewModel : ItemViewModel, IElementViewModel
{ {
public IElement? Element => BaseItem as Element; public IElement? Element => BaseItem as Element;

View File

@@ -3,11 +3,9 @@ using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Models.Extensions; using FileTime.Core.Models.Extensions;
using MvvmGen;
namespace FileTime.App.Core.ViewModels; namespace FileTime.App.Core.ViewModels;
[ViewModel(GenerateConstructor = false)]
public partial class FileViewModel : ElementViewModel, IFileViewModel public partial class FileViewModel : ElementViewModel, IFileViewModel
{ {
public FileViewModel(IItemNameConverterService itemNameConverterService, IAppState appState) : base(itemNameConverterService, appState) public FileViewModel(IItemNameConverterService itemNameConverterService, IAppState appState) : base(itemNameConverterService, appState)

View File

@@ -74,7 +74,7 @@ public abstract partial class ItemViewModel : IItemViewModel
? parentTab.CurrentSelectedItem ? parentTab.CurrentSelectedItem
.Map(EqualsTo) .Map(EqualsTo)
.DistinctUntilChanged() .DistinctUntilChanged()
.Debounce(TimeSpan.FromMilliseconds(10)) .Debounce(TimeSpan.FromMilliseconds(1))
: new DeclarativeProperty<bool>(IsInDeepestPath()); : new DeclarativeProperty<bool>(IsInDeepestPath());
IsAlternative = sourceCollection IsAlternative = sourceCollection
@@ -86,7 +86,7 @@ public abstract partial class ItemViewModel : IItemViewModel
ViewMode = DeclarativePropertyHelpers ViewMode = DeclarativePropertyHelpers
.CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode) .CombineLatest(IsMarked, IsSelected, IsAlternative, GenerateViewMode)
.DistinctUntilChanged() .DistinctUntilChanged()
.Debounce(TimeSpan.FromMilliseconds(100)); .Debounce(TimeSpan.FromMilliseconds(1));
Attributes = item.Attributes; Attributes = item.Attributes;
CreatedAt = item.CreatedAt; CreatedAt = item.CreatedAt;
ModifiedAt = item.ModifiedAt; ModifiedAt = item.ModifiedAt;

View File

@@ -5,17 +5,9 @@ namespace FileTime.ConsoleUI.App;
public interface ITheme public interface ITheme
{ {
IColor? ItemBackgroundColor { get; }
IColor? AlternativeItemBackgroundColor { get; }
IColor? SelectedItemBackgroundColor { get; }
IColor? MarkedItemBackgroundColor { get; }
IColor? MarkedAlternativeItemBackgroundColor { get; }
IColor? MarkedSelectedItemBackgroundColor { get; }
IColor? DefaultForegroundColor { get; } IColor? DefaultForegroundColor { get; }
IColor? DefaultBackgroundColor { get; } IColor? DefaultBackgroundColor { get; }
IColor? AlternativeItemForegroundColor { get; } IColor? ElementColor { get; }
IColor? SelectedItemForegroundColor { get; } IColor? ContainerColor { get; }
IColor? MarkedItemForegroundColor { get; } IColor? MarkedItemColor { get; }
IColor? MarkedAlternativeItemForegroundColor { get; }
IColor? MarkedSelectedItemForegroundColor { get; }
} }

View File

@@ -1,13 +1,11 @@
using System.Collections.ObjectModel; using DeclarativeProperty;
using System.Linq.Expressions;
using DeclarativeProperty;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.Core.Enums;
using TerminalUI; using TerminalUI;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.ViewExtensions; using TerminalUI.ViewExtensions;
using ConsoleColor = TerminalUI.Color.ConsoleColor; using ConsoleColor = TerminalUI.Color.ConsoleColor;
@@ -18,9 +16,8 @@ public class MainWindow
private readonly IConsoleAppState _consoleAppState; private readonly IConsoleAppState _consoleAppState;
private readonly IApplicationContext _applicationContext; private readonly IApplicationContext _applicationContext;
private readonly ITheme _theme; private readonly ITheme _theme;
private ListView<IAppState, IItemViewModel> _selectedItemsView;
private Grid<object> _grid; private IView _root;
public MainWindow( public MainWindow(
IConsoleAppState consoleAppState, IConsoleAppState consoleAppState,
@@ -34,13 +31,55 @@ public class MainWindow
public void Initialize() public void Initialize()
{ {
_selectedItemsView = new() var root = new Grid<IAppState>
{ {
DataContext = _consoleAppState, DataContext = _consoleAppState,
ApplicationContext = _applicationContext ApplicationContext = _applicationContext,
RowDefinitionsObject = "Auto *",
ChildInitializer =
{
new TextBlock<IAppState>()
.Setup(t =>
t.Bind(
t,
appState => appState.SelectedTab.Value.CurrentLocation.Value.FullName.Path,
tb => tb.Text,
value => value
)
),
new Grid<IAppState>
{
ColumnDefinitionsObject = "* 4* 4*",
ChildInitializer =
{
ParentsItemsView(),
SelectedItemsView(),
SelectedsItemsView(),
},
Extensions =
{
new GridPositionExtension(0, 1)
}
}
}
};
_root = root;
}
private ListView<IAppState, IItemViewModel> SelectedItemsView()
{
var list = new ListView<IAppState, IItemViewModel>
{
DataContext = _consoleAppState,
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(1, 0)
}
}; };
_selectedItemsView.ItemTemplate = item => list.ItemTemplate = item =>
{ {
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>(); var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
textBlock.Bind( textBlock.Bind(
@@ -50,19 +89,120 @@ public class MainWindow
); );
textBlock.Bind( textBlock.Bind(
textBlock, textBlock,
dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value), dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Foreground tb => tb.Foreground
); );
textBlock.Bind(
textBlock,
dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Background
);
return textBlock; return textBlock;
}; };
_selectedItemsView.Bind( list.Bind(
_selectedItemsView, list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(),
v => v.ItemsSource); v => v.ItemsSource);
TestGrid(); list.Bind(
list,
appState =>
appState == null
? null
: appState.SelectedTab.Value == null
? null
: appState.SelectedTab.Value.CurrentSelectedItem.Value,
v => v.SelectedItem);
return list;
}
private ListView<IAppState, IItemViewModel> SelectedsItemsView()
{
var list = new ListView<IAppState, IItemViewModel>
{
DataContext = _consoleAppState,
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(2, 0)
}
};
list.ItemTemplate = item =>
{
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
textBlock.Bind(
textBlock,
dc => dc == null ? string.Empty : dc.DisplayNameText,
tb => tb.Text
);
textBlock.Bind(
textBlock,
dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Foreground
);
textBlock.Bind(
textBlock,
dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Background
);
return textBlock;
};
list.Bind(
list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.SelectedsChildren).Switch(),
v => v.ItemsSource);
return list;
}
private ListView<IAppState, IItemViewModel> ParentsItemsView()
{
var list = new ListView<IAppState, IItemViewModel>
{
DataContext = _consoleAppState,
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(0, 0)
}
};
list.ItemTemplate = item =>
{
var textBlock = item.CreateChild<TextBlock<IItemViewModel>>();
textBlock.Bind(
textBlock,
dc => dc == null ? string.Empty : dc.DisplayNameText,
tb => tb.Text
);
textBlock.Bind(
textBlock,
dc => dc == null ? _theme.DefaultForegroundColor : ToForegroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Foreground
);
textBlock.Bind(
textBlock,
dc => dc == null ? _theme.DefaultBackgroundColor : ToBackgroundColor(dc.ViewMode.Value, dc.BaseItem.Type),
tb => tb.Background
);
return textBlock;
};
list.Bind(
list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.ParentsChildren).Switch(),
v => v.ItemsSource);
return list;
} }
private void TestGrid() private void TestGrid()
@@ -117,35 +257,37 @@ public class MainWindow
} }
}; };
_grid = grid; //_grid = grid;
} }
public IEnumerable<IView> RootViews() => new IView[] public IEnumerable<IView> RootViews() => new IView[]
{ {
_grid, _selectedItemsView _root
}; };
private IColor? ToForegroundColor(ItemViewMode viewMode) private IColor? ToForegroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) =>
=> viewMode switch (viewMode, absolutePathType) switch
{ {
ItemViewMode.Default => _theme.DefaultForegroundColor, (ItemViewMode.Default, AbsolutePathType.Container) => _theme.ContainerColor,
ItemViewMode.Alternative => _theme.AlternativeItemForegroundColor, (ItemViewMode.Alternative, AbsolutePathType.Container) => _theme.ContainerColor,
ItemViewMode.Selected => _theme.SelectedItemForegroundColor, (ItemViewMode.Default, _) => _theme.ElementColor,
ItemViewMode.Marked => _theme.MarkedItemForegroundColor, (ItemViewMode.Alternative, _) => _theme.ElementColor,
ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemForegroundColor, (ItemViewMode.Selected, _) => ToBackgroundColor(ItemViewMode.Default, absolutePathType)?.AsForeground(),
ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemForegroundColor, (ItemViewMode.Marked, _) => _theme.MarkedItemColor,
(ItemViewMode.MarkedSelected, _) => ToBackgroundColor(ItemViewMode.Marked, absolutePathType)?.AsForeground(),
(ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemColor,
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };
private IColor? ToBackgroundColor(ItemViewMode viewMode) private IColor? ToBackgroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType)
=> viewMode switch => (viewMode, absolutePathType) switch
{ {
ItemViewMode.Default => _theme.DefaultBackgroundColor, (ItemViewMode.Default, _) => _theme.DefaultBackgroundColor,
ItemViewMode.Alternative => _theme.AlternativeItemBackgroundColor, (ItemViewMode.Alternative, _) => _theme.DefaultBackgroundColor,
ItemViewMode.Selected => _theme.SelectedItemBackgroundColor, (ItemViewMode.Selected, _) => ToForegroundColor(ItemViewMode.Default, absolutePathType)?.AsBackground(),
ItemViewMode.Marked => _theme.MarkedItemBackgroundColor, (ItemViewMode.Marked, _) => _theme.MarkedItemColor,
ItemViewMode.MarkedSelected => _theme.MarkedSelectedItemBackgroundColor, (ItemViewMode.MarkedSelected, _) => ToForegroundColor(ItemViewMode.Marked, absolutePathType)?.AsBackground(),
ItemViewMode.MarkedAlternative => _theme.MarkedAlternativeItemBackgroundColor, (ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemColor,
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };
} }

View File

@@ -8,6 +8,7 @@ using FileTime.Core.Interactions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using TerminalUI; using TerminalUI;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
@@ -33,7 +34,8 @@ public static class Startup
services.TryAddSingleton<IApplicationContext>(sp services.TryAddSingleton<IApplicationContext>(sp
=> new ApplicationContext => new ApplicationContext
{ {
ConsoleDriver = sp.GetRequiredService<IConsoleDriver>() ConsoleDriver = sp.GetRequiredService<IConsoleDriver>(),
LoggerFactory = sp.GetRequiredService<ILoggerFactory>()
} }
); );
return services; return services;

View File

@@ -1,54 +1,29 @@
using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.Models;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace FileTime.ConsoleUI.Styles; namespace FileTime.ConsoleUI.Styles;
public record Theme( public record Theme(
IColor? ItemBackgroundColor,
IColor? AlternativeItemBackgroundColor,
IColor? SelectedItemBackgroundColor,
IColor? MarkedItemBackgroundColor,
IColor? MarkedAlternativeItemBackgroundColor,
IColor? MarkedSelectedItemBackgroundColor,
IColor? DefaultForegroundColor, IColor? DefaultForegroundColor,
IColor? DefaultBackgroundColor, IColor? DefaultBackgroundColor,
IColor? AlternativeItemForegroundColor, IColor? ElementColor,
IColor? SelectedItemForegroundColor, IColor? ContainerColor,
IColor? MarkedItemForegroundColor, IColor? MarkedItemColor) : ITheme;
IColor? MarkedAlternativeItemForegroundColor,
IColor? MarkedSelectedItemForegroundColor) : ITheme;
public static class DefaultThemes public static class DefaultThemes
{ {
public static Theme Color256Theme => new( public static Theme Color256Theme => new(
ItemBackgroundColor: Color256Colors.Backgrounds.Black, DefaultForegroundColor: Color256Colors.Foregrounds.Gray,
AlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black, DefaultBackgroundColor: Color256Colors.Foregrounds.Black,
SelectedItemBackgroundColor: Color256Colors.Backgrounds.Black, ElementColor: Color256Colors.Foregrounds.Gray,
MarkedItemBackgroundColor: Color256Colors.Backgrounds.Black, ContainerColor: Color256Colors.Foregrounds.Blue,
MarkedAlternativeItemBackgroundColor: Color256Colors.Backgrounds.Black, MarkedItemColor: Color256Colors.Foregrounds.Black
MarkedSelectedItemBackgroundColor: Color256Colors.Backgrounds.Black, );
DefaultForegroundColor: null,
DefaultBackgroundColor: null,
AlternativeItemForegroundColor: null,
SelectedItemForegroundColor: Color256Colors.Foregrounds.Black,
MarkedItemForegroundColor: Color256Colors.Foregrounds.White,
MarkedAlternativeItemForegroundColor: Color256Colors.Foregrounds.White,
MarkedSelectedItemForegroundColor: Color256Colors.Foregrounds.Cyan);
public static Theme ConsoleColorTheme => new( public static Theme ConsoleColorTheme => new(
ItemBackgroundColor: ConsoleColors.Foregrounds.Black, DefaultForegroundColor: ConsoleColors.Foregrounds.Gray,
AlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black, DefaultBackgroundColor: ConsoleColors.Foregrounds.Black,
SelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black, ElementColor: ConsoleColors.Foregrounds.Gray,
MarkedItemBackgroundColor: ConsoleColors.Foregrounds.Black, ContainerColor: ConsoleColors.Foregrounds.Blue,
MarkedAlternativeItemBackgroundColor: ConsoleColors.Foregrounds.Black, MarkedItemColor: ConsoleColors.Foregrounds.Black);
MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Black,
DefaultForegroundColor: null,
DefaultBackgroundColor: null,
AlternativeItemForegroundColor: null,
SelectedItemForegroundColor: ConsoleColors.Foregrounds.Black,
MarkedItemForegroundColor: ConsoleColors.Foregrounds.White,
MarkedAlternativeItemForegroundColor: ConsoleColors.Foregrounds.White,
MarkedSelectedItemForegroundColor: ConsoleColors.Foregrounds.Cyan);
} }

View File

@@ -23,7 +23,6 @@ public class Tab : ITab
private AbsolutePath? _currentSelectedItemCached; private AbsolutePath? _currentSelectedItemCached;
private PointInTime _currentPointInTime; private PointInTime _currentPointInTime;
private CancellationTokenSource? _setCurrentLocationCancellationTokenSource; private CancellationTokenSource? _setCurrentLocationCancellationTokenSource;
private CancellationTokenSource? _setCurrentItemCancellationTokenSource;
public IDeclarativeProperty<IContainer?> CurrentLocation { get; } public IDeclarativeProperty<IContainer?> CurrentLocation { get; }
public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; } public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; }
@@ -214,9 +213,8 @@ public class Tab : ITab
public async Task SetSelectedItem(AbsolutePath newSelectedItem) public async Task SetSelectedItem(AbsolutePath newSelectedItem)
{ {
_setCurrentItemCancellationTokenSource?.Cancel(); if (_currentRequestItem.Value is {} v && v.Path == newSelectedItem.Path) return;
_setCurrentItemCancellationTokenSource = new CancellationTokenSource(); await _currentRequestItem.SetValue(newSelectedItem);
await _currentRequestItem.SetValue(newSelectedItem, _setCurrentItemCancellationTokenSource.Token);
} }
public void AddItemFilter(ItemFilter filter) => _itemFilters.Add(filter); public void AddItemFilter(ItemFilter filter) => _itemFilters.Add(filter);

View File

@@ -1,10 +1,12 @@
using TerminalUI.ConsoleDrivers; using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
namespace TerminalUI; namespace TerminalUI;
public class ApplicationContext : IApplicationContext public class ApplicationContext : IApplicationContext
{ {
public required IConsoleDriver ConsoleDriver { get; init; } public required IConsoleDriver ConsoleDriver { get; init; }
public ILoggerFactory? LoggerFactory { get; init; }
public IEventLoop EventLoop { get; init; } public IEventLoop EventLoop { get; init; }
public bool IsRunning { get; set; } public bool IsRunning { get; set; }

View File

@@ -6,30 +6,38 @@ using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;
public class Binding<TDataContext, TResult> : IDisposable public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
{ {
private readonly Func<TDataContext, TResult> _dataContextMapper; private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
private IView<TDataContext> _dataSourceView; private IView<TDataContext> _dataSourceView;
private object? _propertySource; private object? _propertySource;
private PropertyInfo _targetProperty; private PropertyInfo _targetProperty;
private readonly Func<TExpressionResult, TResult> _converter;
private readonly TResult? _fallbackValue;
private IDisposableCollection? _propertySourceDisposableCollection; private IDisposableCollection? _propertySourceDisposableCollection;
private PropertyTrackTreeItem? _propertyTrackTreeItem; private PropertyTrackTreeItem? _propertyTrackTreeItem;
private IPropertyChangeTracker? _propertyChangeTracker; private IPropertyChangeTracker? _propertyChangeTracker;
public Binding( public Binding(
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TResult>> dataContextExpression, Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression,
object? propertySource, object? propertySource,
PropertyInfo targetProperty PropertyInfo targetProperty,
Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default
) )
{ {
ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceView);
ArgumentNullException.ThrowIfNull(dataContextExpression); ArgumentNullException.ThrowIfNull(dataContextExpression);
ArgumentNullException.ThrowIfNull(targetProperty); ArgumentNullException.ThrowIfNull(targetProperty);
ArgumentNullException.ThrowIfNull(converter);
_dataSourceView = dataSourceView; _dataSourceView = dataSourceView;
_dataContextMapper = dataContextExpression.Compile(); _dataContextMapper = dataContextExpression.Compile();
_propertySource = propertySource; _propertySource = propertySource;
_targetProperty = targetProperty; _targetProperty = targetProperty;
_converter = converter;
_fallbackValue = fallbackValue;
InitTrackingTree(dataContextExpression); InitTrackingTree(dataContextExpression);
@@ -52,7 +60,7 @@ public class Binding<TDataContext, TResult> : IDisposable
} }
} }
private void InitTrackingTree(Expression<Func<TDataContext?, TResult>> dataContextExpression) private void InitTrackingTree(Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression)
{ {
var properties = new List<string>(); var properties = new List<string>();
FindReactiveProperties(dataContextExpression, properties); FindReactiveProperties(dataContextExpression, properties);
@@ -175,7 +183,19 @@ public class Binding<TDataContext, TResult> : IDisposable
} }
private void UpdateTargetProperty() private void UpdateTargetProperty()
=> _targetProperty.SetValue(_propertySource, _dataContextMapper(_dataSourceView.DataContext)); {
TResult value;
try
{
value = _converter(_dataContextMapper(_dataSourceView.DataContext));
}
catch
{
value = _fallbackValue;
}
_targetProperty.SetValue(_propertySource, value);
}
public void Dispose() public void Dispose()
{ {

View File

@@ -11,4 +11,8 @@ public record struct Color256(byte Color, ColorType Type) : IColor
ColorType.Background => $"\x1b[48;5;{Color}m", ColorType.Background => $"\x1b[48;5;{Color}m",
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
}; };
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
} }

View File

@@ -11,4 +11,7 @@ public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor
ColorType.Background => $"\x1b[48;2;{R};{G};{B};m", ColorType.Background => $"\x1b[48;2;{R};{G};{B};m",
_ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType)) _ => throw new InvalidEnumArgumentException(nameof(Type), (int) Type, typeof(ColorType))
}; };
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
} }

View File

@@ -3,4 +3,7 @@
public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor
{ {
public string ToConsoleColor() => throw new NotImplementedException(); public string ToConsoleColor() => throw new NotImplementedException();
public IColor AsForeground() => this with {Type = ColorType.Foreground};
public IColor AsBackground() => this with {Type = ColorType.Background};
} }

View File

@@ -4,4 +4,6 @@ public interface IColor
{ {
ColorType Type { get; } ColorType Type { get; }
string ToConsoleColor(); string ToConsoleColor();
IColor AsForeground();
IColor AsBackground();
} }

View File

@@ -44,6 +44,6 @@ public class DotnetDriver : IConsoleDriver
Console.BackgroundColor = consoleColor.Color; Console.BackgroundColor = consoleColor.Color;
} }
public Size GetBufferSize() => new(Console.BufferWidth, Console.BufferHeight); public Size GetWindowSize() => new(Console.WindowWidth, Console.WindowHeight);
public void Clear() => Console.Clear(); public void Clear() => Console.Clear();
} }

View File

@@ -17,6 +17,6 @@ public interface IConsoleDriver
void SetCursorVisible(bool cursorVisible); void SetCursorVisible(bool cursorVisible);
void SetForegroundColor(IColor foreground); void SetForegroundColor(IColor foreground);
void SetBackgroundColor(IColor background); void SetBackgroundColor(IColor background);
Size GetBufferSize(); Size GetWindowSize();
void Clear(); void Clear();
} }

View File

@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
{
private readonly ObservableCollection<IView> _children = new();
public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; }
protected ChildContainerView()
{
ChildInitializer = new ChildInitializer<T>(this);
Children = new ReadOnlyObservableCollection<IView>(_children);
_children.CollectionChanged += (o, args) =>
{
if (Attached)
{
if (args.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
}
};
((INotifyPropertyChanged)this).PropertyChanged += (o, args) =>
{
if (args.PropertyName == nameof(ApplicationContext))
{
foreach (var child in Children)
{
child.ApplicationContext = ApplicationContext;
}
}
};
}
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);
_children.Add(child);
return child;
}
public override TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TDataContext : default
{
child = base.AddChild(child, dataContextMapper);
_children.Add(child);
return child;
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections;
namespace TerminalUI.Controls;
public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper);
public class ChildInitializer<T> : IEnumerable<IView>
{
private readonly IChildContainer<T> _childContainer;
public ChildInitializer(IChildContainer<T> childContainer)
{
_childContainer = childContainer;
}
public void Add(IView<T> item) => _childContainer.AddChild(item);
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _childContainer.AddChild(item.Child, item.DataContextMapper);
public IEnumerator<IView> GetEnumerator() => _childContainer.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -5,14 +5,15 @@ using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public class Grid<T> : View<T> public class Grid<T> : ChildContainerView<T>
{ {
private delegate void WithSizes(Span<int> widths, Span<int> heights);
private delegate TResult WithSizes<TResult>(Span<int> widths, Span<int> heights);
private const int ToBeCalculated = -1; private const int ToBeCalculated = -1;
private readonly ObservableCollection<IView> _children = new(); public ObservableCollection<RowDefinition> RowDefinitions { get; } = new() {RowDefinition.Star(1)};
public ReadOnlyObservableCollection<IView> Children { get; } public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)};
public GridChildInitializer<T> ChildInitializer { get; }
public ObservableCollection<RowDefinition> RowDefinitions { get; } = new();
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new();
public object? ColumnDefinitionsObject public object? ColumnDefinitionsObject
{ {
@@ -62,97 +63,30 @@ public class Grid<T> : View<T>
} }
} }
public Grid() public override Size GetRequestedSize()
=> WithCalculatedSize((columnWidths, rowHeights) =>
{ {
ChildInitializer = new GridChildInitializer<T>(this); var width = 0;
Children = new ReadOnlyObservableCollection<IView>(_children); var height = 0;
_children.CollectionChanged += (o, e) =>
{
if (Attached)
{
if (e.NewItems?.OfType<IView>() is { } newItems)
{
foreach (var newItem in newItems)
{
newItem.Attached = true;
}
}
ApplicationContext?.EventLoop.RequestRerender();
}
};
}
public override Size GetRequestedSize() => throw new NotImplementedException();
protected override void DefaultRenderer(Position position, Size size)
{
//TODO: Optimize it, dont calculate all of these only if there is Auto value(s)
var columns = ColumnDefinitions.Count;
Span<int> allWidth = stackalloc int[columns * RowDefinitions.Count];
Span<int> allHeight = stackalloc int[columns * RowDefinitions.Count];
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns);
}
Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[RowDefinitions.Count];
for (var i = 0; i < columnWidths.Length; i++) for (var i = 0; i < columnWidths.Length; i++)
{ {
if (ColumnDefinitions[i].Type == GridUnitType.Pixel) width += columnWidths[i];
{
columnWidths[i] = ColumnDefinitions[i].Value;
}
else if (ColumnDefinitions[i].Type == GridUnitType.Star)
{
columnWidths[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < RowDefinitions.Count; j++)
{
max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns));
}
columnWidths[i] = max;
}
} }
for (var i = 0; i < rowHeights.Length; i++) for (var i = 0; i < rowHeights.Length; i++)
{ {
if (RowDefinitions[i].Type == GridUnitType.Pixel) height += rowHeights[i];
{
rowHeights[i] = RowDefinitions[i].Value;
}
else if (RowDefinitions[i].Type == GridUnitType.Star)
{
rowHeights[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < columns; j++)
{
max = Math.Max(max, allHeight.GetFromMatrix(j, i, columns));
} }
rowHeights[i] = max; return new Size(width, height);
} }, new Option<Size>(new Size(0, 0), false));
}
protected override void DefaultRenderer(Position position, Size size)
=> WithCalculatedSize((columnWidths, rowHeights) =>
{
foreach (var child in Children) foreach (var child in Children)
{ {
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>(); var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0; var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0; var y = positionExtension?.Row ?? 0;
@@ -173,8 +107,131 @@ public class Grid<T> : View<T>
top += rowHeights[i]; top += rowHeights[i];
} }
child.Render(new Position(left, top), new Size(width, height)); child.Render(new Position(position.X + left, position.Y + top), new Size(width, height));
} }
}, new Option<Size>(size, true));
private void WithCalculatedSize(WithSizes actionWithSizes, Option<Size> size)
{
WithCalculatedSize(Helper, size);
object? Helper(Span<int> widths, Span<int> heights)
{
actionWithSizes(widths, heights);
return null;
}
}
private TResult WithCalculatedSize<TResult>(WithSizes<TResult> actionWithSizes, Option<Size> size)
{
//TODO: Optimize it, dont calculate all of these, only if there is Auto value(s)
var columns = ColumnDefinitions.Count;
var rows = RowDefinitions.Count;
if (columns < 1) columns = 1;
if (rows < 1) rows = 1;
Span<int> allWidth = stackalloc int[columns * rows];
Span<int> allHeight = stackalloc int[columns * rows];
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns);
}
Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[rows];
var usedWidth = 0;
var widthStars = 0;
for (var i = 0; i < columnWidths.Length; i++)
{
if (ColumnDefinitions[i].Type == GridUnitType.Pixel)
{
columnWidths[i] = ColumnDefinitions[i].Value;
}
else if (size.IsSome && ColumnDefinitions[i].Type == GridUnitType.Star)
{
widthStars += ColumnDefinitions[i].Value;
columnWidths[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < rows; j++)
{
max = Math.Max(max, allWidth.GetFromMatrix(i, j, columns));
}
columnWidths[i] = max;
}
if (columnWidths[i] != ToBeCalculated)
usedWidth += columnWidths[i];
}
var usedHeight = 0;
var heightStars = 0;
for (var i = 0; i < rowHeights.Length; i++)
{
if (RowDefinitions[i].Type == GridUnitType.Pixel)
{
rowHeights[i] = RowDefinitions[i].Value;
}
else if (size.IsSome && RowDefinitions[i].Type == GridUnitType.Star)
{
heightStars += RowDefinitions[i].Value;
rowHeights[i] = ToBeCalculated;
}
else
{
var max = 0;
for (var j = 0; j < columns; j++)
{
max = Math.Max(max, allHeight.GetFromMatrix(j, i, columns));
}
rowHeights[i] = max;
}
if (rowHeights[i] != ToBeCalculated)
usedHeight += rowHeights[i];
}
if (size.IsSome)
{
var widthLeft = size.Value.Width - usedWidth;
var heightLeft = size.Value.Height - usedHeight;
var widthPerStart = (int) Math.Floor((double) widthLeft / widthStars);
var heightPerStart = (int) Math.Floor((double) heightLeft / heightStars);
for (var i = 0; i < columnWidths.Length; i++)
{
var column = ColumnDefinitions[i];
if (column.Type == GridUnitType.Star)
{
columnWidths[i] = widthPerStart * column.Value;
}
}
for (var i = 0; i < rowHeights.Length; i++)
{
var row = RowDefinitions[i];
if (row.Type == GridUnitType.Star)
{
rowHeights[i] = heightPerStart * row.Value;
}
}
}
return actionWithSizes(columnWidths, rowHeights);
} }
public void SetRowDefinitions(string value) public void SetRowDefinitions(string value)
@@ -190,7 +247,7 @@ public class Grid<T> : View<T>
} }
else if (v.EndsWith("*")) else if (v.EndsWith("*"))
{ {
var starValue = int.Parse(v[0..^1]); var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
RowDefinitions.Add(RowDefinition.Star(starValue)); RowDefinitions.Add(RowDefinition.Star(starValue));
} }
else if (int.TryParse(v, out var pixelValue)) else if (int.TryParse(v, out var pixelValue))
@@ -217,7 +274,7 @@ public class Grid<T> : View<T>
} }
else if (v.EndsWith("*")) else if (v.EndsWith("*"))
{ {
var starValue = int.Parse(v[0..^1]); var starValue = v.Length == 1 ? 1 : int.Parse(v[..^1]);
ColumnDefinitions.Add(ColumnDefinition.Star(starValue)); ColumnDefinitions.Add(ColumnDefinition.Star(starValue));
} }
else if (int.TryParse(v, out var pixelValue)) else if (int.TryParse(v, out var pixelValue))
@@ -230,19 +287,4 @@ public class Grid<T> : View<T>
} }
} }
} }
public override TChild AddChild<TChild>(TChild child)
{
child = base.AddChild(child);
_children.Add(child);
return child;
}
public override TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TDataContext : default
{
child = base.AddChild(child, dataContextMapper);
_children.Add(child);
return child;
}
} }

View File

@@ -1,24 +0,0 @@
using System.Collections;
namespace TerminalUI.Controls;
public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper);
public class GridChildInitializer<T> : IEnumerable<IView>
{
private readonly Grid<T> _grid;
public GridChildInitializer(Grid<T> grid)
{
_grid = grid;
}
public void Add(IView<T> item) => _grid.AddChild(item);
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _grid.AddChild(item.Child, item.DataContextMapper);
public IEnumerator<IView> GetEnumerator() => _grid.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -0,0 +1,8 @@
using System.Collections.ObjectModel;
namespace TerminalUI.Controls;
public interface IChildContainer<T> : IView<T>
{
ReadOnlyObservableCollection<IView> Children { get; }
}

View File

@@ -39,8 +39,8 @@ public interface IView<T> : IView
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper) TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new(); where TChild : IView<TDataContext>, new();
TChild AddChild<TChild>(TChild child) where TChild : IView<T>; public TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper) public TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>; where TChild : IView<TDataContext>;
} }

View File

@@ -1,12 +1,12 @@
using System.Buffers; using System.Buffers;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using DeclarativeProperty; using DeclarativeProperty;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models; using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public class ListView<TDataContext, TItem> : View<TDataContext> public partial class ListView<TDataContext, TItem> : View<TDataContext>
{ {
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared; private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared;
@@ -18,6 +18,8 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
private int _selectedIndex = 0; private int _selectedIndex = 0;
private int _renderStartIndex = 0; private int _renderStartIndex = 0;
private Size _requestedItemSize = new(0, 0); private Size _requestedItemSize = new(0, 0);
[Notify] private int _listPadding = 0;
[Notify] private Orientation _orientation = Orientation.Vertical;
public int SelectedIndex public int SelectedIndex
{ {
@@ -28,7 +30,34 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
{ {
_selectedIndex = value; _selectedIndex = value;
OnPropertyChanged(); OnPropertyChanged();
ApplicationContext?.EventLoop.RequestRerender(); OnPropertyChanged(nameof(SelectedItem));
}
}
}
public TItem? SelectedItem
{
get => _listViewItems is null ? default : _listViewItems[_selectedIndex].DataContext;
set
{
if (_listViewItems is null || value is null) return;
var newSelectedIndex = -1;
for (var i = 0; i < _listViewItemLength; i++)
{
var dataContext = _listViewItems[i].DataContext;
if (dataContext is null) continue;
if (dataContext.Equals(value))
{
newSelectedIndex = i;
break;
}
}
if (newSelectedIndex != -1)
{
SelectedIndex = newSelectedIndex;
} }
} }
} }
@@ -76,60 +105,87 @@ public class ListView<TDataContext, TItem> : View<TDataContext>
_listViewItems = null; _listViewItems = null;
} }
_renderStartIndex = 0;
SelectedIndex = 0;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate;
public ListView()
{
RerenderProperties.Add(nameof(ItemsSource));
RerenderProperties.Add(nameof(SelectedIndex));
RerenderProperties.Add(nameof(Orientation));
}
public override Size GetRequestedSize() public override Size GetRequestedSize()
{ {
if (_listViewItems is null || _listViewItems.Length == 0) InstantiateItemViews();
if (_listViewItems is null || _listViewItemLength == 0)
return new Size(0, 0); return new Size(0, 0);
var itemSize = _listViewItems[0].GetRequestedSize(); var itemSize = _listViewItems[0].GetRequestedSize();
_requestedItemSize = itemSize; _requestedItemSize = itemSize;
return itemSize with {Height = itemSize.Height * _listViewItems.Length}; return itemSize with {Height = itemSize.Height * _listViewItemLength};
} }
protected override void DefaultRenderer(Position position, Size size) protected override void DefaultRenderer(Position position, Size size)
{ {
var requestedItemSize = _requestedItemSize;
if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0)
return;
var listViewItems = InstantiateItemViews(); var listViewItems = InstantiateItemViews();
if (listViewItems.Length == 0) return; if (listViewItems.Length == 0) return;
var requestedItemSize = _requestedItemSize;
var itemsToRender = listViewItems.Length; var itemsToRender = listViewItems.Length;
var heightNeeded = requestedItemSize.Height * listViewItems.Length; var heightNeeded = requestedItemSize.Height * listViewItems.Length;
var renderStartIndex = _renderStartIndex; var renderStartIndex = _renderStartIndex;
if (heightNeeded < size.Height) if (heightNeeded > size.Height)
{ {
var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height); var maxItemsToRender = (int) Math.Floor((double) size.Height / requestedItemSize.Height);
if (SelectedIndex < renderStartIndex) itemsToRender = maxItemsToRender;
if (SelectedIndex - ListPadding < renderStartIndex)
{ {
renderStartIndex = SelectedIndex - 1; renderStartIndex = SelectedIndex - ListPadding;
} }
else if (SelectedIndex > renderStartIndex + maxItemsToRender) else if (SelectedIndex + ListPadding >= renderStartIndex + maxItemsToRender)
{ {
renderStartIndex = SelectedIndex - maxItemsToRender + 1; renderStartIndex = SelectedIndex + ListPadding - maxItemsToRender + 1;
} }
if(renderStartIndex < 0) if (renderStartIndex + itemsToRender > listViewItems.Length)
renderStartIndex = listViewItems.Length - itemsToRender;
if (renderStartIndex < 0)
renderStartIndex = 0; renderStartIndex = 0;
else if (renderStartIndex + maxItemsToRender > listViewItems.Length)
renderStartIndex = listViewItems.Length - maxItemsToRender;
_renderStartIndex = renderStartIndex; _renderStartIndex = renderStartIndex;
} }
var deltaY = 0; var deltaY = 0;
for (var i = renderStartIndex; i < itemsToRender && i < listViewItems.Length; i++) var lastItemIndex = renderStartIndex + itemsToRender;
if (lastItemIndex > listViewItems.Length)
lastItemIndex = listViewItems.Length;
for (var i = renderStartIndex; i < lastItemIndex; i++)
{ {
var item = listViewItems[i]; var item = listViewItems[i];
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize); item.Render(position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width});
deltaY += requestedItemSize.Height; deltaY += requestedItemSize.Height;
} }
var driver = ApplicationContext!.ConsoleDriver;
var placeholder = new string(' ', size.Width);
driver.ResetColor();
for (var i = deltaY; i < size.Height; i++)
{
driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(placeholder);
}
} }
private Span<ListViewItem<TItem>> InstantiateItemViews() private Span<ListViewItem<TItem>> InstantiateItemViews()

View File

@@ -0,0 +1,54 @@
using System.Collections.ObjectModel;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class StackPanel<T> : ChildContainerView<T>
{
private readonly Dictionary<IView, Size> _requestedSizes = new();
[Notify] private Orientation _orientation = Orientation.Vertical;
public override Size GetRequestedSize()
{
_requestedSizes.Clear();
var width = 0;
var height = 0;
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
_requestedSizes.Add(child, childSize);
if (Orientation == Orientation.Vertical)
{
width = Math.Max(width, childSize.Width);
height += childSize.Height;
}
else
{
width += childSize.Width;
height = Math.Max(height, childSize.Height);
}
}
return new Size(width, height);
}
protected override void DefaultRenderer(Position position, Size size)
{
var delta = 0;
foreach (var child in Children)
{
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
var childPosition = Orientation == Orientation.Vertical
? position with {Y = position.Y + delta}
: position with {X = position.X + delta};
child.Render(childPosition, childSize);
delta += Orientation == Orientation.Vertical
? childSize.Height
: childSize.Width;
}
}
}

View File

@@ -14,6 +14,7 @@ public partial class TextBlock<T> : View<T>
[Notify] private string? _text = string.Empty; [Notify] private string? _text = string.Empty;
[Notify] private IColor? _foreground; [Notify] private IColor? _foreground;
[Notify] private IColor? _background; [Notify] private IColor? _background;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left;
public TextBlock() public TextBlock()
{ {
@@ -26,12 +27,15 @@ public partial class TextBlock<T> : View<T>
RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(Foreground)); RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background)); RerenderProperties.Add(nameof(Background));
RerenderProperties.Add(nameof(TextAlignment));
} }
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1); public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1);
protected override void DefaultRenderer(Position position, Size size) protected override void DefaultRenderer(Position position, Size size)
{ {
if (size.Width == 0 || size.Height == 0) return;
var driver = ApplicationContext!.ConsoleDriver; var driver = ApplicationContext!.ConsoleDriver;
var renderContext = new RenderContext(position, Text, _foreground, _background); var renderContext = new RenderContext(position, Text, _foreground, _background);
if (!NeedsRerender(renderContext)) return; if (!NeedsRerender(renderContext)) return;
@@ -52,7 +56,17 @@ public partial class TextBlock<T> : View<T>
driver.SetBackgroundColor(background); driver.SetBackgroundColor(background);
} }
driver.Write(Text); var text = TextAlignment switch
{
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", Text),
_ => string.Format($"{{0,{-size.Width}}}", Text)
};
if (text.Length > size.Width)
{
text = text[..size.Width];
}
driver.Write(text);
} }
private bool NeedsRerender(RenderContext renderContext) private bool NeedsRerender(RenderContext renderContext)

View File

@@ -16,6 +16,7 @@ public abstract partial class View<T> : IView<T>
[Notify] private int? _minHeight; [Notify] private int? _minHeight;
[Notify] private int? _maxHeight; [Notify] private int? _maxHeight;
[Notify] private int? _height; [Notify] private int? _height;
[Notify] private IApplicationContext? _applicationContext;
private bool _attached; private bool _attached;
public bool Attached public bool Attached
@@ -33,7 +34,6 @@ public abstract partial class View<T> : IView<T>
} }
public List<object> Extensions { get; } = new(); public List<object> Extensions { get; } = new();
public Action<Position, Size> RenderMethod { get; set; } public Action<Position, Size> RenderMethod { get; set; }
public IApplicationContext? ApplicationContext { get; set; }
public event Action<IView>? Disposed; public event Action<IView>? Disposed;
protected List<string> RerenderProperties { get; } = new(); protected List<string> RerenderProperties { get; } = new();

View File

@@ -41,14 +41,14 @@ public class EventLoop : IEventLoop
{ {
if (!_rerenderRequested) return; if (!_rerenderRequested) return;
_rerenderRequested = false; _rerenderRequested = false;
viewsToRender = _viewsToRender.ToList(); viewsToRender = _viewsToRender.ToList();
} }
var size =_applicationContext.ConsoleDriver.GetBufferSize(); var size = _applicationContext.ConsoleDriver.GetWindowSize();
foreach (var view in viewsToRender) foreach (var view in viewsToRender)
{ {
view.Attached = true; view.Attached = true;
view.GetRequestedSize();
view.Render(new Position(0, 0), size); view.Render(new Position(0, 0), size);
} }
} }

View File

@@ -6,20 +6,44 @@ namespace TerminalUI.Extensions;
public static class Binding public static class Binding
{ {
public static Binding<TDataContext, TResult> Bind<TView, TDataContext, TResult>( public static Binding<TDataContext, TResult, TResult> Bind<TView, TDataContext, TResult>(
this TView targetView, this TView targetView,
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TResult>> dataContextExpression, Expression<Func<TDataContext?, TResult>> dataContextExpression,
Expression<Func<TView, TResult>> propertyExpression) Expression<Func<TView, TResult>> propertyExpression,
TResult? fallbackValue = default)
{ {
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo}) if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
throw new AggregateException(nameof(propertyExpression) + " must be a property expression"); throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
return new Binding<TDataContext, TResult>( return new Binding<TDataContext, TResult, TResult>(
dataSourceView, dataSourceView,
dataContextExpression, dataContextExpression,
targetView, targetView,
propertyInfo propertyInfo,
value => value,
fallbackValue
);
}
public static Binding<TDataContext, TExpressionResult, TResult> Bind<TView, TDataContext, TExpressionResult, TResult>(
this TView targetView,
IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression,
Expression<Func<TView, TResult>> propertyExpression,
Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default)
{
if (propertyExpression.Body is not MemberExpression {Member: PropertyInfo propertyInfo})
throw new AggregateException(nameof(propertyExpression) + " must be a property expression");
return new Binding<TDataContext, TExpressionResult, TResult>(
dataSourceView,
dataContextExpression,
targetView,
propertyInfo,
converter,
fallbackValue
); );
} }
} }

View File

@@ -11,4 +11,10 @@ public static class ViewExtensions
this IView<TTargetDataContext> view, this IView<TTargetDataContext> view,
Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper) Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper)
=> new(view, dataContextMapper); => new(view, dataContextMapper);
public static TView Setup<TView>(this TView view, Action<TView> action)
{
action(view);
return view;
}
} }

View File

@@ -1,4 +1,5 @@
using TerminalUI.ConsoleDrivers; using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers;
namespace TerminalUI; namespace TerminalUI;
@@ -7,4 +8,5 @@ public interface IApplicationContext
IEventLoop EventLoop { get; init; } IEventLoop EventLoop { get; init; }
bool IsRunning { get; set; } bool IsRunning { get; set; }
IConsoleDriver ConsoleDriver { get; init; } IConsoleDriver ConsoleDriver { get; init; }
ILoggerFactory? LoggerFactory { get; init; }
} }

View File

@@ -0,0 +1,13 @@
namespace TerminalUI.Models;
public readonly ref struct Option<T>
{
public readonly T Value;
public readonly bool IsSome;
public Option(T value, bool isSome)
{
Value = value;
IsSome = isSome;
}
}

View File

@@ -0,0 +1,7 @@
namespace TerminalUI.Models;
public enum Orientation
{
Horizontal,
Vertical
}

View File

@@ -1,3 +1,3 @@
namespace TerminalUI.Models; namespace TerminalUI.Models;
public record Size(int Width, int Height); public record struct Size(int Width, int Height);

View File

@@ -0,0 +1,8 @@
namespace TerminalUI.Models;
public enum TextAlignment
{
Left,
Center,
Right
}

View File

@@ -11,6 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8"> <PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,3 +1,3 @@
namespace TerminalUI.ViewExtensions; namespace TerminalUI.ViewExtensions;
public record GridPositionExtension(int Row, int Column); public record GridPositionExtension(int Column, int Row);