Console upgrades, PossibleCommands VM

This commit is contained in:
2023-08-10 22:54:52 +02:00
parent 96d4eb926d
commit e989a65e81
48 changed files with 983 additions and 400 deletions

View File

@@ -14,7 +14,7 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
private readonly ICommandPaletteService _commandPaletteService; private readonly ICommandPaletteService _commandPaletteService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly IKeyboardConfigurationService _keyboardConfigurationService; private readonly ICommandKeysHelperService _commandKeysHelperService;
private readonly ILogger<CommandPaletteViewModel> _logger; private readonly ILogger<CommandPaletteViewModel> _logger;
string IModalViewModel.Name => "CommandPalette"; string IModalViewModel.Name => "CommandPalette";
@@ -22,14 +22,14 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
ICommandPaletteService commandPaletteService, ICommandPaletteService commandPaletteService,
IIdentifiableUserCommandService identifiableUserCommandService, IIdentifiableUserCommandService identifiableUserCommandService,
IUserCommandHandlerService userCommandHandlerService, IUserCommandHandlerService userCommandHandlerService,
IKeyboardConfigurationService keyboardConfigurationService, ICommandKeysHelperService commandKeysHelperService,
ILogger<CommandPaletteViewModel> logger) ILogger<CommandPaletteViewModel> logger)
: base((a, b) => a.Identifier == b.Identifier) : base((a, b) => a.Identifier == b.Identifier)
{ {
_commandPaletteService = commandPaletteService; _commandPaletteService = commandPaletteService;
_identifiableUserCommandService = identifiableUserCommandService; _identifiableUserCommandService = identifiableUserCommandService;
_userCommandHandlerService = userCommandHandlerService; _userCommandHandlerService = userCommandHandlerService;
_keyboardConfigurationService = keyboardConfigurationService; _commandKeysHelperService = commandKeysHelperService;
_logger = logger; _logger = logger;
ShowWindow = _commandPaletteService.ShowWindow; ShowWindow = _commandPaletteService.ShowWindow;
UpdateFilteredMatchesInternal(); UpdateFilteredMatchesInternal();
@@ -47,47 +47,10 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryV
|| c.Identifier.Contains(SearchText, StringComparison.OrdinalIgnoreCase) || c.Identifier.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
) )
.Select(c => .Select(c =>
(ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title, GetKeyConfigsString(c.Identifier)) (ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title, _commandKeysHelperService.GetKeyConfigsString(c.Identifier))
) )
.ToList(); .ToList();
private string GetKeyConfigsString(string commandIdentifier)
{
var keyConfigs = GetKeyConfigsForCommand(commandIdentifier);
if (keyConfigs.Count == 0) return string.Empty;
return string.Join(
" ; ",
keyConfigs
.Select(ks =>
string.Join(
", ",
ks.Select(FormatKeyConfig)
)
)
);
}
private string FormatKeyConfig(KeyConfig keyConfig)
{
var stringBuilder = new StringBuilder();
if (keyConfig.Ctrl) stringBuilder.Append("Ctrl + ");
if (keyConfig.Shift) stringBuilder.Append("Shift + ");
if (keyConfig.Alt) stringBuilder.Append("Alt + ");
stringBuilder.Append(keyConfig.Key.ToString());
return stringBuilder.ToString();
}
private List<List<KeyConfig>> GetKeyConfigsForCommand(string commandIdentifier)
=> _keyboardConfigurationService
.AllShortcut
.Where(s => s.Command == commandIdentifier)
.Select(k => k.Keys)
.ToList();
public override async Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs) public override async Task<bool> HandleKeyDown(GeneralKeyEventArgs keyEventArgs)
{ {
if (keyEventArgs.Handled) return false; if (keyEventArgs.Handled) return false;

View File

@@ -0,0 +1,10 @@
using FileTime.App.Core.Configuration;
namespace FileTime.App.Core.Services;
public interface ICommandKeysHelperService
{
List<List<KeyConfig>> GetKeysForCommand(string commandName);
string GetKeyConfigsString(string commandIdentifier);
string FormatKeyConfig(KeyConfig keyConfig);
}

View File

@@ -0,0 +1,13 @@
using System.Collections.ObjectModel;
using FileTime.App.Core.Configuration;
namespace FileTime.App.Core.Services;
public interface IPossibleCommandsService
{
ReadOnlyObservableCollection<CommandBindingConfiguration> PossibleCommands { get; set; }
void Clear();
void Add(CommandBindingConfiguration commandBindingConfiguration);
void Remove(CommandBindingConfiguration commandBindingConfiguration);
void AddRange(IEnumerable<CommandBindingConfiguration> commandBindingConfigurations);
}

View File

@@ -16,7 +16,6 @@ public interface IAppState
IDeclarativeProperty<string?> RapidTravelTextDebounced { get; } IDeclarativeProperty<string?> RapidTravelTextDebounced { get; }
IDeclarativeProperty<string?> ContainerStatus { get; } IDeclarativeProperty<string?> ContainerStatus { get; }
List<KeyConfig> PreviousKeys { get; } List<KeyConfig> PreviousKeys { get; }
List<CommandBindingConfiguration> PossibleCommands { get; set; }
bool NoCommandFound { get; set; } bool NoCommandFound { get; set; }
void AddTab(ITabViewModel tabViewModel); void AddTab(ITabViewModel tabViewModel);

View File

@@ -0,0 +1,8 @@
namespace FileTime.App.Core.ViewModels;
public interface IPossibleCommandEntryViewModel
{
public string CommandName { get; }
public string Title { get; }
public string KeysText { get; }
}

View File

@@ -0,0 +1,8 @@
using System.Collections.ObjectModel;
namespace FileTime.App.Core.ViewModels;
public interface IPossibleCommandsViewModel
{
ObservableCollection<IPossibleCommandEntryViewModel> PossibleCommands { get; }
}

View File

@@ -30,6 +30,7 @@
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" />
<ProjectReference Include="..\..\Library\Defer\Defer.csproj" /> <ProjectReference Include="..\..\Library\Defer\Defer.csproj" />
<ProjectReference Include="..\..\Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj" /> <ProjectReference Include="..\..\Providers\FileTime.Providers.Local.Abstractions\FileTime.Providers.Local.Abstractions.csproj" />
<ProjectReference Include="..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" /> <ProjectReference Include="..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" />
<ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" /> <ProjectReference Include="..\..\Tools\FileTime.Tools\FileTime.Tools.csproj" />

View File

@@ -0,0 +1,52 @@
using System.Text;
using FileTime.App.Core.Configuration;
namespace FileTime.App.Core.Services;
public class CommandKeysHelperService : ICommandKeysHelperService
{
private readonly IKeyboardConfigurationService _keyboardConfigurationService;
public CommandKeysHelperService(IKeyboardConfigurationService keyboardConfigurationService)
{
_keyboardConfigurationService = keyboardConfigurationService;
}
public List<List<KeyConfig>> GetKeysForCommand(string commandName)
=> _keyboardConfigurationService
.AllShortcut
.Where(s => s.Command == commandName)
.Select(k => k.Keys)
.ToList();
public string GetKeyConfigsString(string commandIdentifier)
{
var keyConfigs = GetKeysForCommand(commandIdentifier);
if (keyConfigs.Count == 0) return string.Empty;
return string.Join(
" ; ",
keyConfigs
.Select(ks =>
string.Join(
", ",
ks.Select(FormatKeyConfig)
)
)
);
}
public string FormatKeyConfig(KeyConfig keyConfig)
{
var stringBuilder = new StringBuilder();
if (keyConfig.Ctrl) stringBuilder.Append("Ctrl + ");
if (keyConfig.Shift) stringBuilder.Append("Shift + ");
if (keyConfig.Alt) stringBuilder.Append("Alt + ");
stringBuilder.Append(keyConfig.Key.ToString());
return stringBuilder.ToString();
}
}

View File

@@ -21,6 +21,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
private readonly ILogger<DefaultModeKeyInputHandler> _logger; private readonly ILogger<DefaultModeKeyInputHandler> _logger;
private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly IPossibleCommandsService _possibleCommandsService;
private readonly BindedCollection<IModalViewModel> _openModals; private readonly BindedCollection<IModalViewModel> _openModals;
public DefaultModeKeyInputHandler( public DefaultModeKeyInputHandler(
@@ -29,10 +30,12 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
IKeyboardConfigurationService keyboardConfigurationService, IKeyboardConfigurationService keyboardConfigurationService,
ILogger<DefaultModeKeyInputHandler> logger, ILogger<DefaultModeKeyInputHandler> logger,
IUserCommandHandlerService userCommandHandlerService, IUserCommandHandlerService userCommandHandlerService,
IIdentifiableUserCommandService identifiableUserCommandService) IIdentifiableUserCommandService identifiableUserCommandService,
IPossibleCommandsService possibleCommandsService)
{ {
_appState = appState; _appState = appState;
_identifiableUserCommandService = identifiableUserCommandService; _identifiableUserCommandService = identifiableUserCommandService;
_possibleCommandsService = possibleCommandsService;
_keyboardConfigurationService = keyboardConfigurationService; _keyboardConfigurationService = keyboardConfigurationService;
_logger = logger; _logger = logger;
_modalService = modalService; _modalService = modalService;
@@ -99,7 +102,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
{ {
args.Handled = true; args.Handled = true;
_appState.PreviousKeys.Clear(); _appState.PreviousKeys.Clear();
_appState.PossibleCommands = new(); _possibleCommandsService.Clear();
} }
} }
/*else if (key == Key.Enter /*else if (key == Key.Enter
@@ -113,7 +116,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
{ {
args.Handled = true; args.Handled = true;
_appState.PreviousKeys.Clear(); _appState.PreviousKeys.Clear();
_appState.PossibleCommands = new(); _possibleCommandsService.Clear();
var command = _identifiableUserCommandService.GetCommand(selectedCommandBinding.Command); var command = _identifiableUserCommandService.GetCommand(selectedCommandBinding.Command);
if (command is not null) if (command is not null)
{ {
@@ -123,7 +126,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
else if (_keysToSkip.Any(k => k.AreKeysEqual(_appState.PreviousKeys))) else if (_keysToSkip.Any(k => k.AreKeysEqual(_appState.PreviousKeys)))
{ {
_appState.PreviousKeys.Clear(); _appState.PreviousKeys.Clear();
_appState.PossibleCommands = new(); _possibleCommandsService.Clear();
return; return;
} }
else if (_appState.PreviousKeys.Count == 2) else if (_appState.PreviousKeys.Count == 2)
@@ -131,7 +134,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
args.Handled = true; args.Handled = true;
_appState.NoCommandFound = true; _appState.NoCommandFound = true;
_appState.PreviousKeys.Clear(); _appState.PreviousKeys.Clear();
_appState.PossibleCommands = new(); _possibleCommandsService.Clear();
} }
else else
{ {
@@ -145,7 +148,8 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
} }
else else
{ {
_appState.PossibleCommands = possibleCommands; _possibleCommandsService.Clear();
_possibleCommandsService.AddRange(possibleCommands);
} }
} }
} }

View File

@@ -0,0 +1,31 @@
using System.Collections.ObjectModel;
using FileTime.App.Core.Configuration;
namespace FileTime.App.Core.Services;
public class PossibleCommandsService : IPossibleCommandsService
{
private readonly ObservableCollection<CommandBindingConfiguration> _possibleCommands = new();
public ReadOnlyObservableCollection<CommandBindingConfiguration> PossibleCommands { get; set; }
public PossibleCommandsService()
{
PossibleCommands = new ReadOnlyObservableCollection<CommandBindingConfiguration>(_possibleCommands);
}
public void Clear() => _possibleCommands.Clear();
public void Add(CommandBindingConfiguration commandBindingConfiguration)
=> _possibleCommands.Add(commandBindingConfiguration);
public void AddRange(IEnumerable<CommandBindingConfiguration> commandBindingConfigurations)
{
foreach (var commandBindingConfiguration in commandBindingConfigurations)
{
_possibleCommands.Add(commandBindingConfiguration);
}
}
public void Remove(CommandBindingConfiguration commandBindingConfiguration)
=> _possibleCommands.Remove(commandBindingConfiguration);
}

View File

@@ -33,6 +33,9 @@ public static class Startup
serviceCollection.TryAddSingleton<IDefaultModeKeyInputHandler, DefaultModeKeyInputHandler>(); serviceCollection.TryAddSingleton<IDefaultModeKeyInputHandler, DefaultModeKeyInputHandler>();
serviceCollection.TryAddSingleton<IRapidTravelModeKeyInputHandler, RapidTravelModeKeyInputHandler>(); serviceCollection.TryAddSingleton<IRapidTravelModeKeyInputHandler, RapidTravelModeKeyInputHandler>();
serviceCollection.TryAddSingleton<IKeyboardConfigurationService, KeyboardConfigurationService>(); serviceCollection.TryAddSingleton<IKeyboardConfigurationService, KeyboardConfigurationService>();
serviceCollection.TryAddSingleton<ICommandKeysHelperService, CommandKeysHelperService>();
serviceCollection.TryAddSingleton<IPossibleCommandsService, PossibleCommandsService>();
serviceCollection.TryAddSingleton<IPossibleCommandsViewModel, PossibleCommandsViewModel>();
return serviceCollection return serviceCollection
.AddCommandHandlers() .AddCommandHandlers()

View File

@@ -4,9 +4,7 @@ using System.Reactive.Subjects;
using DeclarativeProperty; using DeclarativeProperty;
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels.Timeline;
using FileTime.Core.Models.Extensions; using FileTime.Core.Models.Extensions;
using MvvmGen;
using MoreLinq; using MoreLinq;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
@@ -31,7 +29,6 @@ public abstract partial class AppStateBase : IAppState
public IDeclarativeProperty<string?> ContainerStatus { get; } public IDeclarativeProperty<string?> ContainerStatus { get; }
[Notify] public List<KeyConfig> PreviousKeys { get; } = new(); [Notify] public List<KeyConfig> PreviousKeys { get; } = new();
[Notify] public List<CommandBindingConfiguration> PossibleCommands { get; set; } = new();
[Notify] public bool NoCommandFound { get; set; } [Notify] public bool NoCommandFound { get; set; }
protected AppStateBase() protected AppStateBase()

View File

@@ -0,0 +1,6 @@
namespace FileTime.App.Core.ViewModels;
public record PossibleCommandEntryViewModel(
string CommandName,
string Title,
string KeysText) : IPossibleCommandEntryViewModel;

View File

@@ -0,0 +1,36 @@
using System.Collections.ObjectModel;
using FileTime.App.Core.Configuration;
using FileTime.App.Core.Services;
using ObservableComputations;
namespace FileTime.App.Core.ViewModels;
public class PossibleCommandsViewModel : IPossibleCommandsViewModel, IDisposable
{
private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly OcConsumer _ocConsumer = new();
public ObservableCollection<IPossibleCommandEntryViewModel> PossibleCommands { get; }
public PossibleCommandsViewModel(
IPossibleCommandsService possibleCommandsService,
IIdentifiableUserCommandService identifiableUserCommandService)
{
_identifiableUserCommandService = identifiableUserCommandService;
PossibleCommands = possibleCommandsService
.PossibleCommands
.Selecting(c => CreatePossibleCommandViewModel(c))
.For(_ocConsumer);
}
private IPossibleCommandEntryViewModel CreatePossibleCommandViewModel(CommandBindingConfiguration commandBindingConfiguration)
{
var commandName = commandBindingConfiguration.Command;
var title = _identifiableUserCommandService.GetCommand(commandName)?.Title ?? commandName;
return new PossibleCommandEntryViewModel(
CommandName: commandName,
Title: title,
KeysText: commandBindingConfiguration.GetKeysDisplayText());
}
public void Dispose() => _ocConsumer.Dispose();
}

View File

@@ -4,5 +4,4 @@ namespace FileTime.ConsoleUI.App;
public interface IConsoleAppState : IAppState public interface IConsoleAppState : IAppState
{ {
} }

View File

@@ -0,0 +1,11 @@
using FileTime.App.Core.ViewModels;
namespace FileTime.ConsoleUI.App;
public interface IRootViewModel
{
IConsoleAppState AppState { get; }
IPossibleCommandsViewModel PossibleCommands { get; }
string UserName { get; }
string MachineName { get; }
}

View File

@@ -1,5 +1,4 @@
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 FileTime.Core.Enums;
using TerminalUI; using TerminalUI;
@@ -8,69 +7,103 @@ using TerminalUI.Controls;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.ViewExtensions; using TerminalUI.ViewExtensions;
using ConsoleColor = TerminalUI.Color.ConsoleColor;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
public class MainWindow public class MainWindow
{ {
private readonly IConsoleAppState _consoleAppState; private readonly IRootViewModel _rootViewModel;
private readonly IApplicationContext _applicationContext; private readonly IApplicationContext _applicationContext;
private readonly ITheme _theme; private readonly ITheme _theme;
private IView _root; private IView _root;
public MainWindow( public MainWindow(
IConsoleAppState consoleAppState, IRootViewModel rootViewModel,
IApplicationContext applicationContext, IApplicationContext applicationContext,
ITheme theme) ITheme theme)
{ {
_consoleAppState = consoleAppState; _rootViewModel = rootViewModel;
_applicationContext = applicationContext; _applicationContext = applicationContext;
_theme = theme; _theme = theme;
} }
public void Initialize() public void Initialize()
{ {
var root = new Grid<IAppState> var root = new Grid<IRootViewModel>
{ {
DataContext = _consoleAppState, DataContext = _rootViewModel,
ApplicationContext = _applicationContext, ApplicationContext = _applicationContext,
RowDefinitionsObject = "Auto *", RowDefinitionsObject = "Auto * Auto",
ChildInitializer = ChildInitializer =
{ {
new Grid<IAppState> new Grid<IRootViewModel>
{ {
ColumnDefinitionsObject = "* Auto", ColumnDefinitionsObject = "Auto * Auto",
ChildInitializer = ChildInitializer =
{ {
new TextBlock<IAppState>() new StackPanel<IRootViewModel>
{
Name = "username_panel",
Orientation = Orientation.Horizontal,
Margin = "0 0 1 0",
ChildInitializer =
{ {
Foreground = _theme.ContainerColor new TextBlock<IRootViewModel>()
.Setup(t => t.Bind(
t,
root => root.UserName,
tb => tb.Text
)),
new TextBlock<IRootViewModel>()
.Setup(t => t.Bind(
t,
root => root.MachineName,
tb => tb.Text,
t => $"@{t}"
))
} }
.Setup(t => },
t.Bind( new TextBlock<IRootViewModel>
t, {
appState => appState.SelectedTab.Value.CurrentLocation.Value.FullName.Path, Foreground = _theme.ContainerColor,
tb => tb.Text, Extensions =
value => value {
) new GridPositionExtension(1, 0)
), }
}
.Setup(t => t.Bind(
t,
root => root.AppState.SelectedTab.Value.CurrentLocation.Value.FullName.Path,
tb => tb.Text
)),
TabControl() TabControl()
.WithExtension(new GridPositionExtension(2, 0))
} }
}, },
new Grid<IAppState> new Grid<IRootViewModel>
{ {
ColumnDefinitionsObject = "* 4* 4*", ColumnDefinitionsObject = "* 4* 4*",
ChildInitializer =
{
ParentsItemsView(),
SelectedItemsView(),
SelectedsItemsView(),
},
Extensions = Extensions =
{ {
new GridPositionExtension(0, 1) new GridPositionExtension(0, 1)
},
ChildInitializer =
{
ParentsItemsView().WithExtension(new GridPositionExtension(0, 0)),
SelectedItemsView().WithExtension(new GridPositionExtension(1, 0)),
SelectedsItemsView().WithExtension(new GridPositionExtension(2, 0)),
}
},
new Grid<IRootViewModel>
{
Extensions =
{
new GridPositionExtension(0, 2)
},
ChildInitializer =
{
PossibleCommands()
} }
} }
} }
@@ -78,15 +111,58 @@ public class MainWindow
_root = root; _root = root;
} }
private IView<IAppState> TabControl() private IView<IRootViewModel> PossibleCommands()
{ {
var tabList = new ListView<IAppState, ITabViewModel> //TODO: Create and use DataGrid
var commandBindings = new ListView<IRootViewModel, IPossibleCommandEntryViewModel>
{
ItemTemplate = _ =>
{
var grid = new Grid<IPossibleCommandEntryViewModel>
{
ColumnDefinitionsObject = "10 *",
ChildInitializer =
{
new TextBlock<IPossibleCommandEntryViewModel>()
.Setup(t =>
t.Bind(
t,
dc => dc.KeysText,
tb => tb.Text)
),
new TextBlock<IPossibleCommandEntryViewModel>
{
Extensions =
{
new GridPositionExtension(1, 0)
}
}.Setup(t =>
t.Bind(
t,
dc => dc.Title,
tb => tb.Text)
)
}
};
return grid;
}
};
commandBindings.Bind(
commandBindings,
root => root.PossibleCommands.PossibleCommands,
v => v.ItemsSource,
d => d);
return commandBindings;
}
private IView<IRootViewModel> TabControl()
{
var tabList = new ListView<IRootViewModel, ITabViewModel>
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Extensions =
{
new GridPositionExtension(1, 0)
},
ItemTemplate = item => ItemTemplate = item =>
{ {
var textBlock = item.CreateChild<TextBlock<ITabViewModel>>(); var textBlock = item.CreateChild<TextBlock<ITabViewModel>>();
@@ -96,6 +172,7 @@ public class MainWindow
textBlock, textBlock,
dc => dc.TabNumber.ToString(), dc => dc.TabNumber.ToString(),
tb => tb.Text, tb => tb.Text,
value => $" {value}",
fallbackValue: "?"); fallbackValue: "?");
textBlock.Bind( textBlock.Bind(
@@ -110,23 +187,17 @@ public class MainWindow
tabList.Bind( tabList.Bind(
tabList, tabList,
appState => appState == null ? null : appState.Tabs, root => root.AppState.Tabs,
v => v.ItemsSource); v => v.ItemsSource);
return tabList; return tabList;
} }
private ListView<IAppState, IItemViewModel> SelectedItemsView() private ListView<IRootViewModel, IItemViewModel> SelectedItemsView()
{ {
var list = new ListView<IAppState, IItemViewModel> var list = new ListView<IRootViewModel, IItemViewModel>
{ {
DataContext = _consoleAppState, ListPadding = 8
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(1, 0)
}
}; };
list.ItemTemplate = item => list.ItemTemplate = item =>
@@ -153,33 +224,22 @@ public class MainWindow
list.Bind( list.Bind(
list, list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.CurrentItems).Switch(), root => root.AppState.SelectedTab.Value.CurrentItems.Value,
v => v.ItemsSource); v => v.ItemsSource);
list.Bind( list.Bind(
list, list,
appState => root => root.AppState.SelectedTab.Value.CurrentSelectedItem.Value,
appState == null
? null
: appState.SelectedTab.Value == null
? null
: appState.SelectedTab.Value.CurrentSelectedItem.Value,
v => v.SelectedItem); v => v.SelectedItem);
return list; return list;
} }
private ListView<IAppState, IItemViewModel> SelectedsItemsView() private ListView<IRootViewModel, IItemViewModel> SelectedsItemsView()
{ {
var list = new ListView<IAppState, IItemViewModel> var list = new ListView<IRootViewModel, IItemViewModel>
{ {
DataContext = _consoleAppState, ListPadding = 8
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(2, 0)
}
}; };
list.ItemTemplate = item => list.ItemTemplate = item =>
@@ -206,23 +266,17 @@ public class MainWindow
list.Bind( list.Bind(
list, list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.SelectedsChildren).Switch(), root => root.AppState.SelectedTab.Value.SelectedsChildren.Value,
v => v.ItemsSource); v => v.ItemsSource);
return list; return list;
} }
private ListView<IAppState, IItemViewModel> ParentsItemsView() private ListView<IRootViewModel, IItemViewModel> ParentsItemsView()
{ {
var list = new ListView<IAppState, IItemViewModel> var list = new ListView<IRootViewModel, IItemViewModel>
{ {
DataContext = _consoleAppState, ListPadding = 8
ApplicationContext = _applicationContext,
ListPadding = 8,
Extensions =
{
new GridPositionExtension(0, 0)
}
}; };
list.ItemTemplate = item => list.ItemTemplate = item =>
@@ -249,67 +303,12 @@ public class MainWindow
list.Bind( list.Bind(
list, list,
appState => appState == null ? null : appState.SelectedTab.Map(t => t == null ? null : t.ParentsChildren).Switch(), root => root.AppState.SelectedTab.Value.ParentsChildren.Value,
v => v.ItemsSource); v => v.ItemsSource);
return list; return list;
} }
private void TestGrid()
{
var grid = new Grid<object>
{
ApplicationContext = _applicationContext,
ColumnDefinitionsObject = "Auto Auto",
RowDefinitionsObject = "Auto Auto",
ChildInitializer =
{
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Blue, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(0, 0)
},
Width = 2,
Height = 2,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Red, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(0, 1)
},
Width = 3,
Height = 3,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Green, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(1, 0)
},
Width = 4,
Height = 4,
},
new Rectangle<object>
{
Fill = new ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground),
Extensions =
{
new GridPositionExtension(1, 1)
},
Width = 5,
Height = 5,
}
}
};
//_grid = grid;
}
public IEnumerable<IView> RootViews() => new IView[] public IEnumerable<IView> RootViews() => new IView[]
{ {
_root _root

View File

@@ -0,0 +1,19 @@
using FileTime.App.Core.ViewModels;
namespace FileTime.ConsoleUI.App;
public class RootViewModel : IRootViewModel
{
public string UserName => Environment.UserName;
public string MachineName => Environment.MachineName;
public IPossibleCommandsViewModel PossibleCommands { get; }
public IConsoleAppState AppState { get; }
public RootViewModel(
IConsoleAppState appState,
IPossibleCommandsViewModel possibleCommands)
{
AppState = appState;
PossibleCommands = possibleCommands;
}
}

View File

@@ -28,6 +28,7 @@ public static class Startup
services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>(); services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>();
services.AddSingleton<CustomLoggerSink>(); services.AddSingleton<CustomLoggerSink>();
services.TryAddSingleton(new ApplicationConfiguration(true)); services.TryAddSingleton(new ApplicationConfiguration(true));
services.TryAddSingleton<IRootViewModel, RootViewModel>();
services.Configure<ConsoleApplicationConfiguration>(configuration); services.Configure<ConsoleApplicationConfiguration>(configuration);

View File

@@ -123,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\C
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", "ConsoleApp\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj", "{CCB6F86A-7E80-448E-B543-DF9DB337C42A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", "ConsoleApp\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj", "{CCB6F86A-7E80-448E-B543-DF9DB337C42A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Extensions", "Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj", "{6C3C3151-9341-4792-9B0B-A11C0658524E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -333,6 +335,10 @@ Global
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.Build.0 = Release|Any CPU {CCB6F86A-7E80-448E-B543-DF9DB337C42A}.Release|Any CPU.Build.0 = Release|Any CPU
{6C3C3151-9341-4792-9B0B-A11C0658524E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C3C3151-9341-4792-9B0B-A11C0658524E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C3C3151-9341-4792-9B0B-A11C0658524E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C3C3151-9341-4792-9B0B-A11C0658524E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -390,6 +396,7 @@ Global
{2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
{6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -1,21 +0,0 @@
using System.Globalization;
using Avalonia.Data.Converters;
using FileTime.App.Core.UserCommand;
namespace FileTime.GuiApp.App.Converters;
public class CommandToCommandNameConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if(value is not IUserCommand command) return value;
//TODO: implement
return command.ToString();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -34,7 +34,6 @@
<converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.NotEqual}" x:Key="NotEqualsConverter" /> <converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.NotEqual}" x:Key="NotEqualsConverter" />
<converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.GreaterThan}" x:Key="GreaterThanConverter" /> <converters:CompareConverter ComparisonCondition="{x:Static converters:ComparisonCondition.GreaterThan}" x:Key="GreaterThanConverter" />
<converters:ExceptionToStringConverter x:Key="ExceptionToStringConverter" /> <converters:ExceptionToStringConverter x:Key="ExceptionToStringConverter" />
<converters:CommandToCommandNameConverter x:Key="CommandToCommandNameConverter" />
<converters:ItemToImageConverter x:Key="ItemToImageConverter" /> <converters:ItemToImageConverter x:Key="ItemToImageConverter" />
<converters:StringReplaceConverter <converters:StringReplaceConverter
NewValue="/" NewValue="/"

View File

@@ -19,5 +19,6 @@ public interface IMainWindowViewModel : IMainWindowViewModelBase
IAdminElevationManager AdminElevationManager { get; } IAdminElevationManager AdminElevationManager { get; }
IClipboardService ClipboardService { get; } IClipboardService ClipboardService { get; }
ITimelineViewModel TimelineViewModel { get; } ITimelineViewModel TimelineViewModel { get; }
IPossibleCommandsViewModel PossibleCommands { get; }
Task RunOrOpenItem(IItemViewModel itemViewModel); Task RunOrOpenItem(IItemViewModel itemViewModel);
} }

View File

@@ -37,6 +37,7 @@ namespace FileTime.GuiApp.App.ViewModels;
[Inject(typeof(IClipboardService), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(IClipboardService), PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(IModalService), PropertyName = "_modalService")] [Inject(typeof(IModalService), PropertyName = "_modalService")]
[Inject(typeof(ITimelineViewModel), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(ITimelineViewModel), PropertyAccessModifier = AccessModifier.Public)]
[Inject(typeof(IPossibleCommandsViewModel), PropertyName = "PossibleCommands",PropertyAccessModifier = AccessModifier.Public)]
public partial class MainWindowViewModel : IMainWindowViewModel public partial class MainWindowViewModel : IMainWindowViewModel
{ {
public bool Loading => false; public bool Loading => false;

View File

@@ -750,7 +750,7 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid IsVisible="{Binding AppState.PossibleCommands.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}"> <Grid IsVisible="{Binding PossibleCommands.PossibleCommands.Count, Converter={StaticResource NotEqualsConverter}, ConverterParameter=0}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="1" /> <RowDefinition Height="1" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -763,17 +763,17 @@
Margin="10,0" Margin="10,0"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<ItemsRepeater Grid.Row="1" ItemsSource="{Binding AppState.PossibleCommands}"> <ItemsRepeater Grid.Row="1" ItemsSource="{Binding PossibleCommands.PossibleCommands}">
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="configuration:CommandBindingConfiguration"> <DataTemplate x:DataType="corevm:IPossibleCommandEntryViewModel">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="200" /> <ColumnDefinition Width="200" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="{Binding KeysDisplayText}" /> <TextBlock Text="{Binding KeysText}" />
<TextBlock Grid.Column="1" Text="{Binding Command, Converter={StaticResource CommandToCommandNameConverter}}" /> <TextBlock Grid.Column="1" Text="{Binding Title}" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>

View File

@@ -0,0 +1,13 @@
using System.Collections.ObjectModel;
using System.Linq.Expressions;
namespace ObservableComputations;
public static class Extensions
{
[ObservableComputationsCall]
public static Selecting<TSourceItem, TResultItem> Selecting<TSourceItem, TResultItem>(
this ReadOnlyObservableCollection<TSourceItem> source,
Expression<Func<TSourceItem, TResultItem>> selectorExpression)
=> new(source, selectorExpression);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>ObservableComputations</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ObservableComputations" Version="2.3.0" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,7 @@ public class ApplicationContext : IApplicationContext
public ILoggerFactory? LoggerFactory { 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; }
public char EmptyCharacter { get; init; } = ' ';
public ApplicationContext() public ApplicationContext()
{ {

View File

@@ -20,7 +20,7 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
public Binding( public Binding(
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression, Expression<Func<TDataContext?, TExpressionResult>> dataSourceExpression,
object? propertySource, object? propertySource,
PropertyInfo targetProperty, PropertyInfo targetProperty,
Func<TExpressionResult, TResult> converter, Func<TExpressionResult, TResult> converter,
@@ -28,18 +28,18 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
) )
{ {
ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceView);
ArgumentNullException.ThrowIfNull(dataContextExpression); ArgumentNullException.ThrowIfNull(dataSourceExpression);
ArgumentNullException.ThrowIfNull(targetProperty); ArgumentNullException.ThrowIfNull(targetProperty);
ArgumentNullException.ThrowIfNull(converter); ArgumentNullException.ThrowIfNull(converter);
_dataSourceView = dataSourceView; _dataSourceView = dataSourceView;
_dataContextMapper = dataContextExpression.Compile(); _dataContextMapper = dataSourceExpression.Compile();
_propertySource = propertySource; _propertySource = propertySource;
_targetProperty = targetProperty; _targetProperty = targetProperty;
_converter = converter; _converter = converter;
_fallbackValue = fallbackValue; _fallbackValue = fallbackValue;
InitTrackingTree(dataContextExpression); InitTrackingTree(dataSourceExpression);
UpdateTrackers(); UpdateTrackers();
@@ -55,8 +55,8 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
{ {
if (propertySource is IDisposableCollection propertySourceDisposableCollection) if (propertySource is IDisposableCollection propertySourceDisposableCollection)
{ {
propertySourceDisposableCollection.AddDisposable(this);
_propertySourceDisposableCollection = propertySourceDisposableCollection; _propertySourceDisposableCollection = propertySourceDisposableCollection;
propertySourceDisposableCollection.AddDisposable(this);
} }
} }

View File

@@ -41,6 +41,14 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
}; };
} }
protected override void AttachChildren()
{
foreach (var child in Children)
{
child.Attached = true;
}
}
public override TChild AddChild<TChild>(TChild child) public override TChild AddChild<TChild>(TChild child)
{ {
child = base.AddChild(child); child = base.AddChild(child);

View File

@@ -1,16 +1,65 @@
using TerminalUI.Models; using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
using TerminalUI.Traits; using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public abstract class ContentView<T>: View<T>, IContentRenderer public abstract partial class ContentView<T> : View<T>, IContentRenderer<T>
{ {
private bool _placeholderRenderDone;
[Notify] private RenderMethod _contentRendererMethod;
private IView<T>? _content;
public IView<T>? Content
{
get => _content;
set
{
if (Equals(value, _content)) return;
if (_content is not null)
{
RemoveChild(_content);
}
_content = value;
if (_content is not null)
{
AddChild(_content);
}
OnPropertyChanged();
}
}
protected ContentView() protected ContentView()
{ {
ContentRendererMethod = DefaultContentRender; _contentRendererMethod = DefaultContentRender;
RerenderProperties.Add(nameof(Content));
RerenderProperties.Add(nameof(ContentRendererMethod));
} }
public IView? Content { get; set; }
public Action<Position, Size> ContentRendererMethod { get; set; }
private void DefaultContentRender(Position position, Size size) => Content?.Render(position, size); protected override void AttachChildren()
{
base.AttachChildren();
if (Content is not null)
{
Content.Attached = true;
}
}
private bool DefaultContentRender(RenderContext renderContext, Position position, Size size)
{
if (Content is null)
{
if (_placeholderRenderDone) return false;
_placeholderRenderDone = true;
RenderEmpty(renderContext, position, size);
return true;
}
_placeholderRenderDone = false;
return Content.Render(renderContext, position, size);
}
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
@@ -8,15 +9,79 @@ namespace TerminalUI.Controls;
public class Grid<T> : ChildContainerView<T> public class Grid<T> : ChildContainerView<T>
{ {
private List<RowDefinition> _rowDefinitions = new() {RowDefinition.Star(1)};
private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)};
private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>(); private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>();
private delegate void WithSizes(Span<int> widths, Span<int> heights); private delegate void WithSizes(RenderContext renderContext, Span<int> widths, Span<int> heights);
private delegate TResult WithSizes<TResult>(Span<int> widths, Span<int> heights); private delegate TResult WithSizes<TResult>(RenderContext renderContext, Span<int> widths, Span<int> heights);
private const int ToBeCalculated = -1; private const int ToBeCalculated = -1;
public ObservableCollection<RowDefinition> RowDefinitions { get; } = new() {RowDefinition.Star(1)};
public ObservableCollection<ColumnDefinition> ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)}; public IReadOnlyList<RowDefinition> RowDefinitions
{
get => _rowDefinitions;
set
{
var nextValue = value;
if (value.Count == 0)
{
nextValue = new List<RowDefinition> {RowDefinition.Star(1)};
}
var needUpdate = nextValue.Count != _rowDefinitions.Count;
if (!needUpdate)
{
for (var i = 0; i < nextValue.Count; i++)
{
if (!nextValue[i].Equals(_rowDefinitions[i]))
{
needUpdate = true;
break;
}
}
}
if (needUpdate)
{
_rowDefinitions = nextValue.ToList();
OnPropertyChanged();
}
}
}
public IReadOnlyList<ColumnDefinition> ColumnDefinitions
{
get => _columnDefinitions;
set
{
var nextValue = value;
if (value.Count == 0)
{
nextValue = new List<ColumnDefinition> {ColumnDefinition.Star(1)};
}
var needUpdate = nextValue.Count != _columnDefinitions.Count;
if (!needUpdate)
{
for (var i = 0; i < nextValue.Count; i++)
{
if (!nextValue[i].Equals(_columnDefinitions[i]))
{
needUpdate = true;
break;
}
}
}
if (needUpdate)
{
_columnDefinitions = nextValue.ToList();
OnPropertyChanged();
}
}
}
public object? ColumnDefinitionsObject public object? ColumnDefinitionsObject
{ {
@@ -25,11 +90,7 @@ public class Grid<T> : ChildContainerView<T>
{ {
if (value is IEnumerable<ColumnDefinition> columnDefinitions) if (value is IEnumerable<ColumnDefinition> columnDefinitions)
{ {
ColumnDefinitions.Clear(); ColumnDefinitions = columnDefinitions.ToList();
foreach (var columnDefinition in columnDefinitions)
{
ColumnDefinitions.Add(columnDefinition);
}
} }
else if (value is string s) else if (value is string s)
{ {
@@ -49,11 +110,7 @@ public class Grid<T> : ChildContainerView<T>
{ {
if (value is IEnumerable<RowDefinition> rowDefinitions) if (value is IEnumerable<RowDefinition> rowDefinitions)
{ {
RowDefinitions.Clear(); RowDefinitions = rowDefinitions.ToList();
foreach (var rowDefinition in rowDefinitions)
{
RowDefinitions.Add(rowDefinition);
}
} }
else if (value is string s) else if (value is string s)
{ {
@@ -66,85 +123,135 @@ public class Grid<T> : ChildContainerView<T>
} }
} }
public override Size GetRequestedSize() protected override Size CalculateSize()
=> WithCalculatedSize((columnWidths, rowHeights) => => WithCalculatedSize(
{ RenderContext.Empty,
var width = 0; new Option<Size>(new Size(0, 0), false),
var height = 0; (_, columnWidths, rowHeights) =>
for (var i = 0; i < columnWidths.Length; i++)
{ {
width += columnWidths[i]; var width = 0;
} var height = 0;
for (var i = 0; i < rowHeights.Length; i++) for (var i = 0; i < columnWidths.Length; i++)
{
width += columnWidths[i];
}
for (var i = 0; i < rowHeights.Length; i++)
{
height += rowHeights[i];
}
return new Size(width, height);
});
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
=> WithCalculatedSize(
renderContext,
new Option<Size>(size, true),
(context, columnWidths, rowHeights) =>
{ {
height += rowHeights[i]; foreach (var child in Children)
}
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)
{
var positionExtension = child.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
if (x > columnWidths.Length)
{ {
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y); var (x, y) = GetViewColumnAndRow(child, columnWidths.Length, rowHeights.Length);
x = 0;
var width = columnWidths[x];
var height = rowHeights[y];
var left = position.X;
var top = position.Y;
for (var i = 0; i < x; i++)
{
left += columnWidths[i];
}
for (var i = 0; i < y; i++)
{
top += rowHeights[i];
}
child.Render(context, new Position(left, top), new Size(width, height));
} }
if (y > rowHeights.Length) return true;
{
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y);
y = 0;
}
var width = columnWidths[x]; /*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights);
var height = rowHeights[y]; CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/
});
var left = 0; /*private void CleanUnusedArea(Dictionary<(int, int),List<IView>> viewsByPosition, Span<int> columnWidths, Span<int> rowHeights)
var top = 0;
for (var i = 0; i < x; i++)
{
left += columnWidths[i];
}
for (var i = 0; i < y; i++)
{
top += rowHeights[i];
}
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); for (var x = 0; x < columnWidths.Length; x++)
object? Helper(Span<int> widths, Span<int> heights)
{ {
actionWithSizes(widths, heights); for (var y = 0; y < rowHeights.Length; y++)
{
if (!viewsByPosition.TryGetValue((x, y), out var list)) continue;
}
}
}*/
/*private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
{
Dictionary<ValueTuple<int, int>, List<IView>> viewsByPosition = new();
foreach (var child in Children)
{
var (x, y) = GetViewColumnAndRow(child, columns, rows);
if (viewsByPosition.TryGetValue((x, y), out var list))
{
list.Add(child);
}
else
{
viewsByPosition[(x, y)] = new List<IView> {child};
}
}
return viewsByPosition;
}*/
private ValueTuple<int, int> GetViewColumnAndRow(IView view, int columns, int rows)
{
var positionExtension = view.GetExtension<GridPositionExtension>();
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
if (x > columns)
{
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
x = 0;
}
if (y > rows)
{
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
y = 0;
}
return (x, y);
}
private void WithCalculatedSize(RenderContext renderContext, Option<Size> size, WithSizes actionWithSizes)
{
WithCalculatedSize(renderContext, size, Helper);
object? Helper(RenderContext renderContext1, Span<int> widths, Span<int> heights)
{
actionWithSizes(renderContext1, widths, heights);
return null; return null;
} }
} }
private TResult WithCalculatedSize<TResult>(WithSizes<TResult> actionWithSizes, Option<Size> size) private TResult WithCalculatedSize<TResult>(RenderContext renderContext, Option<Size> size, WithSizes<TResult> actionWithSizes)
{ {
//TODO: Optimize it, dont calculate all of these, only if there is Auto value(s) //TODO: Optimize it, dont calculate all of these, only if there is Auto value(s)
var columns = ColumnDefinitions.Count; var columns = ColumnDefinitions.Count;
var rows = RowDefinitions.Count; var rows = RowDefinitions.Count;
if (columns < 1) columns = 1; Debug.Assert(columns > 0, "Columns must contain at least one element");
if (rows < 1) rows = 1; Debug.Assert(rows > 0, "Rows must contain at least one element");
Span<int> allWidth = stackalloc int[columns * rows]; Span<int> allWidth = stackalloc int[columns * rows];
Span<int> allHeight = stackalloc int[columns * rows]; Span<int> allHeight = stackalloc int[columns * rows];
@@ -152,9 +259,7 @@ public class Grid<T> : ChildContainerView<T>
foreach (var child in Children) foreach (var child in Children)
{ {
var childSize = child.GetRequestedSize(); var childSize = child.GetRequestedSize();
var positionExtension = child.GetExtension<GridPositionExtension>(); var (x, y) = GetViewColumnAndRow(child, columns, rows);
var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0;
allWidth.SetToMatrix(childSize.Width, x, y, columns); allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns); allHeight.SetToMatrix(childSize.Height, x, y, columns);
@@ -246,60 +351,64 @@ public class Grid<T> : ChildContainerView<T>
} }
} }
return actionWithSizes(columnWidths, rowHeights); return actionWithSizes(renderContext, columnWidths, rowHeights);
} }
public void SetRowDefinitions(string value) public void SetRowDefinitions(string value)
{ {
var values = value.Split(' '); var values = value.Split(' ');
RowDefinitions.Clear(); var rowDefinitions = new List<RowDefinition>();
foreach (var v in values) foreach (var v in values)
{ {
if (v == "Auto") if (v == "Auto")
{ {
RowDefinitions.Add(RowDefinition.Auto); rowDefinitions.Add(RowDefinition.Auto);
} }
else if (v.EndsWith("*")) else if (v.EndsWith("*"))
{ {
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^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))
{ {
RowDefinitions.Add(RowDefinition.Pixel(pixelValue)); rowDefinitions.Add(RowDefinition.Pixel(pixelValue));
} }
else else
{ {
throw new ArgumentException("Invalid row definition: " + v); throw new ArgumentException("Invalid row definition: " + v);
} }
} }
RowDefinitions = rowDefinitions;
} }
public void SetColumnDefinitions(string value) public void SetColumnDefinitions(string value)
{ {
var values = value.Split(' '); var values = value.Split(' ');
ColumnDefinitions.Clear(); var columnDefinitions = new List<ColumnDefinition>();
foreach (var v in values) foreach (var v in values)
{ {
if (v == "Auto") if (v == "Auto")
{ {
ColumnDefinitions.Add(ColumnDefinition.Auto); columnDefinitions.Add(ColumnDefinition.Auto);
} }
else if (v.EndsWith("*")) else if (v.EndsWith("*"))
{ {
var starValue = v.Length == 1 ? 1 : int.Parse(v[..^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))
{ {
ColumnDefinitions.Add(ColumnDefinition.Pixel(pixelValue)); columnDefinitions.Add(ColumnDefinition.Pixel(pixelValue));
} }
else else
{ {
throw new ArgumentException("Invalid column definition: " + v); throw new ArgumentException("Invalid column definition: " + v);
} }
} }
ColumnDefinitions = columnDefinitions;
} }
} }

