diff --git a/ReactiveExample/Extensions.cs b/ReactiveExample/Extensions.cs deleted file mode 100644 index 1a653f090..000000000 --- a/ReactiveExample/Extensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Terminal.Gui; - -namespace ReactiveExample { - public static class Extensions - { - public static MemoizedElement StackPanel( - this TOwner owner, - TNew control) - where TOwner : View - where TNew : View => - new MemoizedElement(owner, control); - - public static MemoizedElement Append( - this MemoizedElement owner, - TNew control, - int height = 1) - where TOwner : View - where TOld : View - where TNew : View - { - control.X = Pos.Left(owner.Control); - control.Y = Pos.Top(owner.Control) + height; - return new MemoizedElement(owner.View, control); - } - - public class MemoizedElement - where TOwner : View - where TControl : View - { - public TOwner View { get; } - public TControl Control { get; } - - public MemoizedElement(TOwner owner, TControl control) - { - View = owner; - Control = control; - View.Add(control); - } - } - } -} \ No newline at end of file diff --git a/ReactiveExample/LoginView.cs b/ReactiveExample/LoginView.cs index c633d11ae..4479d55ac 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -10,15 +10,15 @@ namespace ReactiveExample { public LoginView (LoginViewModel viewModel) : base("Reactive Extensions Example") { ViewModel = viewModel; - this.StackPanel (new Label ("Login Form")) - .Append (UsernameLengthLabel ()) - .Append (UsernameInput ()) - .Append (PasswordLengthLabel ()) - .Append (PasswordInput ()) - .Append (ValidationLabel ()) - .Append (LoginButton ()) - .Append (ClearButton ()) - .Append (LoginProgressLabel ()); + var title = TitleLabel (); + var usernameLengthLabel = UsernameLengthLabel (title); + var usernameInput = UsernameInput (usernameLengthLabel); + var passwordLengthLabel = PasswordLengthLabel (usernameInput); + var passwordInput = PasswordInput (passwordLengthLabel); + var validationLabel = ValidationLabel (passwordInput); + var loginButton = LoginButton (validationLabel); + var clearButton = ClearButton (loginButton); + LoginProgressLabel (clearButton); } public LoginViewModel ViewModel { get; set; } @@ -28,8 +28,18 @@ namespace ReactiveExample { base.Dispose (disposing); } - TextField UsernameInput () { - var usernameInput = new TextField (ViewModel.Username) { Width = 40 }; + Label TitleLabel () { + var label = new Label("Login Form"); + Add (label); + return label; + } + + TextField UsernameInput (View previous) { + var usernameInput = new TextField (ViewModel.Username) { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyValue (x => x.Username) .BindTo (usernameInput, x => x.Text) @@ -41,21 +51,31 @@ namespace ReactiveExample { .DistinctUntilChanged () .BindTo (ViewModel, x => x.Username) .DisposeWith (_disposable); + Add (usernameInput); return usernameInput; } - Label UsernameLengthLabel () { - var usernameLengthLabel = new Label { Width = 40 }; + Label UsernameLengthLabel (View previous) { + var usernameLengthLabel = new Label { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyValue (x => x.UsernameLength) .Select (length => ustring.Make ($"Username ({length} characters)")) .BindTo (usernameLengthLabel, x => x.Text) .DisposeWith (_disposable); + Add (usernameLengthLabel); return usernameLengthLabel; } - TextField PasswordInput () { - var passwordInput = new TextField (ViewModel.Password) { Width = 40 }; + TextField PasswordInput (View previous) { + var passwordInput = new TextField (ViewModel.Password) { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyValue (x => x.Password) .BindTo (passwordInput, x => x.Text) @@ -67,23 +87,33 @@ namespace ReactiveExample { .DistinctUntilChanged () .BindTo (ViewModel, x => x.Password) .DisposeWith (_disposable); + Add (passwordInput); return passwordInput; } - Label PasswordLengthLabel () { - var passwordLengthLabel = new Label { Width = 40 }; + Label PasswordLengthLabel (View previous) { + var passwordLengthLabel = new Label { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyValue (x => x.PasswordLength) .Select (length => ustring.Make ($"Password ({length} characters)")) .BindTo (passwordLengthLabel, x => x.Text) .DisposeWith (_disposable); + Add (passwordLengthLabel); return passwordLengthLabel; } - Label ValidationLabel () { + Label ValidationLabel (View previous) { var error = ustring.Make("Please, enter user name and password."); var success = ustring.Make("The input is valid!"); - var validationLabel = new Label(error) { Width = 40 }; + var validationLabel = new Label(error) { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyValue (x => x.IsValid) .Select (valid => valid ? success : error) @@ -94,38 +124,55 @@ namespace ReactiveExample { .Select (valid => valid ? Colors.Base : Colors.Error) .BindTo (validationLabel, x => x.ColorScheme) .DisposeWith (_disposable); + Add (validationLabel); return validationLabel; } - Label LoginProgressLabel () { + Label LoginProgressLabel (View previous) { var progress = ustring.Make ("Logging in..."); var idle = ustring.Make ("Press 'Login' to log in."); - var loginProgressLabel = new Label(idle) { Width = 40 }; + var loginProgressLabel = new Label(idle) { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; ViewModel .WhenAnyObservable (x => x.Login.IsExecuting) .Select (executing => executing ? progress : idle) + .ObserveOn (RxApp.MainThreadScheduler) .BindTo (loginProgressLabel, x => x.Text) .DisposeWith (_disposable); + Add (loginProgressLabel); return loginProgressLabel; } - Button LoginButton () { - var loginButton = new Button ("Login") { Width = 40 }; + Button LoginButton (View previous) { + var loginButton = new Button ("Login") { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; loginButton .Events () .Clicked .InvokeCommand (ViewModel, x => x.Login) .DisposeWith (_disposable); + Add (loginButton); return loginButton; } - Button ClearButton () { - var clearButton = new Button("Clear") { Width = 40 }; + Button ClearButton (View previous) { + var clearButton = new Button("Clear") { + X = Pos.Left(previous), + Y = Pos.Top(previous) + 1, + Width = 40 + }; clearButton .Events () .Clicked .InvokeCommand (ViewModel, x => x.Clear) .DisposeWith (_disposable); + Add (clearButton); return clearButton; } diff --git a/ReactiveExample/LoginViewModel.cs b/ReactiveExample/LoginViewModel.cs index bce330b8f..7a77f6932 100644 --- a/ReactiveExample/LoginViewModel.cs +++ b/ReactiveExample/LoginViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Runtime.Serialization; diff --git a/ReactiveExample/Program.cs b/ReactiveExample/Program.cs index 4a8216a63..9396ee047 100644 --- a/ReactiveExample/Program.cs +++ b/ReactiveExample/Program.cs @@ -1,11 +1,13 @@ -using System; +using System.Reactive.Concurrency; +using ReactiveUI; using Terminal.Gui; namespace ReactiveExample { public static class Program { static void Main (string [] args) { - Application.Init (); // A hacky way to enable instant UI updates. - Application.MainLoop.AddTimeout(TimeSpan.FromSeconds(0.1), a => true); + Application.Init (); + RxApp.MainThreadScheduler = TerminalScheduler.Default; + RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; Application.Run (new LoginView (new LoginViewModel ())); } } diff --git a/ReactiveExample/TerminalScheduler.cs b/ReactiveExample/TerminalScheduler.cs new file mode 100644 index 000000000..4c4ec85b4 --- /dev/null +++ b/ReactiveExample/TerminalScheduler.cs @@ -0,0 +1,41 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Terminal.Gui; + +namespace ReactiveExample { + public class TerminalScheduler : LocalScheduler { + public static readonly TerminalScheduler Default = new TerminalScheduler(); + TerminalScheduler () { } + + public override IDisposable Schedule ( + TState state, TimeSpan dueTime, + Func action) { + + IDisposable PostOnMainLoop() { + var composite = new CompositeDisposable(2); + var cancellation = new CancellationDisposable(); + Application.MainLoop.Invoke (() => { + if (!cancellation.Token.IsCancellationRequested) + composite.Add(action(this, state)); + }); + composite.Add(cancellation); + return composite; + } + + IDisposable PostAsTimeout () { + var composite = new CompositeDisposable(2); + var token = Application.MainLoop.AddTimeout (dueTime, args => { + composite.Add(action (this, state)); + return true; + }); + composite.Add (Disposable.Create (() => Application.MainLoop.RemoveTimeout (token))); + return composite; + } + + return dueTime == TimeSpan.Zero + ? PostOnMainLoop () + : PostAsTimeout (); + } + } +} \ No newline at end of file