diff --git a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs index aa9820b..85f102c 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs @@ -14,7 +14,7 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel _logger; string IModalViewModel.Name => "CommandPalette"; @@ -22,14 +22,14 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel logger) : base((a, b) => a.Identifier == b.Identifier) { _commandPaletteService = commandPaletteService; _identifiableUserCommandService = identifiableUserCommandService; _userCommandHandlerService = userCommandHandlerService; - _keyboardConfigurationService = keyboardConfigurationService; + _commandKeysHelperService = commandKeysHelperService; _logger = logger; ShowWindow = _commandPaletteService.ShowWindow; UpdateFilteredMatchesInternal(); @@ -47,47 +47,10 @@ public class CommandPaletteViewModel : FuzzyPanelViewModel - (ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title, GetKeyConfigsString(c.Identifier)) + (ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title, _commandKeysHelperService.GetKeyConfigsString(c.Identifier)) ) .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> GetKeyConfigsForCommand(string commandIdentifier) - => _keyboardConfigurationService - .AllShortcut - .Where(s => s.Command == commandIdentifier) - .Select(k => k.Keys) - .ToList(); - public override async Task HandleKeyDown(GeneralKeyEventArgs keyEventArgs) { if (keyEventArgs.Handled) return false; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandKeysHelperService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandKeysHelperService.cs new file mode 100644 index 0000000..0c6a3e6 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/ICommandKeysHelperService.cs @@ -0,0 +1,10 @@ +using FileTime.App.Core.Configuration; + +namespace FileTime.App.Core.Services; + +public interface ICommandKeysHelperService +{ + List> GetKeysForCommand(string commandName); + string GetKeyConfigsString(string commandIdentifier); + string FormatKeyConfig(KeyConfig keyConfig); +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPossibleCommandsService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPossibleCommandsService.cs new file mode 100644 index 0000000..79cb296 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IPossibleCommandsService.cs @@ -0,0 +1,13 @@ +using System.Collections.ObjectModel; +using FileTime.App.Core.Configuration; + +namespace FileTime.App.Core.Services; + +public interface IPossibleCommandsService +{ + ReadOnlyObservableCollection PossibleCommands { get; set; } + void Clear(); + void Add(CommandBindingConfiguration commandBindingConfiguration); + void Remove(CommandBindingConfiguration commandBindingConfiguration); + void AddRange(IEnumerable commandBindingConfigurations); +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs index a9a7595..75dd5e4 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs @@ -16,7 +16,6 @@ public interface IAppState IDeclarativeProperty RapidTravelTextDebounced { get; } IDeclarativeProperty ContainerStatus { get; } List PreviousKeys { get; } - List PossibleCommands { get; set; } bool NoCommandFound { get; set; } void AddTab(ITabViewModel tabViewModel); diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandEntryViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandEntryViewModel.cs new file mode 100644 index 0000000..e7d462d --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandEntryViewModel.cs @@ -0,0 +1,8 @@ +namespace FileTime.App.Core.ViewModels; + +public interface IPossibleCommandEntryViewModel +{ + public string CommandName { get; } + public string Title { get; } + public string KeysText { get; } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandsViewModel.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandsViewModel.cs new file mode 100644 index 0000000..6c3ed68 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IPossibleCommandsViewModel.cs @@ -0,0 +1,8 @@ +using System.Collections.ObjectModel; + +namespace FileTime.App.Core.ViewModels; + +public interface IPossibleCommandsViewModel +{ + ObservableCollection PossibleCommands { get; } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj index a0ea7e3..c8371a9 100644 --- a/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj +++ b/src/AppCommon/FileTime.App.Core/FileTime.App.Core.csproj @@ -30,6 +30,7 @@ + diff --git a/src/AppCommon/FileTime.App.Core/Services/CommandKeysHelperService.cs b/src/AppCommon/FileTime.App.Core/Services/CommandKeysHelperService.cs new file mode 100644 index 0000000..f7fe6c4 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/Services/CommandKeysHelperService.cs @@ -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> 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(); + } +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs index 666bf78..d02f689 100644 --- a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs @@ -21,6 +21,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler private readonly ILogger _logger; private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService; + private readonly IPossibleCommandsService _possibleCommandsService; private readonly BindedCollection _openModals; public DefaultModeKeyInputHandler( @@ -29,10 +30,12 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler IKeyboardConfigurationService keyboardConfigurationService, ILogger logger, IUserCommandHandlerService userCommandHandlerService, - IIdentifiableUserCommandService identifiableUserCommandService) + IIdentifiableUserCommandService identifiableUserCommandService, + IPossibleCommandsService possibleCommandsService) { _appState = appState; _identifiableUserCommandService = identifiableUserCommandService; + _possibleCommandsService = possibleCommandsService; _keyboardConfigurationService = keyboardConfigurationService; _logger = logger; _modalService = modalService; @@ -99,7 +102,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler { args.Handled = true; _appState.PreviousKeys.Clear(); - _appState.PossibleCommands = new(); + _possibleCommandsService.Clear(); } } /*else if (key == Key.Enter @@ -113,7 +116,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler { args.Handled = true; _appState.PreviousKeys.Clear(); - _appState.PossibleCommands = new(); + _possibleCommandsService.Clear(); var command = _identifiableUserCommandService.GetCommand(selectedCommandBinding.Command); if (command is not null) { @@ -123,7 +126,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler else if (_keysToSkip.Any(k => k.AreKeysEqual(_appState.PreviousKeys))) { _appState.PreviousKeys.Clear(); - _appState.PossibleCommands = new(); + _possibleCommandsService.Clear(); return; } else if (_appState.PreviousKeys.Count == 2) @@ -131,7 +134,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler args.Handled = true; _appState.NoCommandFound = true; _appState.PreviousKeys.Clear(); - _appState.PossibleCommands = new(); + _possibleCommandsService.Clear(); } else { @@ -145,7 +148,8 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler } else { - _appState.PossibleCommands = possibleCommands; + _possibleCommandsService.Clear(); + _possibleCommandsService.AddRange(possibleCommands); } } } diff --git a/src/AppCommon/FileTime.App.Core/Services/PossibleCommandsService.cs b/src/AppCommon/FileTime.App.Core/Services/PossibleCommandsService.cs new file mode 100644 index 0000000..dc6fe75 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/Services/PossibleCommandsService.cs @@ -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 _possibleCommands = new(); + public ReadOnlyObservableCollection PossibleCommands { get; set; } + + public PossibleCommandsService() + { + PossibleCommands = new ReadOnlyObservableCollection(_possibleCommands); + } + + public void Clear() => _possibleCommands.Clear(); + + public void Add(CommandBindingConfiguration commandBindingConfiguration) + => _possibleCommands.Add(commandBindingConfiguration); + + public void AddRange(IEnumerable commandBindingConfigurations) + { + foreach (var commandBindingConfiguration in commandBindingConfigurations) + { + _possibleCommands.Add(commandBindingConfiguration); + } + } + + public void Remove(CommandBindingConfiguration commandBindingConfiguration) + => _possibleCommands.Remove(commandBindingConfiguration); +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Startup.cs b/src/AppCommon/FileTime.App.Core/Startup.cs index 6485880..c25e728 100644 --- a/src/AppCommon/FileTime.App.Core/Startup.cs +++ b/src/AppCommon/FileTime.App.Core/Startup.cs @@ -33,6 +33,9 @@ public static class Startup serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); return serviceCollection .AddCommandHandlers() diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs index ecfd85d..94d5d4b 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -4,9 +4,7 @@ using System.Reactive.Subjects; using DeclarativeProperty; using FileTime.App.Core.Configuration; using FileTime.App.Core.Models.Enums; -using FileTime.App.Core.ViewModels.Timeline; using FileTime.Core.Models.Extensions; -using MvvmGen; using MoreLinq; using PropertyChanged.SourceGenerator; @@ -31,7 +29,6 @@ public abstract partial class AppStateBase : IAppState public IDeclarativeProperty ContainerStatus { get; } [Notify] public List PreviousKeys { get; } = new(); - [Notify] public List PossibleCommands { get; set; } = new(); [Notify] public bool NoCommandFound { get; set; } protected AppStateBase() diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandEntryViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandEntryViewModel.cs new file mode 100644 index 0000000..0d1dc4b --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandEntryViewModel.cs @@ -0,0 +1,6 @@ +namespace FileTime.App.Core.ViewModels; + +public record PossibleCommandEntryViewModel( + string CommandName, + string Title, + string KeysText) : IPossibleCommandEntryViewModel; \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandsViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandsViewModel.cs new file mode 100644 index 0000000..e28a531 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core/ViewModels/PossibleCommandsViewModel.cs @@ -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 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(); +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs index 00b6fa1..794417a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs @@ -4,5 +4,4 @@ namespace FileTime.ConsoleUI.App; public interface IConsoleAppState : IAppState { - } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs new file mode 100644 index 0000000..0957840 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs @@ -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; } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs index 471a738..314f24a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -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.Core.Enums; using TerminalUI; @@ -8,69 +7,103 @@ using TerminalUI.Controls; using TerminalUI.Extensions; using TerminalUI.Models; using TerminalUI.ViewExtensions; -using ConsoleColor = TerminalUI.Color.ConsoleColor; namespace FileTime.ConsoleUI.App; public class MainWindow { - private readonly IConsoleAppState _consoleAppState; + private readonly IRootViewModel _rootViewModel; private readonly IApplicationContext _applicationContext; private readonly ITheme _theme; private IView _root; public MainWindow( - IConsoleAppState consoleAppState, + IRootViewModel rootViewModel, IApplicationContext applicationContext, ITheme theme) { - _consoleAppState = consoleAppState; + _rootViewModel = rootViewModel; _applicationContext = applicationContext; _theme = theme; } public void Initialize() { - var root = new Grid + var root = new Grid { - DataContext = _consoleAppState, + DataContext = _rootViewModel, ApplicationContext = _applicationContext, - RowDefinitionsObject = "Auto *", + RowDefinitionsObject = "Auto * Auto", ChildInitializer = { - new Grid + new Grid { - ColumnDefinitionsObject = "* Auto", + ColumnDefinitionsObject = "Auto * Auto", ChildInitializer = { - new TextBlock() + new StackPanel + { + Name = "username_panel", + Orientation = Orientation.Horizontal, + Margin = "0 0 1 0", + ChildInitializer = { - Foreground = _theme.ContainerColor + new TextBlock() + .Setup(t => t.Bind( + t, + root => root.UserName, + tb => tb.Text + )), + new TextBlock() + .Setup(t => t.Bind( + t, + root => root.MachineName, + tb => tb.Text, + t => $"@{t}" + )) } - .Setup(t => - t.Bind( - t, - appState => appState.SelectedTab.Value.CurrentLocation.Value.FullName.Path, - tb => tb.Text, - value => value - ) - ), + }, + new TextBlock + { + Foreground = _theme.ContainerColor, + Extensions = + { + new GridPositionExtension(1, 0) + } + } + .Setup(t => t.Bind( + t, + root => root.AppState.SelectedTab.Value.CurrentLocation.Value.FullName.Path, + tb => tb.Text + )), TabControl() + .WithExtension(new GridPositionExtension(2, 0)) } }, - new Grid + new Grid { ColumnDefinitionsObject = "* 4* 4*", - ChildInitializer = - { - ParentsItemsView(), - SelectedItemsView(), - SelectedsItemsView(), - }, Extensions = { 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 + { + Extensions = + { + new GridPositionExtension(0, 2) + }, + ChildInitializer = + { + PossibleCommands() } } } @@ -78,15 +111,58 @@ public class MainWindow _root = root; } - private IView TabControl() + private IView PossibleCommands() { - var tabList = new ListView + //TODO: Create and use DataGrid + var commandBindings = new ListView + { + ItemTemplate = _ => + { + var grid = new Grid + { + ColumnDefinitionsObject = "10 *", + ChildInitializer = + { + new TextBlock() + .Setup(t => + t.Bind( + t, + dc => dc.KeysText, + tb => tb.Text) + ), + new TextBlock + { + 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 TabControl() + { + var tabList = new ListView { Orientation = Orientation.Horizontal, - Extensions = - { - new GridPositionExtension(1, 0) - }, ItemTemplate = item => { var textBlock = item.CreateChild>(); @@ -96,6 +172,7 @@ public class MainWindow textBlock, dc => dc.TabNumber.ToString(), tb => tb.Text, + value => $" {value}", fallbackValue: "?"); textBlock.Bind( @@ -110,23 +187,17 @@ public class MainWindow tabList.Bind( tabList, - appState => appState == null ? null : appState.Tabs, + root => root.AppState.Tabs, v => v.ItemsSource); return tabList; } - private ListView SelectedItemsView() + private ListView SelectedItemsView() { - var list = new ListView + var list = new ListView { - DataContext = _consoleAppState, - ApplicationContext = _applicationContext, - ListPadding = 8, - Extensions = - { - new GridPositionExtension(1, 0) - } + ListPadding = 8 }; list.ItemTemplate = item => @@ -153,33 +224,22 @@ public class MainWindow list.Bind( 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); list.Bind( list, - appState => - appState == null - ? null - : appState.SelectedTab.Value == null - ? null - : appState.SelectedTab.Value.CurrentSelectedItem.Value, + root => root.AppState.SelectedTab.Value.CurrentSelectedItem.Value, v => v.SelectedItem); return list; } - private ListView SelectedsItemsView() + private ListView SelectedsItemsView() { - var list = new ListView + var list = new ListView { - DataContext = _consoleAppState, - ApplicationContext = _applicationContext, - ListPadding = 8, - Extensions = - { - new GridPositionExtension(2, 0) - } + ListPadding = 8 }; list.ItemTemplate = item => @@ -206,23 +266,17 @@ public class MainWindow list.Bind( 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); return list; } - private ListView ParentsItemsView() + private ListView ParentsItemsView() { - var list = new ListView + var list = new ListView { - DataContext = _consoleAppState, - ApplicationContext = _applicationContext, - ListPadding = 8, - Extensions = - { - new GridPositionExtension(0, 0) - } + ListPadding = 8 }; list.ItemTemplate = item => @@ -249,67 +303,12 @@ public class MainWindow list.Bind( 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); return list; } - private void TestGrid() - { - var grid = new Grid - { - ApplicationContext = _applicationContext, - ColumnDefinitionsObject = "Auto Auto", - RowDefinitionsObject = "Auto Auto", - ChildInitializer = - { - new Rectangle - { - Fill = new ConsoleColor(System.ConsoleColor.Blue, ColorType.Foreground), - Extensions = - { - new GridPositionExtension(0, 0) - }, - Width = 2, - Height = 2, - }, - new Rectangle - { - Fill = new ConsoleColor(System.ConsoleColor.Red, ColorType.Foreground), - Extensions = - { - new GridPositionExtension(0, 1) - }, - Width = 3, - Height = 3, - }, - new Rectangle - { - Fill = new ConsoleColor(System.ConsoleColor.Green, ColorType.Foreground), - Extensions = - { - new GridPositionExtension(1, 0) - }, - Width = 4, - Height = 4, - }, - new Rectangle - { - Fill = new ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground), - Extensions = - { - new GridPositionExtension(1, 1) - }, - Width = 5, - Height = 5, - } - } - }; - - //_grid = grid; - } - public IEnumerable RootViews() => new IView[] { _root diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs new file mode 100644 index 0000000..8945808 --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs index 4c55ff6..815b24d 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -28,6 +28,7 @@ public static class Startup services.TryAddSingleton(); services.AddSingleton(); services.TryAddSingleton(new ApplicationConfiguration(true)); + services.TryAddSingleton(); services.Configure(configuration); diff --git a/src/FileTime.sln b/src/FileTime.sln index d2099c5..3b0fdda 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -123,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CircularBuffer", "Library\C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", "ConsoleApp\FileTime.ConsoleUI.Styles\FileTime.ConsoleUI.Styles.csproj", "{CCB6F86A-7E80-448E-B543-DF9DB337C42A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Extensions", "Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj", "{6C3C3151-9341-4792-9B0B-A11C0658524E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -390,6 +396,7 @@ Global {2F01FC4C-D942-48B0-B61C-7C5BEAED4787} = {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} + {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/CommandToCommandNameConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/CommandToCommandNameConverter.cs deleted file mode 100644 index 0d727f2..0000000 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/CommandToCommandNameConverter.cs +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Converters.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Converters.axaml index 272a4d1..e071951 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Converters.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Converters.axaml @@ -34,7 +34,6 @@ - false; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml index a1f0ab6..f475ad4 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml @@ -750,7 +750,7 @@ - + @@ -763,17 +763,17 @@ Margin="10,0" VerticalAlignment="Center" /> - + - + - - + + diff --git a/src/Library/ObservableComputations.Extensions/Extensions.cs b/src/Library/ObservableComputations.Extensions/Extensions.cs new file mode 100644 index 0000000..d37c797 --- /dev/null +++ b/src/Library/ObservableComputations.Extensions/Extensions.cs @@ -0,0 +1,13 @@ +using System.Collections.ObjectModel; +using System.Linq.Expressions; + +namespace ObservableComputations; + +public static class Extensions +{ + [ObservableComputationsCall] + public static Selecting Selecting( + this ReadOnlyObservableCollection source, + Expression> selectorExpression) + => new(source, selectorExpression); +} \ No newline at end of file diff --git a/src/Library/ObservableComputations.Extensions/ObservableComputations.Extensions.csproj b/src/Library/ObservableComputations.Extensions/ObservableComputations.Extensions.csproj new file mode 100644 index 0000000..8d061c1 --- /dev/null +++ b/src/Library/ObservableComputations.Extensions/ObservableComputations.Extensions.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + ObservableComputations + + + + + + + diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index 2f5d6ae..ad5fb04 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -9,6 +9,7 @@ public class ApplicationContext : IApplicationContext public ILoggerFactory? LoggerFactory { get; init; } public IEventLoop EventLoop { get; init; } public bool IsRunning { get; set; } + public char EmptyCharacter { get; init; } = ' '; public ApplicationContext() { diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 7bdbdf0..521da25 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -20,7 +20,7 @@ public class Binding : IDisposable public Binding( IView dataSourceView, - Expression> dataContextExpression, + Expression> dataSourceExpression, object? propertySource, PropertyInfo targetProperty, Func converter, @@ -28,18 +28,18 @@ public class Binding : IDisposable ) { ArgumentNullException.ThrowIfNull(dataSourceView); - ArgumentNullException.ThrowIfNull(dataContextExpression); + ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(targetProperty); ArgumentNullException.ThrowIfNull(converter); _dataSourceView = dataSourceView; - _dataContextMapper = dataContextExpression.Compile(); + _dataContextMapper = dataSourceExpression.Compile(); _propertySource = propertySource; _targetProperty = targetProperty; _converter = converter; _fallbackValue = fallbackValue; - InitTrackingTree(dataContextExpression); + InitTrackingTree(dataSourceExpression); UpdateTrackers(); @@ -55,8 +55,8 @@ public class Binding : IDisposable { if (propertySource is IDisposableCollection propertySourceDisposableCollection) { - propertySourceDisposableCollection.AddDisposable(this); _propertySourceDisposableCollection = propertySourceDisposableCollection; + propertySourceDisposableCollection.AddDisposable(this); } } diff --git a/src/Library/TerminalUI/Controls/ChildContainerView.cs b/src/Library/TerminalUI/Controls/ChildContainerView.cs index 042bd8c..0da6fd6 100644 --- a/src/Library/TerminalUI/Controls/ChildContainerView.cs +++ b/src/Library/TerminalUI/Controls/ChildContainerView.cs @@ -41,6 +41,14 @@ public abstract class ChildContainerView : View, IChildContainer }; } + protected override void AttachChildren() + { + foreach (var child in Children) + { + child.Attached = true; + } + } + public override TChild AddChild(TChild child) { child = base.AddChild(child); diff --git a/src/Library/TerminalUI/Controls/ContentView.cs b/src/Library/TerminalUI/Controls/ContentView.cs index 9cae970..118a83a 100644 --- a/src/Library/TerminalUI/Controls/ContentView.cs +++ b/src/Library/TerminalUI/Controls/ContentView.cs @@ -1,16 +1,65 @@ -using TerminalUI.Models; +using PropertyChanged.SourceGenerator; +using TerminalUI.Models; using TerminalUI.Traits; namespace TerminalUI.Controls; -public abstract class ContentView: View, IContentRenderer +public abstract partial class ContentView : View, IContentRenderer { + private bool _placeholderRenderDone; + [Notify] private RenderMethod _contentRendererMethod; + private IView? _content; + + public IView? 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() { - ContentRendererMethod = DefaultContentRender; + _contentRendererMethod = DefaultContentRender; + RerenderProperties.Add(nameof(Content)); + RerenderProperties.Add(nameof(ContentRendererMethod)); } - public IView? Content { get; set; } - public Action 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); + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index bbc5925..9ad331f 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Diagnostics; using Microsoft.Extensions.Logging; using TerminalUI.Extensions; using TerminalUI.Models; @@ -8,15 +9,79 @@ namespace TerminalUI.Controls; public class Grid : ChildContainerView { + private List _rowDefinitions = new() {RowDefinition.Star(1)}; + private List _columnDefinitions = new() {ColumnDefinition.Star(1)}; private ILogger>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger>(); - private delegate void WithSizes(Span widths, Span heights); + private delegate void WithSizes(RenderContext renderContext, Span widths, Span heights); - private delegate TResult WithSizes(Span widths, Span heights); + private delegate TResult WithSizes(RenderContext renderContext, Span widths, Span heights); private const int ToBeCalculated = -1; - public ObservableCollection RowDefinitions { get; } = new() {RowDefinition.Star(1)}; - public ObservableCollection ColumnDefinitions { get; } = new() {ColumnDefinition.Star(1)}; + + public IReadOnlyList RowDefinitions + { + get => _rowDefinitions; + set + { + var nextValue = value; + if (value.Count == 0) + { + nextValue = new List {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 ColumnDefinitions + { + get => _columnDefinitions; + set + { + var nextValue = value; + if (value.Count == 0) + { + nextValue = new List {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 { @@ -25,11 +90,7 @@ public class Grid : ChildContainerView { if (value is IEnumerable columnDefinitions) { - ColumnDefinitions.Clear(); - foreach (var columnDefinition in columnDefinitions) - { - ColumnDefinitions.Add(columnDefinition); - } + ColumnDefinitions = columnDefinitions.ToList(); } else if (value is string s) { @@ -49,11 +110,7 @@ public class Grid : ChildContainerView { if (value is IEnumerable rowDefinitions) { - RowDefinitions.Clear(); - foreach (var rowDefinition in rowDefinitions) - { - RowDefinitions.Add(rowDefinition); - } + RowDefinitions = rowDefinitions.ToList(); } else if (value is string s) { @@ -66,85 +123,135 @@ public class Grid : ChildContainerView } } - public override Size GetRequestedSize() - => WithCalculatedSize((columnWidths, rowHeights) => - { - var width = 0; - var height = 0; - - for (var i = 0; i < columnWidths.Length; i++) + protected override Size CalculateSize() + => WithCalculatedSize( + RenderContext.Empty, + new Option(new Size(0, 0), false), + (_, columnWidths, rowHeights) => { - 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, true), + (context, columnWidths, rowHeights) => { - height += rowHeights[i]; - } - - return new Size(width, height); - }, new Option(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(); - var x = positionExtension?.Column ?? 0; - var y = positionExtension?.Row ?? 0; - - if (x > columnWidths.Length) + foreach (var child in Children) { - Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y); - x = 0; + var (x, y) = GetViewColumnAndRow(child, columnWidths.Length, rowHeights.Length); + + 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) - { - Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", child, x, y); - y = 0; - } + return true; - var width = columnWidths[x]; - var height = rowHeights[y]; + /*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights); + CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/ + }); - var left = 0; - 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, true)); - - private void WithCalculatedSize(WithSizes actionWithSizes, Option size) + /*private void CleanUnusedArea(Dictionary<(int, int),List> viewsByPosition, Span columnWidths, Span rowHeights) { - WithCalculatedSize(Helper, size); - - object? Helper(Span widths, Span heights) + for (var x = 0; x < columnWidths.Length; x++) { - 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> GroupViewsByPosition(int columns, int rows) + { + Dictionary, List> 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 {child}; + } + } + + return viewsByPosition; + }*/ + + private ValueTuple GetViewColumnAndRow(IView view, int columns, int rows) + { + var positionExtension = view.GetExtension(); + 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, WithSizes actionWithSizes) + { + WithCalculatedSize(renderContext, size, Helper); + + object? Helper(RenderContext renderContext1, Span widths, Span heights) + { + actionWithSizes(renderContext1, widths, heights); return null; } } - private TResult WithCalculatedSize(WithSizes actionWithSizes, Option size) + private TResult WithCalculatedSize(RenderContext renderContext, Option size, WithSizes actionWithSizes) { //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; + Debug.Assert(columns > 0, "Columns must contain at least one element"); + Debug.Assert(rows > 0, "Rows must contain at least one element"); Span allWidth = stackalloc int[columns * rows]; Span allHeight = stackalloc int[columns * rows]; @@ -152,9 +259,7 @@ public class Grid : ChildContainerView foreach (var child in Children) { var childSize = child.GetRequestedSize(); - var positionExtension = child.GetExtension(); - var x = positionExtension?.Column ?? 0; - var y = positionExtension?.Row ?? 0; + var (x, y) = GetViewColumnAndRow(child, columns, rows); allWidth.SetToMatrix(childSize.Width, x, y, columns); allHeight.SetToMatrix(childSize.Height, x, y, columns); @@ -246,60 +351,64 @@ public class Grid : ChildContainerView } } - return actionWithSizes(columnWidths, rowHeights); + return actionWithSizes(renderContext, columnWidths, rowHeights); } public void SetRowDefinitions(string value) { var values = value.Split(' '); - RowDefinitions.Clear(); + var rowDefinitions = new List(); foreach (var v in values) { if (v == "Auto") { - RowDefinitions.Add(RowDefinition.Auto); + rowDefinitions.Add(RowDefinition.Auto); } else if (v.EndsWith("*")) { 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)) { - RowDefinitions.Add(RowDefinition.Pixel(pixelValue)); + rowDefinitions.Add(RowDefinition.Pixel(pixelValue)); } else { throw new ArgumentException("Invalid row definition: " + v); } } + + RowDefinitions = rowDefinitions; } public void SetColumnDefinitions(string value) { var values = value.Split(' '); - ColumnDefinitions.Clear(); + var columnDefinitions = new List(); foreach (var v in values) { if (v == "Auto") { - ColumnDefinitions.Add(ColumnDefinition.Auto); + columnDefinitions.Add(ColumnDefinition.Auto); } else if (v.EndsWith("*")) { 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)) { - ColumnDefinitions.Add(ColumnDefinition.Pixel(pixelValue)); + columnDefinitions.Add(ColumnDefinition.Pixel(pixelValue)); } else { throw new ArgumentException("Invalid column definition: " + v); } } + + ColumnDefinitions = columnDefinitions; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index eb02ff2..bcd9f92 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -4,23 +4,29 @@ using TerminalUI.Traits; namespace TerminalUI.Controls; +public delegate bool RenderMethod(RenderContext renderContext, Position position, Size size); + public interface IView : INotifyPropertyChanged, IDisposableCollection { object? DataContext { get; set; } int? MinWidth { get; set; } int? MaxWidth { get; set; } int? Width { get; set; } + int ActualWidth { get; } int? MinHeight { get; set; } int? MaxHeight { get; set; } int? Height { get; set; } + int ActualHeight { get; } + Margin Margin { get; set; } bool Attached { get; set; } - Size GetRequestedSize(); + string? Name { get; set; } IApplicationContext? ApplicationContext { get; set; } List Extensions { get; } - - Action RenderMethod { get; set; } + RenderMethod RenderMethod { get; set; } event Action Disposed; - void Render(Position position, Size size); + + Size GetRequestedSize(); + bool Render(RenderContext renderContext, Position position, Size size); } public interface IView : IView @@ -39,8 +45,10 @@ public interface IView : IView TChild CreateChild(Func dataContextMapper) where TChild : IView, new(); - public TChild AddChild(TChild child) where TChild : IView; + TChild AddChild(TChild child) where TChild : IView; - public TChild AddChild(TChild child, Func dataContextMapper) + TChild AddChild(TChild child, Func dataContextMapper) where TChild : IView; + + void RemoveChild(IView child); } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index d11d467..b2fc53b 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Collections.ObjectModel; +using System.Collections.Specialized; using DeclarativeProperty; using PropertyChanged.SourceGenerator; using TerminalUI.Models; @@ -79,18 +80,30 @@ public partial class ListView : View if (_itemsSource is IDeclarativeProperty> observableDeclarativeProperty) { - observableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); - _getItems = () => observableDeclarativeProperty.Value; + throw new NotSupportedException(); } else if (_itemsSource is IDeclarativeProperty> readOnlyObservableDeclarativeProperty) { - readOnlyObservableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); - _getItems = () => readOnlyObservableDeclarativeProperty.Value; + throw new NotSupportedException(); } else if (_itemsSource is IDeclarativeProperty> enumerableDeclarativeProperty) { - enumerableDeclarativeProperty.PropertyChanged += (_, _) => ApplicationContext?.EventLoop.RequestRerender(); - _getItems = () => enumerableDeclarativeProperty.Value; + throw new NotSupportedException(); + } + + if (_itemsSource is ObservableCollection observableDeclarative) + { + ((INotifyCollectionChanged) observableDeclarative).CollectionChanged += + (_, _) => ApplicationContext?.EventLoop.RequestRerender(); + + _getItems = () => observableDeclarative; + } + else if (_itemsSource is ReadOnlyObservableCollection readOnlyObservableDeclarative) + { + ((INotifyCollectionChanged) readOnlyObservableDeclarative).CollectionChanged += + (_, _) => ApplicationContext?.EventLoop.RequestRerender(); + + _getItems = () => readOnlyObservableDeclarative; } else if (_itemsSource is ICollection collection) _getItems = () => collection; @@ -111,7 +124,7 @@ public partial class ListView : View } } - public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; + public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; public ListView() { @@ -120,7 +133,7 @@ public partial class ListView : View RerenderProperties.Add(nameof(Orientation)); } - public override Size GetRequestedSize() + protected override Size CalculateSize() { InstantiateItemViews(); if (_listViewItems is null || _listViewItemLength == 0) @@ -147,19 +160,16 @@ public partial class ListView : View } } - protected override void DefaultRenderer(Position position, Size size) - { - if (Orientation == Orientation.Vertical) - RenderVertical(position, size); - else - RenderHorizontal(position, size); - } + protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size) + => Orientation == Orientation.Vertical + ? RenderVertical(renderContext, position, size) + : RenderHorizontal(renderContext, 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 var listViewItems = InstantiateItemViews(); - if (listViewItems.Length == 0) return; + if (listViewItems.Length == 0) return false; Span requestedSizes = stackalloc Size[_listViewItemLength]; @@ -215,20 +225,22 @@ public partial class ListView : View 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; } + + return true; } - private void RenderVertical(Position position, Size size) + private bool RenderVertical(RenderContext renderContext, Position position, Size size) { //Note: only same height is supported var requestedItemSize = _requestedItemSize; if (requestedItemSize.Height == 0 || requestedItemSize.Width == 0) - return; + return false; var listViewItems = InstantiateItemViews(); - if (listViewItems.Length == 0) return; + if (listViewItems.Length == 0) return false; var itemsToRender = listViewItems.Length; var heightNeeded = requestedItemSize.Height * listViewItems.Length; @@ -264,7 +276,7 @@ public partial class ListView : View for (var i = renderStartIndex; i < lastItemIndex; 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; } @@ -276,6 +288,8 @@ public partial class ListView : View driver.SetCursorPosition(position with {Y = position.Y + i}); driver.Write(placeholder); } + + return true; } private Span> InstantiateItemViews() @@ -300,11 +314,16 @@ public partial class ListView : View { var dataContext = items[i]; var child = CreateChild, TItem>(_ => dataContext); - child.Content = ItemTemplate(child); - ItemTemplate(child); + var newContent = ItemTemplate(child); + child.Content = newContent; newListViewItems[i] = child; } + if (_listViewItems is not null) + { + ListViewItemPool.Return(_listViewItems); + } + _listViewItems = newListViewItems; _listViewItemLength = items.Count; listViewItems = newListViewItems[..items.Count]; @@ -324,5 +343,5 @@ public partial class ListView : View return _listViewItems; } - private static IView? DefaultItemTemplate(ListViewItem listViewItem) => null; + private static IView? DefaultItemTemplate(ListViewItem listViewItem) => null; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs index 29012eb..a4097e7 100644 --- a/src/Library/TerminalUI/Controls/ListViewItem.cs +++ b/src/Library/TerminalUI/Controls/ListViewItem.cs @@ -4,13 +4,13 @@ namespace TerminalUI.Controls; public class ListViewItem : ContentView { - public override Size GetRequestedSize() + protected override Size CalculateSize() { if (Content is null) return new Size(0, 0); 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) { @@ -22,6 +22,6 @@ public class ListViewItem : ContentView + DataContext?.GetType().Name); } - ContentRendererMethod(position, size); + return ContentRendererMethod(renderContext, position, size); } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/Rectangle.cs b/src/Library/TerminalUI/Controls/Rectangle.cs index eef5bbc..6b2593a 100644 --- a/src/Library/TerminalUI/Controls/Rectangle.cs +++ b/src/Library/TerminalUI/Controls/Rectangle.cs @@ -6,19 +6,38 @@ namespace TerminalUI.Controls; public partial class Rectangle : View { - [Notify] private IColor? _fill; - public override Size GetRequestedSize() => new(Width ?? 0, Height ?? 0); + private record RenderState( + 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); - ApplicationContext?.ConsoleDriver.SetBackgroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Background)); - ApplicationContext?.ConsoleDriver.SetForegroundColor(Fill ?? new Color.ConsoleColor(System.ConsoleColor.Yellow, ColorType.Foreground)); - var height = Height ?? size.Height; + var renderState = new RenderState(position, size, Fill); + if (!NeedsRerender(renderState) || Fill is null) return false; + _lastRenderState = renderState; + + 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++) { - ApplicationContext?.ConsoleDriver.SetCursorPosition(position with {Y = position.Y + i}); - ApplicationContext?.ConsoleDriver.Write(s); + driver.SetCursorPosition(position with {Y = position.Y + i}); + driver.Write(s); } + + return true; } + + private bool NeedsRerender(RenderState renderState) + => _lastRenderState is null || _lastRenderState != renderState; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/StackPanel.cs b/src/Library/TerminalUI/Controls/StackPanel.cs index 2ee4e3e..43d915d 100644 --- a/src/Library/TerminalUI/Controls/StackPanel.cs +++ b/src/Library/TerminalUI/Controls/StackPanel.cs @@ -9,7 +9,7 @@ public partial class StackPanel : ChildContainerView private readonly Dictionary _requestedSizes = new(); [Notify] private Orientation _orientation = Orientation.Vertical; - public override Size GetRequestedSize() + protected override Size CalculateSize() { _requestedSizes.Clear(); var width = 0; @@ -35,20 +35,38 @@ public partial class StackPanel : ChildContainerView 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 neededRerender = false; 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); + + 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 ? childSize.Height : childSize.Width; } + + return neededRerender; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index d392a2c..a215253 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -1,5 +1,7 @@ -using PropertyChanged.SourceGenerator; +using System.ComponentModel; +using PropertyChanged.SourceGenerator; using TerminalUI.Color; +using TerminalUI.ConsoleDrivers; using TerminalUI.Extensions; using TerminalUI.Models; @@ -7,9 +9,16 @@ namespace TerminalUI.Controls; public partial class TextBlock : View { - 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 IColor? _foreground; @@ -28,23 +37,41 @@ public partial class TextBlock : View RerenderProperties.Add(nameof(Foreground)); RerenderProperties.Add(nameof(Background)); 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 renderContext = new RenderContext(position, Text, _foreground, _background); - if (!NeedsRerender(renderContext)) return; + var driver = renderContext.ConsoleDriver; + var renderState = new RenderState(position, size, Text, _foreground, _background); + 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(); if (Foreground is { } foreground) { @@ -56,19 +83,31 @@ public partial class TextBlock : View driver.SetBackgroundColor(background); } - 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]; - } + RenderText(_textLines, driver, position, size); - driver.Write(text); + return true; } - private bool NeedsRerender(RenderContext renderContext) - => _renderContext is null || _renderContext != renderContext; + private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size) + { + 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; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index c1328d3..aa122aa 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -13,9 +13,13 @@ public abstract partial class View : IView [Notify] private int? _minWidth; [Notify] private int? _maxWidth; [Notify] private int? _width; + [Notify] private int _actualWidth; [Notify] private int? _minHeight; [Notify] private int? _maxHeight; [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; private bool _attached; @@ -32,17 +36,50 @@ public abstract partial class View : IView } } } + public List Extensions { get; } = new(); - public Action RenderMethod { get; set; } + public RenderMethod RenderMethod { get; set; } public event Action? Disposed; protected List RerenderProperties { get; } = new(); protected View() { 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; } - 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() { @@ -50,7 +87,8 @@ public abstract partial class View : IView 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) || RerenderProperties.Contains(e.PropertyName) ) @@ -60,10 +98,16 @@ public abstract partial class View : IView } } - 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) { throw new NullReferenceException( @@ -74,7 +118,31 @@ public abstract partial class View : IView + 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() where TChild : IView, new() @@ -93,9 +161,9 @@ public abstract partial class View : IView public virtual TChild AddChild(TChild child) where TChild : IView { child.DataContext = DataContext; - child.ApplicationContext = ApplicationContext; + CopyCommonPropertiesToNewChild(child); - var mapper = new DataContextMapper(this, d => child.DataContext = d); + var mapper = new DataContextMapper(this, child, d => d); AddDisposable(mapper); child.AddDisposable(mapper); @@ -106,15 +174,36 @@ public abstract partial class View : IView where TChild : IView { child.DataContext = dataContextMapper(DataContext); - child.ApplicationContext = ApplicationContext; + CopyCommonPropertiesToNewChild(child); - var mapper = new DataContextMapper(this, d => child.DataContext = dataContextMapper(d)); + var mapper = new DataContextMapper(this, child, dataContextMapper); AddDisposable(mapper); child.AddDisposable(mapper); return child; } + private void CopyCommonPropertiesToNewChild(IView child) + { + child.ApplicationContext = ApplicationContext; + child.Attached = Attached; + } + + public virtual void RemoveChild(IView child) + { + var mappers = _disposables + .Where(d => d is DataContextMapper 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 RemoveDisposable(IDisposable disposable) => _disposables.Remove(disposable); diff --git a/src/Library/TerminalUI/DataContextMapper.cs b/src/Library/TerminalUI/DataContextMapper.cs index 8031a56..777f298 100644 --- a/src/Library/TerminalUI/DataContextMapper.cs +++ b/src/Library/TerminalUI/DataContextMapper.cs @@ -3,25 +3,32 @@ using TerminalUI.Controls; namespace TerminalUI; -public class DataContextMapper : IDisposable +public class DataContextMapper : IDisposable { - private readonly IView _source; - private readonly Action _setter; + private readonly Func _mapper; + public IView Source { get; } + public IView Target { get; } - public DataContextMapper(IView source, Action setter) + public DataContextMapper(IView source, IView target, Func mapper) { + _mapper = mapper; ArgumentNullException.ThrowIfNull(source); - _source = source; - _setter = setter; + Source = source; + Target = target; source.PropertyChanged += SourceOnPropertyChanged; } private void SourceOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IView.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); + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index c11b8cc..af9f53c 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -45,11 +45,12 @@ public class EventLoop : IEventLoop } var size = _applicationContext.ConsoleDriver.GetWindowSize(); + var renderContext = new RenderContext(_applicationContext.ConsoleDriver); foreach (var view in viewsToRender) { view.Attached = true; view.GetRequestedSize(); - view.Render(new Position(0, 0), size); + view.Render(renderContext, new Position(0, 0), size); } } diff --git a/src/Library/TerminalUI/Extensions/Binding.cs b/src/Library/TerminalUI/Extensions/Binding.cs index 3051221..0061d84 100644 --- a/src/Library/TerminalUI/Extensions/Binding.cs +++ b/src/Library/TerminalUI/Extensions/Binding.cs @@ -9,7 +9,7 @@ public static class Binding public static Binding Bind( this TView targetView, IView dataSourceView, - Expression> dataContextExpression, + Expression> dataSourceExpression, Expression> propertyExpression, TResult? fallbackValue = default) { @@ -18,7 +18,7 @@ public static class Binding return new Binding( dataSourceView, - dataContextExpression, + dataSourceExpression, targetView, propertyInfo, value => value, @@ -29,7 +29,7 @@ public static class Binding public static Binding Bind( this TView targetView, IView dataSourceView, - Expression> dataContextExpression, + Expression> dataSourceExpression, Expression> propertyExpression, Func converter, TResult? fallbackValue = default) @@ -39,7 +39,7 @@ public static class Binding return new Binding( dataSourceView, - dataContextExpression, + dataSourceExpression, targetView, propertyInfo, converter, diff --git a/src/Library/TerminalUI/Extensions/ViewExtensions.cs b/src/Library/TerminalUI/Extensions/ViewExtensions.cs index 4380d9a..168d434 100644 --- a/src/Library/TerminalUI/Extensions/ViewExtensions.cs +++ b/src/Library/TerminalUI/Extensions/ViewExtensions.cs @@ -6,6 +6,12 @@ public static class ViewExtensions { public static T? GetExtension(this IView view) => (T?) view.Extensions.FirstOrDefault(e => e is T); + + public static IView WithExtension(this IView view, object extension) + { + view.Extensions.Add(extension); + return view; + } public static ChildWithDataContextMapper WithDataContextMapper( this IView view, diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index 382edff..88c9aed 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -9,4 +9,5 @@ public interface IApplicationContext bool IsRunning { get; set; } IConsoleDriver ConsoleDriver { get; init; } ILoggerFactory? LoggerFactory { get; init; } + char EmptyCharacter { get; init; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Margin.cs b/src/Library/TerminalUI/Models/Margin.cs new file mode 100644 index 0000000..efba72b --- /dev/null +++ b/src/Library/TerminalUI/Models/Margin.cs @@ -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)) + }; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/RenderContext.cs b/src/Library/TerminalUI/Models/RenderContext.cs new file mode 100644 index 0000000..9f328c9 --- /dev/null +++ b/src/Library/TerminalUI/Models/RenderContext.cs @@ -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!); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Size.cs b/src/Library/TerminalUI/Models/Size.cs index 82d3767..c96dc30 100644 --- a/src/Library/TerminalUI/Models/Size.cs +++ b/src/Library/TerminalUI/Models/Size.cs @@ -1,3 +1,3 @@ namespace TerminalUI.Models; -public record struct Size(int Width, int Height); \ No newline at end of file +public readonly record struct Size(int Width, int Height); \ No newline at end of file diff --git a/src/Library/TerminalUI/Traits/IContentRenderer.cs b/src/Library/TerminalUI/Traits/IContentRenderer.cs index b86ed5a..2ae8580 100644 --- a/src/Library/TerminalUI/Traits/IContentRenderer.cs +++ b/src/Library/TerminalUI/Traits/IContentRenderer.cs @@ -1,10 +1,9 @@ using TerminalUI.Controls; -using TerminalUI.Models; namespace TerminalUI.Traits; -public interface IContentRenderer +public interface IContentRenderer { - IView? Content { get; set; } - Action ContentRendererMethod { get; set; } + IView? Content { get; set; } + RenderMethod ContentRendererMethod { get; set; } } \ No newline at end of file