View File

@@ -4,23 +4,29 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public delegate bool RenderMethod(RenderContext renderContext, Position position, Size size);
public interface IView : INotifyPropertyChanged, IDisposableCollection public interface IView : INotifyPropertyChanged, IDisposableCollection
{ {
object? DataContext { get; set; } object? DataContext { get; set; }
int? MinWidth { get; set; } int? MinWidth { get; set; }
int? MaxWidth { get; set; } int? MaxWidth { get; set; }
int? Width { get; set; } int? Width { get; set; }
int ActualWidth { get; }
int? MinHeight { get; set; } int? MinHeight { get; set; }
int? MaxHeight { get; set; } int? MaxHeight { get; set; }
int? Height { get; set; } int? Height { get; set; }
int ActualHeight { get; }
Margin Margin { get; set; }
bool Attached { get; set; } bool Attached { get; set; }
Size GetRequestedSize(); string? Name { get; set; }
IApplicationContext? ApplicationContext { get; set; } IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; } List<object> Extensions { get; }
RenderMethod RenderMethod { get; set; }
Action<Position, Size> RenderMethod { get; set; }
event Action<IView> Disposed; event Action<IView> Disposed;
void Render(Position position, Size size);
Size GetRequestedSize();
bool Render(RenderContext renderContext, Position position, Size size);
} }
public interface IView<T> : IView public interface IView<T> : IView
@@ -39,8 +45,10 @@ 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();
public TChild AddChild<TChild>(TChild child) where TChild : IView<T>; TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
public TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper) TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>; where TChild : IView<TDataContext>;
void RemoveChild<TDataContext>(IView<TDataContext> child);
} }

View File

@@ -1,5 +1,6 @@
using System.Buffers; using System.Buffers;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using DeclarativeProperty; using DeclarativeProperty;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Models; using TerminalUI.Models;
@@ -79,18 +80,30 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty) if (_itemsSource is IDeclarativeProperty<ObservableCollection<TItem>> observableDeclarativeProperty)
{ {
observableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); throw new NotSupportedException();
_getItems = () => observableDeclarativeProperty.Value;
} }
else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty) else if (_itemsSource is IDeclarativeProperty<ReadOnlyObservableCollection<TItem>> readOnlyObservableDeclarativeProperty)
{ {
readOnlyObservableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); throw new NotSupportedException();
_getItems = () => readOnlyObservableDeclarativeProperty.Value;
} }
else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty) else if (_itemsSource is IDeclarativeProperty<IEnumerable<TItem>> enumerableDeclarativeProperty)
{ {
enumerableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); throw new NotSupportedException();
_getItems = () => enumerableDeclarativeProperty.Value; }
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
{
((INotifyCollectionChanged) observableDeclarative).CollectionChanged +=
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
_getItems = () => observableDeclarative;
}
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
{
((INotifyCollectionChanged) readOnlyObservableDeclarative).CollectionChanged +=
(_, _) => ApplicationContext?.EventLoop.RequestRerender();
_getItems = () => readOnlyObservableDeclarative;
} }
else if (_itemsSource is ICollection<TItem> collection) else if (_itemsSource is ICollection<TItem> collection)
_getItems = () => collection; _getItems = () => collection;
@@ -111,7 +124,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
} }
} }
public Func<ListViewItem<TItem>, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; public Func<ListViewItem<TItem>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
public ListView() public ListView()
{ {
@@ -120,7 +133,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
RerenderProperties.Add(nameof(Orientation)); RerenderProperties.Add(nameof(Orientation));
} }
public override Size GetRequestedSize() protected override Size CalculateSize()
{ {
InstantiateItemViews(); InstantiateItemViews();
if (_listViewItems is null || _listViewItemLength == 0) if (_listViewItems is null || _listViewItemLength == 0)
@@ -147,19 +160,16 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
} }
} }
protected override void DefaultRenderer(Position position, Size size) protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ => Orientation == Orientation.Vertical
if (Orientation == Orientation.Vertical) ? RenderVertical(renderContext, position, size)
RenderVertical(position, size); : RenderHorizontal(renderContext, position, size);
else
RenderHorizontal(position, size);
}
private void RenderHorizontal(Position position, Size size) private bool RenderHorizontal(RenderContext renderContext, Position position, Size size)
{ {
//Note: no support for same width elements //Note: no support for same width elements
var listViewItems = InstantiateItemViews(); var listViewItems = InstantiateItemViews();
if (listViewItems.Length == 0) return; if (listViewItems.Length == 0) return false;
Span<Size> requestedSizes = stackalloc Size[_listViewItemLength]; Span<Size> requestedSizes = stackalloc Size[_listViewItemLength];
@@ -215,20 +225,22 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
width = size.Width - deltaX; width = size.Width - deltaX;
} }
item.Render(position with {X = position.X + deltaX}, size with {Width = width}); item.Render(renderContext, position with {X = position.X + deltaX}, size with {Width = width});
deltaX = nextDeltaX; deltaX = nextDeltaX;
} }
return true;
} }
private void RenderVertical(Position position, Size size) private bool RenderVertical(RenderContext renderContext, Position position, Size size)
{ {
//Note: only same height is supported //Note: only same height is supported
var requestedItemSize = _requestedItemSize; var requestedItemSize = _requestedItemSize;
if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0) if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0)
return; return false;
var listViewItems = InstantiateItemViews(); var listViewItems = InstantiateItemViews();
if (listViewItems.Length == 0) return; if (listViewItems.Length == 0) return false;
var itemsToRender = listViewItems.Length; var itemsToRender = listViewItems.Length;
var heightNeeded = requestedItemSize.Height * listViewItems.Length; var heightNeeded = requestedItemSize.Height * listViewItems.Length;
@@ -264,7 +276,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
for (var i = renderStartIndex; i < lastItemIndex; i++) for (var i = renderStartIndex; i < lastItemIndex; i++)
{ {
var item = listViewItems[i]; var item = listViewItems[i];
item.Render(position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width}); item.Render(renderContext, position with {Y = position.Y + deltaY}, requestedItemSize with {Width = size.Width});
deltaY += requestedItemSize.Height; deltaY += requestedItemSize.Height;
} }
@@ -276,6 +288,8 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
driver.SetCursorPosition(position with {Y = position.Y + i}); driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(placeholder); driver.Write(placeholder);
} }
return true;
} }
private Span<ListViewItem<TItem>> InstantiateItemViews() private Span<ListViewItem<TItem>> InstantiateItemViews()
@@ -300,11 +314,16 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
{ {
var dataContext = items[i]; var dataContext = items[i];
var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext); var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext);
child.Content = ItemTemplate(child); var newContent = ItemTemplate(child);
ItemTemplate(child); child.Content = newContent;
newListViewItems[i] = child; newListViewItems[i] = child;
} }
if (_listViewItems is not null)
{
ListViewItemPool.Return(_listViewItems);
}
_listViewItems = newListViewItems; _listViewItems = newListViewItems;
_listViewItemLength = items.Count; _listViewItemLength = items.Count;
listViewItems = newListViewItems[..items.Count]; listViewItems = newListViewItems[..items.Count];
@@ -324,5 +343,5 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return _listViewItems; return _listViewItems;
} }
private static IView? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null; private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null;
} }

View File

@@ -4,13 +4,13 @@ namespace TerminalUI.Controls;
public class ListViewItem<T> : ContentView<T> public class ListViewItem<T> : ContentView<T>
{ {
public override Size GetRequestedSize() protected override Size CalculateSize()
{ {
if (Content is null) return new Size(0, 0); if (Content is null) return new Size(0, 0);
return Content.GetRequestedSize(); return Content.GetRequestedSize();
} }
protected override void DefaultRenderer(Position position, Size size) protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ {
if (ContentRendererMethod is null) if (ContentRendererMethod is null)
{ {
@@ -22,6 +22,6 @@ public class ListViewItem<T> : ContentView<T>
+ DataContext?.GetType().Name); + DataContext?.GetType().Name);
} }
ContentRendererMethod(position, size); return ContentRendererMethod(renderContext, position, size);
} }
} }

View File

@@ -6,19 +6,38 @@ namespace TerminalUI.Controls;
public partial class Rectangle<T> : View<T> public partial class Rectangle<T> : View<T>
{ {
[Notify] private IColor? _fill; private record RenderState(
public override Size GetRequestedSize() => new(Width ?? 0, Height ?? 0); Position Position,
Size Size,
IColor? Fill);
protected override void DefaultRenderer(Position position, Size size) private RenderState? _lastRenderState;
[Notify] private IColor? _fill;
protected override Size CalculateSize() => new(Width ?? 0, Height ?? 0);
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ {
var s = new string('█', Width ?? size.Width); var renderState = new RenderState(position, size, Fill);
ApplicationContext?.ConsoleDriver.SetBackgroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Background)); if (!NeedsRerender(renderState) || Fill is null) return false;
ApplicationContext?.ConsoleDriver.SetForegroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground)); _lastRenderState = renderState;
var height = Height ?? size.Height;
var driver = renderContext.ConsoleDriver;
var s = new string('█', size.Width);
driver.SetBackgroundColor(Fill);
driver.SetForegroundColor(Fill);
var height = size.Height;
for (var i = 0; i < height; i++) for (var i = 0; i < height; i++)
{ {
ApplicationContext?.ConsoleDriver.SetCursorPosition(position with {Y = position.Y + i}); driver.SetCursorPosition(position with {Y = position.Y + i});
ApplicationContext?.ConsoleDriver.Write(s); driver.Write(s);
} }
return true;
} }
private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState;
} }

View File

@@ -9,7 +9,7 @@ public partial class StackPanel<T> : ChildContainerView<T>
private readonly Dictionary<IView, Size> _requestedSizes = new(); private readonly Dictionary<IView, Size> _requestedSizes = new();
[Notify] private Orientation _orientation = Orientation.Vertical; [Notify] private Orientation _orientation = Orientation.Vertical;
public override Size GetRequestedSize() protected override Size CalculateSize()
{ {
_requestedSizes.Clear(); _requestedSizes.Clear();
var width = 0; var width = 0;
@@ -35,20 +35,38 @@ public partial class StackPanel<T> : ChildContainerView<T>
return new Size(width, height); return new Size(width, height);
} }
protected override void DefaultRenderer(Position position, Size size) protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ {
var delta = 0; var delta = 0;
var neededRerender = false;
foreach (var child in Children) foreach (var child in Children)
{ {
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found"); if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
var childPosition = Orientation == Orientation.Vertical var childPosition = Orientation == Orientation.Vertical
? position with {Y = position.Y + delta} ? position with {Y = position.Y + delta}
: position with {X = position.X + delta}; : position with {X = position.X + delta};
child.Render(childPosition, childSize);
var endX = position.X + size.Width;
var endY = position.Y + size.Height;
if (childPosition.X > endX || childPosition.Y > endY) break;
if (childPosition.X + childSize.Width > endX)
{
childSize = childSize with {Width = endX - childPosition.X};
}
if (childPosition.Y + childSize.Height > endY)
{
childSize = childSize with {Height = endY - childPosition.Y};
}
neededRerender = child.Render(renderContext, childPosition, childSize) || neededRerender;
delta += Orientation == Orientation.Vertical delta += Orientation == Orientation.Vertical
? childSize.Height ? childSize.Height
: childSize.Width; : childSize.Width;
} }
return neededRerender;
} }
} }

View File

@@ -1,5 +1,7 @@
using PropertyChanged.SourceGenerator; using System.ComponentModel;
using PropertyChanged.SourceGenerator;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
@@ -7,9 +9,16 @@ namespace TerminalUI.Controls;
public partial class TextBlock<T> : View<T> public partial class TextBlock<T> : View<T>
{ {
private record RenderContext(Position Position, string? Text, IColor? Foreground, IColor? Background); private record RenderState(
Position Position,
Size Size,
string? Text,
IColor? Foreground,
IColor? Background);
private RenderContext? _renderContext; private RenderState? _lastRenderState;
private string[]? _textLines;
private bool _placeholderRenderDone;
[Notify] private string? _text = string.Empty; [Notify] private string? _text = string.Empty;
[Notify] private IColor? _foreground; [Notify] private IColor? _foreground;
@@ -28,23 +37,41 @@ public partial class TextBlock<T> : View<T>
RerenderProperties.Add(nameof(Foreground)); RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background)); RerenderProperties.Add(nameof(Background));
RerenderProperties.Add(nameof(TextAlignment)); RerenderProperties.Add(nameof(TextAlignment));
((INotifyPropertyChanged) this).PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(Text))
{
_textLines = Text?.Split(Environment.NewLine);
}
};
} }
public override Size GetRequestedSize() => new(Text?.Length ?? 0, 1); protected override Size CalculateSize() => new(_textLines?.Max(l => l.Length) ?? 0, _textLines?.Length ?? 0);
protected override void DefaultRenderer(Position position, Size size) protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ {
if (size.Width == 0 || size.Height == 0) return; if (size.Width == 0 || size.Height == 0) return false;
var driver = ApplicationContext!.ConsoleDriver; var driver = renderContext.ConsoleDriver;
var renderContext = new RenderContext(position, Text, _foreground, _background); var renderState = new RenderState(position, size, Text, _foreground, _background);
if (!NeedsRerender(renderContext)) return; if (!NeedsRerender(renderState)) return false;
_renderContext = renderContext; _lastRenderState = renderState;
if (Text is null) return; if (_textLines is null)
{
if (_placeholderRenderDone)
{
_placeholderRenderDone = true;
RenderEmpty(renderContext, position, size);
}
return false;
}
_placeholderRenderDone = false;
driver.SetCursorPosition(position);
driver.ResetColor(); driver.ResetColor();
if (Foreground is { } foreground) if (Foreground is { } foreground)
{ {
@@ -56,19 +83,31 @@ public partial class TextBlock<T> : View<T>
driver.SetBackgroundColor(background); driver.SetBackgroundColor(background);
} }
var text = TextAlignment switch RenderText(_textLines, driver, position, size);
{
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); return true;
} }
private bool NeedsRerender(RenderContext renderContext) private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size)
=> _renderContext is null || _renderContext != renderContext; {
for (var i = 0; i < textLines.Length; i++)
{
var text = textLines[i];
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.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(text);
}
}
private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState;
} }

View File

@@ -13,9 +13,13 @@ public abstract partial class View<T> : IView<T>
[Notify] private int? _minWidth; [Notify] private int? _minWidth;
[Notify] private int? _maxWidth; [Notify] private int? _maxWidth;
[Notify] private int? _width; [Notify] private int? _width;
[Notify] private int _actualWidth;
[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 int _actualHeight;
[Notify] private Margin _margin = new Margin(0, 0, 0, 0);
[Notify] private string? _name;
[Notify] private IApplicationContext? _applicationContext; [Notify] private IApplicationContext? _applicationContext;
private bool _attached; private bool _attached;
@@ -32,17 +36,50 @@ 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 RenderMethod RenderMethod { get; set; }
public event Action<IView>? Disposed; public event Action<IView>? Disposed;
protected List<string> RerenderProperties { get; } = new(); protected List<string> RerenderProperties { get; } = new();
protected View() protected View()
{ {
RenderMethod = DefaultRenderer; RenderMethod = DefaultRenderer;
RerenderProperties.Add(nameof(MinWidth));
RerenderProperties.Add(nameof(MaxWidth));
RerenderProperties.Add(nameof(MinHeight));
RerenderProperties.Add(nameof(MaxHeight));
RerenderProperties.Add(nameof(Margin));
((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged; ((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged;
} }
public abstract Size GetRequestedSize();
public virtual Size GetRequestedSize()
{
var size = CalculateSize();
if (MinWidth.HasValue && size.Width < MinWidth.Value)
size = size with {Width = MinWidth.Value};
else if (MaxWidth.HasValue && size.Width > MaxWidth.Value)
size = size with {Width = MaxWidth.Value};
if (MinHeight.HasValue && size.Height < MinHeight.Value)
size = size with {Height = MinHeight.Value};
else if (MaxHeight.HasValue && size.Height > MaxHeight.Value)
size = size with {Height = MaxHeight.Value};
if (Margin.Left != 0 || Margin.Right != 0)
size = size with {Width = size.Width + Margin.Left + Margin.Right};
if (Margin.Top != 0 || Margin.Bottom != 0)
size = size with {Height = size.Height + Margin.Top + Margin.Bottom};
return size;
}
protected abstract Size CalculateSize();
protected virtual void AttachChildren() protected virtual void AttachChildren()
{ {
@@ -50,7 +87,8 @@ public abstract partial class View<T> : IView<T>
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e) private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName is not null if (Attached
&& e.PropertyName is not null
&& (e.PropertyName == nameof(IView.DataContext) && (e.PropertyName == nameof(IView.DataContext)
|| RerenderProperties.Contains(e.PropertyName) || RerenderProperties.Contains(e.PropertyName)
) )
@@ -60,10 +98,16 @@ public abstract partial class View<T> : IView<T>
} }
} }
protected abstract void DefaultRenderer(Position position, Size size); protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size);
public void Render(Position position, Size size) public bool Render(RenderContext renderContext, Position position, Size size)
{ {
if (!Attached)
throw new InvalidOperationException("Cannot render unattached view");
ActualWidth = size.Width;
ActualHeight = size.Height;
if (RenderMethod is null) if (RenderMethod is null)
{ {
throw new NullReferenceException( throw new NullReferenceException(
@@ -74,7 +118,31 @@ public abstract partial class View<T> : IView<T>
+ DataContext?.GetType().Name); + DataContext?.GetType().Name);
} }
RenderMethod(position, size); if (Margin.Left != 0 || Margin.Top != 0 || Margin.Right != 0 || Margin.Bottom != 0)
{
position = new Position(
X: position.X + Margin.Left,
Y: position.Y + Margin.Top
);
size = new Size(
size.Width - Margin.Left - Margin.Right,
size.Height - Margin.Top - Margin.Bottom
);
}
return RenderMethod(renderContext, position, size);
}
protected void RenderEmpty(RenderContext renderContext, Position position, Size size)
{
var driver = renderContext.ConsoleDriver;
var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width);
for (var i = 0; i < size.Height; i++)
{
driver.SetCursorPosition(position with {Y = position.Y + i});
driver.Write(placeHolder);
}
} }
public TChild CreateChild<TChild>() where TChild : IView<T>, new() public TChild CreateChild<TChild>() where TChild : IView<T>, new()
@@ -93,9 +161,9 @@ public abstract partial class View<T> : IView<T>
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T> public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
{ {
child.DataContext = DataContext; child.DataContext = DataContext;
child.ApplicationContext = ApplicationContext; CopyCommonPropertiesToNewChild(child);
var mapper = new DataContextMapper<T>(this, d => child.DataContext = d); var mapper = new DataContextMapper<T, T>(this, child, d => d);
AddDisposable(mapper); AddDisposable(mapper);
child.AddDisposable(mapper); child.AddDisposable(mapper);
@@ -106,15 +174,36 @@ public abstract partial class View<T> : IView<T>
where TChild : IView<TDataContext> where TChild : IView<TDataContext>
{ {
child.DataContext = dataContextMapper(DataContext); child.DataContext = dataContextMapper(DataContext);
child.ApplicationContext = ApplicationContext; CopyCommonPropertiesToNewChild(child);
var mapper = new DataContextMapper<T>(this, d => child.DataContext = dataContextMapper(d)); var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper);
AddDisposable(mapper); AddDisposable(mapper);
child.AddDisposable(mapper); child.AddDisposable(mapper);
return child; return child;
} }
private void CopyCommonPropertiesToNewChild(IView child)
{
child.ApplicationContext = ApplicationContext;
child.Attached = Attached;
}
public virtual void RemoveChild<TDataContext>(IView<TDataContext> child)
{
var mappers = _disposables
.Where(d => d is DataContextMapper<T, TDataContext> mapper && mapper.Target == child)
.ToList();
foreach (var mapper in mappers)
{
mapper.Dispose();
RemoveDisposable(mapper);
}
child.Attached = false;
}
public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable); public void AddDisposable(IDisposable disposable) => _disposables.Add(disposable);
public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable); public void RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable);

View File

@@ -3,25 +3,32 @@ using TerminalUI.Controls;
namespace TerminalUI; namespace TerminalUI;
public class DataContextMapper<T> : IDisposable public class DataContextMapper<TSource, TTarget> : IDisposable
{ {
private readonly IView<T> _source; private readonly Func<TSource?, TTarget?> _mapper;
private readonly Action<T?> _setter; public IView<TSource> Source { get; }
public IView<TTarget> Target { get; }
public DataContextMapper(IView<T> source, Action<T?> setter) public DataContextMapper(IView<TSource> source, IView<TTarget> target, Func<TSource?, TTarget?> mapper)
{ {
_mapper = mapper;
ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(source);
_source = source; Source = source;
_setter = setter; Target = target;
source.PropertyChanged += SourceOnPropertyChanged; source.PropertyChanged += SourceOnPropertyChanged;
} }
private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e) private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName != nameof(IView<object>.DataContext)) return; if (e.PropertyName != nameof(IView<object>.DataContext)) return;
_setter(_source.DataContext); Target.DataContext = _mapper(Source.DataContext);
} }
public void Dispose() => _source.PropertyChanged -= SourceOnPropertyChanged; public void Dispose()
{
Source.PropertyChanged -= SourceOnPropertyChanged;
Source.RemoveDisposable(this);
Target.RemoveDisposable(this);
}
} }

View File

@@ -45,11 +45,12 @@ public class EventLoop : IEventLoop
} }
var size = _applicationContext.ConsoleDriver.GetWindowSize(); var size = _applicationContext.ConsoleDriver.GetWindowSize();
var renderContext = new RenderContext(_applicationContext.ConsoleDriver);
foreach (var view in viewsToRender) foreach (var view in viewsToRender)
{ {
view.Attached = true; view.Attached = true;
view.GetRequestedSize(); view.GetRequestedSize();
view.Render(new Position(0, 0), size); view.Render(renderContext, new Position(0, 0), size);
} }
} }

View File

@@ -9,7 +9,7 @@ public static class Binding
public static Binding<TDataContext, TResult, 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>> dataSourceExpression,
Expression<Func<TView, TResult>> propertyExpression, Expression<Func<TView, TResult>> propertyExpression,
TResult? fallbackValue = default) TResult? fallbackValue = default)
{ {
@@ -18,7 +18,7 @@ public static class Binding
return new Binding<TDataContext, TResult, TResult>( return new Binding<TDataContext, TResult, TResult>(
dataSourceView, dataSourceView,
dataContextExpression, dataSourceExpression,
targetView, targetView,
propertyInfo, propertyInfo,
value => value, value => value,
@@ -29,7 +29,7 @@ public static class Binding
public static Binding<TDataContext, TExpressionResult, TResult> Bind<TView, TDataContext, TExpressionResult, TResult>( public static Binding<TDataContext, TExpressionResult, TResult> Bind<TView, TDataContext, TExpressionResult, TResult>(
this TView targetView, this TView targetView,
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression, Expression<Func<TDataContext?, TExpressionResult>> dataSourceExpression,
Expression<Func<TView, TResult>> propertyExpression, Expression<Func<TView, TResult>> propertyExpression,
Func<TExpressionResult, TResult> converter, Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default) TResult? fallbackValue = default)
@@ -39,7 +39,7 @@ public static class Binding
return new Binding<TDataContext, TExpressionResult, TResult>( return new Binding<TDataContext, TExpressionResult, TResult>(
dataSourceView, dataSourceView,
dataContextExpression, dataSourceExpression,
targetView, targetView,
propertyInfo, propertyInfo,
converter, converter,

View File

@@ -6,6 +6,12 @@ public static class ViewExtensions
{ {
public static T? GetExtension<T>(this IView view) public static T? GetExtension<T>(this IView view)
=> (T?) view.Extensions.FirstOrDefault(e => e is T); => (T?) view.Extensions.FirstOrDefault(e => e is T);
public static IView<TDataContext> WithExtension<TDataContext>(this IView<TDataContext> view, object extension)
{
view.Extensions.Add(extension);
return view;
}
public static ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext> WithDataContextMapper<TSourceDataContext, TTargetDataContext>( public static ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext> WithDataContextMapper<TSourceDataContext, TTargetDataContext>(
this IView<TTargetDataContext> view, this IView<TTargetDataContext> view,

View File

@@ -9,4 +9,5 @@ public interface IApplicationContext
bool IsRunning { get; set; } bool IsRunning { get; set; }
IConsoleDriver ConsoleDriver { get; init; } IConsoleDriver ConsoleDriver { get; init; }
ILoggerFactory? LoggerFactory { get; init; } ILoggerFactory? LoggerFactory { get; init; }
char EmptyCharacter { get; init; }
} }

View File

@@ -0,0 +1,18 @@
namespace TerminalUI.Models;
public record Margin(int Left, int Top, int Right, int Bottom)
{
public static implicit operator Margin(int value) => new(value, value, value, value);
public static implicit operator Margin((int Left, int Top, int Right, int Bottom) value) => new(value.Left, value.Top, value.Right, value.Bottom);
public static implicit operator Margin(string s)
{
var parts = s.Split(' ');
return parts.Length switch
{
1 => new Margin(int.Parse(parts[0])),
2 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[0]), int.Parse(parts[1])),
4 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])),
_ => throw new ArgumentException("Invalid margin format", nameof(s))
};
}
}

View File

@@ -0,0 +1,18 @@
using TerminalUI.ConsoleDrivers;
namespace TerminalUI.Models;
public readonly ref struct RenderContext
{
private static int _renderId = 0;
public readonly int RenderId;
public readonly IConsoleDriver ConsoleDriver;
public RenderContext(IConsoleDriver consoleDriver)
{
ConsoleDriver = consoleDriver;
RenderId = _renderId++;
}
public static RenderContext Empty => new(null!);
}

View File

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

View File

@@ -1,10 +1,9 @@
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.Models;
namespace TerminalUI.Traits; namespace TerminalUI.Traits;
public interface IContentRenderer public interface IContentRenderer<T>
{ {
IView? Content { get; set; } IView<T>? Content { get; set; }
Action<Position, Size> ContentRendererMethod { get; set; } RenderMethod ContentRendererMethod { get; set; }
} }