diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..5308df437 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,79 @@ +## CI/CD Workflows + +The repository uses multiple GitHub Actions workflows. What runs and when: + +### 1) Build Solution (`.github/workflows/build.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`); supports `workflow_call` +- **Runner/timeout**: `ubuntu-latest`, 10 minutes +- **Steps**: +- Checkout and setup .NET 8.x GA +- `dotnet restore` +- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612` +- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612` +- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612` +- Restore NativeAot/SelfContained examples, then restore solution again +- Build Release for `Examples/NativeAot` and `Examples/SelfContained` +- Build Release solution +- Upload artifacts named `build-artifacts`, retention 1 day + +### 2) Build & Run Unit Tests (`.github/workflows/unit-tests.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) +- **Matrix**: Ubuntu/Windows/macOS +- **Timeout**: 15 minutes per job +- **Process**: +1. Calls build workflow to build solution once +2. Downloads build artifacts +3. Runs `dotnet restore` (required for `--no-build` to work) +4. **Performance optimizations**: + - Disables Windows Defender on Windows runners (significant speedup) + - Collects code coverage **only on Linux** (ubuntu-latest) for performance + - Windows and macOS skip coverage collection to reduce test time + - Increased blame-hang-timeout to 120s for Windows/macOS (60s for Linux) +5. Runs two test jobs: + - **Non-parallel UnitTests**: `Tests/UnitTests` with blame/diag flags; `xunit.stopOnFail=false` + - **Parallel UnitTestsParallelizable**: `Tests/UnitTestsParallelizable` with blame/diag flags; `xunit.stopOnFail=false` +6. Uploads test logs and diagnostic data from all runners +7. **Uploads code coverage to Codecov only from Linux runner** + +**Test results**: All tests output to unified `TestResults/` directory at repository root + +### 3) Build & Run Integration Tests (`.github/workflows/integration-tests.yml`) + +- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) +- **Matrix**: Ubuntu/Windows/macOS +- **Timeout**: 15 minutes +- **Process**: +1. Calls build workflow +2. Downloads build artifacts +3. Runs `dotnet restore` +4. **Performance optimizations** (same as unit tests): + - Disables Windows Defender on Windows runners + - Collects code coverage **only on Linux** + - Increased blame-hang-timeout to 120s for Windows/macOS +5. Runs IntegrationTests with blame/diag flags; `xunit.stopOnFail=true` +6. Uploads logs per-OS +7. **Uploads coverage to Codecov only from Linux runner** + +### 4) Publish to NuGet (`.github/workflows/publish.yml`) + +- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*`(ignores `**.md`) +- Uses GitVersion to compute SemVer, builds Release, packs with symbols, and pushes to NuGet.org using `NUGET_API_KEY` + +### 5) Build and publish API docs (`.github/workflows/api-docs.yml`) + +- **Triggers**: push to `v1_release` and `v2_develop` +- Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `v2_release` or `v2_develop` + + +### Replicating CI Locally + +```bash +# Full CI sequence: +dotnet restore +dotnet build --configuration Debug --no-restore +dotnet test Tests/UnitTests --no-build --verbosity normal +dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal +dotnet build --configuration Release --no-restore +``` diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1542752d7..5200800cf 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -157,10 +157,10 @@ jobs: - name: Run UnitTestsParallelizable (10 iterations with varying parallelization) shell: bash run: | - # Run tests 10 times with different parallelization settings to expose concurrency issues - for RUN in {1..10}; do + # Run tests 3 times with different parallelization settings to expose concurrency issues + for RUN in {1..3}; do echo "============================================" - echo "Starting test run $RUN of 10" + echo "Starting test run $RUN of 3" echo "============================================" # Use a combination of run number and timestamp to create different execution patterns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e985e560..0f24d1c6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,19 +7,16 @@ Welcome! This guide provides everything you need to know to contribute effective ## Table of Contents - [Project Overview](#project-overview) -- [Building and Testing](#building-and-testing) +- [Key Architecture Concepts](#key-architecture-concepts) - [Coding Conventions](#coding-conventions) +- [Building and Testing](#building-and-testing) - [Testing Requirements](#testing-requirements) - [API Documentation Requirements](#api-documentation-requirements) - [Pull Request Guidelines](#pull-request-guidelines) - [CI/CD Workflows](#cicd-workflows) - [Repository Structure](#repository-structure) - [Branching Model](#branching-model) -- [Key Architecture Concepts](#key-architecture-concepts) - [What NOT to Do](#what-not-to-do) -- [Additional Resources](#additional-resources) - ---- ## Project Overview @@ -32,8 +29,18 @@ Welcome! This guide provides everything you need to know to contribute effective - **Version**: v2 (Alpha), v1 (maintenance mode) - **Branching**: GitFlow model (v2_develop is default/active development) ---- +## Key Architecture Concepts +**⚠️ CRITICAL - AI Agents MUST understand these concepts before starting work.** + +- **Application Lifecycle** - How `Application.Init`, `Application.Run`, and `Application.Shutdown` work - [Application Deep Dive](./docfx/docs/application.md) +- **Cancellable Workflow Patern** - [CWP Deep Dive](./docfx/docs/cancellable-work-pattern.md) +- **View Hierarchy** - Understanding `View`, `Runnable`, `Window`, and view containment - [View Deep Dive](./docfx/docs/View.md) +- **Layout System** - Pos, Dim, and automatic layout - [Layout System](./docfx/docs/layout.md) +- **Event System** - How keyboard, mouse, and application events flow - [Events Deep Dive](./docfx/docs/events.md) +- **Driver Architecture** - How console drivers abstract platform differences - [Drivers](./docfx/docs/drivers.md) +- **Drawing Model** - How rendering works with Attributes, Colors, and Glyphs - [Drawing Deep Dive](./docfx/docs/drivers.md) + ## Building and Testing ### Required Tools @@ -89,28 +96,18 @@ Welcome! This guide provides everything you need to know to contribute effective ### Common Build Issues -#### Issue: Build Warnings -- **Expected**: None warnings (~100 currently). -- **Action**: Don't add new warnings; fix warnings in code you modify - #### Issue: NativeAot/SelfContained Build + - **Solution**: Restore these projects explicitly: ```bash dotnet restore ./Examples/NativeAot/NativeAot.csproj -f dotnet restore ./Examples/SelfContained/SelfContained.csproj -f ``` -### Running Examples - -**UICatalog** (comprehensive demo app): -```bash -dotnet run --project Examples/UICatalog/UICatalog.csproj -``` - ---- - ## Coding Conventions +**⚠️ CRITICAL - These rules MUST be followed in ALL new or modified code** + ### Code Style Tenets 1. **Six-Year-Old Reading Level** - Readability over terseness @@ -161,8 +158,6 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj **⚠️ CRITICAL - These conventions apply to ALL code - production code, test code, examples, and samples.** ---- - ## Testing Requirements ### Code Coverage @@ -178,19 +173,17 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj ### Test Patterns -- **Parallelizable tests preferred** - Add new tests to `UnitTestsParallelizable` when possible -- **Avoid static dependencies** - Don't use `Application.Init`, `ConfigurationManager` in tests -- **Don't use `[AutoInitShutdown]`** - Legacy pattern, being phased out - **Make tests granular** - Each test should cover smallest area possible - Follow existing test patterns in respective test projects +- **Avoid adding new tests to the `UnitTests` Project** - Make them parallelizable and add them to `UnitTests.Parallelizable` +- **Avoid static dependencies** - DO NOT use the legacy/static `Application` API or `ConfigurationManager` in tests unless the tests explicitly test related functionality. +- **Don't use `[AutoInitShutdown]` or `[SetupFakeApplication]`** - Legacy pattern, being phased out ### Test Configuration - `xunit.runner.json` - xUnit configuration - `coverlet.runsettings` - Coverage settings (OpenCover format) ---- - ## API Documentation Requirements **All public APIs MUST have XML documentation:** @@ -202,16 +195,15 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj - Complex topics → `docfx/docs/*.md` files - Proper English and grammar - Clear, concise, complete. Use imperative mood. ---- - ## Pull Request Guidelines ### PR Requirements +- **ALWAYS** include instructions for pulling down locally at end of Description + - **Title**: "Fixes #issue. Terse description". If multiple issues, list all, separated by commas (e.g. "Fixes #123, #456. Terse description") - **Description**: - Include "- Fixes #issue" for each issue near the top - - **ALWAYS** include instructions for pulling down locally at end of Description - Suggest user setup a remote named `copilot` pointing to your fork - Example: ```markdown @@ -220,99 +212,14 @@ dotnet run --project Examples/UICatalog/UICatalog.csproj git fetch copilot git checkout copilot/ ``` -- **Coding Style**: Follow all coding conventions in this document for new and modified code - **Tests**: Add tests for new functionality (see [Testing Requirements](#testing-requirements)) - **Coverage**: Maintain or increase code coverage - **Scenarios**: Update UICatalog scenarios when adding features - **Warnings**: **CRITICAL - PRs must not introduce any new warnings** - Any file modified in a PR that currently generates warnings **MUST** be fixed to remove those warnings - Exception: Warnings caused by `[Obsolete]` attributes can remain - - Expected baseline: ~326 warnings (mostly nullable reference warnings, unused variables, xUnit suggestions) - Action: Before submitting a PR, verify your changes don't add new warnings and fix any warnings in files you modify ---- - -## CI/CD Workflows - -The repository uses multiple GitHub Actions workflows. What runs and when: - -### 1) Build Solution (`.github/workflows/build.yml`) - -- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`); supports `workflow_call` -- **Runner/timeout**: `ubuntu-latest`, 10 minutes -- **Steps**: -- Checkout and setup .NET 8.x GA -- `dotnet restore` -- Build Debug: `dotnet build --configuration Debug --no-restore -property:NoWarn=0618%3B0612` -- Build Release (library): `dotnet build Terminal.Gui/Terminal.Gui.csproj --configuration Release --no-incremental --force -property:NoWarn=0618%3B0612` -- Pack Release: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612` -- Restore NativeAot/SelfContained examples, then restore solution again -- Build Release for `Examples/NativeAot` and `Examples/SelfContained` -- Build Release solution -- Upload artifacts named `build-artifacts`, retention 1 day - -### 2) Build & Run Unit Tests (`.github/workflows/unit-tests.yml`) - -- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) -- **Matrix**: Ubuntu/Windows/macOS -- **Timeout**: 15 minutes per job -- **Process**: -1. Calls build workflow to build solution once -2. Downloads build artifacts -3. Runs `dotnet restore` (required for `--no-build` to work) -4. **Performance optimizations**: - - Disables Windows Defender on Windows runners (significant speedup) - - Collects code coverage **only on Linux** (ubuntu-latest) for performance - - Windows and macOS skip coverage collection to reduce test time - - Increased blame-hang-timeout to 120s for Windows/macOS (60s for Linux) -5. Runs two test jobs: - - **Non-parallel UnitTests**: `Tests/UnitTests` with blame/diag flags; `xunit.stopOnFail=false` - - **Parallel UnitTestsParallelizable**: `Tests/UnitTestsParallelizable` with blame/diag flags; `xunit.stopOnFail=false` -6. Uploads test logs and diagnostic data from all runners -7. **Uploads code coverage to Codecov only from Linux runner** - -**Test results**: All tests output to unified `TestResults/` directory at repository root - -### 3) Build & Run Integration Tests (`.github/workflows/integration-tests.yml`) - -- **Triggers**: push and pull_request to `v2_release`, `v2_develop` (ignores `**.md`) -- **Matrix**: Ubuntu/Windows/macOS -- **Timeout**: 15 minutes -- **Process**: -1. Calls build workflow -2. Downloads build artifacts -3. Runs `dotnet restore` -4. **Performance optimizations** (same as unit tests): - - Disables Windows Defender on Windows runners - - Collects code coverage **only on Linux** - - Increased blame-hang-timeout to 120s for Windows/macOS -5. Runs IntegrationTests with blame/diag flags; `xunit.stopOnFail=true` -6. Uploads logs per-OS -7. **Uploads coverage to Codecov only from Linux runner** - -### 4) Publish to NuGet (`.github/workflows/publish.yml`) - -- **Triggers**: push to `v2_release`, `v2_develop`, and tags `v*`(ignores `**.md`) -- Uses GitVersion to compute SemVer, builds Release, packs with symbols, and pushes to NuGet.org using `NUGET_API_KEY` - -### 5) Build and publish API docs (`.github/workflows/api-docs.yml`) - -- **Triggers**: push to `v1_release` and `v2_develop` -- Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `v2_release` or `v2_develop` - - -### Replicating CI Locally - -```bash -# Full CI sequence: -dotnet restore -dotnet build --configuration Debug --no-restore -dotnet test Tests/UnitTests --no-build --verbosity normal -dotnet test Tests/UnitTestsParallelizable --no-build --verbosity normal -dotnet build --configuration Release --no-restore -``` - ---- ## Repository Structure @@ -364,7 +271,6 @@ dotnet build --configuration Release --no-restore **`/.github/workflows/`** - CI/CD pipelines (see [CI/CD Workflows](#cicd-workflows)) ---- ## Branching Model @@ -374,31 +280,6 @@ dotnet build --configuration Release --no-restore - `v2_release` - Stable releases, matches NuGet - `v1_develop`, `v1_release` - Legacy v1 (maintenance only) ---- - -## Key Architecture Concepts - -**⚠️ CRITICAL - Contributors should understand these concepts before starting work.** - -See `/docfx/docs/` for deep dives on: - -- **Application Lifecycle** - How `Application.Init`, `Application.Run`, and `Application.Shutdown` work -- **View Hierarchy** - Understanding `View`, `Toplevel`, `Window`, and view containment -- **Layout System** - Pos, Dim, and automatic layout -- **Event System** - How keyboard, mouse, and application events flow -- **Driver Architecture** - How console drivers abstract platform differences -- **Drawing Model** - How rendering works with Attributes, Colors, and Glyphs - -Key documentation: -- [View Documentation](https://gui-cs.github.io/Terminal.Gui/docs/View.html) -- [Events Deep Dive](https://gui-cs.github.io/Terminal.Gui/docs/events.html) -- [Layout System](https://gui-cs.github.io/Terminal.Gui/docs/layout.html) -- [Keyboard Handling](https://gui-cs.github.io/Terminal.Gui/docs/keyboard.html) -- [Mouse Support](https://gui-cs.github.io/Terminal.Gui/docs/mouse.html) -- [Drivers](https://gui-cs.github.io/Terminal.Gui/docs/drivers.html) - ---- - ## What NOT to Do - ❌ Don't add new linters/formatters (use existing) @@ -412,17 +293,4 @@ Key documentation: - ❌ **Don't use redundant type names with `new`** (**ALWAYS PREFER** target-typed `new ()`) - ❌ **Don't introduce new warnings** (fix warnings in files you modify; exception: `[Obsolete]` warnings) ---- - -## Additional Resources - -- **Full Documentation**: https://gui-cs.github.io/Terminal.Gui -- **API Reference**: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.App.html -- **Deep Dives**: `/docfx/docs/` directory -- **Getting Started**: https://gui-cs.github.io/Terminal.Gui/docs/getting-started.html -- **Migrating from v1 to v2**: https://gui-cs.github.io/Terminal.Gui/docs/migratingfromv1.html -- **Showcase**: https://gui-cs.github.io/Terminal.Gui/docs/showcase.html - ---- - **Thank you for contributing to Terminal.Gui!** 🎉 diff --git a/Examples/CommunityToolkitExample/LoginView.cs b/Examples/CommunityToolkitExample/LoginView.cs index 70ec87f07..8a2f0cf38 100644 --- a/Examples/CommunityToolkitExample/LoginView.cs +++ b/Examples/CommunityToolkitExample/LoginView.cs @@ -64,8 +64,6 @@ internal partial class LoginView : IRecipient> } } SetText (); - // BUGBUG: This should not be needed: - Application.LayoutAndDraw (); } private void SetText () diff --git a/Examples/CommunityToolkitExample/LoginViewModel.cs b/Examples/CommunityToolkitExample/LoginViewModel.cs index bdec99519..af7d594c3 100644 --- a/Examples/CommunityToolkitExample/LoginViewModel.cs +++ b/Examples/CommunityToolkitExample/LoginViewModel.cs @@ -12,7 +12,7 @@ internal partial class LoginViewModel : ObservableObject private const string INVALID_LOGIN_MESSAGE = "Please enter a valid user name and password."; private const string LOGGING_IN_PROGRESS_MESSAGE = "Logging in..."; private const string VALID_LOGIN_MESSAGE = "The input is valid!"; - + [ObservableProperty] private bool _canLogin; @@ -28,7 +28,7 @@ internal partial class LoginViewModel : ObservableObject [ObservableProperty] private string _usernameLengthMessage; - + [ObservableProperty] private Scheme? _validationScheme; @@ -105,7 +105,7 @@ internal partial class LoginViewModel : ObservableObject { switch (loginAction) { - case LoginActions.Clear: + case LoginActions.Clear: LoginProgressMessage = message; ValidationMessage = INVALID_LOGIN_MESSAGE; ValidationScheme = SchemeManager.GetScheme ("Error"); @@ -115,7 +115,7 @@ internal partial class LoginViewModel : ObservableObject break; case LoginActions.Validation: ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE; - ValidationScheme = CanLogin ? SchemeManager.GetScheme ("Base") : SchemeManager.GetScheme("Error"); + ValidationScheme = CanLogin ? SchemeManager.GetScheme ("Base") : SchemeManager.GetScheme ("Error"); break; } WeakReferenceMessenger.Default.Send (new Message { Value = loginAction }); diff --git a/Examples/CommunityToolkitExample/Message.cs b/Examples/CommunityToolkitExample/Message.cs index f0e8ad530..fbd85604f 100644 --- a/Examples/CommunityToolkitExample/Message.cs +++ b/Examples/CommunityToolkitExample/Message.cs @@ -1,5 +1,4 @@ namespace CommunityToolkitExample; - internal class Message { public T? Value { get; set; } diff --git a/Examples/CommunityToolkitExample/Program.cs b/Examples/CommunityToolkitExample/Program.cs index ab1357224..75ada5665 100644 --- a/Examples/CommunityToolkitExample/Program.cs +++ b/Examples/CommunityToolkitExample/Program.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using Terminal.Gui.Configuration; using Terminal.Gui.App; -using Terminal.Gui.ViewBase; - +using Terminal.Gui.Configuration; namespace CommunityToolkitExample; @@ -14,10 +12,10 @@ public static class Program { ConfigurationManager.Enable (ConfigLocations.All); Services = ConfigureServices (); - Application.Init (); - Application.Run (Services.GetRequiredService ()); - Application.TopRunnable?.Dispose (); - Application.Shutdown (); + using IApplication app = Application.Create (); + app.Init (); + using var loginView = Services.GetRequiredService (); + app.Run (loginView); } private static IServiceProvider ConfigureServices () @@ -25,6 +23,7 @@ public static class Program var services = new ServiceCollection (); services.AddTransient (); services.AddTransient (); + return services.BuildServiceProvider (); } -} \ No newline at end of file +} diff --git a/Examples/CommunityToolkitExample/README.md b/Examples/CommunityToolkitExample/README.md index 908ae592b..1f7d5d877 100644 --- a/Examples/CommunityToolkitExample/README.md +++ b/Examples/CommunityToolkitExample/README.md @@ -6,9 +6,10 @@ Right away we use IoC to load our views and view models. ``` csharp // As a public property for access further in the application if needed. -public static IServiceProvider Services { get; private set; } +public static IServiceProvider? Services { get; private set; } ... // In Main +ConfigurationManager.Enable (ConfigLocations.All); Services = ConfigureServices (); ... private static IServiceProvider ConfigureServices () @@ -20,16 +21,19 @@ private static IServiceProvider ConfigureServices () } ``` -Now, we start the app and get our main view. +Now, we start the app using the modern Terminal.Gui model and get our main view. ``` csharp -Application.Run (Services.GetRequiredService ()); +using IApplication app = Application.Create (); +app.Init (); +using var loginView = Services.GetRequiredService (); +app.Run (loginView); ``` Our view implements `IRecipient` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created. ``` csharp -internal partial class LoginView : IRecipient> +internal partial class LoginView : IRecipient> { public LoginView (LoginViewModel viewModel) { @@ -41,15 +45,16 @@ internal partial class LoginView : IRecipient> passwordInput.TextChanged += (_, _) => { ViewModel.Password = passwordInput.Text; - SetText (); }; - loginButton.Accept += (_, _) => + loginButton.Accepting += (_, e) => { if (!ViewModel.CanLogin) { return; } ViewModel.LoginCommand.Execute (null); + // When Accepting is handled, set e.Handled to true to prevent further processing. + e.Handled = true; }; ... - // Let the view model know the view is intialized. + // Let the view model know the view is initialized. Initialized += (_, _) => { ViewModel.Initialized (); }; } ... @@ -101,54 +106,53 @@ The use of `WeakReferenceMessenger` provides one method of signaling the view fr ... private async Task Login () { - SendMessage (LoginAction.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE); + SendMessage (LoginActions.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE); await Task.Delay (TimeSpan.FromSeconds (1)); Clear (); } -private void SendMessage (LoginAction loginAction, string message = "") +private void SendMessage (LoginActions loginAction, string message = "") { switch (loginAction) { - case LoginAction.LoginProgress: + case LoginActions.LoginProgress: LoginProgressMessage = message; break; - case LoginAction.Validation: + case LoginActions.Validation: ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE; - ValidationScheme = CanLogin ? Colors.Schemes ["Base"] : Colors.Schemes ["Error"]; + ValidationScheme = CanLogin ? SchemeManager.GetScheme ("Base") : SchemeManager.GetScheme ("Error"); break; } - WeakReferenceMessenger.Default.Send (new Message { Value = loginAction }); + WeakReferenceMessenger.Default.Send (new Message { Value = loginAction }); } private void ValidateLogin () { CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password); - SendMessage (LoginAction.Validation); + SendMessage (LoginActions.Validation); } ... ``` -And the view's `Receive` function which provides an `Application.Refresh()` call to update the UI immediately. +The view's `Receive` function updates the UI based on messages from the view model. In the modern Terminal.Gui model, UI updates are automatically refreshed, so no manual `Application.Refresh()` call is needed. ``` csharp -public void Receive (Message message) +public void Receive (Message message) { switch (message.Value) { - case LoginAction.LoginProgress: + case LoginActions.LoginProgress: { loginProgressLabel.Text = ViewModel.LoginProgressMessage; break; } - case LoginAction.Validation: + case LoginActions.Validation: { validationLabel.Text = ViewModel.ValidationMessage; - validationLabel.Scheme = ViewModel.ValidationScheme; + validationLabel.SetScheme (ViewModel.ValidationScheme); break; } } - SetText(); - Application.Refresh (); + SetText (); } ``` diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index f6c13eb6b..9d3fd863f 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -3,30 +3,28 @@ // This is a simple example application. For the full range of functionality // see the UICatalog project -using Terminal.Gui.Configuration; using Terminal.Gui.App; -using Terminal.Gui.Drawing; +using Terminal.Gui.Configuration; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -using Attribute = Terminal.Gui.Drawing.Attribute; // Override the default configuration for the application to use the Light theme -//ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; -ConfigurationManager.Enable(ConfigLocations.All); +ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; +ConfigurationManager.Enable (ConfigLocations.All); +IApplication app = Application.Create (); +app.Run (); -Application.Run ().Dispose (); - -// Before the application exits, reset Terminal.Gui for clean shutdown -Application.Shutdown (); +// Dispose the app to clean up and enable Console.WriteLine below +app.Dispose (); // To see this output on the screen it must be done after shutdown, // which restores the previous screen. Console.WriteLine ($@"Username: {ExampleWindow.UserName}"); // Defines a top-level window with border and title -public class ExampleWindow : Window +public sealed class ExampleWindow : Window { public static string UserName { get; set; } @@ -74,39 +72,32 @@ public class ExampleWindow : Window // When login button is clicked display a message popup btnLogin.Accepting += (s, e) => - { - if (userNameText.Text == "admin" && passwordText.Text == "password") - { - MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); - UserName = userNameText.Text; - Application.RequestStop (); - } - else - { - MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); - } - // When Accepting is handled, set e.Handled to true to prevent further processing. - e.Handled = true; - }; + { + if (userNameText.Text == "admin" && passwordText.Text == "password") + { + MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); + UserName = userNameText.Text; + Application.RequestStop (); + } + else + { + MessageBox.ErrorQuery (App, "Logging In", "Incorrect username or password", "Ok"); + } + + // When Accepting is handled, set e.Handled to true to prevent further processing. + e.Handled = true; + }; // Add the views to the Window Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin); - ListView lv = new ListView () + var lv = new ListView { - Y = Pos.AnchorEnd(), - Height= Dim.Auto(), - Width = Dim.Auto() + Y = Pos.AnchorEnd (), + Height = Dim.Auto (), + Width = Dim.Auto () }; lv.SetSource (["One", "Two", "Three", "Four"]); Add (lv); } - - public override void EndInit () - { - base.EndInit (); - // Set the theme to "Anders" if it exists, otherwise use "Default" - ThemeManager.Theme = ThemeManager.GetThemeNames ().FirstOrDefault (x => x == "Anders") ?? "Default"; - } } - diff --git a/Examples/Example/README.md b/Examples/Example/README.md index 2cb0a9870..f1de23be5 100644 --- a/Examples/Example/README.md +++ b/Examples/Example/README.md @@ -1,11 +1,8 @@ # Terminal.Gui C# Example -This example shows how to use the Terminal.Gui library to create a simple GUI application in C#. +This example shows how to use the Terminal.Gui library to create a simple TUI application in C#. This is the same code found in the Terminal.Gui README.md file. To explore the full range of functionality in Terminal.Gui, see the [UICatalog](../UICatalog) project -See [README.md](https://github.com/gui-cs/Terminal.Gui) for a list of all Terminal.Gui samples. - -Note, the old `demo.cs` example has been deleted because it was not a very good example. It can still be found in the [git history](https://github.com/gui-cs/Terminal.Gui/tree/v1.8.2). \ No newline at end of file diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs index e27caf26e..026a98134 100644 --- a/Examples/FluentExample/Program.cs +++ b/Examples/FluentExample/Program.cs @@ -5,63 +5,31 @@ using Terminal.Gui.Drawing; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -#if POST_4148 +IApplication? app = Application.Create () + .Init () + .Run (); + // Run the application with fluent API - automatically creates, runs, and disposes the runnable +Color? result = app.GetResult () as Color?; -// Display the result -if (Application.Create () - .Init () - .Run () - .Shutdown () is Color { } result) +// Shut down the app with Dispose before we can use Console.WriteLine +app.Dispose (); + +if (result is { }) { - Console.WriteLine (@$"Selected Color: {(Color?)result}"); -} -else -{ - Console.WriteLine (@"No color selected"); -} -#else - -// Run using traditional approach -IApplication app = Application.Create (); -app.Init (); -var colorPicker = new ColorPickerView (); -app.Run (colorPicker); - -Color? resultColor = colorPicker.Result; - -colorPicker.Dispose (); -app.Shutdown (); - -if (resultColor is { } result) -{ - Console.WriteLine (@$"Selected Color: {(Color?)result}"); + Console.WriteLine (@$"Selected Color: {result}"); } else { Console.WriteLine (@"No color selected"); } -#endif - -#if POST_4148 /// /// A runnable view that allows the user to select a color. -/// Demonstrates IRunnable pattern with automatic disposal. +/// Demonstrates the Runnable with type pattern with automatic disposal. /// public class ColorPickerView : Runnable { - -#else -/// -/// A runnable view that allows the user to select a color. -/// Uses the traditional approach without automatic disposal/Fluent API. -/// -public class ColorPickerView : Toplevel -{ - public Color? Result { get; set; } - -#endif public ColorPickerView () { Title = "Select a Color (Esc to quit)"; @@ -126,7 +94,6 @@ public class ColorPickerView : Toplevel Add (instructions, colorPicker, okButton, cancelButton); } -#if POST_4148 protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) { // Alternative place to extract result before stopping @@ -134,10 +101,9 @@ public class ColorPickerView : Toplevel if (!newIsRunning && Result is null) { // User pressed Esc - could extract current selection here - // Result = _colorPicker.SelectedColor; + //Result = SelectedColor; } return base.OnIsRunningChanging (oldIsRunning, newIsRunning); } -#endif } diff --git a/Examples/ReactiveExample/Program.cs b/Examples/ReactiveExample/Program.cs index 2dcb27b90..e70611afc 100644 --- a/Examples/ReactiveExample/Program.cs +++ b/Examples/ReactiveExample/Program.cs @@ -1,9 +1,7 @@ using System.Reactive.Concurrency; using ReactiveUI; -using ReactiveUI.SourceGenerators; -using Terminal.Gui.Configuration; using Terminal.Gui.App; -using Terminal.Gui.ViewBase; +using Terminal.Gui.Configuration; namespace ReactiveExample; @@ -12,11 +10,12 @@ public static class Program private static void Main (string [] args) { ConfigurationManager.Enable (ConfigLocations.All); - Application.Init (); - RxApp.MainThreadScheduler = TerminalScheduler.Default; + using IApplication app = Application.Create (); + app.Init (); + RxApp.MainThreadScheduler = new TerminalScheduler (app); RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; - Application.Run (new LoginView (new LoginViewModel ())); - Application.TopRunnable.Dispose (); - Application.Shutdown (); + var loginView = new LoginView (new ()); + app.Run (loginView); + loginView.Dispose (); } } diff --git a/Examples/ReactiveExample/README.md b/Examples/ReactiveExample/README.md index 9e7dae7fd..62fbcd0e7 100644 --- a/Examples/ReactiveExample/README.md +++ b/Examples/ReactiveExample/README.md @@ -7,10 +7,14 @@ This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` wi In order to use reactive extensions scheduling, copy-paste the `TerminalScheduler.cs` file into your project, and add the following lines to the composition root of your `Terminal.Gui` application: ```cs -Application.Init (); -RxApp.MainThreadScheduler = TerminalScheduler.Default; +ConfigurationManager.Enable (ConfigLocations.All); +using IApplication app = Application.Create (); +app.Init (); +RxApp.MainThreadScheduler = new TerminalScheduler (app); RxApp.TaskpoolScheduler = TaskPoolScheduler.Default; -Application.Run (new RootView (new RootViewModel ())); +var loginView = new LoginView (new ()); +app.Run (loginView); +loginView.Dispose (); ``` From now on, you can use `.ObserveOn(RxApp.MainThreadScheduler)` to return to the main loop from a background thread. This is useful when you have a `IObservable` updated from a background thread, and you wish to update the UI with `TValue`s received from that observable. @@ -43,6 +47,6 @@ If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` dat // 'clearButton' is 'Button' clearButton .Events () - .Clicked + .Accepting .InvokeCommand (ViewModel, x => x.Clear); ``` \ No newline at end of file diff --git a/Examples/ReactiveExample/TerminalScheduler.cs b/Examples/ReactiveExample/TerminalScheduler.cs index 3b24cc6d8..7de5d93f0 100644 --- a/Examples/ReactiveExample/TerminalScheduler.cs +++ b/Examples/ReactiveExample/TerminalScheduler.cs @@ -1,4 +1,4 @@ -using System; +#nullable enable using System.Reactive.Concurrency; using System.Reactive.Disposables; using Terminal.Gui.App; @@ -7,8 +7,9 @@ namespace ReactiveExample; public class TerminalScheduler : LocalScheduler { - public static readonly TerminalScheduler Default = new (); - private TerminalScheduler () { } + public TerminalScheduler (IApplication? application) { _application = application; } + + private readonly IApplication? _application = null; public override IDisposable Schedule ( TState state, @@ -21,15 +22,15 @@ public class TerminalScheduler : LocalScheduler var composite = new CompositeDisposable (2); var cancellation = new CancellationDisposable (); - Application.Invoke ( - (_) => - { - if (!cancellation.Token.IsCancellationRequested) - { - composite.Add (action (this, state)); - } - } - ); + _application?.Invoke ( + (_) => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add (action (this, state)); + } + } + ); composite.Add (cancellation); return composite; @@ -39,16 +40,22 @@ public class TerminalScheduler : LocalScheduler { var composite = new CompositeDisposable (2); - object timeout = Application.AddTimeout ( - dueTime, - () => - { - composite.Add (action (this, state)); + object? timeout = _application?.AddTimeout ( + dueTime, + () => + { + composite.Add (action (this, state)); - return false; - } - ); - composite.Add (Disposable.Create (() => Application.RemoveTimeout (timeout))); + return false; + } + ); + composite.Add (Disposable.Create (() => + { + if (timeout is { }) + { + _application?.RemoveTimeout (timeout); + } + })); return composite; } diff --git a/Examples/ReactiveExample/ViewExtensions.cs b/Examples/ReactiveExample/ViewExtensions.cs index f1f639900..b58ec9919 100644 --- a/Examples/ReactiveExample/ViewExtensions.cs +++ b/Examples/ReactiveExample/ViewExtensions.cs @@ -1,5 +1,4 @@ -using System; -using Terminal.Gui.ViewBase; +using Terminal.Gui.ViewBase; using Terminal.Gui.Views; namespace ReactiveExample; diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 8dadab75e..1eb5e9e11 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -83,7 +83,7 @@ if (formRunnable.Result is { } formData) formRunnable.Dispose (); -app.Shutdown (); +app.Dispose (); // Helper method to create a custom form View CreateCustomForm () diff --git a/Examples/SelfContained/Program.cs b/Examples/SelfContained/Program.cs index aa226273b..319ae859f 100644 --- a/Examples/SelfContained/Program.cs +++ b/Examples/SelfContained/Program.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using Terminal.Gui.Configuration; using Terminal.Gui.App; +using Terminal.Gui.Drawing; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; @@ -16,7 +17,9 @@ public static class Program private static void Main (string [] args) { ConfigurationManager.Enable (ConfigLocations.All); - Application.Init (); + + IApplication app = Application.Create (); + app.Init (); #region The code in this region is not intended for use in a self-contained single-file. It's just here to make sure there is no functionality break with localization in Terminal.Gui using single-file @@ -33,28 +36,25 @@ public static class Program #endregion - ExampleWindow app = new (); - Application.Run (app); + using ExampleWindow exampleWindow = new (); + string? userName = app.Run (exampleWindow) as string; - // Dispose the app object before shutdown + + // Shutdown the application in order to free resources and clean up the terminal app.Dispose (); - // Before the application exits, reset Terminal.Gui for clean shutdown - Application.Shutdown (); - // To see this output on the screen it must be done after shutdown, // which restores the previous screen. - Console.WriteLine ($@"Username: {ExampleWindow.UserName}"); + Console.WriteLine ($@"Username: {userName}"); } } // Defines a top-level window with border and title -public class ExampleWindow : Window +public class ExampleWindow : Runnable { - public static string? UserName; - public ExampleWindow () { + BorderStyle = LineStyle.Single; Title = $"Example App ({Application.QuitKey} to quit)"; // Create input components and labels @@ -101,8 +101,8 @@ public class ExampleWindow : Window if (userNameText.Text == "admin" && passwordText.Text == "password") { MessageBox.Query (App, "Logging In", "Login Successful", "Ok"); - UserName = userNameText.Text; - Application.RequestStop (); + Result = userNameText.Text; + App?.RequestStop (); } else { diff --git a/Examples/SelfContained/README.md b/Examples/SelfContained/README.md index f4bd041ee..e7c147aa6 100644 --- a/Examples/SelfContained/README.md +++ b/Examples/SelfContained/README.md @@ -2,6 +2,32 @@ This project aims to test the `Terminal.Gui` library to create a simple `self-contained` `single-file` GUI application in C#, ensuring that all its features are available. +## Modern Terminal.Gui API + +This example uses the modern Terminal.Gui application model: + +```csharp +ConfigurationManager.Enable (ConfigLocations.All); + +IApplication app = Application.Create (); +app.Init (); + +using ExampleWindow exampleWindow = new (); +string? userName = app.Run (exampleWindow) as string; + +app.Dispose (); + +Console.WriteLine ($@"Username: {userName}"); +``` + +Key aspects of the modern model: +- Use `Application.Create()` to create an `IApplication` instance +- Call `app.Init()` to initialize the application +- Use `app.Run(view)` to run views with proper resource management +- Call `app.Dispose()` to clean up resources and restore the terminal +- Event handling uses `Accepting` event instead of legacy `Accept` event +- Set `e.Handled = true` in event handlers to prevent further processing + With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`. To publish the self-contained single file in `Debug` or `Release` mode, it is not necessary to select it in the `Solution Configurations`, just choose the `Debug` or `Release` configuration in the `Publish Profile`. diff --git a/Examples/UICatalog/README.md b/Examples/UICatalog/README.md index c9c810f42..ac9b37e09 100644 --- a/Examples/UICatalog/README.md +++ b/Examples/UICatalog/README.md @@ -80,7 +80,7 @@ The default `Window` shows the Scenario name and supports exiting the Scenario t ![screenshot](generic_screenshot.png) -To build a more advanced scenario, where control of the `Toplevel` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply use `Application.Top` per normal Terminal.Gui programming, as seen in the `Notepad` scenario. +To build a more advanced scenario, where control of the `Runnable` and `Window` is needed (e.g. for scenarios using `MenuBar` or `StatusBar`), simply use `Application.Top` per normal Terminal.Gui programming, as seen in the `Notepad` scenario. For complete control, the `Init` and `Run` overrides can be implemented. The `base.Init` creates `Win`. The `base.Run` simply calls `Application.Run(Application.Top)`. diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 17d6edcf5..74586e878 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -11,7 +11,7 @@ "Hot Dog Stand": { "Schemes": [ { - "Toplevel": { + "Runnable": { "Normal": { "Foreground": "Black", "Background": "#FFFF00" @@ -177,7 +177,7 @@ } }, { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "DarkGray", "Background": "White" diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index d52308b84..bcf0b1cb1 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -219,11 +219,11 @@ public class Scenario : IDisposable } } - // BUGBUG: This is incompatible with modals. This should be using the new equivalent of Toplevel.Ready + // BUGBUG: This is incompatible with modals. This should be using the new equivalent of Runnable.Ready // BUGBUG: which will be IsRunningChanged with newIsRunning == true private void OnApplicationSessionBegun (object? sender, SessionTokenEventArgs e) { - SubscribeAllSubViews (Application.TopRunnable!); + SubscribeAllSubViews (Application.TopRunnableView!); _demoKeys = GetDemoKeyStrokes (); diff --git a/Examples/UICatalog/Scenarios/AllViewsTester.cs b/Examples/UICatalog/Scenarios/AllViewsTester.cs index aae301c3a..8c89b4eb5 100644 --- a/Examples/UICatalog/Scenarios/AllViewsTester.cs +++ b/Examples/UICatalog/Scenarios/AllViewsTester.cs @@ -220,6 +220,13 @@ public class AllViewsTester : Scenario { Debug.Assert (_curView is null); + // Skip RunnableWrapper types as they have generic constraints that cannot be satisfied + if (type.IsGenericType && type.GetGenericTypeDefinition().Name.StartsWith("RunnableWrapper")) + { + Logging.Warning ($"Cannot create an instance of {type.Name} because it is a RunnableWrapper with unsatisfiable generic constraints."); + return; + } + // If we are to create a generic Type if (type.IsGenericType) { diff --git a/Examples/UICatalog/Scenarios/Arrangement.cs b/Examples/UICatalog/Scenarios/Arrangement.cs index 7b261db6b..35c658527 100644 --- a/Examples/UICatalog/Scenarios/Arrangement.cs +++ b/Examples/UICatalog/Scenarios/Arrangement.cs @@ -183,9 +183,9 @@ public class Arrangement : Scenario datePicker.SetScheme (new Scheme ( new Attribute ( - SchemeManager.GetScheme (Schemes.Toplevel).Normal.Foreground.GetBrighterColor (), - SchemeManager.GetScheme (Schemes.Toplevel).Normal.Background.GetBrighterColor (), - SchemeManager.GetScheme (Schemes.Toplevel).Normal.Style))); + SchemeManager.GetScheme (Schemes.Runnable).Normal.Foreground.GetBrighterColor (), + SchemeManager.GetScheme (Schemes.Runnable).Normal.Background.GetBrighterColor (), + SchemeManager.GetScheme (Schemes.Runnable).Normal.Style))); TransparentView transparentView = new () { @@ -237,7 +237,7 @@ public class Arrangement : Scenario Width = Dim.Auto (minimumContentDim: 15), Height = Dim.Auto (minimumContentDim: 3), Title = $"Overlapped{id} _{GetNextHotKey ()}", - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel), + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Runnable), Id = $"Overlapped{id}", ShadowStyle = ShadowStyle.Transparent, BorderStyle = LineStyle.Double, diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 688af56cf..19a561631 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -14,9 +14,9 @@ public class Bars : Scenario public override void Main () { Application.Init (); - Toplevel app = new (); + Runnable app = new (); - app.Loaded += App_Loaded; + app.IsModalChanged += App_Loaded; Application.Run (app); app.Dispose (); @@ -28,7 +28,7 @@ public class Bars : Scenario // QuitKey and it only sticks if changed after init private void App_Loaded (object sender, EventArgs e) { - Application.TopRunnable!.Title = GetQuitKeyAndName (); + Application.TopRunnableView!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); ListView eventLog = new ListView () @@ -37,11 +37,11 @@ public class Bars : Scenario X = Pos.AnchorEnd (), Width = Dim.Auto (), Height = Dim.Fill (), // Make room for some wide things - SchemeName = "Toplevel", + SchemeName = "Runnable", Source = new ListWrapper (eventSource) }; eventLog.Border!.Thickness = new (0, 1, 0, 0); - Application.TopRunnable.Add (eventLog); + Application.TopRunnableView.Add (eventLog); FrameView menuBarLikeExamples = new () { @@ -51,7 +51,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent(33), }; - Application.TopRunnable.Add (menuBarLikeExamples); + Application.TopRunnableView.Add (menuBarLikeExamples); Label label = new Label () { @@ -98,7 +98,7 @@ public class Bars : Scenario Width = Dim.Fill () - Dim.Width (eventLog), Height = Dim.Percent (33), }; - Application.TopRunnable.Add (menuLikeExamples); + Application.TopRunnableView.Add (menuLikeExamples); label = new Label () { @@ -212,7 +212,7 @@ public class Bars : Scenario Width = Dim.Width (menuLikeExamples), Height = Dim.Percent (33), }; - Application.TopRunnable.Add (statusBarLikeExamples); + Application.TopRunnableView.Add (statusBarLikeExamples); label = new Label () { @@ -249,7 +249,7 @@ public class Bars : Scenario ConfigStatusBar (bar); statusBarLikeExamples.Add (bar); - foreach (FrameView frameView in Application.TopRunnable.SubViews.Where (f => f is FrameView)!) + foreach (FrameView frameView in Application.TopRunnableView.SubViews.Where (f => f is FrameView)!) { foreach (Bar barView in frameView.SubViews.Where (b => b is Bar)!) { @@ -383,7 +383,7 @@ public class Bars : Scenario // contextMenu.Add (newMenu, open, save, saveAs); - // contextMenu.KeyBindings.Add (Key.Esc, Command.QuitToplevel); + // contextMenu.KeyBindings.Add (Key.Esc, Command.Quit); // contextMenu.Initialized += Menu_Initialized; diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index 404bbf4e4..f0078564a 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -294,7 +294,7 @@ public class Buttons : Scenario X = 2, Y = Pos.Bottom (osAlignment) + 1, Width = Dim.Width (computedFrame) - 2, - SchemeName = "TopLevel", + SchemeName = "Runnable", Text = mhkb }; moveHotKeyBtn.Accepting += (s, e) => @@ -311,7 +311,7 @@ public class Buttons : Scenario X = Pos.Left (absoluteFrame) + 1, Y = Pos.Bottom (osAlignment) + 1, Width = Dim.Width (absoluteFrame) - 2, - SchemeName = "TopLevel", + SchemeName = "Runnable", Text = muhkb }; moveUnicodeHotKeyBtn.Accepting += (s, e) => diff --git a/Examples/UICatalog/Scenarios/Clipping.cs b/Examples/UICatalog/Scenarios/Clipping.cs index b9bb47528..2dcd549ed 100644 --- a/Examples/UICatalog/Scenarios/Clipping.cs +++ b/Examples/UICatalog/Scenarios/Clipping.cs @@ -118,7 +118,7 @@ public class Clipping : Scenario Height = Dim.Auto (minimumContentDim: 4), Width = Dim.Auto (minimumContentDim: 14), Title = $"Overlapped{id} _{GetNextHotKey ()}", - SchemeName = SchemeManager.SchemesToSchemeName(Schemes.Toplevel), + SchemeName = SchemeManager.SchemesToSchemeName(Schemes.Runnable), Id = $"Overlapped{id}", ShadowStyle = ShadowStyle.Transparent, BorderStyle = LineStyle.Double, diff --git a/Examples/UICatalog/Scenarios/CombiningMarks.cs b/Examples/UICatalog/Scenarios/CombiningMarks.cs index 5a729760e..4ffa787b9 100644 --- a/Examples/UICatalog/Scenarios/CombiningMarks.cs +++ b/Examples/UICatalog/Scenarios/CombiningMarks.cs @@ -8,12 +8,12 @@ public class CombiningMarks : Scenario public override void Main () { Application.Init (); - var top = new Toplevel (); + var top = new Runnable (); top.DrawComplete += (s, e) => { // Forces reset _lineColsOffset because we're dealing with direct draw - Application.TopRunnable!.SetNeedsDraw (); + Application.TopRunnableView!.SetNeedsDraw (); var i = -1; top.Move (0, ++i); diff --git a/Examples/UICatalog/Scenarios/ComboBoxIteration.cs b/Examples/UICatalog/Scenarios/ComboBoxIteration.cs index 003926396..6e4cb9443 100644 --- a/Examples/UICatalog/Scenarios/ComboBoxIteration.cs +++ b/Examples/UICatalog/Scenarios/ComboBoxIteration.cs @@ -25,7 +25,7 @@ public class ComboBoxIteration : Scenario var lbComboBox = new Label { - SchemeName = "TopLevel", + SchemeName = "Runnable", X = Pos.Right (lbListView) + 1, Width = Dim.Percent (40) }; diff --git a/Examples/UICatalog/Scenarios/ComputedLayout.cs b/Examples/UICatalog/Scenarios/ComputedLayout.cs index 6a2320f91..1cfe67fce 100644 --- a/Examples/UICatalog/Scenarios/ComputedLayout.cs +++ b/Examples/UICatalog/Scenarios/ComputedLayout.cs @@ -280,7 +280,7 @@ public class ComputedLayout : Scenario Y = Pos.Percent (50), Width = Dim.Percent (80), Height = Dim.Percent (10), - SchemeName = "TopLevel" + SchemeName = "Runnable" }; textView.Text = diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index 600f4b98c..5fba97bcc 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -60,7 +60,7 @@ public class ConfigurationEditor : Scenario win.Add (_tabView, statusBar); - win.Loaded += (s, a) => + win.IsModalChanged += (s, a) => { Open (); }; @@ -75,7 +75,7 @@ public class ConfigurationEditor : Scenario void ConfigurationManagerOnApplied (object? sender, ConfigurationManagerEventArgs e) { - Application.TopRunnable?.SetNeedsDraw (); + Application.TopRunnableView?.SetNeedsDraw (); } } public void Save () diff --git a/Examples/UICatalog/Scenarios/ContextMenus.cs b/Examples/UICatalog/Scenarios/ContextMenus.cs index 541b11943..4ed0f02c8 100644 --- a/Examples/UICatalog/Scenarios/ContextMenus.cs +++ b/Examples/UICatalog/Scenarios/ContextMenus.cs @@ -26,7 +26,7 @@ public class ContextMenus : Scenario { Title = GetQuitKeyAndName (), Arrangement = ViewArrangement.Fixed, - SchemeName = "Toplevel" + SchemeName = "Runnable" }; _appWindow.Initialized += AppWindowOnInitialized; @@ -84,7 +84,11 @@ public class ContextMenus : Scenario _appWindow.MouseClick += OnAppWindowOnMouseClick; CultureInfo originalCulture = Thread.CurrentThread.CurrentUICulture; - _appWindow.Closed += (s, e) => { Thread.CurrentThread.CurrentUICulture = originalCulture; }; + _appWindow.IsRunningChanged += (s, e) => { + if (!e.Value) + { + Thread.CurrentThread.CurrentUICulture = originalCulture; + } }; } void OnAppWindowOnMouseClick (object? s, MouseEventArgs e) diff --git a/Examples/UICatalog/Scenarios/CsvEditor.cs b/Examples/UICatalog/Scenarios/CsvEditor.cs index 5831b8feb..aad85f6aa 100644 --- a/Examples/UICatalog/Scenarios/CsvEditor.cs +++ b/Examples/UICatalog/Scenarios/CsvEditor.cs @@ -575,9 +575,9 @@ public class CsvEditor : Scenario _selectedCellTextField.SuperView.Enabled = true; } - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; + Application.TopRunnableView.Title = $"{GetName ()} - {Path.GetFileName (_currentFile)}"; } } catch (Exception ex) diff --git a/Examples/UICatalog/Scenarios/Dialogs.cs b/Examples/UICatalog/Scenarios/Dialogs.cs index 8e8a6ec99..fb4a4fbd6 100644 --- a/Examples/UICatalog/Scenarios/Dialogs.cs +++ b/Examples/UICatalog/Scenarios/Dialogs.cs @@ -340,7 +340,13 @@ public class Dialogs : Scenario }; dialog.Add (addChar); - dialog.Closed += (s, e) => { buttonPressedLabel.Text = $"{clicked}"; }; + dialog.IsRunningChanged += (s, e) => + { + if (!e.Value) + { + buttonPressedLabel.Text = $"{clicked}"; + } + }; } catch (FormatException) { diff --git a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs index a0cdb48e3..ac558f22a 100644 --- a/Examples/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicStatusBar.cs @@ -15,7 +15,7 @@ public class DynamicStatusBar : Scenario public override void Main () { Application.Init (); - Application.Run ().Dispose (); + Application.Run (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs index d2eac26dc..857663577 100644 --- a/Examples/UICatalog/Scenarios/Editor.cs +++ b/Examples/UICatalog/Scenarios/Editor.cs @@ -170,7 +170,14 @@ public class Editor : Scenario _appWindow.Add (statusBar); - _appWindow.Closed += (s, e) => Thread.CurrentThread.CurrentUICulture = new ("en-US"); + _appWindow.IsRunningChanged += (s, e) => + { + if (!e.Value) + { + // BUGBUG: This should restore the original culture info + Thread.CurrentThread.CurrentUICulture = new ("en-US"); + } + }; CreateFindReplace (); diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs index 334450fbb..a9702d39c 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ArrangementEditor.cs @@ -45,7 +45,7 @@ public sealed class ArrangementEditor : EditorBase if (ViewToEdit.Arrangement.HasFlag (ViewArrangement.Overlapped)) { ViewToEdit.ShadowStyle = ShadowStyle.Transparent; - ViewToEdit.SchemeName = "Toplevel"; + ViewToEdit.SchemeName = "Runnable"; } else { diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index fd80d82f3..4621356f1 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -243,7 +243,7 @@ public class FileDialogExamples : Scenario IReadOnlyList multiSelected = fd.MultiSelected; string path = fd.Path; - // This needs to be disposed before opening other toplevel + // This needs to be disposed before opening other runnable fd.Dispose (); if (canceled) diff --git a/Examples/UICatalog/Scenarios/Keys.cs b/Examples/UICatalog/Scenarios/Keys.cs index 0c17a15f8..49b9ccc70 100644 --- a/Examples/UICatalog/Scenarios/Keys.cs +++ b/Examples/UICatalog/Scenarios/Keys.cs @@ -86,7 +86,7 @@ public class Keys : Scenario Height = Dim.Fill (), Source = new ListWrapper (keyList) }; - appKeyListView.SchemeName = "TopLevel"; + appKeyListView.SchemeName = "Runnable"; win.Add (appKeyListView); // View key events... @@ -114,7 +114,7 @@ public class Keys : Scenario Height = Dim.Fill (), Source = new ListWrapper (keyDownList) }; - appKeyListView.SchemeName = "TopLevel"; + appKeyListView.SchemeName = "Runnable"; win.Add (onKeyDownListView); // KeyDownNotHandled @@ -134,7 +134,7 @@ public class Keys : Scenario Height = Dim.Fill (), Source = new ListWrapper (keyDownNotHandledList) }; - appKeyListView.SchemeName = "TopLevel"; + appKeyListView.SchemeName = "Runnable"; win.Add (onKeyDownNotHandledListView); @@ -155,7 +155,7 @@ public class Keys : Scenario Height = Dim.Fill (), Source = new ListWrapper (swallowedList) }; - appKeyListView.SchemeName = "TopLevel"; + appKeyListView.SchemeName = "Runnable"; win.Add (onSwallowedListView); Application.Driver!.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b", "Esc")); }; diff --git a/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs b/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs index 6a826e4be..37cd0777d 100644 --- a/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs +++ b/Examples/UICatalog/Scenarios/LineCanvasExperiment.cs @@ -101,7 +101,7 @@ public class LineCanvasExperiment : Scenario // Width = view4.Width, // Height = 5, - // //Scheme = Colors.Schemes ["TopLevel"], + // //Scheme = Colors.Schemes ["Runnable"], // SuperViewRendersLineCanvas = true, // BorderStyle = LineStyle.Double //}; diff --git a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs index a20eb67af..dd6ac2bf4 100644 --- a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs @@ -98,7 +98,7 @@ public class ListViewWithSelection : Scenario Height = Dim.Fill (), Source = new ListWrapper (_eventList) }; - _eventListView.SchemeName = "TopLevel"; + _eventListView.SchemeName = "Runnable"; _appWindow.Add (_eventListView); _listView.SelectedItemChanged += (s, a) => LogEvent (s as View, a, "SelectedItemChanged"); diff --git a/Examples/UICatalog/Scenarios/ListsAndCombos.cs b/Examples/UICatalog/Scenarios/ListsAndCombos.cs index 775c78d24..186b147aa 100644 --- a/Examples/UICatalog/Scenarios/ListsAndCombos.cs +++ b/Examples/UICatalog/Scenarios/ListsAndCombos.cs @@ -35,7 +35,7 @@ public class ListsAndCombos : Scenario // ListView var lbListView = new Label { - SchemeName = "TopLevel", + SchemeName = "Runnable", X = 0, Width = Dim.Percent (40), @@ -91,7 +91,7 @@ public class ListsAndCombos : Scenario // ComboBox var lbComboBox = new Label { - SchemeName = "TopLevel", + SchemeName = "Runnable", X = Pos.Right (lbListView) + 1, Width = Dim.Percent (40), diff --git a/Examples/UICatalog/Scenarios/Localization.cs b/Examples/UICatalog/Scenarios/Localization.cs index 0da975790..c7eee617b 100644 --- a/Examples/UICatalog/Scenarios/Localization.cs +++ b/Examples/UICatalog/Scenarios/Localization.cs @@ -181,7 +181,7 @@ public class Localization : Scenario wizardButton.Accepting += (sender, e) => ShowWizard (); win.Add (wizardButton); - win.Unloaded += (sender, e) => Quit (); + win.IsRunningChanged += (sender, e) => Quit (); win.Add (menu); diff --git a/Examples/UICatalog/Scenarios/Mazing.cs b/Examples/UICatalog/Scenarios/Mazing.cs index 04cca789d..6d7a08d6f 100644 --- a/Examples/UICatalog/Scenarios/Mazing.cs +++ b/Examples/UICatalog/Scenarios/Mazing.cs @@ -9,7 +9,7 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Games")] public class Mazing : Scenario { - private Toplevel? _top; + private Window? _top; private MazeGenerator? _m; private List? _potions; @@ -33,17 +33,17 @@ public class Mazing : Scenario _top.KeyBindings.Add (Key.CursorDown, Command.Down); // Changing the key-bindings of a View is not allowed, however, - // by default, Toplevel doesn't bind any of our movement keys, so + // by default, Runnable doesn't bind any of our movement keys, so // we can take advantage of the CommandNotBound event to handle them // - // An alternative implementation would be to create a TopLevel subclass that + // An alternative implementation would be to create a Runnable subclass that // calls AddCommand/KeyBindings.Add in the constructor. See the Snake game scenario // for an example. _top.CommandNotBound += TopCommandNotBound; _top.DrawingContent += (s, _) => { - if (s is not Toplevel top) + if (s is not Runnable top) { return; } @@ -171,7 +171,7 @@ public class Mazing : Scenario if (_m.PlayerHp <= 0) { _message = "You died!"; - Application.TopRunnable!.SetNeedsDraw (); // trigger redraw + Application.TopRunnableView!.SetNeedsDraw (); // trigger redraw _dead = true; return; // Stop further action if dead @@ -190,7 +190,7 @@ public class Mazing : Scenario _message = string.Empty; } - Application.TopRunnable!.SetNeedsDraw (); // trigger redraw + Application.TopRunnableView!.SetNeedsDraw (); // trigger redraw } // Optional win condition: @@ -200,7 +200,7 @@ public class Mazing : Scenario _m = new (); // Generate a new maze _m.PlayerHp = hp; GenerateNpcs (); - Application.TopRunnable!.SetNeedsDraw (); // trigger redraw + Application.TopRunnableView!.SetNeedsDraw (); // trigger redraw } } } diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index 4a5313145..7733e0584 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/Examples/UICatalog/Scenarios/Menus.cs @@ -21,7 +21,7 @@ public class Menus : Scenario Logging.Logger = CreateLogger (); Application.Init (); - Toplevel app = new (); + Runnable app = new (); app.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); @@ -32,7 +32,7 @@ public class Menus : Scenario X = Pos.AnchorEnd (), Width = Dim.Auto (), Height = Dim.Fill (), // Make room for some wide things - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (eventSource) }; eventLog.Border!.Thickness = new (0, 1, 0, 0); diff --git a/Examples/UICatalog/Scenarios/Mouse.cs b/Examples/UICatalog/Scenarios/Mouse.cs index cb047614c..d56b3e82a 100644 --- a/Examples/UICatalog/Scenarios/Mouse.cs +++ b/Examples/UICatalog/Scenarios/Mouse.cs @@ -247,7 +247,7 @@ public class Mouse : Scenario Y = Pos.Bottom (label), Width = 50, Height = Dim.Fill (), - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (appLogList) }; win.Add (label, appLog); @@ -278,7 +278,7 @@ public class Mouse : Scenario Y = Pos.Bottom (label), Width = Dim.Percent (50), Height = Dim.Fill (), - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (winLogList) }; win.Add (label, winLog); diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index dfedfc4d1..7c78ef359 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -180,7 +180,7 @@ public class Navigation : Scenario X = 1, Y = 7, Id = "datePicker", - SchemeName = "TopLevel", + SchemeName = "Runnable", ShadowStyle = ShadowStyle.Transparent, BorderStyle = LineStyle.Double, CanFocus = true, // Can't drag without this? BUGBUG @@ -237,7 +237,7 @@ public class Navigation : Scenario Height = Dim.Auto (), Width = Dim.Auto (), Title = $"Overlapped{id} _{GetNextHotKey ()}", - SchemeName = "TopLevel", + SchemeName = "Runnable", Id = $"Overlapped{id}", ShadowStyle = ShadowStyle.Transparent, BorderStyle = LineStyle.Double, diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 6d3ac2c82..f597f2fad 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -110,10 +110,13 @@ public class Notepad : Scenario _tabView.SelectedTabChanged += TabView_SelectedTabChanged; _tabView.HasFocusChanging += (s, e) => _focusedTabView = _tabView; - top.Ready += (s, e) => + top.IsModalChanged += (s, e) => { - New (); - LenShortcut.Title = $"Len:{_focusedTabView?.Text?.Length ?? 0}"; + if (e.Value) + { + New (); + LenShortcut.Title = $"Len:{_focusedTabView?.Text?.Length ?? 0}"; + } }; Application.Run (top); diff --git a/Examples/UICatalog/Scenarios/PosAlignDemo.cs b/Examples/UICatalog/Scenarios/PosAlignDemo.cs index 714155bff..5f99a04e5 100644 --- a/Examples/UICatalog/Scenarios/PosAlignDemo.cs +++ b/Examples/UICatalog/Scenarios/PosAlignDemo.cs @@ -20,7 +20,7 @@ public sealed class PosAlignDemo : Scenario Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()} - {GetDescription ()}" }; - SetupControls (appWindow, Dimension.Width, Schemes.Toplevel); + SetupControls (appWindow, Dimension.Width, Schemes.Runnable); SetupControls (appWindow, Dimension.Height, Schemes.Error); diff --git a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs index aafc12f99..6fbab174d 100644 --- a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs @@ -27,7 +27,7 @@ public class ProgressBarStyles : Scenario { Application.Init (); - Window app = new () + Window win = new () { Title = GetQuitKeyAndName (), BorderStyle = LineStyle.Single, }; @@ -38,7 +38,7 @@ public class ProgressBarStyles : Scenario ShowViewIdentifier = true }; - app.Add (editor); + win.Add (editor); View container = new () { @@ -47,7 +47,7 @@ public class ProgressBarStyles : Scenario Width = Dim.Fill (), Height = Dim.Fill (), }; - app.Add (container); + win.Add (container); const float fractionStep = 0.01F; @@ -278,8 +278,8 @@ public class ProgressBarStyles : Scenario - app.Initialized += App_Initialized; - app.Unloaded += App_Unloaded; + win.Initialized += Win_Initialized; + win.IsRunningChanged += Win_IsRunningChanged; _pulseTimer = new Timer ( _ => @@ -292,14 +292,18 @@ public class ProgressBarStyles : Scenario 0, 300 ); - Application.Run (app); - app.Dispose (); + Application.Run (win); + win.Dispose (); Application.Shutdown (); return; - void App_Unloaded (object sender, EventArgs args) + void Win_IsRunningChanged (object sender, EventArgs args) { + if (args.Value) + { + return; + } if (_fractionTimer != null) { _fractionTimer.Dispose (); @@ -312,11 +316,11 @@ public class ProgressBarStyles : Scenario _pulseTimer = null; } - app.Unloaded -= App_Unloaded; + win.IsRunningChanged -= Win_IsRunningChanged; } } - private void App_Initialized (object sender, EventArgs e) + private void Win_Initialized (object sender, EventArgs e) { _pbList.SelectedItem = 0; } diff --git a/Examples/UICatalog/Scenarios/RunTExample.cs b/Examples/UICatalog/Scenarios/RunTExample.cs index 7a66e54e5..1d5f4e5a5 100644 --- a/Examples/UICatalog/Scenarios/RunTExample.cs +++ b/Examples/UICatalog/Scenarios/RunTExample.cs @@ -8,7 +8,7 @@ public class RunTExample : Scenario public override void Main () { // No need to call Init if Application.Run is used - Application.Run ().Dispose (); + Application.Run (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/Scrolling.cs b/Examples/UICatalog/Scenarios/Scrolling.cs index b79964596..a503538e7 100644 --- a/Examples/UICatalog/Scenarios/Scrolling.cs +++ b/Examples/UICatalog/Scenarios/Scrolling.cs @@ -16,13 +16,13 @@ public class Scrolling : Scenario { Application.Init (); - var app = new Window + var win = new Window { Title = GetQuitKeyAndName () }; var label = new Label { X = 0, Y = 0 }; - app.Add (label); + win.Add (label); var demoView = new AllViewsView { @@ -42,7 +42,7 @@ public class Scrolling : Scenario $"{demoView}\nContentSize: {demoView.GetContentSize ()}\nViewport.Location: {demoView.Viewport.Location}"; }; - app.Add (demoView); + win.Add (demoView); var hCheckBox = new CheckBox { @@ -51,7 +51,7 @@ public class Scrolling : Scenario Text = "_HorizontalScrollBar.Visible", CheckedState = demoView.HorizontalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked }; - app.Add (hCheckBox); + win.Add (hCheckBox); hCheckBox.CheckedStateChanged += (sender, args) => { demoView.HorizontalScrollBar.Visible = args.Value == CheckState.Checked; }; var vCheckBox = new CheckBox @@ -61,7 +61,7 @@ public class Scrolling : Scenario Text = "_VerticalScrollBar.Visible", CheckedState = demoView.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked }; - app.Add (vCheckBox); + win.Add (vCheckBox); vCheckBox.CheckedStateChanged += (sender, args) => { demoView.VerticalScrollBar.Visible = args.Value == CheckState.Checked; }; var ahCheckBox = new CheckBox @@ -77,7 +77,7 @@ public class Scrolling : Scenario demoView.HorizontalScrollBar.AutoShow = e.Result == CheckState.Checked; demoView.VerticalScrollBar.AutoShow = e.Result == CheckState.Checked; }; - app.Add (ahCheckBox); + win.Add (ahCheckBox); demoView.VerticalScrollBar.VisibleChanging += (sender, args) => { vCheckBox.CheckedState = args.NewValue ? CheckState.Checked : CheckState.UnChecked; }; @@ -92,19 +92,19 @@ public class Scrolling : Scenario X = Pos.Center (), Y = Pos.AnchorEnd (), Width = Dim.Fill () }; - app.Add (progress); + win.Add (progress); - app.Initialized += AppOnInitialized; - app.Unloaded += AppUnloaded; + win.Initialized += WinOnInitialized; + win.IsRunningChanged += WinIsRunningChanged; - Application.Run (app); - app.Unloaded -= AppUnloaded; - app.Dispose (); + Application.Run (win); + win.IsRunningChanged -= WinIsRunningChanged; + win.Dispose (); Application.Shutdown (); return; - void AppOnInitialized (object? sender, EventArgs e) + void WinOnInitialized (object? sender, EventArgs e) { bool TimerFn () { @@ -116,9 +116,9 @@ public class Scrolling : Scenario _progressTimer = Application.AddTimeout (TimeSpan.FromMilliseconds (200), TimerFn); } - void AppUnloaded (object? sender, EventArgs args) + void WinIsRunningChanged (object? sender, EventArgs args) { - if (_progressTimer is { }) + if (!args.Value && _progressTimer is { }) { Application.RemoveTimeout (_progressTimer); _progressTimer = null; diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 7af36ef92..a2e241c6c 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -15,7 +15,7 @@ public class Shortcuts : Scenario var quitKey = Application.QuitKey; Window app = new (); - app.Loaded += App_Loaded; + app.IsModalChanged += App_Loaded; Application.Run (app); app.Dispose (); @@ -28,7 +28,7 @@ public class Shortcuts : Scenario private void App_Loaded (object? sender, EventArgs e) { Application.QuitKey = Key.F4.WithCtrl; - Application.TopRunnable!.Title = GetQuitKeyAndName (); + Application.TopRunnableView!.Title = GetQuitKeyAndName (); ObservableCollection eventSource = new (); @@ -38,7 +38,7 @@ public class Shortcuts : Scenario X = Pos.AnchorEnd (), Y = 0, Height = Dim.Fill (4), - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (eventSource), BorderStyle = LineStyle.Double, Title = "E_vents" @@ -46,14 +46,14 @@ public class Shortcuts : Scenario eventLog.Width = Dim.Func ( _ => Math.Min ( - Application.TopRunnable.Viewport.Width / 2, + Application.TopRunnableView.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); eventLog.Width = Dim.Func ( _ => Math.Min ( eventLog.SuperView!.Viewport.Width / 2, eventLog?.MaxLength + eventLog!.GetAdornmentsThickness ().Horizontal ?? 0)); - Application.TopRunnable.Add (eventLog); + Application.TopRunnableView.Add (eventLog); var alignKeysShortcut = new Shortcut { @@ -86,7 +86,7 @@ public class Shortcuts : Scenario }; - Application.TopRunnable.Add (alignKeysShortcut); + Application.TopRunnableView.Add (alignKeysShortcut); var commandFirstShortcut = new Shortcut { @@ -115,7 +115,7 @@ public class Shortcuts : Scenario $"{commandFirstShortcut.Id}.CommandView.CheckedStateChanging: {cb.Text}"); eventLog.MoveDown (); - IEnumerable toAlign = Application.TopRunnable.SubViews.OfType (); + IEnumerable toAlign = Application.TopRunnableView.SubViews.OfType (); IEnumerable enumerable = toAlign as View [] ?? toAlign.ToArray (); foreach (View view in enumerable) @@ -134,7 +134,7 @@ public class Shortcuts : Scenario } }; - Application.TopRunnable.Add (commandFirstShortcut); + Application.TopRunnableView.Add (commandFirstShortcut); var canFocusShortcut = new Shortcut { @@ -159,7 +159,7 @@ public class Shortcuts : Scenario SetCanFocus (e.Result == CheckState.Checked); } }; - Application.TopRunnable.Add (canFocusShortcut); + Application.TopRunnableView.Add (canFocusShortcut); var appShortcut = new Shortcut { @@ -173,7 +173,7 @@ public class Shortcuts : Scenario BindKeyToApplication = true }; - Application.TopRunnable.Add (appShortcut); + Application.TopRunnableView.Add (appShortcut); var buttonShortcut = new Shortcut { @@ -193,7 +193,7 @@ public class Shortcuts : Scenario var button = (Button)buttonShortcut.CommandView; buttonShortcut.Accepting += Button_Clicked; - Application.TopRunnable.Add (buttonShortcut); + Application.TopRunnableView.Add (buttonShortcut); var optionSelectorShortcut = new Shortcut { @@ -221,7 +221,7 @@ public class Shortcuts : Scenario } }; - Application.TopRunnable.Add (optionSelectorShortcut); + Application.TopRunnableView.Add (optionSelectorShortcut); var sliderShortcut = new Shortcut { @@ -248,7 +248,7 @@ public class Shortcuts : Scenario eventLog.MoveDown (); }; - Application.TopRunnable.Add (sliderShortcut); + Application.TopRunnableView.Add (sliderShortcut); ListView listView = new ListView () { @@ -270,7 +270,7 @@ public class Shortcuts : Scenario Key = Key.F5.WithCtrl, }; - Application.TopRunnable.Add (listViewShortcut); + Application.TopRunnableView.Add (listViewShortcut); var noCommandShortcut = new Shortcut { @@ -282,7 +282,7 @@ public class Shortcuts : Scenario Key = Key.D0 }; - Application.TopRunnable.Add (noCommandShortcut); + Application.TopRunnableView.Add (noCommandShortcut); var noKeyShortcut = new Shortcut { @@ -295,7 +295,7 @@ public class Shortcuts : Scenario HelpText = "Keyless" }; - Application.TopRunnable.Add (noKeyShortcut); + Application.TopRunnableView.Add (noKeyShortcut); var noHelpShortcut = new Shortcut { @@ -308,7 +308,7 @@ public class Shortcuts : Scenario HelpText = "" }; - Application.TopRunnable.Add (noHelpShortcut); + Application.TopRunnableView.Add (noHelpShortcut); noHelpShortcut.SetFocus (); var framedShortcut = new Shortcut @@ -339,8 +339,8 @@ public class Shortcuts : Scenario framedShortcut.KeyView.SchemeName = framedShortcut.KeyView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base); } - framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel); - Application.TopRunnable.Add (framedShortcut); + framedShortcut.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Runnable); + Application.TopRunnableView.Add (framedShortcut); // Horizontal var progressShortcut = new Shortcut @@ -387,7 +387,7 @@ public class Shortcuts : Scenario }; timer.Start (); - Application.TopRunnable.Add (progressShortcut); + Application.TopRunnableView.Add (progressShortcut); var textField = new TextField { @@ -408,7 +408,7 @@ public class Shortcuts : Scenario }; textField.CanFocus = true; - Application.TopRunnable.Add (textFieldShortcut); + Application.TopRunnableView.Add (textFieldShortcut); var bgColorShortcut = new Shortcut { @@ -450,19 +450,19 @@ public class Shortcuts : Scenario eventSource.Add ($"ColorChanged: {o.GetType ().Name} - {args.Result}"); eventLog.MoveDown (); - Application.TopRunnable.SetScheme ( - new (Application.TopRunnable.GetScheme ()) + Application.TopRunnableView.SetScheme ( + new (Application.TopRunnableView.GetScheme ()) { Normal = new ( - Application.TopRunnable!.GetAttributeForRole (VisualRole.Normal).Foreground, + Application.TopRunnableView!.GetAttributeForRole (VisualRole.Normal).Foreground, args.Result, - Application.TopRunnable!.GetAttributeForRole (VisualRole.Normal).Style) + Application.TopRunnableView!.GetAttributeForRole (VisualRole.Normal).Style) }); } }; bgColorShortcut.CommandView = bgColor; - Application.TopRunnable.Add (bgColorShortcut); + Application.TopRunnableView.Add (bgColorShortcut); var appQuitShortcut = new Shortcut { @@ -476,9 +476,9 @@ public class Shortcuts : Scenario }; appQuitShortcut.Accepting += (o, args) => { Application.RequestStop (); }; - Application.TopRunnable.Add (appQuitShortcut); + Application.TopRunnableView.Add (appQuitShortcut); - foreach (Shortcut shortcut in Application.TopRunnable.SubViews.OfType ()) + foreach (Shortcut shortcut in Application.TopRunnableView.SubViews.OfType ()) { shortcut.Selecting += (o, args) => { @@ -529,7 +529,7 @@ public class Shortcuts : Scenario void SetCanFocus (bool canFocus) { - foreach (Shortcut peer in Application.TopRunnable!.SubViews.OfType ()) + foreach (Shortcut peer in Application.TopRunnableView!.SubViews.OfType ()) { if (peer.CanFocus) { @@ -542,7 +542,7 @@ public class Shortcuts : Scenario { var max = 0; - IEnumerable toAlign = Application.TopRunnable!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); + IEnumerable toAlign = Application.TopRunnableView!.SubViews.OfType ().Where(s => !s.Y.Has(out _)).Cast(); IEnumerable enumerable = toAlign as Shortcut [] ?? toAlign.ToArray (); if (align) diff --git a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs index e5b8c301f..424523c00 100644 --- a/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/Examples/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -5,7 +5,7 @@ using System.ComponentModel; namespace UICatalog.Scenarios; -[ScenarioMetadata ("Single BackgroundWorker", "A single BackgroundWorker threading opening another Toplevel")] +[ScenarioMetadata ("Single BackgroundWorker", "A single BackgroundWorker threading opening another Runnable")] [ScenarioCategory ("Threading")] [ScenarioCategory ("Arrangement")] [ScenarioCategory ("Runnable")] @@ -13,7 +13,7 @@ public class SingleBackgroundWorker : Scenario { public override void Main () { - Application.Run ().Dispose (); + Application.Run (); Application.Shutdown (); } @@ -174,7 +174,7 @@ public class SingleBackgroundWorker : Scenario StagingUIController builderUI = new (_startStaging, e.Result as ObservableCollection); - Toplevel? top = Application.TopRunnable; + View? top = Application.TopRunnableView; if (top is { }) { @@ -200,7 +200,7 @@ public class SingleBackgroundWorker : Scenario public class StagingUIController : Window { - private Toplevel? _top; + private Runnable? _top; public StagingUIController (DateTime? start, ObservableCollection? list) { @@ -209,7 +209,6 @@ public class SingleBackgroundWorker : Scenario Title = "_top", Width = Dim.Fill (), Height = Dim.Fill (), - Modal = true }; _top.KeyDown += (s, e) => diff --git a/Examples/UICatalog/Scenarios/Sliders.cs b/Examples/UICatalog/Scenarios/Sliders.cs index 8d954d100..d23eae48d 100644 --- a/Examples/UICatalog/Scenarios/Sliders.cs +++ b/Examples/UICatalog/Scenarios/Sliders.cs @@ -590,7 +590,7 @@ public class Sliders : Scenario Y = Pos.Bottom (spacingOptions), Width = Dim.Fill (), Height = Dim.Fill (), - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (eventSource) }; configView.Add (eventLog); diff --git a/Examples/UICatalog/Scenarios/SpinnerStyles.cs b/Examples/UICatalog/Scenarios/SpinnerStyles.cs index 87e2b1b3b..020f12368 100644 --- a/Examples/UICatalog/Scenarios/SpinnerStyles.cs +++ b/Examples/UICatalog/Scenarios/SpinnerStyles.cs @@ -14,7 +14,7 @@ public class SpinnerViewStyles : Scenario { Application.Init (); - Window app = new () + Window win = new () { Title = GetQuitKeyAndName () }; @@ -40,7 +40,7 @@ public class SpinnerViewStyles : Scenario //Title = "Preview", BorderStyle = LineStyle.Single }; - app.Add (preview); + win.Add (preview); var spinner = new SpinnerView { X = Pos.Center (), Y = 0 }; preview.Add (spinner); @@ -54,7 +54,7 @@ public class SpinnerViewStyles : Scenario CheckedState = CheckState.Checked, Text = "Ascii Only" }; - app.Add (ckbAscii); + win.Add (ckbAscii); var ckbNoSpecial = new CheckBox { @@ -64,28 +64,28 @@ public class SpinnerViewStyles : Scenario CheckedState = CheckState.Checked, Text = "No Special" }; - app.Add (ckbNoSpecial); + win.Add (ckbNoSpecial); var ckbReverse = new CheckBox { X = Pos.Center () - 22, Y = Pos.Bottom (preview) + 1, CheckedState = CheckState.UnChecked, Text = "Reverse" }; - app.Add (ckbReverse); + win.Add (ckbReverse); var ckbBounce = new CheckBox { X = Pos.Right (ckbReverse) + 2, Y = Pos.Bottom (preview) + 1, CheckedState = CheckState.UnChecked, Text = "Bounce" }; - app.Add (ckbBounce); + win.Add (ckbBounce); var delayLabel = new Label { X = Pos.Right (ckbBounce) + 2, Y = Pos.Bottom (preview) + 1, Text = "Delay:" }; - app.Add (delayLabel); + win.Add (delayLabel); var delayField = new TextField { X = Pos.Right (delayLabel), Y = Pos.Bottom (preview) + 1, Width = 5, Text = DEFAULT_DELAY.ToString () }; - app.Add (delayField); + win.Add (delayField); delayField.TextChanged += (s, e) => { @@ -96,13 +96,13 @@ public class SpinnerViewStyles : Scenario }; var customLabel = new Label { X = Pos.Right (delayField) + 2, Y = Pos.Bottom (preview) + 1, Text = "Custom:" }; - app.Add (customLabel); + win.Add (customLabel); var customField = new TextField { X = Pos.Right (customLabel), Y = Pos.Bottom (preview) + 1, Width = 12, Text = DEFAULT_CUSTOM }; - app.Add (customField); + win.Add (customField); string [] styleArray = styleDict.Select (e => e.Value.Key).ToArray (); @@ -117,7 +117,7 @@ public class SpinnerViewStyles : Scenario }; styles.SetSource (new ObservableCollection (styleArray)); styles.SelectedItem = 0; // SpinnerStyle.Custom; - app.Add (styles); + win.Add (styles); SetCustom (); customField.TextChanged += (s, e) => @@ -166,7 +166,7 @@ public class SpinnerViewStyles : Scenario ckbBounce.CheckedStateChanging += (s, e) => { spinner.SpinBounce = e.Result == CheckState.Checked; }; - app.Unloaded += App_Unloaded; + win.IsRunningChanged += WinIsRunningChanged; void SetCustom () { @@ -199,23 +199,23 @@ public class SpinnerViewStyles : Scenario } } - void App_Unloaded (object sender, EventArgs args) + void WinIsRunningChanged (object sender, EventArgs args) { - if (spinner is {}) + if (!args.Value && spinner is {}) { spinner.Dispose (); spinner = null; } } - Application.Run (app); - app.Unloaded -= App_Unloaded; + Application.Run (win); + win.IsRunningChanged -= WinIsRunningChanged; if (spinner is { }) { spinner.Dispose (); spinner = null; } - app.Dispose (); + win.Dispose (); Application.Shutdown (); } diff --git a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs index 3328264ee..c1f5c49c5 100644 --- a/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs +++ b/Examples/UICatalog/Scenarios/SyntaxHighlighting.cs @@ -120,7 +120,7 @@ public class SyntaxHighlighting : Scenario Application.Init (); // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new (); + Runnable appWindow = new (); var menu = new MenuBar (); diff --git a/Examples/UICatalog/Scenarios/TableEditor.cs b/Examples/UICatalog/Scenarios/TableEditor.cs index 7d130da78..4880f4c95 100644 --- a/Examples/UICatalog/Scenarios/TableEditor.cs +++ b/Examples/UICatalog/Scenarios/TableEditor.cs @@ -499,7 +499,7 @@ public class TableEditor : Scenario Application.Init (); // Setup - Create a top-level application window and configure it. - Toplevel appWindow = new (); + Runnable appWindow = new (); _tableView = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; diff --git a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs index 15e9db4c6..ba25683e5 100644 --- a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs +++ b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs @@ -23,14 +23,11 @@ public class TextEffectsScenario : Scenario Title = "Text Effects Scenario" }; - w.Loaded += (s, e) => { SetupGradientLineCanvas (w, w.Frame.Size); }; + w.IsModalChanged += (s, e) => { SetupGradientLineCanvas (w, w.Frame.Size); }; - w.SizeChanging += (s, e) => + w.ViewportChanged += (s, e) => { - if (e.Size.HasValue) - { - SetupGradientLineCanvas (w, e.Size.Value); - } + SetupGradientLineCanvas (w, e.NewViewport.Size); }; w.SetScheme (new () diff --git a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs index 08319eefb..885191c88 100644 --- a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs +++ b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs @@ -30,7 +30,7 @@ public class TextFormatterDemo : Scenario var blockText = new Label { - SchemeName = "TopLevel", + SchemeName = "Runnable", X = 0, Y = 0, diff --git a/Examples/UICatalog/Scenarios/TextStyles.cs b/Examples/UICatalog/Scenarios/TextStyles.cs index 92c9c723b..191e5c2bf 100644 --- a/Examples/UICatalog/Scenarios/TextStyles.cs +++ b/Examples/UICatalog/Scenarios/TextStyles.cs @@ -5,7 +5,7 @@ namespace UICatalog.Scenarios; [ScenarioMetadata ("Text Styles", "Shows Attribute.TextStyles including bold, italic, etc...")] [ScenarioCategory ("Text and Formatting")] [ScenarioCategory ("Colors")] -public sealed class TestStyles : Scenario +public sealed class TextStyles : Scenario { private CheckBox? _drawDirectly; diff --git a/Examples/UICatalog/Scenarios/Themes.cs b/Examples/UICatalog/Scenarios/Themes.cs index 68d73ed40..31a30933c 100644 --- a/Examples/UICatalog/Scenarios/Themes.cs +++ b/Examples/UICatalog/Scenarios/Themes.cs @@ -129,7 +129,7 @@ public sealed class Themes : Scenario { if (_view is { }) { - Application.TopRunnable!.SchemeName = args.NewValue; + Application.TopRunnableView!.SchemeName = args.NewValue; if (_view.HasScheme) { diff --git a/Examples/UICatalog/Scenarios/Threading.cs b/Examples/UICatalog/Scenarios/Threading.cs index dc97292b2..92ecf608c 100644 --- a/Examples/UICatalog/Scenarios/Threading.cs +++ b/Examples/UICatalog/Scenarios/Threading.cs @@ -75,7 +75,7 @@ public class Threading : Scenario Y = Pos.Y (_btnActionCancel) + 6, Width = 10, Height = 10, - SchemeName = "TopLevel" + SchemeName = "Runnable" }; win.Add (new Label { X = Pos.Right (_itemsList) + 10, Y = Pos.Y (_btnActionCancel) + 4, Text = "Task Logs:" }); @@ -86,7 +86,7 @@ public class Threading : Scenario Y = Pos.Y (_itemsList), Width = 50, Height = Dim.Fill (), - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (_log) }; @@ -162,10 +162,10 @@ public class Threading : Scenario void Win_Loaded (object sender, EventArgs args) { _btnActionCancel.SetFocus (); - win.Loaded -= Win_Loaded; + win.IsModalChanged -= Win_Loaded; } - win.Loaded += Win_Loaded; + win.IsModalChanged += Win_Loaded; Application.Run (win); win.Dispose (); diff --git a/Examples/UICatalog/Scenarios/TreeUseCases.cs b/Examples/UICatalog/Scenarios/TreeUseCases.cs index a7c8772e5..133288c81 100644 --- a/Examples/UICatalog/Scenarios/TreeUseCases.cs +++ b/Examples/UICatalog/Scenarios/TreeUseCases.cs @@ -71,10 +71,13 @@ public class TreeUseCases : Scenario appWindow.Add (menu, statusBar); - appWindow.Ready += (sender, args) => + appWindow.IsModalChanged += (sender, args) => { - // Start with the most basic use case - LoadSimpleNodes (); + if (args.Value) + { + // Start with the most basic use case + LoadSimpleNodes (); + } }; Application.Run (appWindow); @@ -92,9 +95,9 @@ public class TreeUseCases : Scenario if (_currentTree is { }) { - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Remove (_currentTree); + Application.TopRunnableView.Remove (_currentTree); } _currentTree.Dispose (); @@ -116,9 +119,9 @@ public class TreeUseCases : Scenario tree.TreeBuilder = new GameObjectTreeBuilder (); } - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Add (tree); + Application.TopRunnableView.Add (tree); } tree.AddObject (army1); @@ -141,9 +144,9 @@ public class TreeUseCases : Scenario if (_currentTree is { }) { - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Remove (_currentTree); + Application.TopRunnableView.Remove (_currentTree); } _currentTree.Dispose (); @@ -151,9 +154,9 @@ public class TreeUseCases : Scenario TreeView tree = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Add (tree); + Application.TopRunnableView.Add (tree); } tree.AddObject (myHouse); @@ -165,9 +168,9 @@ public class TreeUseCases : Scenario { if (_currentTree is { }) { - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Remove (_currentTree); + Application.TopRunnableView.Remove (_currentTree); } _currentTree.Dispose (); @@ -175,9 +178,9 @@ public class TreeUseCases : Scenario TreeView tree = new () { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; - if (Application.TopRunnable is { }) + if (Application.TopRunnableView is { }) { - Application.TopRunnable.Add (tree); + Application.TopRunnableView.Add (tree); } TreeNode root1 = new ("Root1"); diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index e687a078f..b4934a064 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -102,7 +102,7 @@ public class ViewportSettings : Scenario Title = GetQuitKeyAndName (), // Use a different colorscheme so ViewSettings.ClearContentOnly is obvious - SchemeName = "Toplevel", + SchemeName = "Runnable", BorderStyle = LineStyle.None }; diff --git a/Examples/UICatalog/Scenarios/WizardAsView.cs b/Examples/UICatalog/Scenarios/WizardAsView.cs index 67e23685f..6ce39b6f0 100644 --- a/Examples/UICatalog/Scenarios/WizardAsView.cs +++ b/Examples/UICatalog/Scenarios/WizardAsView.cs @@ -66,7 +66,7 @@ public class WizardAsView : Scenario // Set Modal to false to cause the Wizard class to render without a frame and // behave like an non-modal View (vs. a modal/pop-up Window). - wizard.Modal = false; + // wizard.Modal = false; wizard.MovingBack += (s, args) => { @@ -148,11 +148,11 @@ public class WizardAsView : Scenario lastStep.HelpText = "The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing Esc will cancel."; - Window topLevel = new (); - topLevel.Add (menu, wizard); + Window window = new (); + window.Add (menu, wizard); - Application.Run (topLevel); - topLevel.Dispose (); + Application.Run (window); + window.Dispose (); Application.Shutdown (); } } diff --git a/Examples/UICatalog/Scenarios/Wizards.cs b/Examples/UICatalog/Scenarios/Wizards.cs index 2f90c3420..c97591de3 100644 --- a/Examples/UICatalog/Scenarios/Wizards.cs +++ b/Examples/UICatalog/Scenarios/Wizards.cs @@ -81,10 +81,10 @@ public class Wizards : Scenario void Win_Loaded (object sender, EventArgs args) { frame.Height = widthEdit.Frame.Height + heightEdit.Frame.Height + titleEdit.Frame.Height + 2; - win.Loaded -= Win_Loaded; + win.IsModalChanged -= Win_Loaded; } - win.Loaded += Win_Loaded; + win.IsModalChanged += Win_Loaded; label = new () { diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 884e65fd1..21634ac0b 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -73,8 +73,8 @@ public class UICatalog CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); } - UICatalogTop.CachedScenarios = Scenario.GetScenarios (); - UICatalogTop.CachedCategories = Scenario.GetAllCategories (); + UICatalogRunnable.CachedScenarios = Scenario.GetScenarios (); + UICatalogRunnable.CachedCategories = Scenario.GetAllCategories (); // Process command line args @@ -136,7 +136,7 @@ public class UICatalog "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.", getDefaultValue: () => "none" ).FromAmong ( - UICatalogTop.CachedScenarios.Select (s => s.GetName ()) + UICatalogRunnable.CachedScenarios.Select (s => s.GetName ()) .Append ("none") .ToArray () ); @@ -249,7 +249,7 @@ public class UICatalog /// killed and the Scenario is run as though it were Application.TopRunnable. When the Scenario exits, this function exits. /// /// - private static Scenario RunUICatalogTopLevel () + private static Scenario RunUICatalogRunnable () { // Run UI Catalog UI. When it exits, if _selectedScenario is != null then // a Scenario was selected. Otherwise, the user wants to quit UI Catalog. @@ -261,12 +261,11 @@ public class UICatalog _uiCatalogDriver = Application.Driver!.GetName (); - Toplevel top = Application.Run (); - top.Dispose (); + Application.Run (); Application.Shutdown (); VerifyObjectsWereDisposed (); - return UICatalogTop.CachedSelectedScenario!; + return UICatalogRunnable.CachedSelectedScenario!; } [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] @@ -347,7 +346,7 @@ public class UICatalog private static void ConfigFileChanged (object sender, FileSystemEventArgs e) { - if (Application.TopRunnable == null) + if (Application.TopRunnableView == null) { return; } @@ -372,15 +371,15 @@ public class UICatalog ConfigurationManager.Enable (ConfigLocations.All); } - int item = UICatalogTop.CachedScenarios!.IndexOf ( - UICatalogTop.CachedScenarios!.FirstOrDefault ( + int item = UICatalogRunnable.CachedScenarios!.IndexOf ( + UICatalogRunnable.CachedScenarios!.FirstOrDefault ( s => s.GetName () .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase) )!); - UICatalogTop.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTop.CachedScenarios [item].GetType ())!; + UICatalogRunnable.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogRunnable.CachedScenarios [item].GetType ())!; - BenchmarkResults? results = RunScenario (UICatalogTop.CachedSelectedScenario, options.Benchmark); + BenchmarkResults? results = RunScenario (UICatalogRunnable.CachedSelectedScenario, options.Benchmark); if (results is { }) { @@ -416,7 +415,7 @@ public class UICatalog StartConfigWatcher (); } - while (RunUICatalogTopLevel () is { } scenario) + while (RunUICatalogRunnable () is { } scenario) { #if DEBUG_IDISPOSABLE VerifyObjectsWereDisposed (); @@ -495,7 +494,7 @@ public class UICatalog var maxScenarios = 5; - foreach (Scenario s in UICatalogTop.CachedScenarios!) + foreach (Scenario s in UICatalogRunnable.CachedScenarios!) { resultsList.Add (RunScenario (s, true)!); maxScenarios--; @@ -654,7 +653,6 @@ public class UICatalog if (!View.EnableDebugIDisposableAsserts) { View.Instances.Clear (); - SessionToken.Instances.Clear (); return; } @@ -668,16 +666,6 @@ public class UICatalog } View.Instances.Clear (); - - // Validate there are no outstanding Application sessions - // after a scenario was selected to run. This proves the main UI Catalog - // 'app' closed cleanly. - foreach (SessionToken? inst in SessionToken.Instances) - { - Debug.Assert (inst.WasDisposed); - } - - SessionToken.Instances.Clear (); #endif } } diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogRunnable.cs similarity index 96% rename from Examples/UICatalog/UICatalogTop.cs rename to Examples/UICatalog/UICatalogRunnable.cs index 0b46293bc..a163edab6 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -14,7 +14,7 @@ namespace UICatalog; /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on /// the command line) and each time a Scenario ends. /// -public class UICatalogTop : Toplevel +public class UICatalogRunnable : Runnable { // When a scenario is run, the main app is killed. The static // members are cached so that when the scenario exits the @@ -23,12 +23,12 @@ public class UICatalogTop : Toplevel // Note, we used to pass this to scenarios that run, but it just added complexity // So that was removed. But we still have this here to demonstrate how changing // the scheme works. - public static string? CachedTopLevelScheme { get; set; } + public static string? CachedRunnableScheme { get; set; } // Diagnostics private static ViewDiagnosticFlags _diagnosticFlags; - public UICatalogTop () + public UICatalogRunnable () { _diagnosticFlags = Diagnostics; @@ -39,8 +39,8 @@ public class UICatalogTop : Toplevel Add (_menuBar, _categoryList, _scenarioList, _statusBar); - Loaded += LoadedHandler; - Unloaded += UnloadedHandler; + IsModalChanged += IsModalChangedHandler; + IsRunningChanged += IsRunningChangedHandler; // Restore previous selections if (_categoryList.Source?.Count > 0) { @@ -50,15 +50,20 @@ public class UICatalogTop : Toplevel } _scenarioList.SelectedRow = _cachedScenarioIndex; - SchemeName = CachedTopLevelScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); + SchemeName = CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); ConfigurationManager.Applied += ConfigAppliedHandler; } private static bool _isFirstRunning = true; - private void LoadedHandler (object? sender, EventArgs? args) + private void IsModalChangedHandler (object? sender, EventArgs args) { + if (!args.Value) + { + return; + } + if (_disableMouseCb is { }) { _disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; @@ -85,15 +90,18 @@ public class UICatalogTop : Toplevel _statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; }; } - Loaded -= LoadedHandler; + IsModalChanged -= IsModalChangedHandler; _categoryList!.EnsureSelectedItemVisible (); _scenarioList.EnsureSelectedCellIsVisible (); } - private void UnloadedHandler (object? sender, EventArgs? args) + private void IsRunningChangedHandler (object? sender, EventArgs args) { - ConfigurationManager.Applied -= ConfigAppliedHandler; - Unloaded -= UnloadedHandler; + if (!args.Value) + { + ConfigurationManager.Applied -= ConfigAppliedHandler; + IsRunningChanged -= IsRunningChangedHandler; + } } #region MenuBar @@ -240,14 +248,14 @@ public class UICatalogTop : Toplevel { return; } - CachedTopLevelScheme = SchemeManager.GetSchemesForCurrentTheme ()!.Keys.ToArray () [(int)args.Value]; - SchemeName = CachedTopLevelScheme; + CachedRunnableScheme = SchemeManager.GetSchemesForCurrentTheme ()!.Keys.ToArray () [(int)args.Value]; + SchemeName = CachedRunnableScheme; SetNeedsDraw (); }; menuItem = new () { - Title = "Scheme for Toplevel", + Title = "Scheme for Runnable", SubMenu = new ( [ new () @@ -392,12 +400,12 @@ public class UICatalogTop : Toplevel _topSchemesSelector.Labels = SchemeManager.GetSchemeNames ().ToArray (); _topSchemesSelector.Value = selectedScheme; - if (CachedTopLevelScheme is null || !SchemeManager.GetSchemeNames ().Contains (CachedTopLevelScheme)) + if (CachedRunnableScheme is null || !SchemeManager.GetSchemeNames ().Contains (CachedRunnableScheme)) { - CachedTopLevelScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); + CachedRunnableScheme = SchemeManager.SchemesToSchemeName (Schemes.Base); } - int newSelectedItem = SchemeManager.GetSchemeNames ().IndexOf (CachedTopLevelScheme!); + int newSelectedItem = SchemeManager.GetSchemeNames ().IndexOf (CachedRunnableScheme!); // if the item is in bounds then select it if (newSelectedItem >= 0 && newSelectedItem < SchemeManager.GetSchemeNames ().Count) { @@ -686,7 +694,7 @@ public class UICatalogTop : Toplevel { UpdateThemesMenu (); - SchemeName = CachedTopLevelScheme; + SchemeName = CachedRunnableScheme; if (_shQuit is { }) { @@ -701,7 +709,7 @@ public class UICatalogTop : Toplevel _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.TopRunnable?.SetNeedsDraw (); + Application.TopRunnableView?.SetNeedsDraw (); } private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigApplied (); } diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index c3c3cdf16..5262376ed 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -23,6 +23,7 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// public static IApplication Create () { + //Debug.Fail ("Application.Create() called"); ApplicationImpl.MarkInstanceBasedModelUsed (); return new ApplicationImpl (); @@ -48,9 +49,9 @@ public static partial class Application // Lifecycle (Init/Shutdown) internal set => ApplicationImpl.Instance.MainThreadId = value; } - /// + /// [Obsolete ("The legacy static Application object is going away.")] - public static void Shutdown () => ApplicationImpl.Instance.Shutdown (); + public static void Shutdown () => ApplicationImpl.Instance.Dispose (); /// [Obsolete ("The legacy static Application object is going away.")] diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 81cea2171..9864e61b4 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -42,28 +42,22 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// [Obsolete ("The legacy static Application object is going away.")] - public static SessionToken Begin (Toplevel toplevel) => ApplicationImpl.Instance.Begin (toplevel); + public static SessionToken Begin (IRunnable runnable) => ApplicationImpl.Instance.Begin (runnable)!; /// [Obsolete ("The legacy static Application object is going away.")] public static bool PositionCursor () => ApplicationImpl.Instance.PositionCursor (); - /// + /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] [Obsolete ("The legacy static Application object is going away.")] - public static Toplevel Run (Func? errorHandler = null, string? driverName = null) => ApplicationImpl.Instance.Run (errorHandler, driverName); + public static IApplication Run (Func? errorHandler = null, string? driverName = null) + where TRunnable : IRunnable, new() => ApplicationImpl.Instance.Run (errorHandler, driverName); - /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] + /// [Obsolete ("The legacy static Application object is going away.")] - public static TView Run (Func? errorHandler = null, string? driverName = null) - where TView : Toplevel, new() => ApplicationImpl.Instance.Run (errorHandler, driverName); - - /// - [Obsolete ("The legacy static Application object is going away.")] - public static void Run (Toplevel view, Func? errorHandler = null) => ApplicationImpl.Instance.Run (view, errorHandler); + public static void Run (IRunnable runnable, Func? errorHandler = null) => ApplicationImpl.Instance.Run (runnable, errorHandler); /// [Obsolete ("The legacy static Application object is going away.")] @@ -76,7 +70,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// /// [Obsolete ("The legacy static Application object is going away.")] - public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; + public static ITimedEvents? TimedEvents => ApplicationImpl.Instance.TimedEvents; /// [Obsolete ("The legacy static Application object is going away.")] @@ -98,11 +92,11 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E set => ApplicationImpl.Instance.StopAfterFirstIteration = value; } - /// + /// [Obsolete ("The legacy static Application object is going away.")] - public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top); + public static void RequestStop (IRunnable? runnable = null) => ApplicationImpl.Instance.RequestStop (runnable); - /// + /// [Obsolete ("The legacy static Application object is going away.")] public static void End (SessionToken sessionToken) => ApplicationImpl.Instance.End (sessionToken); @@ -124,7 +118,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// [Obsolete ("The legacy static Application object is going away.")] - public static event EventHandler? SessionEnded + public static event EventHandler? SessionEnded { add => ApplicationImpl.Instance.SessionEnded += value; remove => ApplicationImpl.Instance.SessionEnded -= value; diff --git a/Terminal.Gui/App/Application.Screen.cs b/Terminal.Gui/App/Application.Screen.cs index 195c33ea6..6aeadb0b9 100644 --- a/Terminal.Gui/App/Application.Screen.cs +++ b/Terminal.Gui/App/Application.Screen.cs @@ -12,14 +12,6 @@ public static partial class Application // Screen related stuff; intended to hid set => ApplicationImpl.Instance.Screen = value; } - /// - [Obsolete ("The legacy static Application object is going away.")] - public static event EventHandler>? ScreenChanged - { - add => ApplicationImpl.Instance.ScreenChanged += value; - remove => ApplicationImpl.Instance.ScreenChanged -= value; - } - /// [Obsolete ("The legacy static Application object is going away.")] diff --git a/Terminal.Gui/App/Application.TopRunnable.cs b/Terminal.Gui/App/Application.TopRunnable.cs index 85a25cd06..be0b83da7 100644 --- a/Terminal.Gui/App/Application.TopRunnable.cs +++ b/Terminal.Gui/App/Application.TopRunnable.cs @@ -4,15 +4,13 @@ namespace Terminal.Gui.App; public static partial class Application // TopRunnable handling { - /// - [Obsolete ("The legacy static Application object is going away.")] public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; - - /// The that is on the top of the . + /// The that is on the top of the . /// The top runnable. [Obsolete ("The legacy static Application object is going away.")] - public static Toplevel? TopRunnable - { - get => ApplicationImpl.Instance.TopRunnable; - internal set => ApplicationImpl.Instance.TopRunnable = value; - } + public static View? TopRunnableView => ApplicationImpl.Instance.TopRunnableView; + + /// The that is on the top of the . + /// The top runnable. + [Obsolete ("The legacy static Application object is going away.")] + public static IRunnable? TopRunnable => ApplicationImpl.Instance.TopRunnable; } diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index e0a7390b7..7cb701605 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -1,12 +1,11 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; namespace Terminal.Gui.App; public partial class ApplicationImpl { - /// + /// public int? MainThreadId { get; set; } /// @@ -97,23 +96,75 @@ public partial class ApplicationImpl SynchronizationContext.SetSynchronizationContext (new ()); MainThreadId = Thread.CurrentThread.ManagedThreadId; + _result = null; + return this; } - /// Shutdown an application initialized with . - public object? Shutdown () - { - // Extract result from framework-owned runnable before disposal - object? result = null; - IRunnable? runnableToDispose = FrameworkOwnedRunnable; + #region IDisposable Implementation - if (runnableToDispose is { }) + private bool _disposed; + + /// + /// Disposes the application instance and releases all resources. + /// + /// + /// + /// This method implements the pattern and performs the same cleanup + /// as , but without returning a result. + /// + /// + /// After calling , use or + /// to retrieve the result from the last run session. + /// + /// + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + /// + /// Disposes the application instance and releases all resources. + /// + /// + /// if called from ; + /// if called from finalizer. + /// + protected virtual void Dispose (bool disposing) + { + if (_disposed) { - // Extract the result using reflection to get the Result property value - PropertyInfo? resultProperty = runnableToDispose.GetType ().GetProperty ("Result"); - result = resultProperty?.GetValue (runnableToDispose); + return; } + if (disposing) + { + // Dispose managed resources + DisposeCore (); + } + + // For the singleton instance (legacy Application.Init/Shutdown pattern), + // we need to allow re-initialization after disposal. This enables: + // Application.Init() -> Application.Shutdown() -> Application.Init() + // For modern instance-based usage, this doesn't matter as new instances are created. + if (this == _instance) + { + // Reset disposed flag to allow re-initialization + _disposed = false; + } + else + { + // For instance-based usage, mark as disposed + _disposed = true; + } + } + + /// + /// Core disposal logic - same as Shutdown() but without returning result. + /// + private void DisposeCore () + { // Stop the coordinator if running Coordinator?.Stop (); @@ -135,17 +186,6 @@ public partial class ApplicationImpl } #endif - // Dispose the framework-owned runnable if it exists - if (runnableToDispose is { }) - { - if (runnableToDispose is IDisposable disposable) - { - disposable.Dispose (); - } - - FrameworkOwnedRunnable = null; - } - // Clean up all application state (including sync context) // ResetState handles the case where Initialized is false ResetState (); @@ -162,10 +202,26 @@ public partial class ApplicationImpl // Clear the event to prevent memory leaks InitializedChanged = null; + } + + #endregion IDisposable Implementation + + /// Shutdown an application initialized with . + [Obsolete ("Use Dispose() or a using statement instead. This method will be removed in a future version.")] + public object? Shutdown () + { + // Shutdown is now just a wrapper around Dispose that returns the result + object? result = GetResult (); + Dispose (); return result; } + private object? _result; + + /// + public object? GetResult () => _result; + /// public void ResetState (bool ignoreDisposed = false) { @@ -176,10 +232,13 @@ public partial class ApplicationImpl // === 0. Stop all timers === TimedEvents?.StopAll (); - // === 1. Stop all running toplevels === - foreach (Toplevel t in SessionStack) + // === 1. Stop all running runnables === + foreach (SessionToken token in SessionStack!.Reverse ()) { - t.Running = false; + if (token.Runnable is { }) + { + End (token); + } } // === 2. Close and dispose popover === @@ -194,29 +253,18 @@ public partial class ApplicationImpl Popover?.Dispose (); Popover = null; - // === 3. Clean up toplevels === - SessionStack.Clear (); - RunnableSessionStack?.Clear (); + // === 3. Clean up runnables === + SessionStack?.Clear (); #if DEBUG_IDISPOSABLE // Don't dispose the TopRunnable. It's up to caller dispose it - if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && TopRunnable is { }) + if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && TopRunnableView is { }) { - Debug.Assert (TopRunnable.WasDisposed, $"Title = {TopRunnable.Title}, Id = {TopRunnable.Id}"); - - // If End wasn't called _CachedSessionTokenToplevel may be null - if (CachedSessionTokenToplevel is { }) - { - Debug.Assert (CachedSessionTokenToplevel.WasDisposed); - Debug.Assert (CachedSessionTokenToplevel == TopRunnable); - } + Debug.Assert (TopRunnableView.WasDisposed, $"Title = {TopRunnableView.Title}, Id = {TopRunnableView.Id}"); } #endif - TopRunnable = null; - CachedSessionTokenToplevel = null; - // === 4. Clean up driver === if (Driver is { }) { diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index e790e3ca0..45ad59fab 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -1,312 +1,44 @@ -using System.Diagnostics; +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; public partial class ApplicationImpl { - #region Begin->Run->Stop->End + // Lock object to protect session stack operations and cached state updates + private readonly object _sessionStackLock = new (); + + #region Session State - Stack and TopRunnable + + /// + public ConcurrentStack? SessionStack { get; } = new (); + + /// + public IRunnable? TopRunnable { get; private set; } + + /// + public View? TopRunnableView => TopRunnable as View; - // TODO: This API is not used anywhere; it can be deleted /// public event EventHandler? SessionBegun; - // TODO: This API is not used anywhere; it can be deleted /// - public event EventHandler? SessionEnded; + public event EventHandler? SessionEnded; - /// - public SessionToken Begin (Toplevel toplevel) - { - ArgumentNullException.ThrowIfNull (toplevel); + #endregion Session State - Stack and TopRunnable - // Ensure the mouse is ungrabbed. - if (Mouse.MouseGrabView is { }) - { - Mouse.UngrabMouse (); - } - - var rs = new SessionToken (toplevel); - -#if DEBUG_IDISPOSABLE - if (View.EnableDebugIDisposableAsserts && TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable)) - { - // This assertion confirm if the TopRunnable was already disposed - Debug.Assert (TopRunnable.WasDisposed); - Debug.Assert (TopRunnable == CachedSessionTokenToplevel); - } -#endif - - lock (SessionStack) - { - if (TopRunnable is { } && toplevel != TopRunnable && !SessionStack.Contains (TopRunnable)) - { - // If TopRunnable was already disposed and isn't on the Toplevels Stack, - // clean it up here if is the same as _CachedSessionTokenToplevel - if (TopRunnable == CachedSessionTokenToplevel) - { - TopRunnable = null; - } - else - { - // Probably this will never hit - throw new ObjectDisposedException (TopRunnable.GetType ().FullName); - } - } - - // BUGBUG: We should not depend on `Id` internally. - // BUGBUG: It is super unclear what this code does anyway. - if (string.IsNullOrEmpty (toplevel.Id)) - { - var count = 1; - var id = (SessionStack.Count + count).ToString (); - - while (SessionStack.Count > 0 && SessionStack.FirstOrDefault (x => x.Id == id) is { }) - { - count++; - id = (SessionStack.Count + count).ToString (); - } - - toplevel.Id = (SessionStack.Count + count).ToString (); - - SessionStack.Push (toplevel); - } - else - { - Toplevel? dup = SessionStack.FirstOrDefault (x => x.Id == toplevel.Id); - - if (dup is null) - { - SessionStack.Push (toplevel); - } - } - } - - if (TopRunnable is null) - { - toplevel.App = this; - TopRunnable = toplevel; - } - - if ((TopRunnable?.Modal == false && toplevel.Modal) - || (TopRunnable?.Modal == false && !toplevel.Modal) - || (TopRunnable?.Modal == true && toplevel.Modal)) - { - if (toplevel.Visible) - { - if (TopRunnable is { HasFocus: true }) - { - TopRunnable.HasFocus = false; - } - - // Force leave events for any entered views in the old TopRunnable - if (Mouse.LastMousePosition is { }) - { - Mouse.RaiseMouseEnterLeaveEvents (Mouse.LastMousePosition!.Value, new ()); - } - - TopRunnable?.OnDeactivate (toplevel); - Toplevel previousTop = TopRunnable!; - - TopRunnable = toplevel; - TopRunnable.App = this; - TopRunnable.OnActivate (previousTop); - } - } - - // View implements ISupportInitializeNotification which is derived from ISupportInitialize - if (!toplevel.IsInitialized) - { - toplevel.BeginInit (); - toplevel.EndInit (); // Calls Layout - } - - // Try to set initial focus to any TabStop - if (!toplevel.HasFocus) - { - toplevel.SetFocus (); - } - - toplevel.OnLoaded (); - - LayoutAndDraw (true); - - if (PositionCursor ()) - { - Driver?.UpdateCursor (); - } - - SessionBegun?.Invoke (this, new (rs)); - - return rs; - } + #region Main Loop Iteration /// public bool StopAfterFirstIteration { get; set; } - /// - public void RaiseIteration () - { - Iteration?.Invoke (null, new (this)); - } - /// public event EventHandler>? Iteration; /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, string? driverName = null) => Run (errorHandler, driverName); + public void RaiseIteration () { Iteration?.Invoke (null, new (this)); } - /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public TView Run (Func? errorHandler = null, string? driverName = null) - where TView : Toplevel, new () - { - if (!Initialized) - { - // Init() has NOT been called. Auto-initialize as per interface contract. - Init (driverName); - } - - TView top = new (); - Run (top, errorHandler); - - return top; - } - - /// - public void Run (Toplevel view, Func? errorHandler = null) - { - Logging.Information ($"Run '{view}'"); - ArgumentNullException.ThrowIfNull (view); - - if (!Initialized) - { - throw new NotInitializedException (nameof (Run)); - } - - if (Driver == null) - { - throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); - } - - TopRunnable = view; - - SessionToken rs = Begin (view); - - TopRunnable.Running = true; - - var firstIteration = true; - - while (SessionStack.TryPeek (out Toplevel? found) && found == view && view.Running) - { - if (Coordinator is null) - { - throw new ($"{nameof (IMainLoopCoordinator)} inexplicably became null during Run"); - } - - Coordinator.RunIteration (); - - if (StopAfterFirstIteration && firstIteration) - { - Logging.Information ("Run - Stopping after first iteration as requested"); - RequestStop ((Toplevel?)view); - } - - firstIteration = false; - } - - Logging.Information ("Run - Calling End"); - End (rs); - } - - /// - public void End (SessionToken sessionToken) - { - ArgumentNullException.ThrowIfNull (sessionToken); - - if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) - { - ApplicationPopover.HideWithQuitCommand (visiblePopover); - } - - sessionToken.Toplevel?.OnUnloaded (); - - // End the Session - // First, take it off the Toplevel Stack - if (SessionStack.TryPop (out Toplevel? topOfStack)) - { - if (topOfStack != sessionToken.Toplevel) - { - // If the top of the stack is not the SessionToken.Toplevel then - // this call to End is not balanced with the call to Begin that started the Session - throw new ArgumentException ("End must be balanced with calls to Begin"); - } - } - - // Notify that it is closing - sessionToken.Toplevel?.OnClosed (sessionToken.Toplevel); - - if (SessionStack.TryPeek (out Toplevel? newTop)) - { - newTop.App = this; - TopRunnable = newTop; - TopRunnable?.SetNeedsDraw (); - } - - if (sessionToken.Toplevel is { HasFocus: true }) - { - sessionToken.Toplevel.HasFocus = false; - } - - if (TopRunnable is { HasFocus: false }) - { - TopRunnable.SetFocus (); - } - - CachedSessionTokenToplevel = sessionToken.Toplevel; - - sessionToken.Toplevel = null; - sessionToken.Dispose (); - - // BUGBUG: Why layout and draw here? This causes the screen to be cleared! - //LayoutAndDraw (true); - - // TODO: This API is not used (correctly) anywhere; it can be deleted - // TODO: Instead, callers should use the new equivalent of Toplevel.Ready - // TODO: which will be IsRunningChanged with newIsRunning == true - SessionEnded?.Invoke (this, new (CachedSessionTokenToplevel)); - } - - /// - public void RequestStop () { RequestStop ((Toplevel?)null); } - - /// - public void RequestStop (Toplevel? top) - { - Logging.Trace ($"TopRunnable: '{(top is { } ? top : "null")}'"); - - top ??= TopRunnable; - - if (top == null) - { - return; - } - - ToplevelClosingEventArgs ev = new (top); - top.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - top.Running = false; - } - - #endregion Begin->Run->Stop->End + #endregion Main Loop Iteration #region Timeouts and Invoke @@ -325,7 +57,7 @@ public partial class ApplicationImpl public void Invoke (Action? action) { // If we are already on the main UI thread - if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (this); @@ -347,7 +79,7 @@ public partial class ApplicationImpl public void Invoke (Action action) { // If we are already on the main UI thread - if (TopRunnable is { Running: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) + if (TopRunnableView is IRunnable { IsRunning: true } && MainThreadId == Thread.CurrentThread.ManagedThreadId) { action?.Invoke (); @@ -367,126 +99,148 @@ public partial class ApplicationImpl #endregion Timeouts and Invoke - #region IRunnable Support + #region Session Lifecycle - Begin /// - public RunnableSessionToken Begin (IRunnable runnable) + public SessionToken? Begin (IRunnable runnable) { ArgumentNullException.ThrowIfNull (runnable); - // Ensure the mouse is ungrabbed - if (Mouse.MouseGrabView is { }) + if (runnable.IsRunning) { - Mouse.UngrabMouse (); + throw new ArgumentException (@"The runnable is already running.", nameof (runnable)); } // Create session token - RunnableSessionToken token = new (runnable); + SessionToken token = new (runnable); - // Set the App property if the runnable is a View (needed for IsRunning/IsModal checks) - if (runnable is View runnableView) - { - runnableView.App = this; - } - - // Get old IsRunning and IsModal values BEFORE any stack changes + // Get old IsRunning value BEFORE any stack changes (safe - cached value) bool oldIsRunning = runnable.IsRunning; - bool oldIsModalValue = runnable.IsModal; - // Raise IsRunningChanging (false -> true) - can be canceled + // Raise IsRunningChanging OUTSIDE lock (false -> true) - can be canceled if (runnable.RaiseIsRunningChanging (oldIsRunning, true)) { // Starting was canceled - return token; + return null; } - // Push token onto RunnableSessionStack (IsRunning becomes true) - RunnableSessionStack?.Push (token); + // Set the application reference in the runnable + runnable.SetApp (this); + + // Ensure the mouse is ungrabbed + Mouse.UngrabMouse (); - // Update TopRunnable to the new top of stack IRunnable? previousTop = null; - // In Phase 1, Toplevel doesn't implement IRunnable yet - // In Phase 2, it will, and this will work properly - if (TopRunnable is IRunnable r) + // CRITICAL SECTION - Atomic stack + cached state update + lock (_sessionStackLock) { - previousTop = r; + // Get the previous top BEFORE pushing new token + if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { }) + { + previousTop = previousToken.Runnable; + } + + if (previousTop == runnable) + { + throw new ArgumentOutOfRangeException (nameof (runnable), runnable, @"Attempt to Run the runnable that's already the top runnable."); + } + + // Push token onto SessionStack + SessionStack?.Push (token); + + TopRunnable = runnable; + + // Update cached state atomically - IsRunning and IsModal are now consistent + SessionBegun?.Invoke (this, new (token)); + runnable.SetIsRunning (true); + runnable.SetIsModal (true); + + // Previous top is no longer modal + if (previousTop != null) + { + previousTop.SetIsModal (false); + } } - // Set TopRunnable (handles both Toplevel and IRunnable) - if (runnable is Toplevel tl) - { - TopRunnable = tl; - } - else if (runnable is View v) - { - // For now, we can't set a non-Toplevel View as TopRunnable - // This is a limitation of the current architecture - // In Phase 2, we'll make TopRunnable an IRunnable property - Logging.Warning ($"WIP on Issue #4148 - Runnable '{runnable}' is a View but not a Toplevel; cannot set as TopRunnable"); - } + // END CRITICAL SECTION - IsRunning/IsModal now thread-safe - // Raise IsRunningChanged (now true) - runnable.RaiseIsRunningChangedEvent (true); - - // If there was a previous top, it's no longer modal + // Fire events AFTER lock released (avoid deadlocks in event handlers) if (previousTop != null) { - // Get old IsModal value (should be true before becoming non-modal) - bool oldIsModal = previousTop.IsModal; - - // Raise IsModalChanging (true -> false) - previousTop.RaiseIsModalChanging (oldIsModal, false); - - // IsModal is now false (derived property) previousTop.RaiseIsModalChangedEvent (false); } - // New runnable becomes modal - // Raise IsModalChanging (false -> true) using the old value we captured earlier - runnable.RaiseIsModalChanging (oldIsModalValue, true); - - // IsModal is now true (derived property) + runnable.RaiseIsRunningChangedEvent (true); runnable.RaiseIsModalChangedEvent (true); - // Initialize if needed - if (runnable is View view && !view.IsInitialized) - { - view.BeginInit (); - view.EndInit (); - - // Initialized event is raised by View.EndInit() - } - - // Initial Layout and draw - LayoutAndDraw (true); - - // Set focus - if (runnable is View viewToFocus && !viewToFocus.HasFocus) - { - viewToFocus.SetFocus (); - } - - if (PositionCursor ()) - { - Driver?.UpdateCursor (); - } + LayoutAndDraw (); return token; } + #endregion Session Lifecycle - Begin + + #region Session Lifecycle - Run + /// - public void Run (IRunnable runnable, Func? errorHandler = null) + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public IApplication Run (Func? errorHandler = null, string? driverName = null) + where TRunnable : IRunnable, new() + { + if (!Initialized) + { + // Init() has NOT been called. Auto-initialize as per interface contract. + Init (driverName); + } + + if (Driver is null) + { + throw new InvalidOperationException (@"Driver is null after Init."); + } + + TRunnable runnable = new (); + object? result = Run (runnable, errorHandler); + + // We created the runnable, so dispose it if it's disposable + if (runnable is IDisposable disposable) + { + disposable.Dispose (); + } + + return this; + } + + /// + public object? Run (IRunnable runnable, Func? errorHandler = null) { ArgumentNullException.ThrowIfNull (runnable); if (!Initialized) { - throw new NotInitializedException (nameof (Run)); + throw new NotInitializedException (@"Init must be called before Run."); } // Begin the session (adds to stack, raises IsRunningChanging/IsRunningChanged) - RunnableSessionToken token = Begin (runnable); + SessionToken? token; + + if (runnable.IsRunning) + { + // Find it on the stack + token = SessionStack?.FirstOrDefault (st => st.Runnable == runnable); + } + else + { + token = Begin (runnable); + } + + if (token is null) + { + Logging.Trace (@"Run - Begin session failed or was cancelled."); + + return null; + } try { @@ -498,33 +252,19 @@ public partial class ApplicationImpl // End the session (raises IsRunningChanging/IsRunningChanged, pops from stack) End (token); } - } - /// - public IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new () - { - if (!Initialized) - { - throw new NotInitializedException (nameof (Run)); - } - - TRunnable runnable = new (); - - // Store the runnable for automatic disposal by Shutdown - FrameworkOwnedRunnable = runnable; - - Run (runnable, errorHandler); - - return this; + return token.Result; } private void RunLoop (IRunnable runnable, Func? errorHandler) { + runnable.StopRequested = false; + // Main loop - blocks until RequestStop() is called - // Note: IsRunning is a derived property (stack.Contains), so we check it each iteration + // Note: IsRunning is now a cached property, safe to check each iteration var firstIteration = true; - while (runnable.IsRunning) + while (runnable is { StopRequested: false, IsRunning: true }) { if (Coordinator is null) { @@ -554,8 +294,12 @@ public partial class ApplicationImpl } } + #endregion Session Lifecycle - Run + + #region Session Lifecycle - End + /// - public void End (RunnableSessionToken token) + public void End (SessionToken token) { ArgumentNullException.ThrowIfNull (token); @@ -564,76 +308,84 @@ public partial class ApplicationImpl return; // Already ended } + // TODO: Move Poppover to utilize IRunnable arch; Get all refs to anyting + // TODO: View-related out of ApplicationImpl. + if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) + { + ApplicationPopover.HideWithQuitCommand (visiblePopover); + } + IRunnable runnable = token.Runnable; - // Get old IsRunning value (should be true before stopping) + // Get old IsRunning value (safe - cached value) bool oldIsRunning = runnable.IsRunning; - // Raise IsRunningChanging (true -> false) - can be canceled + // Raise IsRunningChanging OUTSIDE lock (true -> false) - can be canceled // This is where Result should be extracted! if (runnable.RaiseIsRunningChanging (oldIsRunning, false)) { - // Stopping was canceled + // Stopping was canceled - do not proceed with End return; } - // Current runnable is no longer modal - // Get old IsModal value (should be true before becoming non-modal) - bool oldIsModal = runnable.IsModal; + bool wasModal = runnable.IsModal; + IRunnable? previousRunnable = null; - // Raise IsModalChanging (true -> false) - runnable.RaiseIsModalChanging (oldIsModal, false); - - // IsModal is now false (will be false after pop) - runnable.RaiseIsModalChangedEvent (false); - - // Pop token from RunnableSessionStack (IsRunning becomes false) - if (RunnableSessionStack?.TryPop (out RunnableSessionToken? popped) == true && popped == token) + // CRITICAL SECTION - Atomic stack + cached state update + lock (_sessionStackLock) { - // Restore previous top runnable - if (RunnableSessionStack?.TryPeek (out RunnableSessionToken? previousToken) == true && previousToken?.Runnable is { }) + // Pop token from SessionStack + if (wasModal && SessionStack?.TryPop (out SessionToken? popped) == true && popped == token) { - IRunnable? previousRunnable = previousToken.Runnable; - - // Update TopRunnable if it's a Toplevel - if (previousRunnable is Toplevel tl) + // Restore previous top runnable + if (SessionStack?.TryPeek (out SessionToken? previousToken) == true && previousToken?.Runnable is { }) { - TopRunnable = tl; - } + previousRunnable = previousToken.Runnable; - // Previous runnable becomes modal again - // Get old IsModal value (should be false before becoming modal again) - bool oldIsModalValue = previousRunnable.IsModal; - - // Raise IsModalChanging (false -> true) - previousRunnable.RaiseIsModalChanging (oldIsModalValue, true); - - // IsModal is now true (derived property) - previousRunnable.RaiseIsModalChangedEvent (true); - } - else - { - // No more runnables, clear TopRunnable - if (TopRunnable is IRunnable) - { - TopRunnable = null; + // Previous runnable becomes modal again + previousRunnable.SetIsModal (true); } } + + // Update cached state atomically - IsRunning and IsModal are now consistent + runnable.SetIsRunning (false); + runnable.SetIsModal (false); + } + + // END CRITICAL SECTION - IsRunning/IsModal now thread-safe + + // Fire events AFTER lock released + if (wasModal) + { + runnable.RaiseIsModalChangedEvent (false); + } + + TopRunnable = null; + + if (previousRunnable != null) + { + TopRunnable = previousRunnable; + previousRunnable.RaiseIsModalChangedEvent (true); } - // Raise IsRunningChanged (now false) runnable.RaiseIsRunningChangedEvent (false); - // Set focus to new TopRunnable if exists - if (TopRunnable is View viewToFocus && !viewToFocus.HasFocus) - { - viewToFocus.SetFocus (); - } + token.Result = runnable.Result; - // Clear the token + _result = token.Result; + + // Clear the Runnable from the token token.Runnable = null; + SessionEnded?.Invoke (this, new (token)); } + #endregion Session Lifecycle - End + + #region Session Lifecycle - RequestStop + + /// + public void RequestStop () { RequestStop (null); } + /// public void RequestStop (IRunnable? runnable) { @@ -641,7 +393,7 @@ public partial class ApplicationImpl if (runnable is null) { // Try to get from TopRunnable - if (TopRunnable is IRunnable r) + if (TopRunnableView is IRunnable r) { runnable = r; } @@ -651,15 +403,11 @@ public partial class ApplicationImpl } } - // For Toplevel, use the existing mechanism - if (runnable is Toplevel toplevel) - { - RequestStop (toplevel); - } + runnable.StopRequested = true; // Note: The End() method will be called from the finally block in Run() // and that's where IsRunningChanging/IsRunningChanged will be raised } - #endregion IRunnable Support + #endregion Session Lifecycle - RequestStop } diff --git a/Terminal.Gui/App/ApplicationImpl.Screen.cs b/Terminal.Gui/App/ApplicationImpl.Screen.cs index b5a930794..07be5ff10 100644 --- a/Terminal.Gui/App/ApplicationImpl.Screen.cs +++ b/Terminal.Gui/App/ApplicationImpl.Screen.cs @@ -126,9 +126,8 @@ public partial class ApplicationImpl } /// - /// INTERNAL: Called when the application's size has changed. Sets the size of all s and fires - /// the - /// event. + /// INTERNAL: Called when the application's screen has changed. + /// Raises the event. /// /// The new screen size and position. private void RaiseScreenChangedEvent (Rectangle screen) @@ -137,13 +136,13 @@ public partial class ApplicationImpl ScreenChanged?.Invoke (this, new (screen)); - foreach (Toplevel t in SessionStack) + foreach (SessionToken t in SessionStack!) { - t.OnSizeChanging (new (screen.Size)); - t.SetNeedsLayout (); + if (t.Runnable is View runnableView) + { + runnableView.SetNeedsLayout (); + } } - - LayoutAndDraw (true); } private void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { RaiseScreenChangedEvent (new (new (0, 0), e.Size!.Value)); } @@ -151,7 +150,7 @@ public partial class ApplicationImpl /// public void LayoutAndDraw (bool forceRedraw = false) { - List tops = [.. SessionStack]; + List tops = [.. SessionStack!.Select(r => r.Runnable! as View)!]; if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) { @@ -160,7 +159,7 @@ public partial class ApplicationImpl tops.Insert (0, visiblePopover); } - bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size); + bool neededLayout = View.Layout (tops.ToArray ().Reverse ()!, Screen.Size); if (ClearScreenNextIteration) { @@ -176,7 +175,8 @@ public partial class ApplicationImpl if (Driver is { }) { Driver.Clip = new (Screen); - View.Draw (tops, neededLayout || forceRedraw); + + View.Draw (views: tops!, neededLayout || forceRedraw); Driver.Clip = new (Screen); Driver?.Refresh (); } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 6e968d0d3..b92f388a2 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -27,17 +27,10 @@ public partial class ApplicationImpl : IApplication private string? _driverName; - #region Clipboard - - /// - public IClipboard? Clipboard => Driver?.Clipboard; - - #endregion Clipboard - /// public new string ToString () => Driver?.ToString () ?? string.Empty; - #region Singleton + #region Singleton - Legacy Static Support /// /// Lock object for synchronizing access to ModelUsage and _instance. @@ -166,31 +159,20 @@ public partial class ApplicationImpl : IApplication ResetModelUsageTracking (); } - #endregion Singleton + #endregion Singleton - Legacy Static Support - #region Input + #region Screen and Driver - private IMouse? _mouse; + /// + public IClipboard? Clipboard => Driver?.Clipboard; - /// - /// Handles mouse event state and processing. - /// - public IMouse Mouse - { - get - { - _mouse ??= new MouseImpl { App = this }; + #endregion Screen and Driver - return _mouse; - } - set => _mouse = value ?? throw new ArgumentNullException (nameof (value)); - } + #region Keyboard private IKeyboard? _keyboard; - /// - /// Handles keyboard input and key bindings at the Application level - /// + /// public IKeyboard Keyboard { get @@ -202,24 +184,28 @@ public partial class ApplicationImpl : IApplication set => _keyboard = value ?? throw new ArgumentNullException (nameof (value)); } - #endregion Input + #endregion Keyboard - #region View Management + #region Mouse - private ApplicationPopover? _popover; + private IMouse? _mouse; /// - public ApplicationPopover? Popover + public IMouse Mouse { get { - _popover ??= new () { App = this }; + _mouse ??= new MouseImpl { App = this }; - return _popover; + return _mouse; } - set => _popover = value; + set => _mouse = value ?? throw new ArgumentNullException (nameof (value)); } + #endregion Mouse + + #region Navigation and Popover + private ApplicationNavigation? _navigation; /// @@ -234,34 +220,19 @@ public partial class ApplicationImpl : IApplication set => _navigation = value ?? throw new ArgumentNullException (nameof (value)); } - private Toplevel? _topRunnable; + private ApplicationPopover? _popover; /// - public Toplevel? TopRunnable + public ApplicationPopover? Popover { - get => _topRunnable; - set + get { - _topRunnable = value; + _popover ??= new () { App = this }; - if (_topRunnable is { }) - { - _topRunnable.App = this; - } + return _popover; } + set => _popover = value; } - /// - public ConcurrentStack SessionStack { get; } = new (); - - /// - public Toplevel? CachedSessionTokenToplevel { get; set; } - - /// - public ConcurrentStack? RunnableSessionStack { get; } = new (); - - /// - public IRunnable? FrameworkOwnedRunnable { get; set; } - - #endregion View Management + #endregion Navigation and Popover } diff --git a/Terminal.Gui/App/ApplicationNavigation.cs b/Terminal.Gui/App/ApplicationNavigation.cs index 6012d4629..871bd3691 100644 --- a/Terminal.Gui/App/ApplicationNavigation.cs +++ b/Terminal.Gui/App/ApplicationNavigation.cs @@ -113,6 +113,6 @@ public class ApplicationNavigation { return visiblePopover.AdvanceFocus (direction, behavior); } - return App?.TopRunnable is { } && App.TopRunnable.AdvanceFocus (direction, behavior); + return App?.TopRunnableView is { } && App.TopRunnableView.AdvanceFocus (direction, behavior); } } diff --git a/Terminal.Gui/App/ApplicationPopover.cs b/Terminal.Gui/App/ApplicationPopover.cs index 3a7aac879..a8d3ba00b 100644 --- a/Terminal.Gui/App/ApplicationPopover.cs +++ b/Terminal.Gui/App/ApplicationPopover.cs @@ -41,8 +41,7 @@ public sealed class ApplicationPopover : IDisposable { if (popover is { } && !IsRegistered (popover)) { - // When created, set IPopover.Toplevel to the current Application.TopRunnable - popover.Current ??= App?.TopRunnable; + popover.Current ??= App?.TopRunnableView as IRunnable; if (popover is View popoverView) { @@ -166,7 +165,7 @@ public sealed class ApplicationPopover : IDisposable { _activePopover = null; popoverView.Visible = false; - popoverView.App?.TopRunnable?.SetNeedsDraw (); + popoverView.App?.TopRunnableView?.SetNeedsDraw (); } } @@ -215,7 +214,7 @@ public sealed class ApplicationPopover : IDisposable { if (popover == activePopover || popover is not View popoverView - || (popover.Current is { } && popover.Current != App?.TopRunnable)) + || (popover.Current is { } && popover.Current != App?.TopRunnableView)) { continue; } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index a4a8c902e..4d0959a2f 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -7,37 +7,15 @@ namespace Terminal.Gui.App; /// Interface for instances that provide backing functionality to static /// gateway class . /// -public interface IApplication +/// +/// +/// Implements to support automatic resource cleanup via using statements. +/// Call or use a using statement to properly clean up resources. +/// +/// +public interface IApplication : IDisposable { - #region Keyboard - - /// - /// Handles keyboard input and key bindings at the Application level. - /// - /// - /// - /// Provides access to keyboard state, key bindings, and keyboard event handling. Set during . - /// - /// - IKeyboard Keyboard { get; set; } - - #endregion Keyboard - - #region Mouse - - /// - /// Handles mouse event state and processing. - /// - /// - /// - /// Provides access to mouse state, mouse grabbing, and mouse event handling. Set during . - /// - /// - IMouse Mouse { get; set; } - - #endregion Mouse - - #region Initialization and Shutdown + #region Lifecycle - App Initialization and Shutdown /// /// Gets or sets the managed thread ID of the application's main UI thread, which is set during @@ -48,7 +26,6 @@ public interface IApplication /// public int? MainThreadId { get; internal set; } - /// Initializes a new instance of Application. /// /// The short name (e.g. "dotnet", "windows", "unix", or "fake") of the @@ -56,22 +33,42 @@ public interface IApplication /// /// This instance for fluent API chaining. /// - /// Call this method once per instance (or after has been called). + /// Call this method once per instance (or after has been called). /// /// This function loads the right for the platform, creates a main loop coordinator, /// initializes keyboard and mouse handlers, and subscribes to driver events. /// /// - /// must be called when the application is closing (typically after - /// has returned) to ensure resources are cleaned up and terminal settings restored. + /// must be called when the application is closing (typically after + /// has returned) to ensure all resources are cleaned up (disposed) and + /// terminal settings are restored. /// /// - /// The function combines and - /// into a single call. An application can use - /// without explicitly calling . + /// Supports fluent API with automatic resource management: /// /// - /// Supports fluent API: Application.Create().Init().Run<MyView>().Shutdown() + /// Recommended pattern (using statement): + /// + /// using (var app = Application.Create().Init()) + /// { + /// app.Run<MyDialog>(); + /// var result = app.GetResult<MyResultType>(); + /// } // app.Dispose() called automatically + /// + /// + /// + /// Alternative pattern (manual disposal): + /// + /// var app = Application.Create().Init(); + /// app.Run<MyDialog>(); + /// var result = app.GetResult<MyResultType>(); + /// app.Dispose(); // Must call explicitly + /// + /// + /// + /// Note: Runnables created by are automatically disposed when + /// that method returns. Runnables passed to + /// must be disposed by the caller. /// /// [RequiresUnreferencedCode ("AOT")] @@ -79,7 +76,7 @@ public interface IApplication public IApplication Init (string? driverName = null); /// - /// This event is raised after the and methods have been called. + /// This event is raised after the and methods have been called. /// /// /// Intended to support unit tests that need to know when the application has been initialized. @@ -89,123 +86,165 @@ public interface IApplication /// Gets or sets whether the application has been initialized. bool Initialized { get; set; } - /// Shutdown an application initialized with . - /// - /// The result from the last call, or if none. - /// Automatically disposes any runnable created by . - /// - /// - /// - /// Shutdown must be called for every call to or - /// to ensure all resources are cleaned - /// up (Disposed) and terminal settings are restored. - /// - /// - /// When used in a fluent chain with , this method automatically - /// disposes the runnable instance and extracts its result for return. - /// - /// - /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType - /// - /// - public object? Shutdown (); - /// - /// Resets the state of this instance. + /// INTERNAL: Resets the state of this instance. Called by Dispose. /// /// If true, ignores disposed state checks during reset. /// /// /// Encapsulates all setting of initial state for Application; having this in a function like this ensures we /// don't make mistakes in guaranteeing that the state of this singleton is deterministic when - /// starts running and after returns. + /// starts running and after returns. /// /// /// IMPORTANT: Ensure all property/fields are reset here. See Init_ResetState_Resets_Properties unit test. /// /// - public void ResetState (bool ignoreDisposed = false); + internal void ResetState (bool ignoreDisposed = false); - #endregion Initialization and Shutdown + #endregion App Initialization and Shutdown - #region Begin->Run->Iteration->Stop->End + #region Session Management - Begin->Run->Iteration->Stop->End /// - /// Building block API: Creates a and prepares the provided for - /// execution. Not usually called directly by applications. Use - /// instead. + /// Gets the stack of all active runnable session tokens. + /// Sessions execute serially - the top of stack is the currently modal session. /// - /// - /// The that needs to be passed to the method upon - /// completion. - /// - /// The to prepare execution for. /// /// - /// This method prepares the provided for running. It adds this to the - /// list of s, lays out the SubViews, focuses the first element, and draws the - /// on the screen. This is usually followed by starting the main loop, and then the + /// Session tokens are pushed onto the stack when is called and + /// popped when + /// completes. The stack grows during nested modal calls and + /// shrinks as they complete. + /// + /// + /// Only the top session () has exclusive keyboard/mouse input ( + /// = true). + /// All other sessions on the stack continue to be laid out, drawn, and receive iteration events ( + /// = true), + /// but they don't receive user input. + /// + /// + /// Stack during nested modals: + /// + /// RunnableSessionStack (top to bottom): + /// - MessageBox (TopRunnable, IsModal=true, IsRunning=true, has input) + /// - FileDialog (IsModal=false, IsRunning=true, continues to update/draw) + /// - MainWindow (IsModal=false, IsRunning=true, continues to update/draw) + /// + /// + /// + ConcurrentStack? SessionStack { get; } + + /// + /// Raised when has been called and has created a new . + /// + /// + /// If is , callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public event EventHandler? SessionBegun; + + #region TopRunnable Properties + + /// Gets the Runnable that is on the top of the . + /// + /// + /// The top runnable in the session stack captures all mouse and keyboard input. + /// This is set by and cleared by . + /// + /// + IRunnable? TopRunnable { get; } + + /// Gets the View that is on the top of the . + /// + /// + /// This is a convenience property that casts to a . + /// + /// + View? TopRunnableView { get; } + + #endregion TopRunnable Properties + + /// + /// Building block API: Creates a and prepares the provided + /// for + /// execution. Not usually called directly by applications. Use + /// instead. + /// + /// The to prepare execution for. + /// + /// The that needs to be passed to the + /// method upon + /// completion. + /// + /// + /// + /// This method prepares the provided for running. It adds this to the + /// , lays out the SubViews, focuses the first element, and draws the + /// runnable on the screen. This is usually followed by starting the main loop, and then the /// method upon termination which will undo these changes. /// /// - /// Raises the event before returning. + /// Raises the , , + /// and events. /// /// - public SessionToken Begin (Toplevel toplevel); + /// The session token. if the operation was cancelled. + SessionToken? Begin (IRunnable runnable); /// - /// Runs a new Session creating a and calling . When the session is - /// stopped, will be called. + /// Runs a new Session with the provided runnable view. /// - /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). - /// - /// The driver name. If not specified the default driver for the platform will be used. Must be - /// if has already been called. - /// - /// The created . The caller is responsible for disposing this object. + /// The runnable to execute. + /// Optional handler for unhandled exceptions (resumes when returns true, rethrows when null). /// - /// Calling first is not needed as this function will initialize the application. /// - /// must be called when the application is closing (typically after Run has returned) to - /// ensure resources are cleaned up and terminal settings restored. + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal views such as dialogs. /// /// - /// The caller is responsible for disposing the object returned by this method. + /// To make stop execution, call + /// or . + /// + /// + /// Calling is equivalent to calling + /// , followed by starting the main loop, and then calling + /// . + /// + /// + /// In RELEASE builds: When is any exceptions will be + /// rethrown. Otherwise, will be called. If + /// returns the main loop will resume; otherwise this method will exit. /// /// - [RequiresUnreferencedCode ("AOT")] - [RequiresDynamicCode ("AOT")] - public Toplevel Run (Func? errorHandler = null, string? driverName = null); + object? Run (IRunnable runnable, Func? errorHandler = null); /// - /// Runs a new Session creating a -derived object of type - /// and calling . When the session is stopped, + /// Runs a new Session creating a -derived object of type + /// and calling . When the session is stopped, /// will be called. /// - /// The type of to create and run. + /// /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). /// /// The driver name. If not specified the default driver for the platform will be used. Must be /// if has already been called. /// - /// The created object. The caller is responsible for disposing this object. + /// + /// The created object. The caller is responsible for calling + /// on this + /// object. + /// /// /// /// This method is used to start processing events for the main application, but it is also used to run other /// modal s such as boxes. /// /// - /// To make stop execution, call - /// or . - /// - /// - /// Calling is equivalent to calling - /// , followed by starting the main loop, and then calling - /// . - /// - /// - /// When using or , - /// will be called automatically. + /// To make stop execution, call + /// or . /// /// /// In RELEASE builds: When is any exceptions will be @@ -213,7 +252,8 @@ public interface IApplication /// returns the main loop will resume; otherwise this method will exit. /// /// - /// must be called when the application is closing (typically after Run has returned) to + /// must be called when the application is closing (typically after Run has + /// returned) to /// ensure resources are cleaned up and terminal settings restored. /// /// @@ -227,48 +267,10 @@ public interface IApplication /// [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public TView Run (Func? errorHandler = null, string? driverName = null) - where TView : Toplevel, new(); + public IApplication Run (Func? errorHandler = null, string? driverName = null) + where TRunnable : IRunnable, new (); - /// - /// Runs a new Session using the provided view and calling - /// . - /// When the session is stopped, will be called.. - /// - /// The to run as a modal. - /// Handler for any unhandled exceptions (resumes when returns true, rethrows when null). - /// - /// - /// This method is used to start processing events for the main application, but it is also used to run other - /// modal s such as boxes. - /// - /// - /// To make stop execution, call - /// or . - /// - /// - /// Calling is equivalent to calling - /// , followed by starting the main loop, and then calling - /// . - /// - /// - /// When using or , - /// will be called automatically. - /// - /// - /// must be called when the application is closing (typically after Run has returned) to - /// ensure resources are cleaned up and terminal settings restored. - /// - /// - /// In RELEASE builds: When is any exceptions will be - /// rethrown. Otherwise, will be called. If - /// returns the main loop will resume; otherwise this method will exit. - /// - /// - /// The caller is responsible for disposing the object returned by this method. - /// - /// - public void Run (Toplevel view, Func? errorHandler = null); + #region Iteration & Invoke /// /// Raises the event. @@ -286,7 +288,8 @@ public interface IApplication /// The event args contain the current application instance. /// /// - /// . + /// + /// . public event EventHandler>? Iteration; /// Runs on the main UI loop thread. @@ -311,243 +314,44 @@ public interface IApplication /// void Invoke (Action action); - /// - /// Building block API: Ends a Session and completes the execution of a that was started with - /// . Not usually called directly by applications. - /// - /// will automatically call this method when the session is stopped. - /// - /// The returned by the method. - /// - /// - /// This method removes the from the stack, raises the - /// event, and disposes the . - /// - /// - public void End (SessionToken sessionToken); - - /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. - /// - /// This will cause to return. - /// - /// This is equivalent to calling with as the - /// parameter. - /// - /// - void RequestStop (); - - /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. - /// - /// The to stop. If , stops the currently running - /// . - /// - /// - /// This will cause to return. - /// - /// Calling is equivalent to setting the - /// property on the specified to . - /// - /// - void RequestStop (Toplevel? top); + #endregion Iteration & Invoke /// /// Set to to cause the session to stop running after first iteration. /// /// /// - /// Used primarily for unit testing. When , will be called + /// Used primarily for unit testing. When , will be + /// called /// automatically after the first main loop iteration. /// /// bool StopAfterFirstIteration { get; set; } - // TODO: This API is not used anywhere; it can be deleted - /// - /// Raised when has been called and has created a new . - /// - /// - /// If is , callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// - public event EventHandler? SessionBegun; - - // TODO: This API is not used anywhere; it can be deleted - /// - /// Raised when was called and the session is stopping. The event args contain a - /// reference to the - /// that was active during the session. This can be used to ensure the Toplevel is disposed of properly. - /// - /// - /// If is , callers to - /// must also subscribe to and manually dispose of the token - /// when the application is done. - /// - public event EventHandler? SessionEnded; - - #endregion Begin->Run->Iteration->Stop->End - - #region Toplevel Management - - /// Gets or sets the Toplevel that is on the top of the . + /// Requests that the currently running Session stop. The Session will stop after the current iteration completes. /// + /// This will cause to return. /// - /// The top runnable in the session stack captures all mouse and keyboard input. - /// This is set by and cleared by . + /// This is equivalent to calling with as the + /// parameter. /// /// - Toplevel? TopRunnable { get; set; } - - /// Gets the stack of all active Toplevel sessions. - /// - /// - /// Toplevels are added to this stack by and removed by - /// . - /// - /// - ConcurrentStack SessionStack { get; } - - /// - /// Caches the Toplevel associated with the current Session. - /// - /// - /// Used internally to optimize Toplevel state transitions. - /// - Toplevel? CachedSessionTokenToplevel { get; set; } - - #endregion Toplevel Management - - #region IRunnable Management - - /// - /// Gets the stack of all active runnable session tokens. - /// Sessions execute serially - the top of stack is the currently modal session. - /// - /// - /// - /// Session tokens are pushed onto the stack when is called and - /// popped when - /// completes. The stack grows during nested modal calls and - /// shrinks as they complete. - /// - /// - /// Only the top session () has exclusive keyboard/mouse input ( - /// = true). - /// All other sessions on the stack continue to be laid out, drawn, and receive iteration events ( - /// = true), - /// but they don't receive user input. - /// - /// - /// Stack during nested modals: - /// - /// RunnableSessionStack (top to bottom): - /// - MessageBox (TopRunnable, IsModal=true, IsRunning=true, has input) - /// - FileDialog (IsModal=false, IsRunning=true, continues to update/draw) - /// - MainWindow (IsModal=false, IsRunning=true, continues to update/draw) - /// - /// - /// - ConcurrentStack? RunnableSessionStack { get; } - - /// - /// Gets or sets the runnable that was created by for automatic disposal. - /// - /// - /// - /// When creates a runnable instance, it stores it here so - /// can automatically dispose it and extract its result. - /// - /// - /// This property is if was used - /// with an externally-created runnable. - /// - /// - IRunnable? FrameworkOwnedRunnable { get; set; } - - /// - /// Building block API: Creates a and prepares the provided - /// for - /// execution. Not usually called directly by applications. Use - /// instead. - /// - /// The to prepare execution for. - /// - /// The that needs to be passed to the - /// method upon - /// completion. - /// - /// - /// - /// This method prepares the provided for running. It adds this to the - /// , lays out the SubViews, focuses the first element, and draws the - /// runnable on the screen. This is usually followed by starting the main loop, and then the - /// method upon termination which will undo these changes. - /// - /// - /// Raises the , , - /// , and events. - /// - /// - RunnableSessionToken Begin (IRunnable runnable); - - /// - /// Runs a new Session with the provided runnable view. - /// - /// The runnable to execute. - /// Optional handler for unhandled exceptions (resumes when returns true, rethrows when null). - /// - /// - /// This method is used to start processing events for the main application, but it is also used to run other - /// modal views such as dialogs. - /// - /// - /// To make stop execution, call - /// or . - /// - /// - /// Calling is equivalent to calling - /// , followed by starting the main loop, and then calling - /// . - /// - /// - /// In RELEASE builds: When is any exceptions will be - /// rethrown. Otherwise, will be called. If - /// returns the main loop will resume; otherwise this method will exit. - /// - /// - void Run (IRunnable runnable, Func? errorHandler = null); - - /// - /// Creates and runs a new session with a of the specified type. - /// - /// The type of runnable to create and run. Must have a parameterless constructor. - /// Optional handler for unhandled exceptions (resumes when returns true, rethrows when null). - /// This instance for fluent API chaining. The created runnable is stored internally for disposal. - /// - /// - /// This is a convenience method that creates an instance of and runs it. - /// The framework owns the created instance and will automatically dispose it when is called. - /// - /// - /// To access the result, use which returns the result from . - /// - /// - /// Supports fluent API: var result = Application.Create().Init().Run<MyView>().Shutdown() as MyResultType - /// - /// - IApplication Run (Func? errorHandler = null) where TRunnable : IRunnable, new(); + void RequestStop (); /// /// Requests that the specified runnable session stop. /// - /// The runnable to stop. If , stops the current . + /// + /// The runnable to stop. If , stops the current + /// . + /// /// /// /// This will cause to return. /// /// /// Raises , , - /// , and events. + /// and events. /// /// void RequestStop (IRunnable? runnable); @@ -559,22 +363,70 @@ public interface IApplication /// will automatically call this method when the session is stopped. /// /// - /// The returned by the + /// The returned by the /// method. /// /// /// - /// This method removes the from the , + /// This method removes the from the , /// raises the lifecycle events, and disposes the . /// /// /// Raises , , - /// , and events. + /// and events. /// /// - void End (RunnableSessionToken sessionToken); + void End (SessionToken sessionToken); - #endregion IRunnable Management + /// + /// Raised when was called and the session is stopping. The event args contain a + /// reference to the + /// that was active during the session. This can be used to ensure the Runnable is disposed of properly. + /// + /// + /// If is , callers to + /// must also subscribe to and manually dispose of the token + /// when the application is done. + /// + public event EventHandler? SessionEnded; + + #endregion Session Management - Begin->Run->Iteration->Stop->End + + #region Result Management + + /// + /// Gets the result from the last or + /// call. + /// + /// + /// The result from the last run session, or if no session has been run or the result was null. + /// + object? GetResult (); + + /// + /// Gets the result from the last or + /// call, cast to type . + /// + /// The expected result type. + /// + /// The result cast to , or if the result is null or cannot be cast. + /// + /// + /// + /// using (var app = Application.Create().Init()) + /// { + /// app.Run<ColorPickerDialog>(); + /// var selectedColor = app.GetResult<Color>(); + /// if (selectedColor.HasValue) + /// { + /// // Use the color + /// } + /// } + /// + /// + T? GetResult () where T : class => GetResult () as T; + + #endregion Result Management #region Screen and Driver @@ -637,7 +489,7 @@ public interface IApplication /// /// /// This is typically set to when a View's changes and that view - /// has no SuperView (e.g. when is moved or resized). + /// has no SuperView (e.g. when is moved or resized). /// /// /// Automatically reset to after processes it. @@ -653,10 +505,38 @@ public interface IApplication #endregion Screen and Driver + #region Keyboard + + /// + /// Handles keyboard input and key bindings at the Application level. + /// + /// + /// + /// Provides access to keyboard state, key bindings, and keyboard event handling. Set during . + /// + /// + IKeyboard Keyboard { get; set; } + + #endregion Keyboard + + #region Mouse + + /// + /// Handles mouse event state and processing. + /// + /// + /// + /// Provides access to mouse state, mouse grabbing, and mouse event handling. Set during . + /// + /// + IMouse Mouse { get; set; } + + #endregion Mouse + #region Layout and Drawing /// - /// Causes any Toplevels that need layout to be laid out, then draws any Toplevels that need display. Only Views + /// Causes any Runnables that need layout to be laid out, then draws any Runnables that need display. Only Views /// that need to be laid out (see ) will be laid out. Only Views that need to be drawn /// (see ) will be drawn. /// @@ -691,14 +571,6 @@ public interface IApplication #region Navigation and Popover - /// Gets or sets the popover manager. - /// - /// - /// Manages application-level popover views. Initialized during . - /// - /// - ApplicationPopover? Popover { get; set; } - /// Gets or sets the navigation manager. /// /// @@ -707,6 +579,14 @@ public interface IApplication /// ApplicationNavigation? Navigation { get; set; } + /// Gets or sets the popover manager. + /// + /// + /// Manages application-level popover views. Initialized during . + /// + /// + ApplicationPopover? Popover { get; set; } + #endregion Navigation and Popover #region Timeouts @@ -725,10 +605,10 @@ public interface IApplication /// When the time specified passes, the callback will be invoked on the main UI thread. /// /// - /// calls StopAll on to remove all timeouts. + /// calls StopAll on to remove all timeouts. /// /// - object AddTimeout (TimeSpan time, Func callback); + object? AddTimeout (TimeSpan time, Func callback); /// Removes a previously scheduled timeout. /// The token returned by . diff --git a/Terminal.Gui/App/IPopover.cs b/Terminal.Gui/App/IPopover.cs index 816c69613..be577b871 100644 --- a/Terminal.Gui/App/IPopover.cs +++ b/Terminal.Gui/App/IPopover.cs @@ -51,11 +51,11 @@ public interface IPopover { /// /// Gets or sets the that this Popover is associated with. If null, it is not associated with - /// any Toplevel and will receive all keyboard - /// events from the . If set, it will only receive keyboard events the Toplevel would normally + /// any Runnable and will receive all keyboard + /// events from the . If set, it will only receive keyboard events the Runnable would normally /// receive. /// When is called, the is set to the current - /// if not already set. + /// if not already set. /// - Toplevel? Current { get; set; } + IRunnable? Current { get; set; } } diff --git a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs index 7b4d26e2d..dbdd2d67c 100644 --- a/Terminal.Gui/App/Keyboard/KeyboardImpl.cs +++ b/Terminal.Gui/App/Keyboard/KeyboardImpl.cs @@ -150,9 +150,6 @@ internal class KeyboardImpl : IKeyboard, IDisposable /// public bool RaiseKeyDownEvent (Key key) { - //ebug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId); - //Logging.Debug ($"{key}"); - // TODO: Add a way to ignore certain keys, esp for debugging. //#if DEBUG // if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl) @@ -175,18 +172,18 @@ internal class KeyboardImpl : IKeyboard, IDisposable return true; } - if (App?.TopRunnable is null) + if (App?.TopRunnableView is null) { if (App?.SessionStack is { }) { - foreach (Toplevel topLevel in App.SessionStack.ToList ()) + foreach (IRunnable? runnable in App.SessionStack.Select(r => r.Runnable)) { - if (topLevel.NewKeyDownEvent (key)) + if (runnable is View view && view.NewKeyDownEvent (key)) { return true; } - if (topLevel.Modal) + if (runnable!.IsModal) { break; } @@ -195,7 +192,7 @@ internal class KeyboardImpl : IKeyboard, IDisposable } else { - if (App.TopRunnable.NewKeyDownEvent (key)) + if (App.TopRunnableView.NewKeyDownEvent (key)) { return true; } @@ -230,14 +227,14 @@ internal class KeyboardImpl : IKeyboard, IDisposable if (App?.SessionStack is { }) { - foreach (Toplevel topLevel in App.SessionStack.ToList ()) + foreach (IRunnable? runnable in App.SessionStack.Select (r => r.Runnable)) { - if (topLevel.NewKeyUpEvent (key)) + if (runnable is View view && view.NewKeyUpEvent (key)) { return true; } - if (topLevel.Modal) + if (runnable!.IsModal) { break; } diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index 3ad75c4e6..a1a064103 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -81,11 +81,6 @@ public class ApplicationMainLoop : IApplicationMainLoop _sizeMonitor = value; } - /// - /// Handles raising events and setting required draw status etc when changes - /// - public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); - /// /// Initializes the class with the provided subcomponents /// @@ -142,18 +137,10 @@ public class ApplicationMainLoop : IApplicationMainLoop : IApplicationMainLoop? currentViewsUnderMouse = App?.TopRunnable?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); + List? currentViewsUnderMouse = App?.TopRunnableView?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse); View? deepestViewUnderMouse = currentViewsUnderMouse?.LastOrDefault (); @@ -126,7 +126,7 @@ internal class MouseImpl : IMouse, IDisposable // if the mouse is outside the Application.TopRunnable or Popover hierarchy, we don't want to // send the mouse event to the deepest view under the mouse. - if (!View.IsInHierarchy (App?.TopRunnable, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) + if (!View.IsInHierarchy (App?.TopRunnableView, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true)) { return; } diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs index 70f771de1..ff118df35 100644 --- a/Terminal.Gui/App/PopoverBaseImpl.cs +++ b/Terminal.Gui/App/PopoverBaseImpl.cs @@ -73,16 +73,16 @@ public abstract class PopoverBaseImpl : View, IPopover } } - private Toplevel? _current; + private IRunnable? _current; /// - public Toplevel? Current + public IRunnable? Current { get => _current; set { _current = value; - App ??= _current?.App; + App ??= (_current as View)?.App; } } @@ -119,7 +119,7 @@ public abstract class PopoverBaseImpl : View, IPopover // Whenever visible is changing to false, we need to reset the focus if (ApplicationNavigation.IsInHierarchy (this, App?.Navigation?.GetFocused ())) { - App?.Navigation?.SetFocused (App?.TopRunnable?.MostFocused); + App?.Navigation?.SetFocused (App?.TopRunnableView?.MostFocused); } } diff --git a/Terminal.Gui/App/Runnable/IRunnable.cs b/Terminal.Gui/App/Runnable/IRunnable.cs index 75ef00b1b..6b55454f4 100644 --- a/Terminal.Gui/App/Runnable/IRunnable.cs +++ b/Terminal.Gui/App/Runnable/IRunnable.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.App; /// /// /// This interface enables storing heterogeneous runnables in collections (e.g., -/// ) +/// ) /// while preserving type safety at usage sites via . /// /// @@ -27,24 +27,70 @@ namespace Terminal.Gui.App; /// public interface IRunnable { - #region Running or not (added to/removed from RunnableSessionStack) + #region Result /// - /// Gets whether this runnable session is currently running (i.e., on the - /// ). + /// Gets or sets the result data extracted when the session was accepted, or if not accepted. /// /// /// - /// Read-only property derived from stack state. Returns if this runnable - /// is currently on the , otherwise. + /// This is the non-generic version of the result property. For type-safe access, cast to + /// or access the derived interface's Result property directly. + /// + /// + /// Implementations should set this in the method + /// (when stopping, i.e., newIsRunning == false) by extracting data from + /// views before they are disposed. + /// + /// + /// indicates the session was stopped without accepting (ESC key, close without action). + /// Non- contains the result data. + /// + /// + object? Result { get; set; } + + #endregion Result + + #region Running or not (added to/removed from RunnableSessionStack) + + /// + /// Sets the application context for this runnable. Called from . + /// + /// + void SetApp (IApplication app); + + /// + /// Gets whether this runnable session is currently running (i.e., on the + /// ). + /// + /// + /// + /// This property returns a cached value that is updated atomically when the runnable is added to or + /// removed from the session stack. The cached state ensures thread-safe access without race conditions. + /// + /// + /// Returns if this runnable is currently on the , + /// otherwise. /// /// /// Runnables are added to the stack during and removed in - /// . + /// . /// /// bool IsRunning { get; } + /// + /// Sets the cached IsRunning state. Called by ApplicationImpl within the session stack lock. + /// This method is internal to the framework and should not be called by application code. + /// + /// The new IsRunning value. + void SetIsRunning (bool value); + + /// + /// Requests that this runnable session stop. + /// + public void RequestStop (); + /// /// Called by the framework to raise the event. /// @@ -65,7 +111,7 @@ public interface IRunnable /// /// Raised when is changing (e.g., when or - /// is called). + /// is called). /// Can be canceled by setting `args.Cancel` to . /// /// @@ -92,7 +138,7 @@ public interface IRunnable /// /// Raised after has changed (after the runnable has been added to or removed from the - /// ). + /// ). /// /// /// @@ -112,13 +158,17 @@ public interface IRunnable #region Modal or not (top of RunnableSessionStack or not) /// - /// Gets whether this runnable session is at the top of the and thus + /// Gets whether this runnable session is at the top of the and thus /// exclusively receiving mouse and keyboard input. /// /// /// - /// Read-only property derived from stack state. Returns if this runnable - /// is at the top of the stack (i.e., this == app.TopRunnable), otherwise. + /// This property returns a cached value that is updated atomically when the runnable's modal state changes. + /// The cached state ensures thread-safe access without race conditions. + /// + /// + /// Returns if this runnable is at the top of the stack (i.e., this == app.TopRunnable), + /// otherwise. /// /// /// The runnable at the top of the stack gets all mouse/keyboard input and thus is running "modally". @@ -127,33 +177,16 @@ public interface IRunnable bool IsModal { get; } /// - /// Called by the framework to raise the event. + /// Sets the cached IsModal state. Called by ApplicationImpl within the session stack lock. + /// This method is internal to the framework and should not be called by application code. /// - /// The current value of . - /// The new value of (true = becoming modal/top, false = no longer modal). - /// if the change was canceled; otherwise . - /// - /// This method implements the Cancellable Work Pattern. It calls the protected virtual method first, - /// then raises the event if not canceled. - /// - bool RaiseIsModalChanging (bool oldIsModal, bool newIsModal); + /// The new IsModal value. + void SetIsModal (bool value); /// - /// Raised when this runnable is about to become modal (top of stack) or cease being modal. - /// Can be canceled by setting `args.Cancel` to . + /// Gets or sets whether a stop has been requested for this runnable session. /// - /// - /// - /// Subscribe to this event to participate in modal state transitions before they occur. - /// When is , the runnable is becoming modal (top - /// of stack). - /// When , another runnable is becoming modal and this one will no longer receive input. - /// - /// - /// This event follows the Terminal.Gui Cancellable Work Pattern (CWP). - /// - /// - event EventHandler>? IsModalChanging; + bool StopRequested { get; set; } /// /// Called by the framework to raise the event. @@ -229,5 +262,5 @@ public interface IRunnable : IRunnable /// Non- contains the type-safe result data. /// /// - TResult? Result { get; set; } + new TResult? Result { get; set; } } diff --git a/Terminal.Gui/App/Runnable/IToplevelTransitionManager.cs b/Terminal.Gui/App/Runnable/IToplevelTransitionManager.cs deleted file mode 100644 index d532de67e..000000000 --- a/Terminal.Gui/App/Runnable/IToplevelTransitionManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Terminal.Gui.App; - - -// TODO: This whole concept is bogus and over-engineered. -// TODO: Remove it and just subscribers use the IApplication.Iteration -// TODO: If the requirement is they know if it's the first iteration, they can -// TODO: count invocations. - -/// -/// Interface for class that handles bespoke behaviours that occur when application -/// top level changes. -/// -public interface IToplevelTransitionManager -{ - /// - /// Raises the event on tahe current top level - /// if it has not been raised before now. - /// - /// - void RaiseReadyEventIfNeeded (IApplication? app); - - /// - /// Handles any state change needed when the application top changes e.g. - /// setting redraw flags - /// - /// - void HandleTopMaybeChanging (IApplication? app); -} diff --git a/Terminal.Gui/App/Runnable/RunnableSessionToken.cs b/Terminal.Gui/App/Runnable/RunnableSessionToken.cs deleted file mode 100644 index 0f386b635..000000000 --- a/Terminal.Gui/App/Runnable/RunnableSessionToken.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Concurrent; - -namespace Terminal.Gui.App; - -/// -/// Represents a running session created by . -/// Wraps an instance and is stored in . -/// -public class RunnableSessionToken : IDisposable -{ - internal RunnableSessionToken (IRunnable runnable) { Runnable = runnable; } - - /// - /// Gets or sets the runnable associated with this session. - /// Set to by when the session completes. - /// - public IRunnable? Runnable { get; internal set; } - - /// - /// Releases all resource used by the object. - /// - /// - /// - /// Call when you are finished using the . - /// - /// - /// method leaves the in an unusable state. After - /// calling - /// , you must release all references to the so the - /// garbage collector can - /// reclaim the memory that the was occupying. - /// - /// - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); - -#if DEBUG_IDISPOSABLE - WasDisposed = true; -#endif - } - - /// - /// Releases all resource used by the object. - /// - /// If set to we are disposing and should dispose held objects. - protected virtual void Dispose (bool disposing) - { - if (Runnable is { } && disposing) - { - // Runnable must be null before disposing - throw new InvalidOperationException ( - "Runnable must be null before calling RunnableSessionToken.Dispose" - ); - } - } - -#if DEBUG_IDISPOSABLE -#pragma warning disable CS0419 // Ambiguous reference in cref attribute - /// - /// Gets whether was called on this RunnableSessionToken or not. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. - /// - public bool WasDisposed { get; private set; } - - /// - /// Gets the number of times was called on this object. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. - /// - public int DisposedCount { get; private set; } - - /// - /// Gets the list of RunnableSessionToken objects that have been created and not yet disposed. - /// Note, this is a static property and will affect all RunnableSessionToken objects. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. - /// - public static ConcurrentBag Instances { get; } = []; - - /// Creates a new RunnableSessionToken object. - public RunnableSessionToken () { Instances.Add (this); } -#pragma warning restore CS0419 // Ambiguous reference in cref attribute -#endif -} diff --git a/Terminal.Gui/App/Runnable/SessionToken.cs b/Terminal.Gui/App/Runnable/SessionToken.cs index 6505727bb..38410facd 100644 --- a/Terminal.Gui/App/Runnable/SessionToken.cs +++ b/Terminal.Gui/App/Runnable/SessionToken.cs @@ -1,77 +1,23 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; namespace Terminal.Gui.App; -/// Defines a session token for a running . -public class SessionToken : IDisposable +/// +/// Represents a running session created by . +/// Wraps an instance and is stored in . +/// +public class SessionToken { - /// Initializes a new class. - /// - public SessionToken (Toplevel view) { Toplevel = view; } - - /// The belonging to this . - public Toplevel? Toplevel { get; internal set; } - - /// Releases all resource used by the object. - /// Call when you are finished using the . - /// - /// method leaves the in an unusable state. After calling - /// , you must release all references to the so the garbage collector can - /// reclaim the memory that the was occupying. - /// - public void Dispose () - { - Dispose (true); - GC.SuppressFinalize (this); -#if DEBUG_IDISPOSABLE - WasDisposed = true; -#endif - } - - /// Releases all resource used by the object. - /// If set to we are disposing and should dispose held objects. - protected virtual void Dispose (bool disposing) - { - if (Toplevel is { } && disposing) - { - // Previously we were requiring Toplevel be disposed here. - // But that is not correct becaue `Begin` didn't create the TopLevel, `Init` did; thus - // disposing should be done by `Shutdown`, not `End`. - throw new InvalidOperationException ( - "Toplevel must be null before calling Application.SessionToken.Dispose" - ); - } - } - -#if DEBUG_IDISPOSABLE -#pragma warning disable CS0419 // Ambiguous reference in cref attribute - /// - /// Gets whether was called on this SessionToken or not. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. - /// - public bool WasDisposed { get; private set; } + internal SessionToken (IRunnable runnable) { Runnable = runnable; } /// - /// Gets the number of times was called on this object. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. + /// Gets or sets the runnable associated with this session. + /// Set to by when the session completes. /// - public int DisposedCount { get; private set; } = 0; + public IRunnable? Runnable { get; internal set; } /// - /// Gets the list of SessionToken objects that have been created and not yet disposed. - /// Note, this is a static property and will affect all SessionToken objects. - /// For debug purposes to verify objects are being disposed properly. - /// Only valid when DEBUG_IDISPOSABLE is defined. + /// The result of the session. Typically set by the runnable in /// - public static ConcurrentBag Instances { get; private set; } = []; - - /// Creates a new SessionToken object. - public SessionToken () - { - Instances.Add (this); - } -#pragma warning restore CS0419 // Ambiguous reference in cref attribute -#endif + public object? Result { get; set; } } diff --git a/Terminal.Gui/App/Runnable/ToplevelTransitionManager.cs b/Terminal.Gui/App/Runnable/ToplevelTransitionManager.cs deleted file mode 100644 index ee80619d8..000000000 --- a/Terminal.Gui/App/Runnable/ToplevelTransitionManager.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Terminal.Gui.App; - -// TODO: This whole concept is bogus and over-engineered. -// TODO: Remove it and just let subscribers use the IApplication.Iteration -// TODO: If the requirement is they know if it's the first iteration, they can -// TODO: count invocations. - -/// -/// Handles bespoke behaviours that occur when application top level changes. -/// -public class ToplevelTransitionManager : IToplevelTransitionManager -{ - private readonly HashSet _readiedTopLevels = new (); - - private View? _lastTop; - - /// - /// - public void RaiseReadyEventIfNeeded (IApplication? app) - { - Toplevel? top = app?.TopRunnable; - - if (top != null && !_readiedTopLevels.Contains (top)) - { - top.OnReady (); - _readiedTopLevels.Add (top); - - // Views can be closed and opened and run again multiple times, see End_Does_Not_Dispose - top.Closed += (s, e) => _readiedTopLevels.Remove (top); - } - } - - /// - /// - public void HandleTopMaybeChanging (IApplication? app) - { - Toplevel? newTop = app?.TopRunnable; - - if (_lastTop != null && _lastTop != newTop && newTop != null) - { - newTop.SetNeedsDraw (); - } - - _lastTop = app?.TopRunnable; - } -} diff --git a/Terminal.Gui/Configuration/ThemeScope.cs b/Terminal.Gui/Configuration/ThemeScope.cs index 29bef03d8..753502bb4 100644 --- a/Terminal.Gui/Configuration/ThemeScope.cs +++ b/Terminal.Gui/Configuration/ThemeScope.cs @@ -15,7 +15,7 @@ namespace Terminal.Gui.Configuration; /// "Default": { /// "Schemes": [ /// { -/// "TopLevel": { +/// "Runnable": { /// "Normal": { /// "Foreground": "BrightGreen", /// "Background": "Black" diff --git a/Terminal.Gui/Drawing/Color/StandardColors.cs b/Terminal.Gui/Drawing/Color/StandardColors.cs index 7e5b83831..9f17ac1ac 100644 --- a/Terminal.Gui/Drawing/Color/StandardColors.cs +++ b/Terminal.Gui/Drawing/Color/StandardColors.cs @@ -5,75 +5,90 @@ using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.Drawing; /// -/// Helper class for transforming to and from enum. +/// Helper class for transforming to and from enum. /// internal static class StandardColors { - private static readonly ImmutableArray _names; - private static readonly FrozenDictionary _argbNameMap; + // Lazy initialization to avoid static constructor convoy effect in parallel scenarios + private static readonly Lazy> _names = new ( + NamesValueFactory, + LazyThreadSafetyMode.PublicationOnly); - static StandardColors () + private static ImmutableArray NamesValueFactory () { - // Populate based on names because enums with same numerical value - // are not otherwise distinguishable from each other. string [] standardNames = Enum.GetNames ().Order ().ToArray (); + return ImmutableArray.Create (standardNames); + } + + private static readonly Lazy> _argbNameMap = new ( + MapValueFactory, + LazyThreadSafetyMode.PublicationOnly); + + private static FrozenDictionary MapValueFactory () + { + string [] standardNames = Enum.GetNames () + .Order () + .ToArray (); Dictionary map = new (standardNames.Length); + foreach (string name in standardNames) { - StandardColor standardColor = Enum.Parse (name); + var standardColor = Enum.Parse (name); uint argb = GetArgb (standardColor); + // TODO: Collect aliases? _ = map.TryAdd (argb, name); } - _names = ImmutableArray.Create (standardNames); - _argbNameMap = map.ToFrozenDictionary (); + return map.ToFrozenDictionary (); } /// - /// Gets read-only list of the W3C colors in alphabetical order. + /// Gets read-only list of the W3C colors in alphabetical order. /// - public static IReadOnlyList GetColorNames () - { - return _names; - } + public static IReadOnlyList GetColorNames () => _names.Value; /// - /// Converts the given Standard (W3C+) color name to equivalent color value. + /// Converts the given Standard (W3C+) color name to equivalent color value. /// /// Standard (W3C+) color name. /// The successfully converted Standard (W3C+) color value. /// True if the conversion succeeded; otherwise false. public static bool TryParseColor (ReadOnlySpan name, out Color color) { - if (!Enum.TryParse (name, ignoreCase: true, out StandardColor standardColor) || + if (!Enum.TryParse (name, true, out StandardColor standardColor) + || + // Any numerical value converts to undefined enum value. !Enum.IsDefined (standardColor)) { - color = default; + color = default (Color); + return false; } uint argb = GetArgb (standardColor); - color = new Color (argb); + color = new (argb); + return true; } /// - /// Converts the given color value to a Standard (W3C+) color name. + /// Converts the given color value to a Standard (W3C+) color name. /// /// Color value to match Standard (W3C+)color. /// The successfully converted Standard (W3C+) color name. /// True if conversion succeeded; otherwise false. public static bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) { - if (_argbNameMap.TryGetValue (color.Argb, out name)) + if (_argbNameMap.Value.TryGetValue (color.Argb, out name)) { return true; } name = null; + return false; } @@ -82,9 +97,10 @@ internal static class StandardColors const int ALPHA_SHIFT = 24; const uint ALPHA_MASK = 0xFFU << ALPHA_SHIFT; - int rgb = (int)standardColor; + var rgb = (int)standardColor; uint argb = (uint)rgb | ALPHA_MASK; + return argb; } } diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index 9bd612537..d513fc1e7 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -162,7 +162,7 @@ public record Scheme : IEqualityOperators new (SchemeManager.SchemesToSchemeName (Schemes.Dialog)!, CreateDialog ()), new (SchemeManager.SchemesToSchemeName (Schemes.Error)!, CreateError ()), new (SchemeManager.SchemesToSchemeName (Schemes.Menu)!, CreateMenu ()), - new (SchemeManager.SchemesToSchemeName (Schemes.Toplevel)!, CreateToplevel ()), + new (SchemeManager.SchemesToSchemeName (Schemes.Runnable)!, CreateRunnable ()), ] ); @@ -198,7 +198,7 @@ public record Scheme : IEqualityOperators }; } - Scheme CreateToplevel () + Scheme CreateRunnable () { return new () { diff --git a/Terminal.Gui/Drawing/Schemes.cs b/Terminal.Gui/Drawing/Schemes.cs index 5408f6b8c..30d66458f 100644 --- a/Terminal.Gui/Drawing/Schemes.cs +++ b/Terminal.Gui/Drawing/Schemes.cs @@ -23,9 +23,9 @@ public enum Schemes Dialog, /// - /// The application Toplevel scheme, used for the Toplevel View. + /// The scheme used for views that support . /// - Toplevel, + Runnable, /// /// The scheme for showing errors. diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs index 7732bd319..d619539b4 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqRequests.cs @@ -16,7 +16,7 @@ public static class EscSeqRequests /// /// The terminator. /// The number of requests. - public static void Add (string terminator, int numRequests = 1) + public static void Add (string? terminator, int numRequests = 1) { ArgumentException.ThrowIfNullOrEmpty (terminator); diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs index b8a0a5240..b30d871ce 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeInput.cs @@ -12,16 +12,25 @@ public class FakeInput : InputImpl, ITestableInput _testInput = new (); + private int _peekCallCount; + + /// + /// Gets the number of times has been called. + /// This is useful for verifying that the input loop throttling is working correctly. + /// + internal int PeekCallCount => _peekCallCount; + /// /// Creates a new FakeInput. /// - public FakeInput () - { } + public FakeInput () { } /// public override bool Peek () { // Will be called on the input thread. + Interlocked.Increment (ref _peekCallCount); + return !_testInput.IsEmpty; } @@ -35,7 +44,7 @@ public class FakeInput : InputImpl, ITestableInput + /// public void AddInput (ConsoleKeyInfo input) { //Logging.Trace ($"Enqueuing input: {input.Key}"); diff --git a/Terminal.Gui/Drivers/InputImpl.cs b/Terminal.Gui/Drivers/InputImpl.cs index b70df2b44..5cf573a0f 100644 --- a/Terminal.Gui/Drivers/InputImpl.cs +++ b/Terminal.Gui/Drivers/InputImpl.cs @@ -18,7 +18,7 @@ public abstract class InputImpl : IInput /// public Func Now { get; set; } = () => DateTime.Now; - /// + /// public CancellationTokenSource? ExternalCancellationTokenSource { get; set; } /// @@ -46,8 +46,6 @@ public abstract class InputImpl : IInput do { - DateTime dt = Now (); - while (Peek ()) { foreach (TInputRecord r in Read ()) @@ -57,6 +55,11 @@ public abstract class InputImpl : IInput } effectiveToken.ThrowIfCancellationRequested (); + + // Throttle the input loop to avoid CPU spinning when no input is available + // This is especially important when multiple ApplicationImpl instances are created + // in parallel tests without calling Shutdown() - prevents thread pool exhaustion + Task.Delay (20, effectiveToken).Wait (effectiveToken); } while (!effectiveToken.IsCancellationRequested); } @@ -64,7 +67,7 @@ public abstract class InputImpl : IInput { } finally { - Logging.Trace($"Stopping input processing"); + Logging.Trace ("Stopping input processing"); linkedCts?.Dispose (); } } diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs index 30f112b4b..ffe254851 100644 --- a/Terminal.Gui/Drivers/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs @@ -140,9 +140,6 @@ public class OutputBufferImpl : IOutputBuffer { string text = grapheme; - int textWidth = -1; - bool validLocation = IsValidLocation (text, Col, Row); - if (Contents is null) { return; @@ -152,13 +149,19 @@ public class OutputBufferImpl : IOutputBuffer Rectangle clipRect = Clip!.GetBounds (); - if (validLocation) - { - text = text.MakePrintable (); - textWidth = text.GetColumns (); + int textWidth = -1; + bool validLocation = false; - lock (Contents) + lock (Contents) + { + // Validate location inside the lock to prevent race conditions + validLocation = IsValidLocation (text, Col, Row); + + if (validLocation) { + text = text.MakePrintable (); + textWidth = text.GetColumns (); + Contents [Row, Col].Attribute = CurrentAttribute; Contents [Row, Col].IsDirty = true; @@ -177,7 +180,7 @@ public class OutputBufferImpl : IOutputBuffer { Contents [Row, Col].Grapheme = text; - if (Col < clipRect.Right - 1) + if (Col < clipRect.Right - 1 && Col + 1 < Cols) { Contents [Row, Col + 1].IsDirty = true; } @@ -187,22 +190,23 @@ public class OutputBufferImpl : IOutputBuffer if (!Clip.Contains (Col + 1, Row)) { // We're at the right edge of the clip, so we can't display a wide character. - // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col].Grapheme = Rune.ReplacementChar.ToString (); } else if (!Clip.Contains (Col, Row)) { // Our 1st column is outside the clip, so we can't display a wide character. - Contents [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString (); + if (Col + 1 < Cols) + { + Contents [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString (); + } } else { Contents [Row, Col].Grapheme = text; - if (Col < clipRect.Right - 1) + if (Col < clipRect.Right - 1 && Col + 1 < Cols) { // Invalidate cell to right so that it doesn't get drawn - // TODO: Figure out if it is better to show a replacement character or ' ' Contents [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString (); Contents [Row, Col + 1].IsDirty = true; } @@ -225,18 +229,19 @@ public class OutputBufferImpl : IOutputBuffer { Debug.Assert (textWidth <= 2); - if (validLocation && Col < clipRect.Right) + if (validLocation) { lock (Contents!) { - // This is a double-width character, and we are not at the end of the line. - // Col now points to the second column of the character. Ensure it doesn't - // Get rendered. - Contents [Row, Col].IsDirty = false; - Contents [Row, Col].Attribute = CurrentAttribute; - - // TODO: Determine if we should wipe this out (for now now) - //Contents [Row, Col].Text = (Text)' '; + // Re-validate Col is still in bounds after increment + if (Col < Cols && Row < Rows && Col < clipRect.Right) + { + // This is a double-width character, and we are not at the end of the line. + // Col now points to the second column of the character. Ensure it doesn't + // Get rendered. + Contents [Row, Col].IsDirty = false; + Contents [Row, Col].Attribute = CurrentAttribute; + } } } diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 65283b351..141429fbb 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -50,7 +50,7 @@ "TurboPascal 5": { "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "White", "Background": "Blue" @@ -123,7 +123,7 @@ "Anders": { "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "WhiteSmoke", "Background": "DimGray" @@ -185,7 +185,7 @@ "Button.DefaultShadow": "Opaque", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "LightGray", "Background": "Black", @@ -469,7 +469,7 @@ "Button.DefaultShadow": "Opaque", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "DimGray", "Background": "WhiteSmoke", @@ -751,7 +751,7 @@ "Menu.DefaultBorderStyle": "Single", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "GreenPhosphor", "Background": "Black", @@ -888,7 +888,7 @@ "Menu.DefaultBorderStyle": "Single", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "AmberPhosphor", "Background": "Black", @@ -1162,7 +1162,7 @@ "Glyphs.ShadowHorizontalEnd": "-", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "White", "Background": "Black" diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index 3b2caefe4..ac0a593a1 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -657,7 +657,7 @@ public partial class Border if (Parent!.SuperView is null) { // Redraw the entire app window. - App?.TopRunnable?.SetNeedsDraw (); + App?.TopRunnableView?.SetNeedsDraw (); } else { diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs index bfbe92a08..951879e9f 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.cs @@ -141,7 +141,7 @@ public partial class Border : Adornment }; CloseButton.Accept += (s, e) => { - e.Handled = Parent.InvokeCommand (Command.QuitToplevel) == true; + e.Handled = Parent.InvokeCommand (Command.Quit) == true; }; Add (CloseButton); diff --git a/Terminal.Gui/ViewBase/Runnable.cs b/Terminal.Gui/ViewBase/Runnable/Runnable.cs similarity index 61% rename from Terminal.Gui/ViewBase/Runnable.cs rename to Terminal.Gui/ViewBase/Runnable/Runnable.cs index cb2fef4ca..018cbf087 100644 --- a/Terminal.Gui/ViewBase/Runnable.cs +++ b/Terminal.Gui/ViewBase/Runnable/Runnable.cs @@ -1,35 +1,71 @@ namespace Terminal.Gui.ViewBase; /// -/// Base implementation of for views that can be run as blocking sessions. +/// Base implementation of for views that can be run as blocking sessions without returning a result. /// -/// The type of result data returned when the session completes. /// /// -/// Views can derive from this class or implement directly. +/// Views that don't need to return a result can derive from this class instead of . /// /// -/// This class provides default implementations of the interface +/// This class provides default implementations of the interface /// following the Terminal.Gui Cancellable Work Pattern (CWP). /// +/// +/// For views that need to return a result, use instead. +/// /// -public class Runnable : View, IRunnable +public class Runnable : View, IRunnable { + // Cached state - eliminates race conditions from stack queries + private bool _isRunning; + private bool _isModal; + + /// + /// Constructs a new instance of the class. + /// + public Runnable () + { + CanFocus = true; + TabStop = TabBehavior.TabGroup; + Arrangement = ViewArrangement.Overlapped; + Width = Dim.Fill (); + Height = Dim.Fill (); + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Runnable); + } + /// - public TResult? Result { get; set; } + public object? Result { get; set; } #region IRunnable Implementation - IsRunning (from base interface) + /// + public void SetApp (IApplication app) + { + App = app; + } + /// - public bool IsRunning => App?.RunnableSessionStack?.Any (token => token.Runnable == this) ?? false; + public bool IsRunning => _isRunning; + + /// + public void SetIsRunning (bool value) { _isRunning = value; } + + /// + public virtual void RequestStop () + { + // Use the IRunnable-specific RequestStop if the App supports it + App?.RequestStop (this); + } /// public bool RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning) { - // Clear previous result when starting + // Clear previous result when starting (for non-generic Runnable) + // Derived Runnable will clear its typed Result in OnIsRunningChanging override if (newIsRunning) { - Result = default (TResult); + Result = null; } // CWP Phase 1: Virtual method (pre-notification) @@ -52,6 +88,14 @@ public class Runnable : View, IRunnable /// public void RaiseIsRunningChangedEvent (bool newIsRunning) { + // Initialize if needed when starting + if (newIsRunning && !IsInitialized) + { + BeginInit (); + EndInit (); + // Initialized event is raised by View.EndInit() + } + // CWP Phase 3: Post-notification (work already done by Application.Begin/End) OnIsRunningChanged (newIsRunning); @@ -63,8 +107,7 @@ public class Runnable : View, IRunnable public event EventHandler>? IsRunningChanged; /// - /// Called before event. Override to cancel state change or extract - /// . + /// Called before event. Override to cancel state change or perform cleanup. /// /// The current value of . /// The new value of (true = starting, false = stopping). @@ -75,10 +118,7 @@ public class Runnable : View, IRunnable /// /// /// IMPORTANT: When is (stopping), this is the ideal - /// place - /// to extract from views before the runnable is removed from the stack. - /// At this point, all views are still alive and accessible, and subscribers can inspect the result - /// and optionally cancel the stop. + /// place to perform cleanup or validation before the runnable is removed from the stack. /// /// /// @@ -86,10 +126,7 @@ public class Runnable : View, IRunnable /// { /// if (!newIsRunning) // Stopping /// { - /// // Extract result before removal from stack - /// Result = _textField.Text; - /// - /// // Or check if user wants to save first + /// // Check if user wants to save first /// if (HasUnsavedChanges ()) /// { /// int result = MessageBox.Query (App, "Save?", "Save changes?", "Yes", "No", "Cancel"); @@ -122,52 +159,13 @@ public class Runnable : View, IRunnable #region IRunnable Implementation - IsModal (from base interface) /// - public bool IsModal - { - get - { - if (App is null) - { - return false; - } - - // Check if this runnable is at the top of the RunnableSessionStack - // The top of the stack is the modal runnable - if (App.RunnableSessionStack is { } && App.RunnableSessionStack.TryPeek (out RunnableSessionToken? topToken)) - { - return topToken?.Runnable == this; - } - - // Fallback: Check if this is the TopRunnable (for Toplevel compatibility) - // In Phase 1, TopRunnable is still Toplevel?, so we need to check both cases - if (this is Toplevel tl && App.TopRunnable == tl) - { - return true; - } - - return false; - } - } + public bool IsModal => _isModal; /// - public bool RaiseIsModalChanging (bool oldIsModal, bool newIsModal) - { - // CWP Phase 1: Virtual method (pre-notification) - if (OnIsModalChanging (oldIsModal, newIsModal)) - { - return true; // Canceled - } + public void SetIsModal (bool value) { _isModal = value; } - // CWP Phase 2: Event notification - bool newValue = newIsModal; - CancelEventArgs args = new (in oldIsModal, ref newValue); - IsModalChanging?.Invoke (this, args); - - return args.Cancel; - } - - /// - public event EventHandler>? IsModalChanging; + /// + public bool StopRequested { get; set; } /// public void RaiseIsModalChangedEvent (bool newIsModal) @@ -177,22 +175,30 @@ public class Runnable : View, IRunnable EventArgs args = new (newIsModal); IsModalChanged?.Invoke (this, args); + + // Layout may need to change when modal state changes + SetNeedsLayout (); + SetNeedsDraw (); + + if (newIsModal) + { + // Set focus to self if becoming modal + if (HasFocus is false) + { + SetFocus (); + } + + // Position cursor and update driver + if (App?.PositionCursor () == true) + { + App?.Driver?.UpdateCursor (); + } + } } /// public event EventHandler>? IsModalChanged; - /// - /// Called before event. Override to cancel activation/deactivation. - /// - /// The current value of . - /// The new value of (true = becoming modal/top, false = no longer modal). - /// to cancel; to proceed. - /// - /// Default implementation returns (allow change). - /// - protected virtual bool OnIsModalChanging (bool oldIsModal, bool newIsModal) => false; - /// /// Called after has changed. Override for post-activation logic. /// @@ -211,13 +217,4 @@ public class Runnable : View, IRunnable } #endregion - - /// - /// Requests that this runnable session stop. - /// - public virtual void RequestStop () - { - // Use the IRunnable-specific RequestStop if the App supports it - App?.RequestStop (this); - } } diff --git a/Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs b/Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs new file mode 100644 index 000000000..44245b235 --- /dev/null +++ b/Terminal.Gui/ViewBase/Runnable/RunnableTResult.cs @@ -0,0 +1,54 @@ +namespace Terminal.Gui.ViewBase; + +/// +/// Base implementation of for views that can be run as blocking sessions. +/// +/// The type of result data returned when the session completes. +/// +/// +/// Views can derive from this class or implement directly. +/// +/// +/// This class provides default implementations of the interface +/// following the Terminal.Gui Cancellable Work Pattern (CWP). +/// +/// +/// For views that don't need to return a result, use instead. +/// +/// +/// This class inherits from to avoid code duplication and ensure consistent behavior. +/// +/// +public class Runnable : Runnable, IRunnable +{ + /// + /// Constructs a new instance of the class. + /// + public Runnable () + { + // Base constructor handles common initialization + } + + /// + public new TResult? Result + { + get => base.Result is TResult typedValue ? typedValue : default; + set => base.Result = value; + } + + /// + /// Override to clear typed result when starting. + /// Called by base before events are raised. + /// + protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) + { + // Clear previous typed result when starting + if (newIsRunning) + { + Result = default; + } + + // Call base implementation to allow further customization + return base.OnIsRunningChanging (oldIsRunning, newIsRunning); + } +} diff --git a/Terminal.Gui/ViewBase/RunnableWrapper.cs b/Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs similarity index 97% rename from Terminal.Gui/ViewBase/RunnableWrapper.cs rename to Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs index 6d030c600..bf10b4c0f 100644 --- a/Terminal.Gui/ViewBase/RunnableWrapper.cs +++ b/Terminal.Gui/ViewBase/Runnable/RunnableWrapper.cs @@ -9,7 +9,7 @@ namespace Terminal.Gui.ViewBase; /// /// /// This class enables any View to be run as a blocking session with -/// +/// /// without requiring the View to implement or derive from /// . /// diff --git a/Terminal.Gui/ViewBase/ViewRunnableExtensions.cs b/Terminal.Gui/ViewBase/Runnable/ViewRunnableExtensions.cs similarity index 100% rename from Terminal.Gui/ViewBase/ViewRunnableExtensions.cs rename to Terminal.Gui/ViewBase/Runnable/ViewRunnableExtensions.cs diff --git a/Terminal.Gui/ViewBase/View.Hierarchy.cs b/Terminal.Gui/ViewBase/View.Hierarchy.cs index 0b18e4e3e..729212d1a 100644 --- a/Terminal.Gui/ViewBase/View.Hierarchy.cs +++ b/Terminal.Gui/ViewBase/View.Hierarchy.cs @@ -367,7 +367,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, /// The superview view. internal View? GetTopSuperView (View? view = null, View? superview = null) { - View? top = superview ?? App?.TopRunnable; + View? top = superview ?? App?.TopRunnableView; for (View? v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView) { diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index 27eaaac0e..175c35e34 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -437,9 +437,10 @@ public partial class View // Layout APIs private void NeedsClearScreenNextIteration () { - if (App is { TopRunnable: { } } && App.TopRunnable == this && App.SessionStack.Count == 1) + if (App is { TopRunnableView: { } } && App.TopRunnableView == this + && App.SessionStack!.Select (r => r.Runnable as View).Count() == 1) { - // If this is the only TopLevel, we need to redraw the screen + // If this is the only Runnable, we need to redraw the screen App.ClearScreenNextIteration = true; } } @@ -1113,8 +1114,8 @@ public partial class View // Layout APIs { // TODO: Get rid of refs to Top Size superViewContentSize = SuperView?.GetContentSize () - ?? (App?.TopRunnable is { } && App?.TopRunnable != this && App!.TopRunnable.IsInitialized - ? App.TopRunnable.GetContentSize () + ?? (App?.TopRunnableView is { } && App?.TopRunnableView != this && App!.TopRunnableView.IsInitialized + ? App.TopRunnableView.GetContentSize () : App?.Screen.Size ?? new (2048, 2048)); return superViewContentSize; @@ -1130,7 +1131,7 @@ public partial class View // Layout APIs /// /// /// If does not have a or it's SuperView is not - /// the position will be bound by . + /// the position will be bound by . /// /// The View that is to be moved. /// The target x location. @@ -1138,7 +1139,7 @@ public partial class View // Layout APIs /// The new x location that will ensure will be fully visible. /// The new y location that will ensure will be fully visible. /// - /// Either (if does not have a Super View) or + /// Either (if does not have a Super View) or /// 's SuperView. This can be used to ensure LayoutSubViews is called on the correct View. /// internal static View? GetLocationEnsuringFullVisibility ( @@ -1154,10 +1155,10 @@ public partial class View // Layout APIs IApplication? app = viewToMove.App; - if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnableView || viewToMove?.SuperView == app?.TopRunnableView) { maxDimension = app?.Screen.Width ?? 0; - superView = app?.TopRunnable; + superView = app?.TopRunnableView; } else { @@ -1194,7 +1195,7 @@ public partial class View // Layout APIs ny = Math.Max (targetY, maxDimension); - if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnable || viewToMove?.SuperView == app?.TopRunnable) + if (viewToMove?.SuperView is null || viewToMove == app?.TopRunnableView || viewToMove?.SuperView == app?.TopRunnableView) { if (app is { }) { @@ -1250,7 +1251,7 @@ public partial class View // Layout APIs // PopoverHost - If visible, start with it instead of Top if (App?.Popover?.GetActivePopover () is View { Visible: true } visiblePopover) { - // BUGBUG: We do not traverse all visible toplevels if there's an active popover. This may be a bug. + // BUGBUG: We do not traverse all visible runnables if there's an active popover. This may be a bug. List result = []; result.AddRange (GetViewsUnderLocation (visiblePopover, screenLocation, excludeViewportSettingsFlags)); @@ -1263,14 +1264,14 @@ public partial class View // Layout APIs var checkedTop = false; - // Traverse all visible toplevels, topmost first (reverse stack order) - if (App?.SessionStack.Count > 0) + // Traverse all visible runnables, topmost first (reverse stack order) + if (App?.SessionStack!.Count > 0) { - foreach (Toplevel toplevel in App.SessionStack) + foreach (View? runnable in App.SessionStack!.Select(r => r.Runnable as View)) { - if (toplevel.Visible && toplevel.Contains (screenLocation)) + if (runnable!.Visible && runnable.Contains (screenLocation)) { - List result = GetViewsUnderLocation (toplevel, screenLocation, excludeViewportSettingsFlags); + List result = GetViewsUnderLocation (runnable, screenLocation, excludeViewportSettingsFlags); // Only return if the result is not empty if (result.Count > 0) @@ -1279,17 +1280,17 @@ public partial class View // Layout APIs } } - if (toplevel == App.TopRunnable) + if (runnable == App.TopRunnableView) { checkedTop = true; } } } - // Fallback: If TopLevels is empty or Top is not in TopLevels, check Top directly (for test compatibility) - if (!checkedTop && App?.TopRunnable is { Visible: true } top) + // Fallback: If Runnables is empty or Top is not in Runnables, check Top directly (for test compatibility) + if (!checkedTop && App?.TopRunnableView is { Visible: true } top) { - // For root toplevels, allow hit-testing even if location is outside bounds (for drag/move) + // For root runnables, allow hit-testing even if location is outside bounds (for drag/move) List result = GetViewsUnderLocation (top, screenLocation, excludeViewportSettingsFlags); if (result.Count > 0) diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index fb6f13030..da2ab45d0 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -395,7 +395,7 @@ public partial class View // Focus and cross-view navigation management (TabStop public event EventHandler? FocusedChanged; /// Returns a value indicating if this View is currently on Top (Active) - public bool IsCurrentTop => App?.TopRunnable == this; + public bool IsCurrentTop => App?.TopRunnableView == this; /// /// Returns the most focused SubView down the subview-hierarchy. @@ -854,17 +854,17 @@ public partial class View // Focus and cross-view navigation management (TabStop } // Application.TopRunnable? - if (newFocusedView is null && App?.TopRunnable is { CanFocus: true, HasFocus: false }) + if (newFocusedView is null && App?.TopRunnableView is { CanFocus: true, HasFocus: false }) { // Temporarily ensure this view can't get focus bool prevCanFocus = _canFocus; _canFocus = false; - bool restoredFocus = App?.TopRunnable.RestoreFocus () ?? false; + bool restoredFocus = App?.TopRunnableView.RestoreFocus () ?? false; _canFocus = prevCanFocus; - if (App?.TopRunnable is { CanFocus: true, HasFocus: true }) + if (App?.TopRunnableView is { CanFocus: true, HasFocus: true }) { - newFocusedView = App?.TopRunnable; + newFocusedView = App?.TopRunnableView; } else if (restoredFocus) { diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 094b7a9ba..d61e4f87d 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -281,8 +281,8 @@ public class CharMap : View, IDesignable } } - private void CopyCodePoint () { App?.Clipboard?.SetClipboardData($"U+{SelectedCodePoint:x5}"); } - private void CopyGlyph () { App?.Clipboard?.SetClipboardData($"{new Rune (SelectedCodePoint)}"); } + private void CopyCodePoint () { App?.Clipboard?.SetClipboardData ($"U+{SelectedCodePoint:x5}"); } + private void CopyGlyph () { App?.Clipboard?.SetClipboardData ($"{new Rune (SelectedCodePoint)}"); } private bool? Move (ICommandContext? commandContext, int cpOffset) { @@ -375,8 +375,13 @@ public class CharMap : View, IDesignable waitIndicator.Add (errorLabel); waitIndicator.Add (spinner); - waitIndicator.Ready += async (s, a) => + waitIndicator.IsModalChanged += async (s, a) => { + if (!a.Value) + { + return; + } + try { decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false); diff --git a/Terminal.Gui/Views/Color/ColorBar.cs b/Terminal.Gui/Views/Color/ColorBar.cs index 940798eee..c3590d955 100644 --- a/Terminal.Gui/Views/Color/ColorBar.cs +++ b/Terminal.Gui/Views/Color/ColorBar.cs @@ -83,17 +83,14 @@ internal abstract class ColorBar : View, IColorBar SetNeedsDraw (); } - /// - protected override bool OnDrawingContent () + /// + protected override void OnSubViewsLaidOut (LayoutEventArgs args) { + base.OnSubViewsLaidOut (args); var xOffset = 0; if (!string.IsNullOrWhiteSpace (Text)) { - Move (0, 0); - SetAttribute (HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal)); - AddStr (Text); - // TODO: is there a better method than this? this is what it is in TableView xOffset = Text.EnumerateRunes ().Sum (c => c.GetColumns ()); } @@ -101,7 +98,21 @@ internal abstract class ColorBar : View, IColorBar _barWidth = Viewport.Width - xOffset; _barStartsAt = xOffset; - DrawBar (xOffset, 0, _barWidth); + // Each 1 unit of X in the bar corresponds to this much of Value + _cellValue = (double)MaxValue / (_barWidth - 1); + } + + /// + protected override bool OnDrawingContent () + { + if (!string.IsNullOrWhiteSpace (Text)) + { + Move (0, 0); + SetAttribute (HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal)); + AddStr (Text); + } + + DrawBar (_barStartsAt, 0, _barWidth); return true; } @@ -168,9 +179,6 @@ internal abstract class ColorBar : View, IColorBar private void DrawBar (int xOffset, int yOffset, int width) { - // Each 1 unit of X in the bar corresponds to this much of Value - _cellValue = (double)MaxValue / (width - 1); - for (var x = 0; x < width; x++) { double fraction = (double)x / (width - 1); diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index 11609be67..907305471 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -9,11 +9,12 @@ public partial class ColorPicker /// is false or true, respectively, for /// and colors. /// + /// The instance ot use. /// The title to show in the dialog. /// The current attribute used. /// The new attribute. /// if a new color was accepted, otherwise . - public static bool Prompt (string title, Attribute? currentAttribute, out Attribute newAttribute) + public static bool Prompt (IApplication app, string title, Attribute? currentAttribute, out Attribute newAttribute) { var accept = false; @@ -114,12 +115,12 @@ public partial class ColorPicker d.Add (cpForeground, cpBackground); - Application.Run (d); + app.Run (d); d.Dispose (); Color newForeColor = Application.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor; Color newBackColor = Application.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor; newAttribute = new (newForeColor, newBackColor); - + app.Dispose (); return accept; } } diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs index f0cdc76b9..60badf165 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.cs @@ -34,9 +34,9 @@ public partial class ColorPicker : View, IDesignable private Color _selectedColor = Color.Black; // TODO: Add interface - private readonly IColorNameResolver _colorNameResolver = new MultiStandardColorNameResolver (); + private readonly IColorNameResolver _colorNameResolver = new StandardColorsNameResolver (); - private List _bars = new (); + private List _bars = []; /// /// Rebuild the user interface to reflect the new state of . @@ -47,21 +47,21 @@ public partial class ColorPicker : View, IDesignable DisposeOldViews (); var y = 0; - const int textFieldWidth = 4; + const int TEXT_FIELD_WIDTH = 4; foreach (ColorBar bar in _strategy.CreateBars (Style.ColorModel)) { bar.Y = y; - bar.Width = Dim.Fill (Style.ShowTextFields ? textFieldWidth : 0); + bar.Width = Dim.Fill (Style.ShowTextFields ? TEXT_FIELD_WIDTH : 0); TextField? tfValue = null; if (Style.ShowTextFields) { tfValue = new TextField { - X = Pos.AnchorEnd (textFieldWidth), + X = Pos.AnchorEnd (TEXT_FIELD_WIDTH), Y = y, - Width = textFieldWidth + Width = TEXT_FIELD_WIDTH }; tfValue.HasFocusChanged += UpdateSingleBarValueFromTextField; tfValue.Accepting += (s, _) => UpdateSingleBarValueFromTextField (s); @@ -233,7 +233,10 @@ public partial class ColorPicker : View, IDesignable } } - private void RebuildColorFromBar (object? sender, EventArgs e) { SetSelectedColor (_strategy.GetColorFromBars (_bars, Style.ColorModel), false); } + private void RebuildColorFromBar (object? sender, EventArgs e) + { + SetSelectedColor (_strategy.GetColorFromBars (_bars, Style.ColorModel), false); + } private void SetSelectedColor (Color value, bool syncBars) { @@ -242,9 +245,7 @@ public partial class ColorPicker : View, IDesignable Color old = _selectedColor; _selectedColor = value; - ColorChanged?.Invoke ( - this, - new (value)); + ColorChanged?.Invoke (this, new (value)); } SyncSubViewValues (syncBars); @@ -290,15 +291,16 @@ public partial class ColorPicker : View, IDesignable } private void UpdateSingleBarValueFromTextField (object? sender) { - foreach (KeyValuePair kvp in _textFields) { - if (kvp.Value == sender) + if (kvp.Value != sender) { - if (int.TryParse (kvp.Value.Text, out int v)) - { - kvp.Key.Value = v; - } + continue; + } + + if (int.TryParse (kvp.Value.Text, out int v)) + { + kvp.Key.Value = v; } } } @@ -314,6 +316,7 @@ public partial class ColorPicker : View, IDesignable // it is a leave event so update UpdateValueFromName (); } + private void UpdateValueFromName () { if (_tfName == null) diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 03797368d..ddb813f7a 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,13 +1,13 @@ namespace Terminal.Gui.Views; /// -/// A . Supports a simple API for adding s +/// Supports a simple API for adding s /// across the bottom. By default, the is centered and used the /// scheme. /// /// /// To run the modally, create the , and pass it to -/// . This will execute the dialog until +/// . This will execute the dialog until /// it terminates via the (`Esc` by default), /// or when one of the views or buttons added to the dialog calls /// . @@ -27,7 +27,7 @@ public class Dialog : Window /// /// By default, , , , and are /// set - /// such that the will be centered in, and no larger than 90% of , if + /// such that the will be centered in, and no larger than 90% of , if /// there is one. Otherwise, /// it will be bound by the screen dimensions. /// @@ -44,7 +44,6 @@ public class Dialog : Window SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog); - Modal = true; ButtonAlignment = DefaultButtonAlignment; ButtonAlignmentModes = DefaultButtonAlignmentModes; } diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index b0dfeb9d6..afaa624c8 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -51,7 +51,6 @@ public class FileDialog : Dialog, IDesignable private bool _currentSortIsAsc = true; private bool _disposed; private string? _feedback; - private bool _loaded; private bool _pushingState; private Dictionary _treeRoots = new (); @@ -106,7 +105,7 @@ public class FileDialog : Dialog, IDesignable e.Handled = true; - if (Modal) + if (IsModal) { (s as View)?.App?.RequestStop (); } @@ -436,19 +435,17 @@ public class FileDialog : Dialog, IDesignable } /// - public override void OnLoaded () + protected override void OnIsRunningChanged (bool newIsRunning) { - base.OnLoaded (); + base.OnIsRunningChanged (newIsRunning); - if (_loaded) + if (!newIsRunning) { return; } Arrangement |= ViewArrangement.Resizable; - _loaded = true; - // May have been updated after instance was constructed _btnOk.Text = Style.OkButtonText; _btnCancel.Text = Style.CancelButtonText; @@ -877,7 +874,7 @@ public class FileDialog : Dialog, IDesignable Canceled = false; - if (Modal) + if (IsModal) { App?.RequestStop (); } @@ -1649,9 +1646,7 @@ public class FileDialog : Dialog, IDesignable bool IDesignable.EnableForDesign () { - Modal = false; - OnLoaded (); - + OnIsRunningChanged (true); return true; } } \ No newline at end of file diff --git a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs index 0940436d2..3d053b06a 100644 --- a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs @@ -18,15 +18,15 @@ namespace Terminal.Gui.Views; /// /// /// The open dialog can be used to select files for opening, it can be configured to allow multiple items to be -/// selected (based on the AllowsMultipleSelection) variable and you can control whether this should allow files or +/// selected (based on the AllowsMultipleSelection) variable, and you can control whether this should allow files or /// directories to be selected. /// /// /// To use, create an instance of , and pass it to -/// . This will run the dialog modally, and when this returns, +/// . This will run the dialog modally, and when this returns, /// the list of files will be available on the property. /// -/// To select more than one file, users can use the spacebar, or control-t. +/// To select more than one file, users can use the space key, or CTRL-T. /// public class OpenDialog : FileDialog { diff --git a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs index 6e0da27e2..5ed07726a 100644 --- a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs @@ -18,7 +18,7 @@ namespace Terminal.Gui.Views; /// /// /// To use, create an instance of , and pass it to -/// . This will run the dialog modally, and when this returns, +/// . This will run the dialog modally, and when this returns, /// the property will contain the selected file name or null if the user canceled. /// /// diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index e9e874285..e6d3cebd3 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -114,7 +114,7 @@ public static class MessageBox /// /// Displays an error with fixed dimensions. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -153,7 +153,7 @@ public static class MessageBox /// /// Displays an auto-sized error . /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines and will be word-wrapped. /// Array of button labels. @@ -182,7 +182,7 @@ public static class MessageBox /// /// Displays an error with fixed dimensions and a default button. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -223,7 +223,7 @@ public static class MessageBox /// /// Displays an auto-sized error with a default button. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines and will be word-wrapped. /// Index of the default button (0-based). @@ -253,7 +253,7 @@ public static class MessageBox /// /// Displays an error with fixed dimensions, a default button, and word-wrap control. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -299,7 +299,7 @@ public static class MessageBox /// /// Displays an auto-sized error with a default button and word-wrap control. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines. /// Index of the default button (0-based). @@ -340,7 +340,7 @@ public static class MessageBox /// /// Displays a with fixed dimensions. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -372,7 +372,7 @@ public static class MessageBox /// /// Displays an auto-sized . /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines and will be word-wrapped. /// Array of button labels. @@ -401,7 +401,7 @@ public static class MessageBox /// /// Displays a with fixed dimensions and a default button. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -442,7 +442,7 @@ public static class MessageBox /// /// Displays an auto-sized with a default button. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines and will be word-wrapped. /// Index of the default button (0-based). @@ -472,7 +472,7 @@ public static class MessageBox /// /// Displays a with fixed dimensions, a default button, and word-wrap control. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Width for the MessageBox. /// Height for the MessageBox. /// Title for the MessageBox. @@ -518,7 +518,7 @@ public static class MessageBox /// /// Displays an auto-sized with a default button and word-wrap control. /// - /// The application instance. If , uses . + /// The application instance. If , uses . /// Title for the MessageBox. /// Message to display. May contain multiple lines. /// Index of the default button (0-based). diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index a0b7c5683..561fb4bae 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -107,7 +107,7 @@ public class Shortcut : View, IOrientation, IDesignable public override void EndInit () { base.EndInit (); - App ??= SuperView?.App; + App ??= SuperView?.App; // HACK: Remove once legacy static Application is gone Debug.Assert (App is { }); UpdateKeyBindings (Key.Empty); } @@ -634,7 +634,8 @@ public class Shortcut : View, IOrientation, IDesignable get => _bindKeyToApplication; set { - App ??= SuperView?.App; + App ??= SuperView?.App ?? ApplicationImpl.Instance; // HACK: Remove once legacy static Application is gone + Debug.Assert (App is { }); if (value == _bindKeyToApplication) { diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 5191a90e8..a964058a2 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -1,8 +1,8 @@ namespace Terminal.Gui.Views; /// -/// A status bar is a that snaps to the bottom of a displaying set of -/// s. The should be context sensitive. This means, if the main menu +/// A status bar is a that snaps to the bottom of the Viewport displaying set of +/// s. The should be context-sensitive. This means, if the main menu /// and an open text editor are visible, the items probably shown will be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog /// to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a /// new instance of a status bar. diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index f401c500b..ae5b3d89e 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -111,7 +111,7 @@ public class TabView : View if (view is { CanFocus: true, Enabled: true, Visible: true }) { - // Let toplevel handle it + // Let runnable handle it return false; } } @@ -145,7 +145,7 @@ public class TabView : View if (view is { CanFocus: true, Enabled: true, Visible: true }) { - // Let toplevel handle it + // Let runnable handle it return false; } } diff --git a/Terminal.Gui/Views/TextInput/TextView.cs b/Terminal.Gui/Views/TextInput/TextView.cs index 23fd1e89a..b16c85da3 100644 --- a/Terminal.Gui/Views/TextInput/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView.cs @@ -1121,6 +1121,7 @@ public class TextView : View, IDesignable public void PromptForColors () { if (!ColorPicker.Prompt ( + App!, "Colors", GetSelectedCellAttribute (), out Attribute newAttribute diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs deleted file mode 100644 index e3feb8ef9..000000000 --- a/Terminal.Gui/Views/Toplevel.cs +++ /dev/null @@ -1,331 +0,0 @@ -namespace Terminal.Gui.Views; - -/// -/// Toplevel views are used for both an application's main view (filling the entire screen and for modal (pop-up) -/// views such as , , and ). -/// -/// -/// -/// Toplevel views can run as modal (popup) views, started by calling -/// . They return control to the caller when -/// has been called (which sets the -/// property to false). -/// -/// -/// A Toplevel is created when an application initializes Terminal.Gui by calling . -/// The application Toplevel can be accessed via . Additional Toplevels can be created -/// and run (e.g. s). To run a Toplevel, create the and call -/// . -/// -/// -public partial class Toplevel : View -{ - /// - /// Initializes a new instance of the class, - /// defaulting to full screen. The and properties will be set to the - /// dimensions of the terminal using . - /// - public Toplevel () - { - CanFocus = true; - TabStop = TabBehavior.TabGroup; - Arrangement = ViewArrangement.Overlapped; - Width = Dim.Fill (); - Height = Dim.Fill (); - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Toplevel); - - MouseClick += Toplevel_MouseClick; - } - - #region Keyboard & Mouse - - // TODO: IRunnable: Re-implement - Modal means IRunnable, ViewArrangement.Overlapped where modalView.Z > allOtherViews.Max (v = v.Z), and exclusive key/mouse input. - /// - /// Determines whether the is modal or not. If set to false (the default): - /// - /// - /// events will propagate keys upwards. - /// - /// - /// The Toplevel will act as an embedded view (not a modal/pop-up). - /// - /// - /// If set to true: - /// - /// - /// events will NOT propagate keys upwards. - /// - /// - /// The Toplevel will and look like a modal (pop-up) (e.g. see . - /// - /// - /// - public bool Modal { get; set; } - - private void Toplevel_MouseClick (object? sender, MouseEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; } - - #endregion - - #region SubViews - - //// TODO: Deprecate - Any view can host a menubar in v2 - ///// Gets the latest added into this Toplevel. - //public MenuBar? MenuBar => (MenuBar?)SubViews?.LastOrDefault (s => s is MenuBar); - - #endregion - - #region Life Cycle - - // TODO: IRunnable: Re-implement as a property on IRunnable - /// Gets or sets whether the main loop for this is running or not. - /// Setting this property directly is discouraged. Use instead. - public bool Running { get; set; } - - // TODO: Deprecate. Other than a few tests, this is not used anywhere. - /// - /// if was already loaded by the - /// , otherwise. - /// - public bool IsLoaded { get; private set; } - - // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Activating/Activate - /// Invoked when the Toplevel active. - public event EventHandler? Activate; - - // TODO: IRunnable: Re-implement as an event on IRunnable; IRunnable.Deactivating/Deactivate? - /// Invoked when the Toplevel ceases to be active. - public event EventHandler? Deactivate; - - /// Invoked when the Toplevel's is closed by . - public event EventHandler? Closed; - - /// - /// Invoked when the Toplevel's is being closed by - /// . - /// - public event EventHandler? Closing; - - /// - /// Invoked when the has begun to be loaded. A Loaded event handler - /// is a good place to finalize initialization before calling Run. - /// - public event EventHandler? Loaded; - - /// - /// Called from before the redraws for the first - /// time. - /// - /// - /// Overrides must call base.OnLoaded() to ensure any Toplevel subviews are initialized properly and the - /// event is raised. - /// - public virtual void OnLoaded () - { - IsLoaded = true; - - foreach (var view in SubViews.Where (v => v is Toplevel)) - { - var tl = (Toplevel)view; - tl.OnLoaded (); - } - - Loaded?.Invoke (this, EventArgs.Empty); - } - - /// - /// Invoked when the main loop has started it's first iteration. Subscribe to this event to - /// perform tasks when the has been laid out and focus has been set. changes. - /// - /// A Ready event handler is a good place to finalize initialization after calling - /// on this . - /// - /// - public event EventHandler? Ready; - - /// - /// Stops and closes this . If this Toplevel is the top-most Toplevel, - /// will be called, causing the application to exit. - /// - public virtual void RequestStop () - { - App?.RequestStop (App?.TopRunnable); - } - - /// - /// Invoked when the Toplevel has been unloaded. A Unloaded event handler is a good place - /// to dispose objects after calling . - /// - public event EventHandler? Unloaded; - - internal virtual void OnActivate (Toplevel deactivated) { Activate?.Invoke (this, new (deactivated)); } - - internal virtual void OnClosed (Toplevel top) { Closed?.Invoke (this, new (top)); } - - internal virtual bool OnClosing (ToplevelClosingEventArgs ev) - { - Closing?.Invoke (this, ev); - - return ev.Cancel; - } - - internal virtual void OnDeactivate (Toplevel activated) { Deactivate?.Invoke (this, new (activated)); } - - /// - /// Called from run loop after the has entered the first iteration - /// of the loop. - /// - internal virtual void OnReady () - { - foreach (var view in SubViews.Where (v => v is Toplevel)) - { - var tl = (Toplevel)view; - tl.OnReady (); - } - - Ready?.Invoke (this, EventArgs.Empty); - } - - /// Called from before the is disposed. - internal virtual void OnUnloaded () - { - foreach (var view in SubViews.Where (v => v is Toplevel)) - { - var tl = (Toplevel)view; - tl.OnUnloaded (); - } - - Unloaded?.Invoke (this, EventArgs.Empty); - } - - #endregion - - #region Size / Position Management - - // TODO: Make cancelable? - internal void OnSizeChanging (SizeChangedEventArgs size) { SizeChanging?.Invoke (this, size); } - - /// - /// Adjusts the location and size of within this Toplevel. Virtual method enabling - /// implementation of specific positions for inherited views. - /// - /// The Toplevel to adjust. - public virtual void PositionToplevel (Toplevel? top) - { - if (top is null) - { - return; - } - - View? superView = GetLocationEnsuringFullVisibility ( - top, - top.Frame.X, - top.Frame.Y, - out int nx, - out int ny - //, - // out StatusBar? sb - ); - - if (superView is null) - { - return; - } - - //var layoutSubViews = false; - var maxWidth = 0; - - if (superView.Margin is { } && superView == top.SuperView) - { - maxWidth -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; - } - - // BUGBUG: The && true is a temp hack - if ((superView != top || top?.SuperView is { } || (top != App?.TopRunnable && top!.Modal) || (top == App?.TopRunnable && top?.SuperView is null)) - && (top!.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y)) - - { - if (top?.X is null or PosAbsolute && top?.Frame.X != nx) - { - top!.X = nx; - //layoutSubViews = true; - } - - if (top?.Y is null or PosAbsolute && top?.Frame.Y != ny) - { - top!.Y = ny; - //layoutSubViews = true; - } - } - - - //if (superView.IsLayoutNeeded () || layoutSubViews) - //{ - // superView.LayoutSubViews (); - //} - - //if (IsLayoutNeeded ()) - //{ - // LayoutSubViews (); - //} - } - - /// Invoked when the terminal has been resized. The new of the terminal is provided. - public event EventHandler? SizeChanging; - - #endregion -} - -/// -/// Implements the for comparing two s used by -/// . -/// -public class ToplevelEqualityComparer : IEqualityComparer -{ - /// Determines whether the specified objects are equal. - /// The first object of type to compare. - /// The second object of type to compare. - /// if the specified objects are equal; otherwise, . - public bool Equals (Toplevel? x, Toplevel? y) - { - if (y is null && x is null) - { - return true; - } - - if (x is null || y is null) - { - return false; - } - - if (x.Id == y.Id) - { - return true; - } - - return false; - } - - /// Returns a hash code for the specified object. - /// The for which a hash code is to be returned. - /// A hash code for the specified object. - /// - /// The type of is a reference type and - /// is . - /// - public int GetHashCode (Toplevel obj) - { - if (obj is null) - { - throw new ArgumentNullException (); - } - - var hCode = 0; - - if (int.TryParse (obj.Id, out int result)) - { - hCode = result; - } - - return hCode.GetHashCode (); - } -} diff --git a/Terminal.Gui/Views/ToplevelEventArgs.cs b/Terminal.Gui/Views/ToplevelEventArgs.cs deleted file mode 100644 index 52e02d0c8..000000000 --- a/Terminal.Gui/Views/ToplevelEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable disable - -namespace Terminal.Gui.Views; - -/// Args for events that relate to a specific . -public class ToplevelEventArgs : EventArgs -{ - /// Creates a new instance of the class. - /// - public ToplevelEventArgs (Toplevel toplevel) { Toplevel = toplevel; } - - /// Gets the that the event is about. - /// - /// This is usually but may not always be the same as the sender in . For example if - /// the reported event is about a different or the event is raised by a separate class. - /// - public Toplevel Toplevel { get; } -} - -/// implementation for the event. -public class ToplevelClosingEventArgs : EventArgs -{ - /// Initializes the event arguments with the requesting Toplevel. - /// The . - public ToplevelClosingEventArgs (Toplevel requestingTop) { RequestingTop = requestingTop; } - - /// Provides an event cancellation option. - public bool Cancel { get; set; } - - /// The Toplevel requesting stop. - public View RequestingTop { get; } -} diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index 597ba121d..775612dde 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -14,7 +14,7 @@ namespace Terminal.Gui.Views; /// /// /// -public class Window : Toplevel +public class Window : Runnable { private static ShadowStyle _defaultShadow = ShadowStyle.None; // Resources/config.json overrides private static LineStyle _defaultBorderStyle = LineStyle.Single; // Resources/config.json overrides diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 3415c572a..ef1375be2 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -8,8 +8,7 @@ namespace Terminal.Gui.Views; /// /// /// The Wizard can be displayed either as a modal (pop-up) (like ) or as -/// an embedded . By default, is true. In this case launch the -/// Wizard with Application.Run(wizard). See for more details. +/// an embedded . /// /// /// @@ -86,8 +85,8 @@ public class Wizard : Dialog BackButton.Accepting += BackBtn_Accepting; NextFinishButton.Accepting += NextFinishBtn_Accepting; - Loaded += Wizard_Loaded; - Closing += Wizard_Closing; + IsModalChanged += Wizard_IsModalChanged; + IsRunningChanged += Wizard_IsRunningChanged; TitleChanged += Wizard_TitleChanged; SetNeedsLayout (); @@ -107,51 +106,51 @@ public class Wizard : Dialog set => GoToStep (value); } - /// - /// Determines whether the is displayed as modal pop-up or not. The default is - /// . The Wizard will be shown with a frame and title and will behave like any - /// window. If set to false the Wizard will have no frame and will behave like any - /// embedded . To use Wizard as an embedded View - /// - /// - /// Set to false. - /// - /// - /// Add the Wizard to a containing view with . - /// - /// - /// If a non-Modal Wizard is added to the application after - /// has - /// been called the first step must be explicitly set by setting to - /// : - /// - /// wizard.CurrentStep = wizard.GetNextStep(); - /// - /// - public new bool Modal - { - get => base.Modal; - set - { - base.Modal = value; + ///// + ///// Determines whether the is displayed as modal pop-up or not. The default is + ///// . The Wizard will be shown with a frame and title and will behave like any + ///// window. If set to false the Wizard will have no frame and will behave like any + ///// embedded . To use Wizard as an embedded View + ///// + ///// + ///// Set to false. + ///// + ///// + ///// Add the Wizard to a containing view with . + ///// + ///// + ///// If a non-Modal Wizard is added to the application after + ///// has + ///// been called the first step must be explicitly set by setting to + ///// : + ///// + ///// wizard.CurrentStep = wizard.GetNextStep(); + ///// + ///// + //public new bool Modal + //{ + // get => base.Modal; + // set + // { + // base.Modal = value; - foreach (WizardStep step in _steps) - { - SizeStep (step); - } + // foreach (WizardStep step in _steps) + // { + // SizeStep (step); + // } - if (base.Modal) - { - SchemeName = "Dialog"; - BorderStyle = LineStyle.Rounded; - } - else - { - CanFocus = true; - BorderStyle = LineStyle.None; - } - } - } + // if (base.Modal) + // { + // SchemeName = "Dialog"; + // BorderStyle = LineStyle.Rounded; + // } + // else + // { + // CanFocus = true; + // BorderStyle = LineStyle.None; + // } + // } + //} /// /// If the is the last step in the wizard, this button causes the @@ -183,7 +182,6 @@ public class Wizard : Dialog /// /// Raised when the user has cancelled the by pressing the Esc key. To prevent a modal ( - /// is true) Wizard from closing, cancel the event by setting /// to true before returning from the event handler. /// public event EventHandler? Cancelled; @@ -372,16 +370,16 @@ public class Wizard : Dialog /// /// is derived from and Dialog causes Esc to call - /// , closing the Dialog. Wizard overrides + /// , closing the Dialog. Wizard overrides /// to instead fire the event when Wizard is being used as a - /// non-modal (see ). + /// non-modal. /// /// /// protected override bool OnKeyDownNotHandled (Key key) { - //// BUGBUG: Why is this not handled by a key binding??? - if (!Modal) + // BUGBUG: Why is this not handled by a key binding??? + if (!IsModal) { if (key == Key.Esc) { @@ -480,7 +478,7 @@ public class Wizard : Dialog private void SizeStep (WizardStep step) { - if (Modal) + if (IsModal) { // If we're modal, then we expand the WizardStep so that the top and side // borders and not visible. The bottom border is the separator above the buttons. @@ -541,20 +539,22 @@ public class Wizard : Dialog SetNeedsLayout (); } - private void Wizard_Closing (object? sender, ToplevelClosingEventArgs obj) + private void Wizard_IsRunningChanged (object? sender, EventArgs args) { if (!_finishedPressed) { - var args = new WizardButtonEventArgs (); - Cancelled?.Invoke (this, args); + var a = new WizardButtonEventArgs (); + Cancelled?.Invoke (this, a); } } - private void Wizard_Loaded (object? sender, EventArgs args) + private void Wizard_IsModalChanged (object? sender, EventArgs args) { - CurrentStep = GetFirstStep (); - - // gets the first step if CurrentStep == null + if (args.Value) + { + CurrentStep = GetFirstStep (); + // gets the first step if CurrentStep == null + } } private void Wizard_TitleChanged (object? sender, EventArgs e) diff --git a/Terminal.sln b/Terminal.sln index b2fb285cd..aacab4c01 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -40,6 +40,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C .github\workflows\integration-tests.yml = .github\workflows\integration-tests.yml .github\workflows\publish.yml = .github\workflows\publish.yml .github\workflows\quick-build.yml = .github\workflows\quick-build.yml + .github\workflows\README.md = .github\workflows\README.md .github\workflows\stress-tests.yml = .github\workflows\stress-tests.yml .github\workflows\unit-tests.yml = .github\workflows\unit-tests.yml EndProjectSection diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index e6cac2cdd..44cd244e2 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -426,7 +426,7 @@ True True True - True + True True True True diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 50aba981c..e2b9e266b 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -42,15 +42,15 @@ public class FileDialogFluentTests return mockFileSystem; } - private Toplevel NewSaveDialog (out SaveDialog sd, bool modal = true) + private IRunnable NewSaveDialog (out SaveDialog sd) { - return NewSaveDialog (out sd, out _, modal); + return NewSaveDialog (out sd, out _); } - private Toplevel NewSaveDialog (out SaveDialog sd, out MockFileSystem fs, bool modal = true) + private IRunnable NewSaveDialog (out SaveDialog sd, out MockFileSystem fs) { fs = CreateExampleFileSystem (); - sd = new SaveDialog (fs) { Modal = modal }; + sd = new SaveDialog (fs) { }; return sd; } @@ -71,7 +71,7 @@ public class FileDialogFluentTests public void CancelFileDialog_UsingCancelButton_TabThenEnter (TestDriver d) { SaveDialog? sd = null; - using var c = With.A (() => NewSaveDialog (out sd, modal: false), 100, 20, d) + using var c = With.A (() => NewSaveDialog (out sd), 100, 20, d) .ScreenShot ("Save dialog", _out) .Focus private void CommonInit (int width, int height, TestDriver driverType, TimeSpan? timeout) { - _timeout = timeout ?? TimeSpan.FromSeconds (10); + _timeout = timeout ?? TimeSpan.FromSeconds (30); _originalLogger = Logging.Logger; _logsSb = new (); _driverType = driverType; @@ -398,7 +407,10 @@ public partial class GuiTestContext : IDisposable /// new Width for the console. /// new Height for the console. /// - public GuiTestContext ResizeConsole (int width, int height) { return WaitIteration ((app) => { app.Driver!.SetScreenSize (width, height); }); } + public GuiTestContext ResizeConsole (int width, int height) + { + return WaitIteration ((app) => { app.Driver!.SetScreenSize (width, height); }); + } public GuiTestContext ScreenShot (string title, TextWriter? writer) { @@ -438,7 +450,7 @@ public partial class GuiTestContext : IDisposable { try { - App?.Shutdown (); + App?.Dispose (); } catch { @@ -467,7 +479,7 @@ public partial class GuiTestContext : IDisposable try { App?.RequestStop (); - App?.Shutdown (); + App?.Dispose (); } catch (Exception ex) { @@ -533,6 +545,7 @@ public partial class GuiTestContext : IDisposable internal void Fail (string reason) { Logging.Error ($"{reason}"); + WriteOutLogs (_logWriter); throw new (reason); } diff --git a/Tests/TerminalGuiFluentTesting/With.cs b/Tests/TerminalGuiFluentTesting/With.cs index 48c9fcbad..71abdea16 100644 --- a/Tests/TerminalGuiFluentTesting/With.cs +++ b/Tests/TerminalGuiFluentTesting/With.cs @@ -14,7 +14,7 @@ public static class With /// Which v2 testDriver to use for the test /// /// - public static GuiTestContext A (int width, int height, TestDriver testDriver, TextWriter? logWriter = null) where T : Toplevel, new() + public static GuiTestContext A (int width, int height, TestDriver testDriver, TextWriter? logWriter = null) where T : IRunnable, new() { return new (() => new T () { @@ -23,17 +23,17 @@ public static class With } /// - /// Overload that takes a function to create instance after application is initialized. + /// Overload that takes a function to create instance after application is initialized. /// - /// + /// /// /// /// /// /// - public static GuiTestContext A (Func toplevelFactory, int width, int height, TestDriver testDriver, TextWriter? logWriter = null) + public static GuiTestContext A (Func runnableFactory, int width, int height, TestDriver testDriver, TextWriter? logWriter = null) { - return new (toplevelFactory, width, height, testDriver, logWriter, Timeout); + return new (runnableFactory, width, height, testDriver, logWriter, Timeout); } /// /// The global timeout to allow for any given application to run for before shutting down. diff --git a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs b/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs deleted file mode 100644 index b1216b3bd..000000000 --- a/Tests/UnitTests/Application/ApplicationImplBeginEndTests.cs +++ /dev/null @@ -1,503 +0,0 @@ -#nullable enable -using Xunit.Abstractions; - -namespace UnitTests.ApplicationTests; - -/// -/// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack. -/// These tests ensure the fragile state management logic is robust and catches regressions. -/// Tests work directly with ApplicationImpl instances to avoid global Application state issues. -/// -public class ApplicationImplBeginEndTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - private IApplication NewApplicationImpl () - { - IApplication app = ApplicationImpl.Instance; // Force legacy - - return app; - } - - [Fact] - public void Begin_WithNullToplevel_ThrowsArgumentNullException () - { - IApplication app = NewApplicationImpl (); - - try - { - Assert.Throws (() => app.Begin ((Toplevel)null!)); - } - finally - { - app.Shutdown (); - } - } - - [Fact] - public void Begin_SetsCurrent_WhenCurrentIsNull () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel = null; - - try - { - toplevel = new (); - Assert.Null (app.TopRunnable); - - app.Begin (toplevel); - - Assert.NotNull (app.TopRunnable); - Assert.Same (toplevel, app.TopRunnable); - Assert.Single (app.SessionStack); - } - finally - { - toplevel?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void Begin_PushesToSessionStack () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - - app.Begin (toplevel1); - Assert.Single (app.SessionStack); - Assert.Same (toplevel1, app.TopRunnable); - - app.Begin (toplevel2); - Assert.Equal (2, app.SessionStack.Count); - Assert.Same (toplevel2, app.TopRunnable); - } - finally - { - toplevel1?.Dispose (); - toplevel2?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void Begin_SetsUniqueToplevelId_WhenIdIsEmpty () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - Toplevel? toplevel3 = null; - - try - { - toplevel1 = new (); - toplevel2 = new (); - toplevel3 = new (); - - Assert.Empty (toplevel1.Id); - Assert.Empty (toplevel2.Id); - Assert.Empty (toplevel3.Id); - - app.Begin (toplevel1); - app.Begin (toplevel2); - app.Begin (toplevel3); - - Assert.NotEmpty (toplevel1.Id); - Assert.NotEmpty (toplevel2.Id); - Assert.NotEmpty (toplevel3.Id); - - // IDs should be unique - Assert.NotEqual (toplevel1.Id, toplevel2.Id); - Assert.NotEqual (toplevel2.Id, toplevel3.Id); - Assert.NotEqual (toplevel1.Id, toplevel3.Id); - } - finally - { - toplevel1?.Dispose (); - toplevel2?.Dispose (); - toplevel3?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void End_WithNullSessionToken_ThrowsArgumentNullException () - { - IApplication app = NewApplicationImpl (); - - try - { - Assert.Throws (() => app.End ((SessionToken)null!)); - } - finally - { - app.Shutdown (); - } - } - - [Fact] - public void End_PopsSessionStack () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - - SessionToken token1 = app.Begin (toplevel1); - SessionToken token2 = app.Begin (toplevel2); - - Assert.Equal (2, app.SessionStack.Count); - - app.End (token2); - - Assert.Single (app.SessionStack); - Assert.Same (toplevel1, app.TopRunnable); - - app.End (token1); - - Assert.Empty (app.SessionStack); - } - finally - { - toplevel1?.Dispose (); - toplevel2?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void End_ThrowsArgumentException_WhenNotBalanced () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - - SessionToken token1 = app.Begin (toplevel1); - SessionToken token2 = app.Begin (toplevel2); - - // Trying to end token1 when token2 is on top should throw - // NOTE: This throws but has the side effect of popping token2 from the stack - Assert.Throws (() => app.End (token1)); - - // Don't try to clean up with more End calls - the state is now inconsistent - // Let Shutdown/ResetState handle cleanup - } - finally - { - // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions - toplevel1?.Dispose (); - toplevel2?.Dispose (); - - // Shutdown will call ResetState which clears any remaining state - app.Shutdown (); - } - } - - [Fact] - public void End_RestoresCurrentToPreviousToplevel () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - Toplevel? toplevel3 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - toplevel3 = new () { Id = "3" }; - - SessionToken token1 = app.Begin (toplevel1); - SessionToken token2 = app.Begin (toplevel2); - SessionToken token3 = app.Begin (toplevel3); - - Assert.Same (toplevel3, app.TopRunnable); - - app.End (token3); - Assert.Same (toplevel2, app.TopRunnable); - - app.End (token2); - Assert.Same (toplevel1, app.TopRunnable); - - app.End (token1); - } - finally - { - toplevel1?.Dispose (); - toplevel2?.Dispose (); - toplevel3?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void MultipleBeginEnd_MaintainsStackIntegrity () - { - IApplication app = NewApplicationImpl (); - List toplevels = new (); - List tokens = new (); - - try - { - // Begin multiple toplevels - for (var i = 0; i < 5; i++) - { - var toplevel = new Toplevel { Id = $"toplevel-{i}" }; - toplevels.Add (toplevel); - tokens.Add (app.Begin (toplevel)); - } - - Assert.Equal (5, app.SessionStack.Count); - Assert.Same (toplevels [4], app.TopRunnable); - - // End them in reverse order (LIFO) - for (var i = 4; i >= 0; i--) - { - app.End (tokens [i]); - - if (i > 0) - { - Assert.Equal (i, app.SessionStack.Count); - Assert.Same (toplevels [i - 1], app.TopRunnable); - } - else - { - Assert.Empty (app.SessionStack); - } - } - } - finally - { - foreach (Toplevel toplevel in toplevels) - { - toplevel.Dispose (); - } - - app.Shutdown (); - } - } - - [Fact] - public void End_UpdatesCachedSessionTokenToplevel () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel = null; - - try - { - toplevel = new (); - - SessionToken token = app.Begin (toplevel); - Assert.Null (app.CachedSessionTokenToplevel); - - app.End (token); - - Assert.Same (toplevel, app.CachedSessionTokenToplevel); - } - finally - { - toplevel?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void End_NullsSessionTokenToplevel () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel = null; - - try - { - toplevel = new (); - - SessionToken token = app.Begin (toplevel); - Assert.Same (toplevel, token.Toplevel); - - app.End (token); - - Assert.Null (token.Toplevel); - } - finally - { - toplevel?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void ResetState_ClearsSessionStack () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - - app.Begin (toplevel1); - app.Begin (toplevel2); - - Assert.Equal (2, app.SessionStack.Count); - Assert.NotNull (app.TopRunnable); - } - finally - { - // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions - toplevel1?.Dispose (); - toplevel2?.Dispose (); - - // Shutdown calls ResetState, which will clear SessionStack and set Current to null - app.Shutdown (); - - // Verify cleanup happened - Assert.Empty (app.SessionStack); - Assert.Null (app.TopRunnable); - Assert.Null (app.CachedSessionTokenToplevel); - } - } - - [Fact] - public void ResetState_StopsAllRunningToplevels () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1", Running = true }; - toplevel2 = new () { Id = "2", Running = true }; - - app.Begin (toplevel1); - app.Begin (toplevel2); - - Assert.True (toplevel1.Running); - Assert.True (toplevel2.Running); - } - finally - { - // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions - toplevel1?.Dispose (); - toplevel2?.Dispose (); - - // Shutdown calls ResetState, which will stop all running toplevels - app.Shutdown (); - - // Verify toplevels were stopped - Assert.False (toplevel1!.Running); - Assert.False (toplevel2!.Running); - } - } - - [Fact] - public void Begin_ActivatesNewToplevel_WhenCurrentExists () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel1 = null; - Toplevel? toplevel2 = null; - - try - { - toplevel1 = new () { Id = "1" }; - toplevel2 = new () { Id = "2" }; - - var toplevel1Deactivated = false; - var toplevel2Activated = false; - - toplevel1.Deactivate += (s, e) => toplevel1Deactivated = true; - toplevel2.Activate += (s, e) => toplevel2Activated = true; - - app.Begin (toplevel1); - app.Begin (toplevel2); - - Assert.True (toplevel1Deactivated); - Assert.True (toplevel2Activated); - Assert.Same (toplevel2, app.TopRunnable); - } - finally - { - toplevel1?.Dispose (); - toplevel2?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void Begin_DoesNotDuplicateToplevel_WhenIdAlreadyExists () - { - IApplication app = NewApplicationImpl (); - Toplevel? toplevel = null; - - try - { - toplevel = new () { Id = "test-id" }; - - app.Begin (toplevel); - Assert.Single (app.SessionStack); - - // Calling Begin again with same toplevel should not duplicate - app.Begin (toplevel); - Assert.Single (app.SessionStack); - } - finally - { - toplevel?.Dispose (); - app.Shutdown (); - } - } - - [Fact] - public void SessionStack_ContainsAllBegunToplevels () - { - IApplication app = NewApplicationImpl (); - List toplevels = new (); - - try - { - for (var i = 0; i < 10; i++) - { - var toplevel = new Toplevel { Id = $"toplevel-{i}" }; - toplevels.Add (toplevel); - app.Begin (toplevel); - } - - // All toplevels should be in the stack - Assert.Equal (10, app.SessionStack.Count); - - // Verify stack contains all toplevels - List stackList = app.SessionStack.ToList (); - - foreach (Toplevel toplevel in toplevels) - { - Assert.Contains (toplevel, stackList); - } - } - finally - { - foreach (Toplevel toplevel in toplevels) - { - toplevel.Dispose (); - } - - app.Shutdown (); - } - } -} diff --git a/Tests/UnitTests/Application/ApplicationModelFencingTests.cs b/Tests/UnitTests/Application/ApplicationModelFencingTests.cs index a9009baa7..f133c3db4 100644 --- a/Tests/UnitTests/Application/ApplicationModelFencingTests.cs +++ b/Tests/UnitTests/Application/ApplicationModelFencingTests.cs @@ -4,104 +4,105 @@ namespace UnitTests.ApplicationTests; /// Tests to ensure that mixing legacy static Application and modern instance-based models /// throws appropriate exceptions. /// -[Collection ("Global Test Setup")] public class ApplicationModelFencingTests { - public ApplicationModelFencingTests () - { - // Reset the model usage tracking before each test - ApplicationImpl.ResetModelUsageTracking (); - } - [Fact] public void Create_ThenInstanceAccess_ThrowsInvalidOperationException () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Create a modern instance-based application IApplication app = Application.Create (); app.Init ("fake"); // Attempting to initialize using the legacy static model should throw - InvalidOperationException ex = Assert.Throws (() => - { - ApplicationImpl.Instance.Init ("fake"); - }); + var ex = Assert.Throws (() => { ApplicationImpl.Instance.Init ("fake"); }); Assert.Contains ("Cannot use legacy static Application model", ex.Message); Assert.Contains ("after using modern instance-based model", ex.Message); // Clean up - app.Shutdown (); + app.Dispose (); ApplicationImpl.ResetModelUsageTracking (); - } [Fact] public void InstanceAccess_ThenCreate_ThrowsInvalidOperationException () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Initialize using the legacy static model IApplication staticInstance = ApplicationImpl.Instance; staticInstance.Init ("fake"); // Attempting to create and initialize with modern instance-based model should throw - InvalidOperationException ex = Assert.Throws (() => - { - IApplication app = Application.Create (); - app.Init ("fake"); - }); + var ex = Assert.Throws (() => + { + IApplication app = Application.Create (); + app.Init ("fake"); + }); Assert.Contains ("Cannot use modern instance-based model", ex.Message); Assert.Contains ("after using legacy static Application model", ex.Message); // Clean up - staticInstance.Shutdown (); + staticInstance.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } [Fact] public void Init_ThenCreate_ThrowsInvalidOperationException () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Initialize using legacy static API IApplication staticInstance = ApplicationImpl.Instance; staticInstance.Init ("fake"); // Attempting to create a modern instance-based application should throw - InvalidOperationException ex = Assert.Throws (() => - { - IApplication _ = Application.Create (); - }); + var ex = Assert.Throws (() => + { + IApplication _ = Application.Create (); + }); Assert.Contains ("Cannot use modern instance-based model", ex.Message); Assert.Contains ("after using legacy static Application model", ex.Message); // Clean up - staticInstance.Shutdown (); + staticInstance.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } [Fact] public void Create_ThenInit_ThrowsInvalidOperationException () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Create a modern instance-based application IApplication app = Application.Create (); app.Init ("fake"); // Attempting to initialize using the legacy static model should throw - InvalidOperationException ex = Assert.Throws (() => - { - ApplicationImpl.Instance.Init ("fake"); - }); + var ex = Assert.Throws (() => { ApplicationImpl.Instance.Init ("fake"); }); Assert.Contains ("Cannot use legacy static Application model", ex.Message); Assert.Contains ("after using modern instance-based model", ex.Message); // Clean up - app.Shutdown (); + app.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } [Fact] public void MultipleCreate_Calls_DoNotThrow () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Multiple calls to Create should not throw IApplication app1 = Application.Create (); IApplication app2 = Application.Create (); @@ -112,15 +113,18 @@ public class ApplicationModelFencingTests Assert.NotNull (app3); // Clean up - app1.Shutdown (); - app2.Shutdown (); - app3.Shutdown (); + app1.Dispose (); + app2.Dispose (); + app3.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } [Fact] public void MultipleInstanceAccess_DoesNotThrow () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Multiple accesses to Instance should not throw (it's a singleton) IApplication instance1 = ApplicationImpl.Instance; IApplication instance2 = ApplicationImpl.Instance; @@ -131,16 +135,19 @@ public class ApplicationModelFencingTests Assert.Same (instance2, instance3); // Clean up - instance1.Shutdown (); + instance1.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } [Fact] public void ResetModelUsageTracking_AllowsSwitchingModels () { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + // Use modern model IApplication app1 = Application.Create (); - app1.Shutdown (); + app1.Dispose (); // Reset the tracking ApplicationImpl.ResetModelUsageTracking (); @@ -148,7 +155,7 @@ public class ApplicationModelFencingTests // Should now be able to use legacy model IApplication staticInstance = ApplicationImpl.Instance; Assert.NotNull (staticInstance); - staticInstance.Shutdown (); + staticInstance.Dispose (); // Reset again ApplicationImpl.ResetModelUsageTracking (); @@ -156,7 +163,7 @@ public class ApplicationModelFencingTests // Should be able to use modern model again IApplication app2 = Application.Create (); Assert.NotNull (app2); - app2.Shutdown (); + app2.Dispose (); ApplicationImpl.ResetModelUsageTracking (); } } diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs index 6c3e10fc4..6056bee16 100644 --- a/Tests/UnitTests/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -45,7 +45,7 @@ public class ApplicationPopoverTests [Fact] public void Application_End_Does_Not_Reset_PopoverManager () { - Toplevel? top = null; + Runnable? top = null; try { @@ -73,7 +73,7 @@ public class ApplicationPopoverTests [Fact] public void Application_End_Hides_Active () { - Toplevel? top = null; + Runnable? top = null; try { @@ -190,37 +190,39 @@ public class ApplicationPopoverTests } [Fact] - public void Register_SetsTopLevel () + public void Register_SetsRunnable () { try { // Arrange Application.Init ("fake"); - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); PopoverTestClass? popover = new (); // Act Application.Popover?.Register (popover); // Assert - Assert.Equal (Application.TopRunnable, popover.Current); + Assert.Equal (Application.TopRunnableView as IRunnable, popover.Current); } finally { - Application.TopRunnable?.Dispose (); + Application.TopRunnableView?.Dispose (); Application.Shutdown (); } } [Fact] - public void Keyboard_Events_Go_Only_To_Popover_Associated_With_Toplevel () + public void Keyboard_Events_Go_Only_To_Popover_Associated_With_Runnable () { try { // Arrange Application.Init ("fake"); - Application.TopRunnable = new () { Id = "initialTop" }; + + Runnable? initialRunnable = new () { Id = "initialRunnable" }; + Application.Begin (initialRunnable); PopoverTestClass? popover = new (); var keyDownEvents = 0; @@ -233,10 +235,12 @@ public class ApplicationPopoverTests Application.Popover?.Register (popover); // Act - Application.RaiseKeyDownEvent (Key.A); // Goes to initialTop + Application.RaiseKeyDownEvent (Key.A); // Goes to initialRunnable - Application.TopRunnable = new () { Id = "secondaryTop" }; - Application.RaiseKeyDownEvent (Key.A); // Goes to secondaryTop + Runnable? secondaryRunnable = new () { Id = "secondaryRunnable" }; + Application.Begin (secondaryRunnable); + + Application.RaiseKeyDownEvent (Key.A); // Goes to secondaryRunnable // Test Assert.Equal (1, keyDownEvents); @@ -246,20 +250,19 @@ public class ApplicationPopoverTests } finally { - Application.TopRunnable?.Dispose (); Application.Shutdown (); } } // See: https://github.com/gui-cs/Terminal.Gui/issues/4122 [Theory] - [InlineData (0, 0, new [] { "top" })] + [InlineData (0, 0, new [] { "runnable" })] [InlineData (10, 10, new string [] { })] - [InlineData (1, 1, new [] { "top", "view" })] - [InlineData (5, 5, new [] { "top" })] + [InlineData (1, 1, new [] { "runnable", "view" })] + [InlineData (5, 5, new [] { "runnable" })] [InlineData (6, 6, new [] { "popoverSubView" })] - [InlineData (7, 7, new [] { "top" })] - [InlineData (3, 3, new [] { "top" })] + [InlineData (7, 7, new [] { "runnable" })] + [InlineData (3, 3, new [] { "runnable" })] public void GetViewsUnderMouse_Supports_ActivePopover (int mouseX, int mouseY, string [] viewIdStrings) { PopoverTestClass? popover = null; @@ -269,11 +272,12 @@ public class ApplicationPopoverTests // Arrange Application.Init ("fake"); - Application.TopRunnable = new () + Runnable? runnable = new () { Frame = new (0, 0, 10, 10), - Id = "top" + Id = "runnable" }; + Application.Begin (runnable); View? view = new () { @@ -284,7 +288,7 @@ public class ApplicationPopoverTests Height = 2 }; - Application.TopRunnable.Add (view); + runnable.Add (view); popover = new () { @@ -318,8 +322,7 @@ public class ApplicationPopoverTests finally { popover?.Dispose (); - Application.TopRunnable?.Dispose (); - Application.Shutdown(); + Application.Shutdown (); } } diff --git a/Tests/UnitTests/Application/ApplicationScreenTests.cs b/Tests/UnitTests/Application/ApplicationScreenTests.cs index f1ee2ea89..30fe51a5f 100644 --- a/Tests/UnitTests/Application/ApplicationScreenTests.cs +++ b/Tests/UnitTests/Application/ApplicationScreenTests.cs @@ -1,14 +1,72 @@ -using UnitTests; -using Xunit.Abstractions; +using Xunit.Abstractions; namespace UnitTests.ApplicationTests; public class ApplicationScreenTests { - public ApplicationScreenTests (ITestOutputHelper output) - { - } + public ApplicationScreenTests (ITestOutputHelper output) { } + [Fact] + [AutoInitShutdown] + public void ClearContents_Called_When_Top_Frame_Changes () + { + var top = new Runnable (); + SessionToken rs = Application.Begin (top); + + // Arrange + var clearedContentsRaised = 0; + + Application.Driver!.ClearedContents += OnClearedContents; + + // Act + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (0, clearedContentsRaised); + + // Act + Application.TopRunnableView!.SetNeedsLayout (); + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (0, clearedContentsRaised); + + // Act + Application.TopRunnableView.X = 1; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (1, clearedContentsRaised); + + // Act + Application.TopRunnableView.Width = 10; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (2, clearedContentsRaised); + + // Act + Application.TopRunnableView.Y = 1; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (3, clearedContentsRaised); + + // Act + Application.TopRunnableView.Height = 10; + Application.LayoutAndDraw (); + + // Assert + Assert.Equal (4, clearedContentsRaised); + + Application.Driver!.ClearedContents -= OnClearedContents; + + Application.End (rs); + + return; + + void OnClearedContents (object e, EventArgs a) { clearedContentsRaised++; } + } [Fact] public void ClearScreenNextIteration_Resets_To_False_After_LayoutAndDraw () @@ -28,67 +86,6 @@ public class ApplicationScreenTests Application.ResetState (true); } - [Fact] - [AutoInitShutdown] - public void ClearContents_Called_When_Top_Frame_Changes () - { - Toplevel top = new Toplevel (); - SessionToken rs = Application.Begin (top); - // Arrange - var clearedContentsRaised = 0; - - Application.Driver!.ClearedContents += OnClearedContents; - - // Act - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (0, clearedContentsRaised); - - // Act - Application.TopRunnable!.SetNeedsLayout (); - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (0, clearedContentsRaised); - - // Act - Application.TopRunnable.X = 1; - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (1, clearedContentsRaised); - - // Act - Application.TopRunnable.Width = 10; - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (2, clearedContentsRaised); - - // Act - Application.TopRunnable.Y = 1; - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (3, clearedContentsRaised); - - // Act - Application.TopRunnable.Height = 10; - Application.LayoutAndDraw (); - - // Assert - Assert.Equal (4, clearedContentsRaised); - - Application.Driver!.ClearedContents -= OnClearedContents; - - Application.End (rs); - - return; - - void OnClearedContents (object e, EventArgs a) { clearedContentsRaised++; } - } - [Fact] [SetupFakeApplication] public void Screen_Changes_OnScreenChanged_Without_Call_Application_Init () diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs deleted file mode 100644 index d90e567e3..000000000 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ /dev/null @@ -1,937 +0,0 @@ -#nullable enable -using System.Diagnostics; -using Xunit.Abstractions; -using static Terminal.Gui.Configuration.ConfigurationManager; - -// Alias Console to MockConsole so we don't accidentally use Console - -namespace UnitTests.ApplicationTests; - -public class ApplicationTests -{ - public ApplicationTests (ITestOutputHelper output) - { - _output = output; - -#if DEBUG_IDISPOSABLE - View.EnableDebugIDisposableAsserts = true; - View.Instances.Clear (); - SessionToken.Instances.Clear (); -#endif - } - - private readonly ITestOutputHelper _output; - - [Fact] - public void AddTimeout_Fires () - { - IApplication app = ApplicationImpl.Instance; // Force legacy - app.Init ("fake"); - - uint timeoutTime = 100; - var timeoutFired = false; - - // Setup a timeout that will fire - app.AddTimeout ( - TimeSpan.FromMilliseconds (timeoutTime), - () => - { - timeoutFired = true; - - // Return false so the timer does not repeat - return false; - } - ); - - // The timeout has not fired yet - Assert.False (timeoutFired); - - // Block the thread to prove the timeout does not fire on a background thread - Thread.Sleep ((int)timeoutTime * 2); - Assert.False (timeoutFired); - - app.StopAfterFirstIteration = true; - app.Run ().Dispose (); - - // The timeout should have fired - Assert.True (timeoutFired); - - app.Shutdown (); - } - - [Fact] - [SetupFakeApplication] - public void Begin_Null_Toplevel_Throws () - { - // Test null Toplevel - Assert.Throws (() => Application.Begin (null!)); - } - - [Fact] - [SetupFakeApplication] - public void Begin_Sets_Application_Top_To_Console_Size () - { - Assert.Null (Application.TopRunnable); - Application.Driver!.SetScreenSize (80, 25); - Toplevel top = new (); - Application.Begin (top); - Assert.Equal (new (0, 0, 80, 25), Application.TopRunnable!.Frame); - Application.Driver!.SetScreenSize (5, 5); - Assert.Equal (new (0, 0, 5, 5), Application.TopRunnable!.Frame); - top.Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void End_And_Shutdown_Should_Not_Dispose_ApplicationTop () - { - Assert.Null (Application.TopRunnable); - - SessionToken rs = Application.Begin (new ()); - Application.TopRunnable!.Title = "End_And_Shutdown_Should_Not_Dispose_ApplicationTop"; - Assert.Equal (rs.Toplevel, Application.TopRunnable); - Application.End (rs); - -#if DEBUG_IDISPOSABLE - Assert.True (rs.WasDisposed); - Assert.False (Application.TopRunnable!.WasDisposed); // Is true because the rs.Toplevel is the same as Application.TopRunnable -#endif - - Assert.Null (rs.Toplevel); - - Toplevel top = Application.TopRunnable; - -#if DEBUG_IDISPOSABLE - Exception exception = Record.Exception (Application.Shutdown); - Assert.NotNull (exception); - Assert.False (top.WasDisposed); - top.Dispose (); - Assert.True (top.WasDisposed); -#endif - } - - [Fact] - [SetupFakeApplication] - public void Init_Begin_End_Cleans_Up () - { - // Start stopwatch - var stopwatch = new Stopwatch (); - stopwatch.Start (); - - SessionToken? sessionToken = null; - - EventHandler newSessionTokenFn = (s, e) => - { - Assert.NotNull (e.State); - sessionToken = e.State; - }; - Application.SessionBegun += newSessionTokenFn; - - var topLevel = new Toplevel (); - SessionToken rs = Application.Begin (topLevel); - Assert.NotNull (rs); - Assert.NotNull (sessionToken); - Assert.Equal (rs, sessionToken); - - Assert.Equal (topLevel, Application.TopRunnable); - - Application.SessionBegun -= newSessionTokenFn; - Application.End (sessionToken); - - Assert.NotNull (Application.TopRunnable); - Assert.NotNull (Application.Driver); - - topLevel.Dispose (); - - // Stop stopwatch - stopwatch.Stop (); - - _output.WriteLine ($"Load took {stopwatch.ElapsedMilliseconds} ms"); - } - - [Fact] - public void Init_KeyBindings_Are_Not_Reset () - { - Debug.Assert (!IsEnabled); - - try - { - // arrange - ThrowOnJsonErrors = true; - - Application.QuitKey = Key.Q; - Assert.Equal (Key.Q, Application.QuitKey); - - Application.Init ("fake"); - - Assert.Equal (Key.Q, Application.QuitKey); - } - finally - { - Application.ResetState (); - } - } - - [Fact] - public void Init_NoParam_ForceDriver_Works () - { - Application.ForceDriver = "Fake"; - Application.Init (); - - Assert.Equal ("fake", Application.Driver!.GetName ()); - Application.ResetState (); - } - - - [Fact] - public void Init_Null_Driver_Should_Pick_A_Driver () - { - Application.Init (); - - Assert.NotNull (Application.Driver); - - Application.Shutdown (); - } - - [Fact] - public void Init_ResetState_Resets_Properties () - { - ThrowOnJsonErrors = true; - - // For all the fields/properties of Application, check that they are reset to their default values - - // Set some values - - Application.Init (driverName: "fake"); - - // Application.IsInitialized = true; - - // Reset - Application.ResetState (); - - CheckReset (); - - // Set the values that can be set - Application.Initialized = true; - Application.MainThreadId = 1; - - //Application._topLevels = new List (); - Application.CachedViewsUnderMouse.Clear (); - - //Application.SupportedCultures = new List (); - Application.Force16Colors = true; - - //Application.ForceDriver = "driver"; - Application.StopAfterFirstIteration = true; - Application.PrevTabGroupKey = Key.A; - Application.NextTabGroupKey = Key.B; - Application.QuitKey = Key.C; - Application.KeyBindings.Add (Key.D, Command.Cancel); - - Application.CachedViewsUnderMouse.Clear (); - - //Application.WantContinuousButtonPressedView = new View (); - - // Mouse - Application.LastMousePosition = new Point (1, 1); - - Application.ResetState (); - CheckReset (); - - ThrowOnJsonErrors = false; - - return; - - void CheckReset () - { - // Check that all fields and properties are set to their default values - - // Public Properties - Assert.Null (Application.TopRunnable); - Assert.Null (Application.Mouse.MouseGrabView); - - // Don't check Application.ForceDriver - // Assert.Empty (Application.ForceDriver); - // Don't check Application.Force16Colors - //Assert.False (Application.Force16Colors); - Assert.Null (Application.Driver); - Assert.False (Application.StopAfterFirstIteration); - - // Commented out because if CM changed the defaults, those changes should - // persist across Inits. - //Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey); - //Assert.Equal (Key.Tab, Application.NextTabKey); - //Assert.Equal (Key.F6.WithShift, Application.PrevTabGroupKey); - //Assert.Equal (Key.F6, Application.NextTabGroupKey); - //Assert.Equal (Key.Esc, Application.QuitKey); - - // Internal properties - Assert.False (Application.Initialized); - Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); - Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures); - Assert.Null (Application.MainThreadId); - Assert.Empty (Application.SessionStack); - Assert.Empty (Application.CachedViewsUnderMouse); - - // Mouse - // Do not reset _lastMousePosition - //Assert.Null (Application._lastMousePosition); - - // Navigation - // Assert.Null (Application.Navigation); - - // Popover - //Assert.Null (Application.Popover); - - // Events - Can't check - //Assert.Null (GetEventSubscribers (typeof (Application), "InitializedChanged")); - //Assert.Null (GetEventSubscribers (typeof (Application), "SessionBegun")); - //Assert.Null (GetEventSubscribers (typeof (Application), "Iteration")); - //Assert.Null (GetEventSubscribers (typeof (Application), "ScreenChanged")); - //Assert.Null (GetEventSubscribers (typeof (Application.Mouse), "MouseEvent")); - //Assert.Null (GetEventSubscribers (typeof (Application.Keyboard), "KeyDown")); - //Assert.Null (GetEventSubscribers (typeof (Application.Keyboard), "KeyUp")); - } - } - - [Fact] - public void Init_Shutdown_Cleans_Up () - { - // Verify initial state is per spec - //Pre_Init_State (); - - Application.Init ("fake"); - - // Verify post-Init state is correct - //Post_Init_State (); - - Application.Shutdown (); - - // Verify state is back to initial - //Pre_Init_State (); -#if DEBUG_IDISPOSABLE - - // Validate there are no outstanding Responder-based instances - // after a scenario was selected to run. This proves the main UI Catalog - // 'app' closed cleanly. - Assert.Empty (View.Instances); -#endif - } - - [Fact] - public void Init_Shutdown_Fire_InitializedChanged () - { - var initialized = false; - var shutdown = false; - - Application.InitializedChanged += OnApplicationOnInitializedChanged; - - Application.Init (driverName: "fake"); - Assert.True (initialized); - Assert.False (shutdown); - - Application.Shutdown (); - Assert.True (initialized); - Assert.True (shutdown); - - Application.InitializedChanged -= OnApplicationOnInitializedChanged; - - return; - - void OnApplicationOnInitializedChanged (object? s, EventArgs a) - { - if (a.Value) - { - initialized = true; - } - else - { - shutdown = true; - } - } - } - - [Fact] - [SetupFakeApplication] - public void Init_Unbalanced_Throws () - { - Assert.Throws (() => - Application.Init ("fake") - ); - } - - [Fact] - [SetupFakeApplication] - public void Init_Unbalanced_Throws2 () - { - // Now try the other way - Assert.Throws (() => Application.Init ("fake")); - } - - [Fact] - public void Init_WithoutTopLevelFactory_Begin_End_Cleans_Up () - { - Application.StopAfterFirstIteration = true; - - // NOTE: Run, when called after Init has been called behaves differently than - // when called if Init has not been called. - Toplevel topLevel = new (); - Application.Init ("fake"); - - SessionToken? sessionToken = null; - - EventHandler newSessionTokenFn = (s, e) => - { - Assert.NotNull (e.State); - sessionToken = e.State; - }; - Application.SessionBegun += newSessionTokenFn; - - SessionToken rs = Application.Begin (topLevel); - Assert.NotNull (rs); - Assert.NotNull (sessionToken); - Assert.Equal (rs, sessionToken); - - Assert.Equal (topLevel, Application.TopRunnable); - - Application.SessionBegun -= newSessionTokenFn; - Application.End (sessionToken); - - Assert.NotNull (Application.TopRunnable); - Assert.NotNull (Application.Driver); - - topLevel.Dispose (); - Application.Shutdown (); - - Assert.Null (Application.TopRunnable); - Assert.Null (Application.Driver); - } - - [Fact] - [SetupFakeApplication] - public void Internal_Properties_Correct () - { - Assert.True (Application.Initialized); - Assert.Null (Application.TopRunnable); - SessionToken rs = Application.Begin (new ()); - Assert.Equal (Application.TopRunnable, rs.Toplevel); - Assert.Null (Application.Mouse.MouseGrabView); // public - Application.TopRunnable!.Dispose (); - } - - // Invoke Tests - // TODO: Test with threading scenarios - [Fact] - [SetupFakeApplication] - public void Invoke_Adds_Idle () - { - Toplevel top = new (); - SessionToken rs = Application.Begin (top); - - var actionCalled = 0; - Application.Invoke ((_) => { actionCalled++; }); - Application.TimedEvents!.RunTimers (); - Assert.Equal (1, actionCalled); - top.Dispose (); - } - - [Fact] - public void Run_Iteration_Fires () - { - var iteration = 0; - - Application.Init ("fake"); - - Application.Iteration += Application_Iteration; - Application.Run ().Dispose (); - Application.Iteration -= Application_Iteration; - - Assert.Equal (1, iteration); - Application.Shutdown (); - - return; - - void Application_Iteration (object? sender, EventArgs e) - { - if (iteration > 0) - { - Assert.Fail (); - } - - iteration++; - Application.RequestStop (); - } - } - - [Fact] - [SetupFakeApplication] - public void Screen_Size_Changes () - { - IDriver? driver = Application.Driver; - - Application.Driver!.SetScreenSize (80, 25); - - Assert.Equal (new (0, 0, 80, 25), driver!.Screen); - Assert.Equal (new (0, 0, 80, 25), Application.Screen); - - // TODO: Should not be possible to manually change these at whim! - driver.Cols = 100; - driver.Rows = 30; - - // IDriver.Screen isn't assignable - //driver.Screen = new (0, 0, driver.Cols, Rows); - - Application.Driver!.SetScreenSize (100, 30); - - Assert.Equal (new (0, 0, 100, 30), driver.Screen); - - // Assert does not make sense - // Assert.NotEqual (new (0, 0, 100, 30), Application.Screen); - // Assert.Equal (new (0, 0, 80, 25), Application.Screen); - Application.Screen = new (0, 0, driver.Cols, driver.Rows); - Assert.Equal (new (0, 0, 100, 30), driver.Screen); - } - - [Fact] - public void Shutdown_Alone_Does_Nothing () { Application.Shutdown (); } - - //[Fact] - //public void InitState_Throws_If_Driver_Is_Null () - //{ - // Assert.Throws (static () => Application.SubscribeDriverEvents ()); - //} - - #region RunTests - - [Fact] - [SetupFakeApplication] - public void Run_T_After_InitWithDriver_with_TopLevel_Does_Not_Throws () - { - Application.StopAfterFirstIteration = true; - - // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) - // Using another type not derived from Toplevel will throws at compile time - Application.Run (); - Assert.True (Application.TopRunnable is Window); - - Application.TopRunnable!.Dispose (); - } - - [Fact] - public void Run_T_After_InitWithDriver_with_TopLevel_and_Driver_Does_Not_Throws () - { - Application.StopAfterFirstIteration = true; - - // Run when already initialized or not with a Driver will not throw (because Window is derived from Toplevel) - // Using another type not derived from Toplevel will throws at compile time - Application.Run (null, "fake"); - Assert.True (Application.TopRunnable is Window); - - Application.TopRunnable!.Dispose (); - - // Run when already initialized or not with a Driver will not throw (because Dialog is derived from Toplevel) - Application.Run (null, "fake"); - Assert.True (Application.TopRunnable is Dialog); - - Application.TopRunnable!.Dispose (); - Application.Shutdown (); - } - - [Fact] - [SetupFakeApplication] - public void Run_T_After_Init_Does_Not_Disposes_Application_Top () - { - // Init doesn't create a Toplevel and assigned it to Application.TopRunnable - // but Begin does - var initTop = new Toplevel (); - - Application.Iteration += OnApplicationOnIteration; - - Application.Run (); - Application.Iteration -= OnApplicationOnIteration; - -#if DEBUG_IDISPOSABLE - Assert.False (initTop.WasDisposed); - initTop.Dispose (); - Assert.True (initTop.WasDisposed); -#endif - Application.TopRunnable!.Dispose (); - - return; - - void OnApplicationOnIteration (object? s, EventArgs a) - { - Assert.NotEqual (initTop, Application.TopRunnable); -#if DEBUG_IDISPOSABLE - Assert.False (initTop.WasDisposed); -#endif - Application.RequestStop (); - } - } - - [Fact] - [SetupFakeApplication] - public void Run_T_After_InitWithDriver_with_TestTopLevel_DoesNotThrow () - { - Application.StopAfterFirstIteration = true; - - // Init has been called and we're passing no driver to Run. This is ok. - Application.Run (); - - Application.TopRunnable!.Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void Run_T_After_InitNullDriver_with_TestTopLevel_DoesNotThrow () - { - Application.StopAfterFirstIteration = true; - - // Init has been called, selecting FakeDriver; we're passing no driver to Run. Should be fine. - Application.Run (); - - Application.TopRunnable!.Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws () - { - Application.Driver = null; - - // Init has been called, but Driver has been set to null. Bad. - Assert.Throws (() => Application.Run ()); - } - - [Fact] - [SetupFakeApplication] - public void Run_T_NoInit_DoesNotThrow () - { - Application.StopAfterFirstIteration = true; - - Application.Run ().Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void Run_T_NoInit_WithDriver_DoesNotThrow () - { - Application.StopAfterFirstIteration = true; - - // Init has NOT been called and we're passing a valid driver to Run. This is ok. - Application.Run (null, "fake"); - - Application.TopRunnable!.Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void Run_RequestStop_Stops () - { - var top = new Toplevel (); - SessionToken rs = Application.Begin (top); - Assert.NotNull (rs); - - Application.Iteration += OnApplicationOnIteration; - Application.Run (top); - Application.Iteration -= OnApplicationOnIteration; - - top.Dispose (); - - return; - - void OnApplicationOnIteration (object? s, EventArgs a) { Application.RequestStop (); } - } - - [Fact] - [SetupFakeApplication] - public void Run_Sets_Running_True () - { - var top = new Toplevel (); - SessionToken rs = Application.Begin (top); - Assert.NotNull (rs); - - Application.Iteration += OnApplicationOnIteration; - Application.Run (top); - Application.Iteration -= OnApplicationOnIteration; - - top.Dispose (); - - return; - - void OnApplicationOnIteration (object? s, EventArgs a) - { - Assert.True (top.Running); - top.RequestStop (); - } - } - - [Fact] - [SetupFakeApplication] - public void Run_RunningFalse_Stops () - { - var top = new Toplevel (); - SessionToken rs = Application.Begin (top); - Assert.NotNull (rs); - - Application.Iteration += OnApplicationOnIteration; - Application.Run (top); - Application.Iteration -= OnApplicationOnIteration; - - top.Dispose (); - - return; - - void OnApplicationOnIteration (object? s, EventArgs a) { top.Running = false; } - } - - [Fact] - [SetupFakeApplication] - public void Run_Loaded_Ready_Unloaded_Events () - { - Application.StopAfterFirstIteration = true; - - Toplevel top = new (); - var count = 0; - top.Loaded += (s, e) => count++; - top.Ready += (s, e) => count++; - top.Unloaded += (s, e) => count++; - Application.Run (top); - top.Dispose (); - } - - // TODO: All Toplevel layout tests should be moved to ToplevelTests.cs - [Fact] - [SetupFakeApplication] - public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving () - { - // Don't use Dialog here as it has more layout logic. Use Window instead. - var w = new Window - { - Width = 5, Height = 5, - Arrangement = ViewArrangement.Movable - }; - Application.Driver!.SetScreenSize (10, 10); - SessionToken rs = Application.Begin (w); - - // Don't use visuals to test as style of border can change over time. - Assert.Equal (new (0, 0), w.Frame.Location); - - Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); - Assert.Equal (w.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (0, 0), w.Frame.Location); - - // Move down and to the right. - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); - Assert.Equal (new (1, 1), w.Frame.Location); - - Application.End (rs); - w.Dispose (); - } - - [Fact] - [SetupFakeApplication] - public void End_Does_Not_Dispose () - { - var top = new Toplevel (); - - Window w = new (); - Application.StopAfterFirstIteration = true; - Application.Run (w); - -#if DEBUG_IDISPOSABLE - Assert.False (w.WasDisposed); -#endif - - Assert.NotNull (w); - Assert.Equal (string.Empty, w.Title); // Valid - w has not been disposed. The user may want to run it again - Assert.NotNull (Application.TopRunnable); - Assert.Equal (w, Application.TopRunnable); - Assert.NotEqual (top, Application.TopRunnable); - - Application.Run (w); // Valid - w has not been disposed. - -#if DEBUG_IDISPOSABLE - Assert.False (w.WasDisposed); - Exception exception = Record.Exception (Application.Shutdown); // Invalid - w has not been disposed. - Assert.NotNull (exception); - - w.Dispose (); - Assert.True (w.WasDisposed); - - //exception = Record.Exception ( - // () => Application.Run ( - // w)); // Invalid - w has not been disposed. Run it in debug mode will throw, otherwise the user may want to run it again - //Assert.NotNull (exception); - - // TODO: Re-enable this when we are done debug logging of ctx.Source.Title in RaiseSelecting - //exception = Record.Exception (() => Assert.Equal (string.Empty, w.Title)); // Invalid - w has been disposed and cannot be accessed - //Assert.NotNull (exception); - //exception = Record.Exception (() => w.Title = "NewTitle"); // Invalid - w has been disposed and cannot be accessed - //Assert.NotNull (exception); -#endif - } - - [Fact] - public void Run_Creates_Top_Without_Init () - { - Assert.Null (Application.TopRunnable); - Application.StopAfterFirstIteration = true; - - Application.Iteration += OnApplicationOnIteration; - Toplevel top = Application.Run (null, "fake"); - Application.Iteration -= OnApplicationOnIteration; -#if DEBUG_IDISPOSABLE - Assert.Equal (top, Application.TopRunnable); - Assert.False (top.WasDisposed); - Exception exception = Record.Exception (Application.Shutdown); - Assert.NotNull (exception); - Assert.False (top.WasDisposed); -#endif - - // It's up to caller to dispose it - top.Dispose (); - -#if DEBUG_IDISPOSABLE - Assert.True (top.WasDisposed); -#endif - Assert.NotNull (Application.TopRunnable); - - Application.Shutdown (); - Assert.Null (Application.TopRunnable); - - return; - - void OnApplicationOnIteration (object? s, EventArgs e) { Assert.NotNull (Application.TopRunnable); } - } - - [Fact] - public void Run_T_Creates_Top_Without_Init () - { - Assert.Null (Application.TopRunnable); - - Application.StopAfterFirstIteration = true; - - Application.Run (null, "fake"); -#if DEBUG_IDISPOSABLE - Assert.False (Application.TopRunnable!.WasDisposed); - Exception exception = Record.Exception (Application.Shutdown); - Assert.NotNull (exception); - Assert.False (Application.TopRunnable!.WasDisposed); - - // It's up to caller to dispose it - Application.TopRunnable!.Dispose (); - Assert.True (Application.TopRunnable!.WasDisposed); -#endif - Assert.NotNull (Application.TopRunnable); - - Application.Shutdown (); - Assert.Null (Application.TopRunnable); - } - - [Fact] - public void Run_t_Does_Not_Creates_Top_Without_Init () - { - // When a Toplevel is created it must already have all the Application configuration loaded - // This is only possible by two ways: - // 1 - Using Application.Init first - // 2 - Using Application.Run() or Application.Run() - // The Application.Run(new(Toplevel)) must always call Application.Init() first because - // the new(Toplevel) may be a derived class that is possible using Application static - // properties that is only available after the Application.Init was called - - Assert.Null (Application.TopRunnable); - - Assert.Throws (() => Application.Run (new Toplevel ())); - - Application.Init ("fake"); - - Application.Iteration += OnApplication_OnIteration; - Application.Run (new Toplevel ()); - Application.Iteration -= OnApplication_OnIteration; -#if DEBUG_IDISPOSABLE - Assert.False (Application.TopRunnable!.WasDisposed); - Exception exception = Record.Exception (Application.Shutdown); - Assert.NotNull (exception); - Assert.False (Application.TopRunnable!.WasDisposed); - - // It's up to caller to dispose it - Application.TopRunnable!.Dispose (); - Assert.True (Application.TopRunnable!.WasDisposed); -#endif - Assert.NotNull (Application.TopRunnable); - - Application.Shutdown (); - Assert.Null (Application.TopRunnable); - - return; - - void OnApplication_OnIteration (object? s, EventArgs e) - { - Assert.NotNull (Application.TopRunnable); - Application.RequestStop (); - } - } - - private class TestToplevel : Toplevel - { } - - [Fact] - public void Run_T_With_V2_Driver_Does_Not_Call_ResetState_After_Init () - { - Assert.False (Application.Initialized); - Application.Init ("fake"); - Assert.True (Application.Initialized); - - Task.Run (() => { Task.Delay (300).Wait (); }) - .ContinueWith ( - (t, _) => - { - // no longer loading - Application.Invoke ((app) => { app.RequestStop (); }); - }, - TaskScheduler.FromCurrentSynchronizationContext ()); - Application.Run (); - Assert.NotNull (Application.Driver); - Assert.NotNull (Application.TopRunnable); - Assert.False (Application.TopRunnable!.Running); - Application.TopRunnable!.Dispose (); - Application.Shutdown (); - } - - // TODO: Add tests for Run that test errorHandler - - #endregion - - #region ShutdownTests - - [Fact] - public async Task Shutdown_Allows_Async () - { - var isCompletedSuccessfully = false; - - async Task TaskWithAsyncContinuation () - { - await Task.Yield (); - await Task.Yield (); - - isCompletedSuccessfully = true; - } - - Application.Shutdown (); - - Assert.False (isCompletedSuccessfully); - await TaskWithAsyncContinuation (); - Thread.Sleep (100); - Assert.True (isCompletedSuccessfully); - } - - [Fact] - public void Shutdown_Resets_SyncContext () - { - Application.Shutdown (); - Assert.Null (SynchronizationContext.Current); - } - - #endregion -} diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs deleted file mode 100644 index 7fd6468d7..000000000 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseEnterLeaveTests.cs +++ /dev/null @@ -1,481 +0,0 @@ -using System.ComponentModel; - -namespace UnitTests.ViewMouseTests; - -[Trait ("Category", "Input")] -public class ApplicationMouseEnterLeaveTests -{ - private class TestView : View - { - public TestView () - { - X = 1; - Y = 1; - Width = 1; - Height = 1; - } - - public bool CancelOnEnter { get; } - public int OnMouseEnterCalled { get; private set; } - public int OnMouseLeaveCalled { get; private set; } - - protected override bool OnMouseEnter (CancelEventArgs eventArgs) - { - OnMouseEnterCalled++; - eventArgs.Cancel = CancelOnEnter; - - base.OnMouseEnter (eventArgs); - - return eventArgs.Cancel; - } - - protected override void OnMouseLeave () - { - OnMouseLeaveCalled++; - - base.OnMouseLeave (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_MouseEntersView_CallsOnMouseEnter () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - var view = new TestView (); - Application.TopRunnable.Add (view); - var mousePosition = new Point (1, 1); - List currentViewsUnderMouse = new () { view }; - - var mouseEvent = new MouseEventArgs - { - Position = mousePosition, - ScreenPosition = mousePosition - }; - - Application.CachedViewsUnderMouse.Clear (); - - try - { - // Act - Application.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); - - // Assert - Assert.Equal (1, view.OnMouseEnterCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_MouseLeavesView_CallsOnMouseLeave () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - var view = new TestView (); - Application.TopRunnable.Add (view); - var mousePosition = new Point (0, 0); - List currentViewsUnderMouse = new (); - var mouseEvent = new MouseEventArgs (); - - Application.CachedViewsUnderMouse.Clear (); - Application.CachedViewsUnderMouse.Add (view); - - try - { - // Act - Application.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); - - // Assert - Assert.Equal (0, view.OnMouseEnterCalled); - Assert.Equal (1, view.OnMouseLeaveCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenAdjacentViews_CallsOnMouseEnterAndLeave () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - var view1 = new TestView (); // at 1,1 to 2,2 - - var view2 = new TestView () // at 2,2 to 3,3 - { - X = 2, - Y = 2 - }; - Application.TopRunnable.Add (view1); - Application.TopRunnable.Add (view2); - - Application.CachedViewsUnderMouse.Clear (); - - try - { - // Act - var mousePosition = new Point (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (0, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (1, 1); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (3, 3); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (1, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (1, view2.OnMouseLeaveCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_NoViewsUnderMouse_DoesNotCallOnMouseEnterOrLeave () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - var view = new TestView (); - Application.TopRunnable.Add (view); - var mousePosition = new Point (0, 0); - List currentViewsUnderMouse = new (); - var mouseEvent = new MouseEventArgs (); - - Application.CachedViewsUnderMouse.Clear (); - - try - { - // Act - Application.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); - - // Assert - Assert.Equal (0, view.OnMouseEnterCalled); - Assert.Equal (0, view.OnMouseLeaveCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingPeerViews_CallsOnMouseEnterAndLeave () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - - var view1 = new TestView - { - Width = 2 - }; // at 1,1 to 3,2 - - var view2 = new TestView () // at 2,2 to 4,3 - { - Width = 2, - X = 2, - Y = 2 - }; - Application.TopRunnable.Add (view1); - Application.TopRunnable.Add (view2); - - Application.CachedViewsUnderMouse.Clear (); - - try - { - // Act - var mousePosition = new Point (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (0, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (1, 1); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (0, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (3, 3); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (1, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, view2.OnMouseEnterCalled); - Assert.Equal (1, view2.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (2, view2.OnMouseEnterCalled); - Assert.Equal (1, view2.OnMouseLeaveCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } - - [Fact] - public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingSubViews_CallsOnMouseEnterAndLeave () - { - // Arrange - Application.TopRunnable = new () { Frame = new (0, 0, 10, 10) }; - - var view1 = new TestView - { - Id = "view1", - Width = 2, - Height = 2, - Arrangement = ViewArrangement.Overlapped - }; // at 1,1 to 3,3 (screen) - - var subView = new TestView - { - Id = "subView", - Width = 2, - Height = 2, - X = 1, - Y = 1, - Arrangement = ViewArrangement.Overlapped - }; // at 2,2 to 4,4 (screen) - view1.Add (subView); - Application.TopRunnable.Add (view1); - - Application.CachedViewsUnderMouse.Clear (); - - try - { - Assert.Equal (1, view1.FrameToScreen ().X); - Assert.Equal (2, subView.FrameToScreen ().X); - - // Act - var mousePosition = new Point (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (0, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, subView.OnMouseEnterCalled); - Assert.Equal (0, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (1, 1); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (0, subView.OnMouseEnterCalled); - Assert.Equal (0, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (0, view1.OnMouseLeaveCalled); - Assert.Equal (1, subView.OnMouseEnterCalled); - Assert.Equal (0, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (1, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (1, subView.OnMouseEnterCalled); - Assert.Equal (1, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (2, view1.OnMouseEnterCalled); - Assert.Equal (1, view1.OnMouseLeaveCalled); - Assert.Equal (2, subView.OnMouseEnterCalled); - Assert.Equal (1, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (3, 3); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (2, view1.OnMouseEnterCalled); - Assert.Equal (2, view1.OnMouseLeaveCalled); - Assert.Equal (2, subView.OnMouseEnterCalled); - Assert.Equal (2, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (0, 0); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (2, view1.OnMouseEnterCalled); - Assert.Equal (2, view1.OnMouseLeaveCalled); - Assert.Equal (2, subView.OnMouseEnterCalled); - Assert.Equal (2, subView.OnMouseLeaveCalled); - - // Act - mousePosition = new (2, 2); - - Application.RaiseMouseEnterLeaveEvents ( - mousePosition, - Application.TopRunnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); - - // Assert - Assert.Equal (3, view1.OnMouseEnterCalled); - Assert.Equal (2, view1.OnMouseLeaveCalled); - Assert.Equal (3, subView.OnMouseEnterCalled); - Assert.Equal (2, subView.OnMouseLeaveCalled); - } - finally - { - // Cleanup - Application.TopRunnable?.Dispose (); - Application.ResetState (); - } - } -} diff --git a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs index 45926bffc..887b10c0d 100644 --- a/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs +++ b/Tests/UnitTests/Application/Mouse/ApplicationMouseTests.cs @@ -16,7 +16,6 @@ public class ApplicationMouseTests _output = output; #if DEBUG_IDISPOSABLE View.Instances.Clear (); - SessionToken.Instances.Clear (); #endif } @@ -127,7 +126,7 @@ public class ApplicationMouseTests clicked = true; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); @@ -136,105 +135,6 @@ public class ApplicationMouseTests top.Dispose (); } - /// - /// Tests that the mouse coordinates passed to the focused view are correct when the mouse is clicked. With - /// Frames; Frame != Viewport - /// - //[AutoInitShutdown] - [Theory] - - // click on border - [InlineData (0, 0, 0, 0, 0, false)] - [InlineData (0, 1, 0, 0, 0, false)] - [InlineData (0, 0, 1, 0, 0, false)] - [InlineData (0, 9, 0, 0, 0, false)] - [InlineData (0, 0, 9, 0, 0, false)] - - // outside border - [InlineData (0, 10, 0, 0, 0, false)] - [InlineData (0, 0, 10, 0, 0, false)] - - // view is offset from origin ; click is on border - [InlineData (1, 1, 1, 0, 0, false)] - [InlineData (1, 2, 1, 0, 0, false)] - [InlineData (1, 1, 2, 0, 0, false)] - [InlineData (1, 10, 1, 0, 0, false)] - [InlineData (1, 1, 10, 0, 0, false)] - - // outside border - [InlineData (1, -1, 0, 0, 0, false)] - [InlineData (1, 0, -1, 0, 0, false)] - [InlineData (1, 10, 10, 0, 0, false)] - [InlineData (1, 11, 11, 0, 0, false)] - - // view is at origin, click is inside border - [InlineData (0, 1, 1, 0, 0, true)] - [InlineData (0, 2, 1, 1, 0, true)] - [InlineData (0, 1, 2, 0, 1, true)] - [InlineData (0, 8, 1, 7, 0, true)] - [InlineData (0, 1, 8, 0, 7, true)] - [InlineData (0, 8, 8, 7, 7, true)] - - // view is offset from origin ; click inside border - // our view is 10x10, but has a border, so it's bounds is 8x8 - [InlineData (1, 2, 2, 0, 0, true)] - [InlineData (1, 3, 2, 1, 0, true)] - [InlineData (1, 2, 3, 0, 1, true)] - [InlineData (1, 9, 2, 7, 0, true)] - [InlineData (1, 2, 9, 0, 7, true)] - [InlineData (1, 9, 9, 7, 7, true)] - [InlineData (1, 10, 10, 7, 7, false)] - - //01234567890123456789 - // |12345678| - // |xxxxxxxx - public void MouseCoordinatesTest_Border ( - int offset, - int clickX, - int clickY, - int expectedX, - int expectedY, - bool expectedClicked - ) - { - Size size = new (10, 10); - Point pos = new (offset, offset); - - var clicked = false; - - Application.TopRunnable = new Toplevel () - { - Id = "top", - }; - Application.TopRunnable.X = 0; - Application.TopRunnable.Y = 0; - Application.TopRunnable.Width = size.Width * 2; - Application.TopRunnable.Height = size.Height * 2; - Application.TopRunnable.BorderStyle = LineStyle.None; - - var view = new View { Id = "view", X = pos.X, Y = pos.Y, Width = size.Width, Height = size.Height }; - - // Give the view a border. With PR #2920, mouse clicks are only passed if they are inside the view's Viewport. - view.BorderStyle = LineStyle.Single; - view.CanFocus = true; - - Application.TopRunnable.Add (view); - - var mouseEvent = new MouseEventArgs { Position = new (clickX, clickY), ScreenPosition = new (clickX, clickY), Flags = MouseFlags.Button1Clicked }; - - view.MouseClick += (s, e) => - { - Assert.Equal (expectedX, e.Position.X); - Assert.Equal (expectedY, e.Position.Y); - clicked = true; - }; - - Application.RaiseMouseEvent (mouseEvent); - Assert.Equal (expectedClicked, clicked); - Application.TopRunnable.Dispose (); - Application.ResetState (ignoreDisposed: true); - - } #endregion mouse coordinate tests @@ -249,7 +149,7 @@ public class ApplicationMouseTests //sv.SetContentSize (new (100, 100)); //sv.Add (tf); - //var top = new Toplevel (); + //var top = new Runnable (); //top.Add (sv); //int iterations = -1; @@ -274,7 +174,7 @@ public class ApplicationMouseTests // else if (iterations == 1) // { // // Application.Mouse.MouseGrabView is null because - // // another toplevel (Dialog) was opened + // // another runnable (Dialog) was opened // Assert.Null (Application.Mouse.MouseGrabView); // Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition }); @@ -390,7 +290,7 @@ public class ApplicationMouseTests var count = 0; var view = new View { Width = 1, Height = 1 }; view.MouseEvent += (s, e) => count++; - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); @@ -439,7 +339,7 @@ public class ApplicationMouseTests View? receivedView = null; grabView.MouseEvent += (_, e) => receivedView = e.View; - var top = new Toplevel { Width = 20, Height = 10 }; + var top = new Runnable { Width = 20, Height = 10 }; top.Add (grabView); top.Add (targetView); // deepestViewUnderMouse = targetView Application.Begin (top); diff --git a/Tests/UnitTests/Application/SessionTokenTests.cs b/Tests/UnitTests/Application/SessionTokenTests.cs deleted file mode 100644 index f77aec9e9..000000000 --- a/Tests/UnitTests/Application/SessionTokenTests.cs +++ /dev/null @@ -1,82 +0,0 @@ - -#nullable enable -namespace UnitTests.ApplicationTests; - -/// These tests focus on Application.SessionToken and the various ways it can be changed. -public class SessionTokenTests -{ - public SessionTokenTests () - { -#if DEBUG_IDISPOSABLE - View.EnableDebugIDisposableAsserts = true; - - View.Instances.Clear (); - SessionToken.Instances.Clear (); -#endif - } - - [Fact] - [AutoInitShutdown] - public void Begin_End_Cleans_Up_SessionToken () - { - // Test null Toplevel - Assert.Throws (() => Application.Begin (null!)); - - Assert.NotNull (Application.Driver); - - Toplevel top = new Toplevel (); - SessionToken rs = Application.Begin (top); - Assert.NotNull (rs); - Application.End (rs); - - Assert.NotNull (Application.TopRunnable); - - // v2 does not use main loop, it uses MainLoop and its internal - //Assert.NotNull (Application.MainLoop); - Assert.NotNull (Application.Driver); - - top.Dispose (); - -#if DEBUG_IDISPOSABLE - Assert.True (rs.WasDisposed); -#endif - } - - [Fact] - public void Dispose_Cleans_Up_SessionToken () - { - var rs = new SessionToken (null!); - Assert.NotNull (rs); - - // Should not throw because Toplevel was null - rs.Dispose (); -#if DEBUG_IDISPOSABLE - Assert.True (rs.WasDisposed); -#endif - var top = new Toplevel (); - rs = new (top); - Assert.NotNull (rs); - - // Should throw because Toplevel was not cleaned up - Assert.Throws (() => rs.Dispose ()); - - rs.Toplevel?.Dispose (); - rs.Toplevel = null; - rs.Dispose (); -#if DEBUG_IDISPOSABLE - Assert.True (rs.WasDisposed); - Assert.True (top.WasDisposed); -#endif - } - - [Fact] - public void New_Creates_SessionToken () - { - var rs = new SessionToken (null!); - Assert.Null (rs.Toplevel); - - var top = new Toplevel (); - rs = new (top); - Assert.Equal (top, rs.Toplevel); - } -} diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 7137389f7..39fee532f 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -39,7 +39,7 @@ public class SyncrhonizationContextTests Task.Run (() => { - while (Application.TopRunnable is null || Application.TopRunnable is { Running: false }) + while (Application.TopRunnable is { IsRunning: false }) { Thread.Sleep (500); } @@ -56,7 +56,7 @@ public class SyncrhonizationContextTests null ); - if (Application.TopRunnable is { Running: true }) + if (Application.TopRunnable is { IsRunning: true }) { Assert.False (success); } @@ -64,7 +64,7 @@ public class SyncrhonizationContextTests ); // blocks here until the RequestStop is processed at the end of the test - Application.Run ().Dispose (); + Application.Run (); Assert.True (success); Application.Shutdown (); @@ -100,7 +100,7 @@ public class SyncrhonizationContextTests ); // blocks here until the RequestStop is processed at the end of the test - Application.Run ().Dispose (); + Application.Run (); Assert.True (success); Application.Shutdown (); } diff --git a/Tests/UnitTests/Clipboard/ClipboardTests.cs b/Tests/UnitTests/Clipboard/ClipboardTests.cs index e829f2db6..13b690ad1 100644 --- a/Tests/UnitTests/Clipboard/ClipboardTests.cs +++ b/Tests/UnitTests/Clipboard/ClipboardTests.cs @@ -35,7 +35,7 @@ public class ClipboardTests Application.Clipboard.SetClipboardData (clipText); ApplicationImpl.Instance.Iteration += (s, a) => ApplicationImpl.Instance.RequestStop (); - ApplicationImpl.Instance.Run().Dispose(); + ApplicationImpl.Instance.Run>().Dispose(); Assert.True(Application.Clipboard.TryGetClipboardData(out string result)); Assert.Equal (clipText, result); diff --git a/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs index 12f891e12..0a611d08b 100644 --- a/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -550,7 +550,7 @@ public class ConfigurationManagerTests (ITestOutputHelper output) ""MessageBox.DefaultButtonAlignment"": ""End"", ""Schemes"": [ { - ""TopLevel"": { + ""Runnable"": { ""Normal"": { ""Foreground"": ""BrightGreen"", ""Background"": ""Black"" @@ -1245,7 +1245,7 @@ public class ConfigurationManagerTests (ITestOutputHelper output) ""MessageBox.DefaultButtonAlignment"": ""End"", ""Schemes"": [ { - ""TopLevel"": { + ""Runnable"": { ""Normal"": { ""Foreground"": ""BrightGreen"", ""Background"": ""Black"" diff --git a/Tests/UnitTests/Configuration/SchemeManagerTests.cs b/Tests/UnitTests/Configuration/SchemeManagerTests.cs index b44d3ff8e..4a7b2399f 100644 --- a/Tests/UnitTests/Configuration/SchemeManagerTests.cs +++ b/Tests/UnitTests/Configuration/SchemeManagerTests.cs @@ -97,10 +97,10 @@ public class SchemeManagerTests Assert.NotNull (menuScheme); Assert.Equal (new Attribute (StandardColor.Charcoal, StandardColor.LightBlue, TextStyle.Bold), menuScheme!.Normal); - // Toplevel - var toplevelScheme = schemes ["Toplevel"]; - Assert.NotNull (toplevelScheme); - Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), toplevelScheme!.Normal.ToString ()); + // Runnable + var runnableScheme = schemes ["Runnable"]; + Assert.NotNull (runnableScheme); + Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), runnableScheme!.Normal.ToString ()); } @@ -132,10 +132,10 @@ public class SchemeManagerTests Assert.NotNull (menuScheme); Assert.Equal (new Attribute (StandardColor.Charcoal, StandardColor.LightBlue, TextStyle.Bold), menuScheme!.Normal); - // Toplevel - var toplevelScheme = schemes ["Toplevel"]; - Assert.NotNull (toplevelScheme); - Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), toplevelScheme!.Normal.ToString ()); + // Runnable + var runnableScheme = schemes ["Runnable"]; + Assert.NotNull (runnableScheme); + Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), runnableScheme!.Normal.ToString ()); } [Fact] public void Not_Case_Sensitive_Disabled () @@ -370,7 +370,7 @@ public class SchemeManagerTests "TestTheme": { "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "AntiqueWhite", "Background": "DimGray" @@ -506,17 +506,17 @@ public class SchemeManagerTests // Capture hardCoded hard-coded scheme colors ImmutableSortedDictionary hardCodedSchemes = SchemeManager.GetHardCodedSchemes ()!; - Color hardCodedTopLevelNormalFg = hardCodedSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedTopLevelNormalFg.ToString ()); + Color hardCodedRunnableNormalFg = hardCodedSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedRunnableNormalFg.ToString ()); Assert.Equal (hardCodedSchemes ["Menu"].Normal.Style, SchemeManager.GetSchemesForCurrentTheme () ["Menu"]!.Normal.Style); // Capture current scheme colors Dictionary currentSchemes = SchemeManager.GetSchemes ()!; - Color currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; + Color currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentTopLevelNormalFg.ToString ()); + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentRunnableNormalFg.ToString ()); // Load the test theme Load (ConfigLocations.Runtime); @@ -524,8 +524,8 @@ public class SchemeManagerTests Assert.Equal (TextStyle.Reverse, SchemeManager.GetSchemesForCurrentTheme () ["Menu"]!.Normal.Style); currentSchemes = SchemeManager.GetSchemesForCurrentTheme ()!; - currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; - Assert.NotEqual (hardCodedTopLevelNormalFg.ToString (), currentTopLevelNormalFg.ToString ()); + currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; + Assert.NotEqual (hardCodedRunnableNormalFg.ToString (), currentRunnableNormalFg.ToString ()); // Now reset everything and reload ResetToHardCodedDefaults (); @@ -534,8 +534,8 @@ public class SchemeManagerTests Assert.Equal ("Default", ThemeManager.Theme); currentSchemes = SchemeManager.GetSchemes ()!; - currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (hardCodedTopLevelNormalFg.ToString (), currentTopLevelNormalFg.ToString ()); + currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (hardCodedRunnableNormalFg.ToString (), currentRunnableNormalFg.ToString ()); } finally @@ -561,7 +561,7 @@ public class SchemeManagerTests "Default": { "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "AntiqueWhite", "Background": "DimGray" @@ -697,17 +697,17 @@ public class SchemeManagerTests // Capture hardCoded hard-coded scheme colors ImmutableSortedDictionary hardCodedSchemes = SchemeManager.GetHardCodedSchemes ()!; - Color hardCodedTopLevelNormalFg = hardCodedSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedTopLevelNormalFg.ToString ()); + Color hardCodedRunnableNormalFg = hardCodedSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedRunnableNormalFg.ToString ()); Assert.Equal (hardCodedSchemes ["Menu"].Normal.Style, SchemeManager.GetSchemesForCurrentTheme () ["Menu"]!.Normal.Style); // Capture current scheme colors Dictionary currentSchemes = SchemeManager.GetSchemes ()!; - Color currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; + Color currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentTopLevelNormalFg.ToString ()); + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentRunnableNormalFg.ToString ()); // Load the test theme Load (ConfigLocations.Runtime); @@ -716,9 +716,9 @@ public class SchemeManagerTests Assert.Equal (TextStyle.Reverse, SchemeManager.GetSchemesForCurrentTheme () ["Menu"]!.Normal.Style); currentSchemes = SchemeManager.GetSchemesForCurrentTheme ()!; - currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; + currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; // BUGBUG: We did not Apply after loading, so schemes should NOT have been updated - //Assert.Equal (hardCodedTopLevelNormalFg.ToString (), currentTopLevelNormalFg.ToString ()); + //Assert.Equal (hardCodedRunnableNormalFg.ToString (), currentRunnableNormalFg.ToString ()); // Now reset everything and reload ResetToHardCodedDefaults (); @@ -727,8 +727,8 @@ public class SchemeManagerTests Assert.Equal ("Default", ThemeManager.Theme); currentSchemes = SchemeManager.GetSchemes ()!; - currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (hardCodedTopLevelNormalFg.ToString (), currentTopLevelNormalFg.ToString ()); + currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (hardCodedRunnableNormalFg.ToString (), currentRunnableNormalFg.ToString ()); } finally @@ -754,7 +754,7 @@ public class SchemeManagerTests "TestTheme": { "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "AntiqueWhite", "Background": "DimGray" @@ -890,13 +890,13 @@ public class SchemeManagerTests // Capture dynamically created hardCoded hard-coded scheme colors ImmutableSortedDictionary hardCodedSchemes = SchemeManager.GetHardCodedSchemes ()!; - Color hardCodedTopLevelNormalFg = hardCodedSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedTopLevelNormalFg.ToString ()); + Color hardCodedRunnableNormalFg = hardCodedSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), hardCodedRunnableNormalFg.ToString ()); // Capture current scheme colors Dictionary currentSchemes = SchemeManager.GetSchemes ()!; - Color currentTopLevelNormalFg = currentSchemes ["TopLevel"].Normal.Foreground; - Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentTopLevelNormalFg.ToString ()); + Color currentRunnableNormalFg = currentSchemes ["Runnable"].Normal.Foreground; + Assert.Equal (new Color (StandardColor.CadetBlue).ToString (), currentRunnableNormalFg.ToString ()); // Load the test theme ConfigurationManager.SourcesManager?.Load (Settings, json, "UpdateFromJson", ConfigLocations.Runtime); @@ -904,7 +904,7 @@ public class SchemeManagerTests Assert.Equal ("TestTheme", ThemeManager.Theme); Assert.Equal (TextStyle.Reverse, SchemeManager.GetSchemesForCurrentTheme () ["Menu"]!.Normal.Style); Dictionary? hardCodedSchemesViaScope = GetHardCodedConfigPropertiesByScope ("ThemeScope")!.ToFrozenDictionary () ["Schemes"].PropertyValue as Dictionary; - Assert.Equal (hardCodedTopLevelNormalFg.ToString (), hardCodedSchemesViaScope! ["TopLevel"].Normal.Foreground.ToString ()); + Assert.Equal (hardCodedRunnableNormalFg.ToString (), hardCodedSchemesViaScope! ["Runnable"].Normal.Foreground.ToString ()); } finally @@ -919,7 +919,7 @@ public class SchemeManagerTests Enable (ConfigLocations.HardCoded); Assert.False (SchemeManager.GetSchemes ()!.ContainsKey ("test")); - Assert.Equal (5, SchemeManager.GetSchemes ().Count); // base, toplevel, menu, error, dialog + Assert.Equal (5, SchemeManager.GetSchemes ().Count); // base, runnable, menu, error, dialog var theme = new ThemeScope (); Assert.NotEmpty (theme); @@ -943,7 +943,7 @@ public class SchemeManagerTests // Act ThemeManager.Theme = "testTheme"; ThemeManager.Themes! [ThemeManager.Theme]!.Apply (); - Assert.Equal (5, SchemeManager.GetSchemes ().Count); // base, toplevel, menu, error, dialog + Assert.Equal (5, SchemeManager.GetSchemes ().Count); // base, runnable, menu, error, dialog // Assert Scheme updatedScheme = SchemeManager.GetSchemes () ["test"]!; diff --git a/Tests/UnitTests/Dialogs/DialogTests.cs b/Tests/UnitTests/Dialogs/DialogTests.cs index a8119f5fd..c9d9289fa 100644 --- a/Tests/UnitTests/Dialogs/DialogTests.cs +++ b/Tests/UnitTests/Dialogs/DialogTests.cs @@ -896,17 +896,15 @@ public class DialogTests (ITestOutputHelper output) { Dialog dlg = new (); - dlg.Ready += Dlg_Ready; + ApplicationImpl.Instance.StopAfterFirstIteration = true; Application.Run (dlg); #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.TopRunnable!.WasDisposed); - Assert.Equal (dlg, Application.TopRunnable); #endif - Assert.True (dlg.Canceled); + Assert.False (dlg.Canceled); // Run it again is possible because it isn't disposed yet Application.Run (dlg); @@ -914,7 +912,6 @@ public class DialogTests (ITestOutputHelper output) // Run another view without dispose the prior will throw an assertion #if DEBUG_IDISPOSABLE Dialog dlg2 = new (); - dlg2.Ready += Dlg_Ready; // Exception exception = Record.Exception (() => Application.Run (dlg2)); // Assert.NotNull (exception); @@ -925,34 +922,16 @@ public class DialogTests (ITestOutputHelper output) Application.Run (dlg2); Assert.True (dlg.WasDisposed); - Assert.False (Application.TopRunnable.WasDisposed); - Assert.Equal (dlg2, Application.TopRunnable); Assert.False (dlg2.WasDisposed); dlg2.Dispose (); - // tznind REMOVED: Why wouldn't you be able to read cancelled after dispose - that makes no sense - // Now an assertion will throw accessing the Canceled property - //var exception = Record.Exception (() => Assert.True (dlg.Canceled))!; - //Assert.NotNull (exception); - //Assert.StartsWith ("Cannot access a disposed object.", exception.Message); - - Assert.True (Application.TopRunnable.WasDisposed); Application.Shutdown (); Assert.True (dlg2.WasDisposed); - Assert.Null (Application.TopRunnable); #endif - - return; - - void Dlg_Ready (object? sender, EventArgs e) - { - ((Dialog)sender!).Canceled = true; - Application.RequestStop (); - } } - [Fact] + [Fact (Skip = "Convoluted test that needs to be rewritten")] [AutoInitShutdown] public void Dialog_In_Window_With_Size_One_Button_Aligns () { @@ -972,11 +951,15 @@ public class DialogTests (ITestOutputHelper output) Application.Iteration += OnApplicationOnIteration; var btn = $"{Glyphs.LeftBracket} Ok {Glyphs.RightBracket}"; - win.Loaded += (s, a) => + win.IsModalChanged += (s, a) => { + if (!a.Value) + { + return; + } var dlg = new Dialog { Width = 18, Height = 3, Buttons = [new () { Text = "Ok" }] }; - dlg.Loaded += (s, a) => + dlg.IsModalChanged += (s, a) => { AutoInitShutdownAttribute.RunIteration (); @@ -1109,7 +1092,7 @@ public class DialogTests (ITestOutputHelper output) } } - [Fact] + [Fact (Skip = "Convoluted test that needs to be rewritten")] [AutoInitShutdown] public void Dialog_Opened_From_Another_Dialog () { @@ -1159,7 +1142,7 @@ public class DialogTests (ITestOutputHelper output) Application.Iteration += OnApplicationOnIteration; - Application.Run ().Dispose (); + Application.Run (); Application.Iteration -= OnApplicationOnIteration; Application.Shutdown (); @@ -1174,8 +1157,8 @@ public class DialogTests (ITestOutputHelper output) switch (iterations) { case 0: - Application.TopRunnable!.SetNeedsLayout (); - Application.TopRunnable.SetNeedsDraw (); + Application.TopRunnableView!.SetNeedsLayout (); + Application.TopRunnableView.SetNeedsDraw (); break; @@ -1216,7 +1199,7 @@ public class DialogTests (ITestOutputHelper output) └───────────────────────┘", output); - Assert.False (Application.TopRunnable!.NewKeyDownEvent (Key.Enter)); + Assert.False (Application.TopRunnableView!.NewKeyDownEvent (Key.Enter)); break; case 7: @@ -1242,8 +1225,8 @@ public class DialogTests (ITestOutputHelper output) { for (var i = 0; i < 8; i++) { + ApplicationImpl.Instance.StopAfterFirstIteration = true; var fd = new FileDialog (); - fd.Ready += (s, e) => Application.RequestStop (); Application.Run (fd); fd.Dispose (); } @@ -1260,6 +1243,7 @@ public class DialogTests (ITestOutputHelper output) }; Application.Begin (d); Application.Driver?.SetScreenSize (100, 100); + Application.LayoutAndDraw (); // Default location is centered, so 100 / 2 - 85 / 2 = 7 var expected = 7; @@ -1296,6 +1280,7 @@ public class DialogTests (ITestOutputHelper output) var d = new Dialog { X = expected, Y = expected, Height = 5, Width = 5 }; Application.Begin (d); Application.Driver?.SetScreenSize (20, 10); + Application.LayoutAndDraw (); // Default location is centered, so 100 / 2 - 85 / 2 = 7 Assert.Equal (new (expected, expected), d.Frame.Location); @@ -1316,7 +1301,7 @@ public class DialogTests (ITestOutputHelper output) [AutoInitShutdown] public void Modal_Captures_All_Mouse () { - var top = new Toplevel + var top = new Runnable { Id = "top" }; @@ -1400,19 +1385,15 @@ public class DialogTests (ITestOutputHelper output) [AutoInitShutdown] public void Run_Does_Not_Dispose_Dialog () { - var top = new Toplevel (); + var top = new Runnable (); - Dialog dlg = new (); - - dlg.Ready += Dlg_Ready; + Dialog dlg = new () { }; + ApplicationImpl.Instance.StopAfterFirstIteration = true; Application.Run (dlg); #if DEBUG_IDISPOSABLE Assert.False (dlg.WasDisposed); - Assert.False (Application.TopRunnable!.WasDisposed); - Assert.NotEqual (top, Application.TopRunnable); - Assert.Equal (dlg, Application.TopRunnable); #endif // dlg wasn't disposed yet and it's possible to access to his properties @@ -1426,15 +1407,8 @@ public class DialogTests (ITestOutputHelper output) top.Dispose (); #if DEBUG_IDISPOSABLE Assert.True (dlg.WasDisposed); - Assert.True (Application.TopRunnable.WasDisposed); - Assert.NotNull (Application.TopRunnable); #endif Application.Shutdown (); - Assert.Null (Application.TopRunnable); - - return; - - void Dlg_Ready (object? sender, EventArgs e) { Application.RequestStop (); } } [Fact] @@ -1449,6 +1423,7 @@ public class DialogTests (ITestOutputHelper output) Application.Begin (d); Application.Driver?.SetScreenSize (100, 100); + Application.LayoutAndDraw (); // Default size is Percent(85) Assert.Equal (new ((int)(100 * .85), (int)(100 * .85)), d.Frame.Size); diff --git a/Tests/UnitTests/Dialogs/WizardTests.cs b/Tests/UnitTests/Dialogs/WizardTests.cs index 951e86900..fcbed9e22 100644 --- a/Tests/UnitTests/Dialogs/WizardTests.cs +++ b/Tests/UnitTests/Dialogs/WizardTests.cs @@ -12,7 +12,7 @@ public class WizardTests wizard.Dispose (); } - [Fact] + [Fact (Skip = "Convoluted test that needs to be rewritten")] [AutoInitShutdown] public void Finish_Button_Closes () { @@ -24,8 +24,8 @@ public class WizardTests var finishedFired = false; wizard.Finished += (s, args) => { finishedFired = true; }; - var closedFired = false; - wizard.Closed += (s, e) => { closedFired = true; }; + var isRunningChangedFired = false; + wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; SessionToken sessionToken = Application.Begin (wizard); AutoInitShutdownAttribute.RunIteration (); @@ -34,7 +34,7 @@ public class WizardTests AutoInitShutdownAttribute.RunIteration (); Application.End (sessionToken); Assert.True (finishedFired); - Assert.True (closedFired); + Assert.True (isRunningChangedFired); step1.Dispose (); wizard.Dispose (); @@ -48,8 +48,8 @@ public class WizardTests finishedFired = false; wizard.Finished += (s, args) => { finishedFired = true; }; - closedFired = false; - wizard.Closed += (s, e) => { closedFired = true; }; + isRunningChangedFired = false; + wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; sessionToken = Application.Begin (wizard); AutoInitShutdownAttribute.RunIteration (); @@ -57,14 +57,14 @@ public class WizardTests Assert.Equal (step1.Title, wizard.CurrentStep.Title); wizard.NextFinishButton.InvokeCommand (Command.Accept); Assert.False (finishedFired); - Assert.False (closedFired); + Assert.False (isRunningChangedFired); Assert.Equal (step2.Title, wizard.CurrentStep.Title); Assert.Equal (wizard.GetLastStep ().Title, wizard.CurrentStep.Title); wizard.NextFinishButton.InvokeCommand (Command.Accept); Application.End (sessionToken); Assert.True (finishedFired); - Assert.True (closedFired); + Assert.True (isRunningChangedFired); step1.Dispose (); step2.Dispose (); @@ -81,8 +81,8 @@ public class WizardTests finishedFired = false; wizard.Finished += (s, args) => { finishedFired = true; }; - closedFired = false; - wizard.Closed += (s, e) => { closedFired = true; }; + isRunningChangedFired = false; + wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; sessionToken = Application.Begin (wizard); AutoInitShutdownAttribute.RunIteration (); @@ -92,7 +92,7 @@ public class WizardTests wizard.NextFinishButton.InvokeCommand (Command.Accept); Application.End (sessionToken); Assert.True (finishedFired); - Assert.True (closedFired); + Assert.True (isRunningChangedFired); wizard.Dispose (); } diff --git a/Tests/UnitTests/FakeDriver/FakeApplicationFactory.cs b/Tests/UnitTests/FakeDriver/FakeApplicationFactory.cs index 7e51c3c74..6d39b6b1d 100644 --- a/Tests/UnitTests/FakeDriver/FakeApplicationFactory.cs +++ b/Tests/UnitTests/FakeDriver/FakeApplicationFactory.cs @@ -29,6 +29,6 @@ public class FakeApplicationFactory // Initialize with a fake driver impl.Init ("fake"); - return new FakeApplicationLifecycle (hardStopTokenSource); + return new FakeApplicationLifecycle (impl, hardStopTokenSource); } } diff --git a/Tests/UnitTests/FakeDriver/FakeApplicationLifecycle.cs b/Tests/UnitTests/FakeDriver/FakeApplicationLifecycle.cs index f24764561..df7e217c1 100644 --- a/Tests/UnitTests/FakeDriver/FakeApplicationLifecycle.cs +++ b/Tests/UnitTests/FakeDriver/FakeApplicationLifecycle.cs @@ -6,14 +6,14 @@ namespace Terminal.Gui.Drivers; /// the provided and shutting down the application. /// /// -internal class FakeApplicationLifecycle (CancellationTokenSource hardStop) : IDisposable +internal class FakeApplicationLifecycle (IApplication? app, CancellationTokenSource? hardStop) : IDisposable { /// public void Dispose () { - hardStop.Cancel (); + hardStop?.Cancel (); - Application.TopRunnable?.Dispose (); - Application.Shutdown (); + app?.TopRunnableView?.Dispose (); + app?.Dispose (); } } diff --git a/Tests/UnitTests/README.md b/Tests/UnitTests/README.md index 217607fd3..db8d0451c 100644 --- a/Tests/UnitTests/README.md +++ b/Tests/UnitTests/README.md @@ -1,3 +1,5 @@ # Automated Unit Tests (non-Parallelizable) +**IMPORTANT:** New tests belong in [UnitTests.Parallelizable](../UnitTestsParallelizable/README.md). Please use the [UnitTests.Parallelizable](../UnitTestsParallelizable/README.md) project for all new tests. + See the [Testing wiki](https://github.com/gui-cs/Terminal.Gui/wiki/Testing) for details on how to add more tests. diff --git a/Tests/UnitTests/SetupFakeApplicationAttribute.cs b/Tests/UnitTests/SetupFakeApplicationAttribute.cs index 06d338436..8907caff5 100644 --- a/Tests/UnitTests/SetupFakeApplicationAttribute.cs +++ b/Tests/UnitTests/SetupFakeApplicationAttribute.cs @@ -19,7 +19,6 @@ public class SetupFakeApplicationAttribute : BeforeAfterTestAttribute { Debug.WriteLine ($"Before: {methodUnderTest.Name}"); - _appDispose?.Dispose (); var appFactory = new FakeApplicationFactory (); _appDispose = appFactory.SetupFakeApplication (); diff --git a/Tests/UnitTests/TestsAllViews.cs b/Tests/UnitTests/TestsAllViews.cs index 583f94457..2098c1e21 100644 --- a/Tests/UnitTests/TestsAllViews.cs +++ b/Tests/UnitTests/TestsAllViews.cs @@ -66,7 +66,7 @@ public class TestsAllViews : FakeDriverBase { // Check if this type parameter has constraints that object can't satisfy Type [] constraints = arg.GetGenericParameterConstraints (); - + // If there's a View constraint, use View instead of object if (constraints.Any (c => c == typeof (View) || c.IsSubclassOf (typeof (View)))) { @@ -189,7 +189,7 @@ public class TestsAllViews : FakeDriverBase } else if (paramType.Name == "View") { - var top = new Toplevel (); + var top = new Runnable (); var view = new View (); top.Add (view); pTypes.Add (view); diff --git a/Tests/UnitTests/Text/AutocompleteTests.cs b/Tests/UnitTests/Text/AutocompleteTests.cs index ec2a0dd44..b517cdf42 100644 --- a/Tests/UnitTests/Text/AutocompleteTests.cs +++ b/Tests/UnitTests/Text/AutocompleteTests.cs @@ -19,7 +19,7 @@ public class AutocompleteTests (ITestOutputHelper output) .Select (s => s.Value) .Distinct () .ToList (); - Toplevel top = new (); + Runnable top = new (); top.Add (tv); SessionToken rs = Application.Begin (top); @@ -165,7 +165,7 @@ This an long line and against TextView.", public void KeyBindings_Command () { var tv = new TextView { Width = 10, Height = 2, Text = " Fortunately super feature." }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); diff --git a/Tests/UnitTests/UICatalog/ScenarioTests.cs b/Tests/UnitTests/UICatalog/ScenarioTests.cs index b4d5849b6..7f0f0c8dc 100644 --- a/Tests/UnitTests/UICatalog/ScenarioTests.cs +++ b/Tests/UnitTests/UICatalog/ScenarioTests.cs @@ -205,7 +205,7 @@ public class ScenarioTests : TestsAllViews Application.Init ("fake"); - var top = new Toplevel (); + var top = new Runnable (); Dictionary viewClasses = GetAllViewClasses ().ToDictionary (t => t.Name); @@ -217,7 +217,7 @@ public class ScenarioTests : TestsAllViews Width = 15, Height = Dim.Fill (1), // for status bar CanFocus = false, - SchemeName = "TopLevel" + SchemeName = "Runnable" }; ListView classListView = new () @@ -227,7 +227,7 @@ public class ScenarioTests : TestsAllViews Width = Dim.Fill (), Height = Dim.Fill (), AllowsMarking = false, - SchemeName = "TopLevel", + SchemeName = "Runnable", Source = new ListWrapper (new (viewClasses.Keys.ToList ())) }; leftPane.Add (classListView); @@ -239,7 +239,7 @@ public class ScenarioTests : TestsAllViews Width = Dim.Fill (), Height = 10, CanFocus = false, - SchemeName = "TopLevel", + SchemeName = "Runnable", Title = "Settings" }; diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 6734d73b8..032f6b6c9 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -26,6 +26,11 @@ true + + + + + @@ -58,7 +63,4 @@ - - - \ No newline at end of file diff --git a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs b/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs deleted file mode 100644 index d5015c7b4..000000000 --- a/Tests/UnitTests/View/Adornment/AdornmentSubViewTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Xunit.Abstractions; - -namespace UnitTests.ViewTests; - -public class AdornmentSubViewTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - [Theory] - [InlineData (0, 0, false)] // Margin has no thickness, so false - [InlineData (0, 1, false)] // Margin has no thickness, so false - [InlineData (1, 0, true)] - [InlineData (1, 1, true)] - [InlineData (2, 1, true)] - public void Adornment_WithSubView_Finds (int viewMargin, int subViewMargin, bool expectedFound) - { - Application.TopRunnable = new Toplevel() - { - Width = 10, - Height = 10 - }; - Application.TopRunnable.Margin!.Thickness = new Thickness (viewMargin); - // Turn of TransparentMouse for the test - Application.TopRunnable.Margin!.ViewportSettings = ViewportSettingsFlags.None; - - var subView = new View () - { - X = 0, - Y = 0, - Width = 5, - Height = 5 - }; - subView.Margin!.Thickness = new Thickness (subViewMargin); - // Turn of TransparentMouse for the test - subView.Margin!.ViewportSettings = ViewportSettingsFlags.None; - - Application.TopRunnable.Margin!.Add (subView); - Application.TopRunnable.Layout (); - - var foundView = Application.TopRunnable.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault (); - - bool found = foundView == subView || foundView == subView.Margin; - Assert.Equal (expectedFound, found); - Application.TopRunnable.Dispose (); - Application.ResetState (ignoreDisposed: true); - } - - [Fact] - public void Adornment_WithNonVisibleSubView_Finds_Adornment () - { - Application.TopRunnable = new Toplevel () - { - Width = 10, - Height = 10 - }; - Application.TopRunnable.Padding.Thickness = new Thickness (1); - - var subView = new View () - { - X = 0, - Y = 0, - Width = 1, - Height = 1, - Visible = false - }; - Application.TopRunnable.Padding.Add (subView); - Application.TopRunnable.Layout (); - - Assert.Equal (Application.TopRunnable.Padding, Application.TopRunnable.GetViewsUnderLocation (new Point(0, 0), ViewportSettingsFlags.None).LastOrDefault ()); - Application.TopRunnable?.Dispose (); - Application.ResetState (ignoreDisposed: true); - } -} diff --git a/Tests/UnitTests/View/Adornment/AdornmentTests.cs b/Tests/UnitTests/View/Adornment/AdornmentTests.cs index 3ce9f3b5e..6c19e7c70 100644 --- a/Tests/UnitTests/View/Adornment/AdornmentTests.cs +++ b/Tests/UnitTests/View/Adornment/AdornmentTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class AdornmentTests (ITestOutputHelper output) { diff --git a/Tests/UnitTests/View/Adornment/BorderTests.cs b/Tests/UnitTests/View/Adornment/BorderTests.cs index e7f02752d..332cd0304 100644 --- a/Tests/UnitTests/View/Adornment/BorderTests.cs +++ b/Tests/UnitTests/View/Adornment/BorderTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class BorderTests (ITestOutputHelper output) { @@ -729,7 +729,7 @@ public class BorderTests (ITestOutputHelper output) [AutoInitShutdown] public void HasSuperView () { - var top = new Toplevel (); + var top = new Runnable (); top.BorderStyle = LineStyle.Double; var frame = new FrameView { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; @@ -756,7 +756,7 @@ public class BorderTests (ITestOutputHelper output) [AutoInitShutdown] public void HasSuperView_Title () { - var top = new Toplevel (); + var top = new Runnable (); top.BorderStyle = LineStyle.Double; var frame = new FrameView { Title = "1234", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; diff --git a/Tests/UnitTests/View/Adornment/MarginTests.cs b/Tests/UnitTests/View/Adornment/MarginTests.cs deleted file mode 100644 index 6a3cd0b8d..000000000 --- a/Tests/UnitTests/View/Adornment/MarginTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.ViewTests; - -public class MarginTests (ITestOutputHelper output) -{ - [Fact] - [SetupFakeApplication] - public void Margin_Is_Transparent () - { - Application.Driver!.SetScreenSize (5, 5); - - var view = new View { Height = 3, Width = 3 }; - view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; - view.Margin.Thickness = new (1); - - Application.TopRunnable = new Toplevel (); - Application.SessionStack.Push (Application.TopRunnable); - - Application.TopRunnable.SetScheme (new() - { - Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) - }); - - Application.TopRunnable.Add (view); - Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - Application.LayoutAndDraw(); - - DriverAssert.AssertDriverContentsAre ( - @"", - output - ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal)); - - Application.ResetState (true); - } - - [Fact] - [SetupFakeApplication] - public void Margin_ViewPortSettings_Not_Transparent_Is_NotTransparent () - { - Application.Driver!.SetScreenSize (5, 5); - - var view = new View { Height = 3, Width = 3 }; - view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; - view.Margin.Thickness = new (1); - view.Margin.ViewportSettings = ViewportSettingsFlags.None; - - Application.TopRunnable = new Toplevel (); - Application.SessionStack.Push (Application.TopRunnable); - - Application.TopRunnable.SetScheme (new () - { - Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) - }); - - Application.TopRunnable.Add (view); - Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - Assert.Equal (ColorName16.Red, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); - - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsAre ( - @" -MMM -M M -MMM", - output - ); - DriverAssert.AssertDriverAttributesAre ("0", output, null, Application.TopRunnable.GetAttributeForRole (VisualRole.Normal)); - - Application.ResetState (true); - } -} diff --git a/Tests/UnitTests/View/Adornment/PaddingTests.cs b/Tests/UnitTests/View/Adornment/PaddingTests.cs index f48191350..ad96a1d0a 100644 --- a/Tests/UnitTests/View/Adornment/PaddingTests.cs +++ b/Tests/UnitTests/View/Adornment/PaddingTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class PaddingTests (ITestOutputHelper output) { diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index 48f24476c..ef8cb488e 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class ShadowStyleTests (ITestOutputHelper output) { @@ -46,7 +46,7 @@ public class ShadowStyleTests (ITestOutputHelper output) new (fg.GetDimColor (), bg.GetDimColor ()) }; - var superView = new Toplevel + var superView = new Runnable { Driver = ApplicationImpl.Instance.Driver, Height = 3, @@ -66,9 +66,10 @@ public class ShadowStyleTests (ITestOutputHelper output) view.SetScheme (new (Attribute.Default)); superView.Add (view); - Application.SessionStack.Push (superView); + Application.Begin (superView); Application.LayoutAndDraw (true); DriverAssert.AssertDriverAttributesAre (expectedAttrs, output, Application.Driver, attributes); + superView.Dispose (); Application.ResetState (true); } @@ -103,7 +104,7 @@ public class ShadowStyleTests (ITestOutputHelper output) { Application.Driver!.SetScreenSize (5, 5); - var superView = new Toplevel + var superView = new Runnable { Driver = ApplicationImpl.Instance.Driver, Width = 4, @@ -120,11 +121,11 @@ public class ShadowStyleTests (ITestOutputHelper output) }; view.ShadowStyle = style; superView.Add (view); - Application.SessionStack.Push (superView); + Application.Begin (superView); Application.LayoutAndDraw (true); DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - view.Dispose (); + superView.Dispose (); Application.ResetState (true); } diff --git a/Tests/UnitTests/View/ArrangementTests.cs b/Tests/UnitTests/View/ArrangementTests.cs index c6b88b8ce..19aa25c5d 100644 --- a/Tests/UnitTests/View/ArrangementTests.cs +++ b/Tests/UnitTests/View/ArrangementTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class ArrangementTests (ITestOutputHelper output) { diff --git a/Tests/UnitTests/View/DiagnosticsTests.cs b/Tests/UnitTests/View/DiagnosticsTests.cs index 258a0ab3b..d5b95b1e7 100644 --- a/Tests/UnitTests/View/DiagnosticsTests.cs +++ b/Tests/UnitTests/View/DiagnosticsTests.cs @@ -1,7 +1,7 @@ #nullable enable using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; /// /// Tests static property and enum. diff --git a/Tests/UnitTests/View/Draw/ClearViewportTests.cs b/Tests/UnitTests/View/Draw/ClearViewportTests.cs index db64eb72d..8c8e3c54f 100644 --- a/Tests/UnitTests/View/Draw/ClearViewportTests.cs +++ b/Tests/UnitTests/View/Draw/ClearViewportTests.cs @@ -3,7 +3,7 @@ using Moq; using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class ClearViewportTests (ITestOutputHelper output) @@ -214,10 +214,11 @@ public class ClearViewportTests (ITestOutputHelper output) view.SetClip (savedClip); e.Cancel = true; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); Application.Driver!.SetScreenSize (20, 10); + Application.LayoutAndDraw (); var expected = @" ┌──────────────────┐ @@ -279,10 +280,11 @@ public class ClearViewportTests (ITestOutputHelper output) view.SetClip (savedClip); e.Cancel = true; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); Application.Driver!.SetScreenSize (20, 10); + Application.LayoutAndDraw (); var expected = @" ┌──────────────────┐ @@ -320,103 +322,4 @@ public class ClearViewportTests (ITestOutputHelper output) top.Dispose (); } - - [Theory (Skip = "This test is too fragile; depends on Library Resoruces/Themes which can easily change.")] - [AutoInitShutdown] - [InlineData (true)] - [InlineData (false)] - public void Clear_Does_Not_Spillover_Its_Parent (bool label) - { - ConfigurationManager.Enable (ConfigLocations.LibraryResources); - - View root = new () { Width = 20, Height = 10 }; - - string text = new ('c', 100); - - View v = label - - // Label has Width/Height == AutoSize, so Frame.Size will be (100, 1) - ? new Label { Text = text } - - // TextView has Width/Height == (Dim.Fill, 1), so Frame.Size will be 20 (width of root), 1 - : new TextView { Width = Dim.Fill (), Height = 1, Text = text }; - - root.Add (v); - - Toplevel top = new (); - top.Add (root); - SessionToken sessionToken = Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - if (label) - { - Assert.False (v.CanFocus); - Assert.Equal (new (0, 0, text.Length, 1), v.Frame); - } - else - { - Assert.True (v.CanFocus); - Assert.Equal (new (0, 0, 20, 1), v.Frame); - } - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -cccccccccccccccccccc", - output - ); - - Attribute [] attributes = - { - SchemeManager.GetSchemes () ["TopLevel"]!.Normal, - SchemeManager.GetSchemes () ["Base"]!.Normal, - SchemeManager.GetSchemes () ["Base"]!.Focus - }; - - if (label) - { - DriverAssert.AssertDriverAttributesAre ( - @" -111111111111111111110 -111111111111111111110", - output, - Application.Driver, - attributes - ); - } - else - { - DriverAssert.AssertDriverAttributesAre ( - @" -222222222222222222220 -111111111111111111110", - output, - Application.Driver, - attributes - ); - } - - if (label) - { - root.CanFocus = true; - v.CanFocus = true; - Assert.True (v.HasFocus); - v.SetFocus (); - Assert.True (v.HasFocus); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -222222222222222222220 -111111111111111111110", - output, - Application.Driver, - attributes - ); - } - - Application.End (sessionToken); - top.Dispose (); - - CM.Disable (resetToHardCodedDefaults: true); - } } diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs index 1efec5d43..565795f85 100644 --- a/Tests/UnitTests/View/Draw/ClipTests.cs +++ b/Tests/UnitTests/View/Draw/ClipTests.cs @@ -3,7 +3,7 @@ using System.Text; using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class ClipTests (ITestOutputHelper _output) diff --git a/Tests/UnitTests/View/Draw/DrawEventTests.cs b/Tests/UnitTests/View/Draw/DrawEventTests.cs index 4b091d971..c037af3d5 100644 --- a/Tests/UnitTests/View/Draw/DrawEventTests.cs +++ b/Tests/UnitTests/View/Draw/DrawEventTests.cs @@ -1,7 +1,7 @@ #nullable enable using UnitTests; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class DrawEventTests @@ -18,7 +18,7 @@ public class DrawEventTests var tv = new TextView { Y = 11, Width = 10, Height = 10 }; tv.DrawComplete += (s, e) => tvCalled = true; - var top = new Toplevel (); + var top = new Runnable (); top.Add (view, tv); Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); diff --git a/Tests/UnitTests/View/Draw/DrawTests.cs b/Tests/UnitTests/View/Draw/DrawTests.cs index 11682d9ce..7cce52b6b 100644 --- a/Tests/UnitTests/View/Draw/DrawTests.cs +++ b/Tests/UnitTests/View/Draw/DrawTests.cs @@ -3,7 +3,7 @@ using System.Text; using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class DrawTests (ITestOutputHelper output) @@ -28,11 +28,12 @@ public class DrawTests (ITestOutputHelper output) var view = new View { Text = r.ToString (), Height = Dim.Fill (), Width = Dim.Fill () }; var tf = new TextField { Text = s, Y = 1, Width = 3 }; win.Add (view, tf); - Toplevel top = new (); + Runnable top = new (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (10, 4); + Application.LayoutAndDraw (); const string expectedOutput = """ @@ -71,7 +72,7 @@ public class DrawTests (ITestOutputHelper output) Height = 6, VerticalTextAlignment = Alignment.End, }; - Toplevel top = new (); + Runnable top = new (); top.Add (viewRight, viewBottom); var rs = Application.Begin (top); @@ -304,7 +305,7 @@ public class DrawTests (ITestOutputHelper output) Height = 5 }; container.Add (content); - Toplevel top = new (); + Runnable top = new (); top.Add (container); var rs = Application.Begin (top); @@ -421,7 +422,7 @@ public class DrawTests (ITestOutputHelper output) Height = 5 }; container.Add (content); - Toplevel top = new (); + Runnable top = new (); top.Add (container); // BUGBUG: v2 - it's bogus to reference .Frame before BeginInit. And why is the clip being set anyway??? @@ -512,7 +513,7 @@ public class DrawTests (ITestOutputHelper output) Height = 5 }; container.Add (content); - Toplevel top = new (); + Runnable top = new (); top.Add (container); Application.Begin (top); @@ -636,12 +637,12 @@ public class DrawTests (ITestOutputHelper output) var view = new Label { Text = r.ToString () }; var tf = new TextField { Text = us, Y = 1, Width = 3 }; win.Add (view, tf); - Toplevel top = new (); + Runnable top = new (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (10, 4); - + Application.LayoutAndDraw (); var expected = """ @@ -662,7 +663,7 @@ public class DrawTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_Throws_IndexOutOfRangeException_With_Negative_Bounds () { - Toplevel top = new (); + Runnable top = new (); var view = new View { X = -2, Text = "view" }; top.Add (view); @@ -713,7 +714,7 @@ public class DrawTests (ITestOutputHelper output) Height = 2, Text = "A text with some long width\n and also with two lines." }; - Toplevel top = new (); + Runnable top = new (); top.Add (label, view); SessionToken sessionToken = Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -761,7 +762,7 @@ At 0,0 Height = 2, Text = "A text with some long width\n and also with two lines." }; - Toplevel top = new (); + Runnable top = new (); top.Add (label, view); SessionToken sessionToken = Application.Begin (top); @@ -814,7 +815,7 @@ At 0,0 Height = 2, Text = "A text with some long width\n and also with two lines." }; - Toplevel top = new (); + Runnable top = new (); top.Add (label, view); SessionToken sessionToken = Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -860,7 +861,7 @@ At 0,0 Height = 2, Text = "A text with some long width\n and also with two lines." }; - Toplevel top = new (); + Runnable top = new (); top.Add (label, view); SessionToken sessionToken = Application.Begin (top); diff --git a/Tests/UnitTests/View/Draw/NeedsDrawTests.cs b/Tests/UnitTests/View/Draw/NeedsDrawTests.cs index 9928dd14c..231590f72 100644 --- a/Tests/UnitTests/View/Draw/NeedsDrawTests.cs +++ b/Tests/UnitTests/View/Draw/NeedsDrawTests.cs @@ -1,7 +1,7 @@ #nullable enable using UnitTests; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class NeedsDrawTests () @@ -33,7 +33,7 @@ public class NeedsDrawTests () frame.Width = 40; frame.Height = 8; - Toplevel top = new (); + Runnable top = new (); top.Add (frame); diff --git a/Tests/UnitTests/View/Draw/TransparentTests.cs b/Tests/UnitTests/View/Draw/TransparentTests.cs index 050ae18cb..39f966a92 100644 --- a/Tests/UnitTests/View/Draw/TransparentTests.cs +++ b/Tests/UnitTests/View/Draw/TransparentTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "Output")] public class TransparentTests (ITestOutputHelper output) diff --git a/Tests/UnitTests/View/Layout/Dim.FillTests.cs b/Tests/UnitTests/View/Layout/Dim.FillTests.cs deleted file mode 100644 index 7345147c0..000000000 --- a/Tests/UnitTests/View/Layout/Dim.FillTests.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Xunit.Abstractions; - -namespace UnitTests.LayoutTests; - -public class DimFillTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - [Fact] - public void DimFill_SizedCorrectly () - { - var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; - var top = new Toplevel (); - top.Add (view); - - top.Layout (); - - view.SetRelativeLayout (new (32, 5)); - Assert.Equal (32, view.Frame.Width); - Assert.Equal (5, view.Frame.Height); - top.Dispose (); - } -} diff --git a/Tests/UnitTests/View/Layout/Dim.Tests.cs b/Tests/UnitTests/View/Layout/Dim.Tests.cs index db4a2b349..6caade5a1 100644 --- a/Tests/UnitTests/View/Layout/Dim.Tests.cs +++ b/Tests/UnitTests/View/Layout/Dim.Tests.cs @@ -24,7 +24,7 @@ public class DimTests // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved // TODO: A new test that calls SetRelativeLayout directly is needed. - [Fact] + [Fact (Skip = "Convoluted test; rewrite")] [AutoInitShutdown] public void Only_DimAbsolute_And_DimFactor_As_A_Different_Procedure_For_Assigning_Value_To_Width_Or_Height () { @@ -32,7 +32,7 @@ public class DimTests Button.DefaultShadow = ShadowStyle.None; // Testing with the Button because it properly handles the Dim class. - Toplevel t = new (); + Runnable t = new (); var w = new Window { Width = 100, Height = 100 }; @@ -111,7 +111,7 @@ public class DimTests w.Add (f1, f2, v1, v2, v3, v4, v5, v6); t.Add (w); - t.Ready += (s, e) => + t.IsModalChanged += (s, e) => { Assert.Equal ("Absolute(100)", w.Width.ToString ()); Assert.Equal ("Absolute(100)", w.Height.ToString ()); diff --git a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs b/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs deleted file mode 100644 index c19411ac6..000000000 --- a/Tests/UnitTests/View/Layout/GetViewsUnderLocationTests.cs +++ /dev/null @@ -1,840 +0,0 @@ -#nullable enable - -namespace UnitTests.ViewMouseTests; - -[Trait ("Category", "Input")] -public class GetViewsUnderLocationTests -{ - [Theory] - [InlineData (0, 0, 0, 0, 0, -1, -1, new string [] { })] - [InlineData (0, 0, 0, 0, 0, 0, 0, new [] { "Top" })] - [InlineData (0, 0, 0, 0, 0, 1, 1, new [] { "Top" })] - [InlineData (0, 0, 0, 0, 0, 4, 4, new [] { "Top" })] - [InlineData (0, 0, 0, 0, 0, 9, 9, new [] { "Top" })] - [InlineData (0, 0, 0, 0, 0, 10, 10, new string [] { })] - [InlineData (1, 1, 0, 0, 0, -1, -1, new string [] { })] - [InlineData (1, 1, 0, 0, 0, 0, 0, new string [] { })] - [InlineData (1, 1, 0, 0, 0, 1, 1, new [] { "Top" })] - [InlineData (1, 1, 0, 0, 0, 4, 4, new [] { "Top" })] - [InlineData (1, 1, 0, 0, 0, 9, 9, new [] { "Top" })] - [InlineData (1, 1, 0, 0, 0, 10, 10, new [] { "Top" })] - [InlineData (0, 0, 1, 0, 0, -1, -1, new string [] { })] - [InlineData (0, 0, 1, 0, 0, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 0, 0, 1, 1, new [] { "Top" })] - [InlineData (0, 0, 1, 0, 0, 4, 4, new [] { "Top" })] - [InlineData (0, 0, 1, 0, 0, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 0, 0, 10, 10, new string [] { })] - [InlineData (0, 0, 1, 1, 0, -1, -1, new string [] { })] - [InlineData (0, 0, 1, 1, 0, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 1, 0, 1, 1, new [] { "Top", "Border" })] - [InlineData (0, 0, 1, 1, 0, 4, 4, new [] { "Top" })] - [InlineData (0, 0, 1, 1, 0, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 1, 0, 10, 10, new string [] { })] - [InlineData (0, 0, 1, 1, 1, -1, -1, new string [] { })] - [InlineData (0, 0, 1, 1, 1, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 1, 1, 1, 1, new [] { "Top", "Border" })] - [InlineData (0, 0, 1, 1, 1, 2, 2, new [] { "Top", "Padding" })] - [InlineData (0, 0, 1, 1, 1, 4, 4, new [] { "Top" })] - [InlineData (0, 0, 1, 1, 1, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (0, 0, 1, 1, 1, 10, 10, new string [] { })] - [InlineData (1, 1, 1, 0, 0, -1, -1, new string [] { })] - [InlineData (1, 1, 1, 0, 0, 0, 0, new string [] { })] - [InlineData (1, 1, 1, 0, 0, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (1, 1, 1, 0, 0, 4, 4, new [] { "Top" })] - [InlineData (1, 1, 1, 0, 0, 9, 9, new [] { "Top" })] - [InlineData (1, 1, 1, 0, 0, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (1, 1, 1, 1, 0, -1, -1, new string [] { })] - [InlineData (1, 1, 1, 1, 0, 0, 0, new string [] { })] - [InlineData (1, 1, 1, 1, 0, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (1, 1, 1, 1, 0, 4, 4, new [] { "Top" })] - [InlineData (1, 1, 1, 1, 0, 9, 9, new [] { "Top", "Border" })] - [InlineData (1, 1, 1, 1, 0, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (1, 1, 1, 1, 1, -1, -1, new string [] { })] - [InlineData (1, 1, 1, 1, 1, 0, 0, new string [] { })] - [InlineData (1, 1, 1, 1, 1, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse - [InlineData (1, 1, 1, 1, 1, 2, 2, new [] { "Top", "Border" })] - [InlineData (1, 1, 1, 1, 1, 3, 3, new [] { "Top", "Padding" })] - [InlineData (1, 1, 1, 1, 1, 4, 4, new [] { "Top" })] - [InlineData (1, 1, 1, 1, 1, 8, 8, new [] { "Top", "Padding" })] - [InlineData (1, 1, 1, 1, 1, 9, 9, new [] { "Top", "Border" })] - [InlineData (1, 1, 1, 1, 1, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse - public void Top_Adornments_Returns_Correct_View ( - int frameX, - int frameY, - int marginThickness, - int borderThickness, - int paddingThickness, - int testX, - int testY, - string [] expectedViewsFound - ) - { - // Arrange - Application.TopRunnable = new () - { - Id = "Top", - Frame = new (frameX, frameY, 10, 10) - }; - Application.TopRunnable.Margin!.Thickness = new (marginThickness); - Application.TopRunnable.Margin!.Id = "Margin"; - Application.TopRunnable.Border!.Thickness = new (borderThickness); - Application.TopRunnable.Border!.Id = "Border"; - Application.TopRunnable.Padding!.Thickness = new (paddingThickness); - Application.TopRunnable.Padding.Id = "Padding"; - - var location = new Point (testX, testY); - - // Act - List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); - - // Assert - if (expectedViewsFound.Length == 0) - { - Assert.Empty (viewsUnderMouse); - } - else - { - string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); - Assert.Equal (expectedViewsFound, foundIds); - } - - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0)] - [InlineData (1, 1)] - [InlineData (2, 2)] - public void Returns_Top_If_No_SubViews (int testX, int testY) - { - // Arrange - Application.TopRunnable = new () - { - Frame = new (0, 0, 10, 10) - }; - - var location = new Point (testX, testY); - - // Act - List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); - - // Assert - Assert.Contains (viewsUnderMouse, v => v == Application.TopRunnable); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation returns the correct view if the start view has no subviews - [Theory] - [InlineData (0, 0)] - [InlineData (1, 1)] - [InlineData (2, 2)] - public void Returns_Start_If_No_SubViews (int testX, int testY) - { - Application.ResetState (true); - - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - Assert.Same (Application.TopRunnable, Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation returns the correct view if the start view has subviews - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (6, 7, false)] - [InlineData (1, 2, true)] - [InlineData (5, 6, true)] - public void Returns_Correct_If_SubViews (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - var subview = new View - { - X = 1, Y = 2, - Width = 5, Height = 5 - }; - Application.TopRunnable.Add (subview); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (6, 7, false)] - [InlineData (1, 2, false)] - [InlineData (5, 6, false)] - public void Returns_Null_If_SubView_NotVisible (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - var subview = new View - { - X = 1, Y = 2, - Width = 5, Height = 5, - Visible = false - }; - Application.TopRunnable.Add (subview); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (6, 7, false)] - [InlineData (1, 2, false)] - [InlineData (5, 6, false)] - public void Returns_Null_If_Not_Visible_And_SubView_Visible (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10, - Visible = false - }; - - var subview = new View - { - X = 1, Y = 2, - Width = 5, Height = 5 - }; - Application.TopRunnable.Add (subview); - subview.Visible = true; - Assert.True (subview.Visible); - Assert.False (Application.TopRunnable.Visible); - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation works if the start view has positive Adornments - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (7, 8, false)] - [InlineData (1, 2, false)] - [InlineData (2, 3, true)] - [InlineData (5, 6, true)] - [InlineData (6, 7, true)] - public void Returns_Correct_If_Start_Has_Adornments (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - Application.TopRunnable.Margin!.Thickness = new (1); - - var subview = new View - { - X = 1, Y = 2, - Width = 5, Height = 5 - }; - Application.TopRunnable.Add (subview); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation works if the start view has offset Viewport location - [Theory] - [InlineData (1, 0, 0, true)] - [InlineData (1, 1, 1, true)] - [InlineData (1, 2, 2, false)] - [InlineData (-1, 3, 3, true)] - [InlineData (-1, 2, 2, true)] - [InlineData (-1, 1, 1, false)] - [InlineData (-1, 0, 0, false)] - public void Returns_Correct_If_Start_Has_Offset_Viewport (int offset, int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10, - ViewportSettings = ViewportSettingsFlags.AllowNegativeLocation - }; - Application.TopRunnable.Viewport = new (offset, offset, 10, 10); - - var subview = new View - { - X = 1, Y = 1, - Width = 2, Height = 2 - }; - Application.TopRunnable.Add (subview); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (9, 9, true)] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (10, 10, false)] - [InlineData (7, 8, false)] - [InlineData (1, 2, false)] - [InlineData (2, 3, false)] - [InlineData (5, 6, false)] - [InlineData (6, 7, false)] - public void Returns_Correct_If_Start_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - Application.TopRunnable.Padding!.Thickness = new (1); - - var subview = new View - { - X = Pos.AnchorEnd (1), Y = Pos.AnchorEnd (1), - Width = 1, Height = 1 - }; - Application.TopRunnable.Padding.Add (subview); - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == subview); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, new string [] { })] - [InlineData (9, 9, new string [] { })] - [InlineData (1, 1, new [] { "Top", "Border" })] - [InlineData (8, 8, new [] { "Top", "Border" })] - [InlineData (2, 2, new [] { "Top", "Padding" })] - [InlineData (7, 7, new [] { "Top", "Padding" })] - [InlineData (5, 5, new [] { "Top" })] - public void Returns_Adornment_If_Start_Has_Adornments (int testX, int testY, string [] expectedViewsFound) - { - Application.ResetState (true); - - Application.TopRunnable = new () - { - Id = "Top", - Width = 10, Height = 10 - }; - Application.TopRunnable.Margin!.Thickness = new (1); - Application.TopRunnable.Margin!.Id = "Margin"; - Application.TopRunnable.Border!.Thickness = new (1); - Application.TopRunnable.Border!.Id = "Border"; - Application.TopRunnable.Padding!.Thickness = new (1); - Application.TopRunnable.Padding.Id = "Padding"; - - var subview = new View - { - Id = "SubView", - X = 1, Y = 1, - Width = 1, Height = 1 - }; - Application.TopRunnable.Add (subview); - - List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); - string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); - - Assert.Equal (expectedViewsFound, foundIds); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation works if the subview has positive Adornments - [Theory] - [InlineData (0, 0, new [] { "Top" })] - [InlineData (1, 1, new [] { "Top" })] - [InlineData (9, 9, new [] { "Top" })] - [InlineData (10, 10, new string [] { })] - [InlineData (7, 8, new [] { "Top" })] - [InlineData (6, 7, new [] { "Top" })] - [InlineData (1, 2, new [] { "Top", "subview", "border" })] - [InlineData (5, 6, new [] { "Top", "subview", "border" })] - [InlineData (2, 3, new [] { "Top", "subview" })] - public void Returns_Correct_If_SubView_Has_Adornments (int testX, int testY, string [] expectedViewsFound) - { - Application.TopRunnable = new () - { - Id = "Top", - Width = 10, Height = 10 - }; - - var subview = new View - { - Id = "subview", - X = 1, Y = 2, - Width = 5, Height = 5 - }; - subview.Border!.Thickness = new (1); - subview.Border!.Id = "border"; - Application.TopRunnable.Add (subview); - - List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); - string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); - - Assert.Equal (expectedViewsFound, foundIds); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation works if the subview has positive Adornments - [Theory] - [InlineData (0, 0, new [] { "Top" })] - [InlineData (1, 1, new [] { "Top" })] - [InlineData (9, 9, new [] { "Top" })] - [InlineData (10, 10, new string [] { })] - [InlineData (7, 8, new [] { "Top" })] - [InlineData (6, 7, new [] { "Top" })] - [InlineData (1, 2, new [] { "Top" })] - [InlineData (5, 6, new [] { "Top" })] - [InlineData (2, 3, new [] { "Top", "subview" })] - public void Returns_Correct_If_SubView_Has_Adornments_With_TransparentMouse (int testX, int testY, string [] expectedViewsFound) - { - Application.TopRunnable = new () - { - Id = "Top", - Width = 10, Height = 10 - }; - - var subview = new View - { - Id = "subview", - X = 1, Y = 2, - Width = 5, Height = 5 - }; - subview.Border!.Thickness = new (1); - subview.Border!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; - subview.Border!.Id = "border"; - Application.TopRunnable.Add (subview); - - List viewsUnderMouse = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); - string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); - - Assert.Equal (expectedViewsFound, foundIds); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (7, 8, false)] - [InlineData (6, 7, false)] - [InlineData (1, 2, false)] - [InlineData (5, 6, false)] - [InlineData (6, 5, false)] - [InlineData (5, 5, true)] - public void Returns_Correct_If_SubView_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - // A subview with + Padding - var subview = new View - { - X = 1, Y = 1, - Width = 5, Height = 5 - }; - subview.Padding!.Thickness = new (1); - - // This subview will be at the bottom-right-corner of subview - // So screen-relative location will be X + Width - 1 = 5 - var paddingSubView = new View - { - X = Pos.AnchorEnd (1), - Y = Pos.AnchorEnd (1), - Width = 1, - Height = 1 - }; - subview.Padding.Add (paddingSubView); - Application.TopRunnable.Add (subview); - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, false)] - [InlineData (1, 1, false)] - [InlineData (9, 9, false)] - [InlineData (10, 10, false)] - [InlineData (7, 8, false)] - [InlineData (6, 7, false)] - [InlineData (1, 2, false)] - [InlineData (5, 6, false)] - [InlineData (6, 5, false)] - [InlineData (5, 5, true)] - public void Returns_Correct_If_SubView_Is_Scrolled_And_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - // A subview with + Padding - var subview = new View - { - X = 1, Y = 1, - Width = 5, Height = 5 - }; - subview.Padding!.Thickness = new (1); - - // Scroll the subview - subview.SetContentSize (new (10, 10)); - subview.Viewport = subview.Viewport with { Location = new (1, 1) }; - - // This subview will be at the bottom-right-corner of subview - // So screen-relative location will be X + Width - 1 = 5 - var paddingSubView = new View - { - X = Pos.AnchorEnd (1), - Y = Pos.AnchorEnd (1), - Width = 1, - Height = 1 - }; - subview.Padding.Add (paddingSubView); - Application.TopRunnable.Add (subview); - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - - Assert.Equal (expectedSubViewFound, found == paddingSubView); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - // Test that GetViewsUnderLocation works with nested subviews - [Theory] - [InlineData (0, 0, -1)] - [InlineData (9, 9, -1)] - [InlineData (10, 10, -1)] - [InlineData (1, 1, 0)] - [InlineData (1, 2, 0)] - [InlineData (2, 2, 1)] - [InlineData (3, 3, 2)] - [InlineData (5, 5, 2)] - public void Returns_Correct_With_NestedSubViews (int testX, int testY, int expectedSubViewFound) - { - Application.TopRunnable = new () - { - Width = 10, Height = 10 - }; - - var numSubViews = 3; - List subviews = new (); - - for (var i = 0; i < numSubViews; i++) - { - var subview = new View - { - X = 1, Y = 1, - Width = 5, Height = 5 - }; - subviews.Add (subview); - - if (i > 0) - { - subviews [i - 1].Add (subview); - } - } - - Application.TopRunnable.Add (subviews [0]); - - View? found = Application.TopRunnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); - Assert.Equal (expectedSubViewFound, subviews.IndexOf (found!)); - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, new [] { "top" })] - [InlineData (9, 9, new [] { "top" })] - [InlineData (10, 10, new string [] { })] - [InlineData (1, 1, new [] { "top", "view" })] - [InlineData (1, 2, new [] { "top", "view" })] - [InlineData (2, 1, new [] { "top", "view" })] - [InlineData (2, 2, new [] { "top", "view", "subView" })] - [InlineData (3, 3, new [] { "top" })] // clipped - [InlineData (2, 3, new [] { "top" })] // clipped - public void Tiled_SubViews (int mouseX, int mouseY, string [] viewIdStrings) - { - // Arrange - Application.TopRunnable = new () - { - Frame = new (0, 0, 10, 10), - Id = "top" - }; - - var view = new View - { - Id = "view", - X = 1, - Y = 1, - Width = 2, - Height = 2, - Arrangement = ViewArrangement.Overlapped - }; // at 1,1 to 3,2 (screen) - - var subView = new View - { - Id = "subView", - X = 1, - Y = 1, - Width = 2, - Height = 2, - Arrangement = ViewArrangement.Overlapped - }; // at 2,2 to 4,3 (screen) - view.Add (subView); - Application.TopRunnable.Add (view); - - List found = Application.TopRunnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); - - string [] foundIds = found.Select (v => v!.Id).ToArray (); - - Assert.Equal (viewIdStrings, foundIds); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Theory] - [InlineData (0, 0, new [] { "top" })] - [InlineData (9, 9, new [] { "top" })] - [InlineData (10, 10, new string [] { })] - [InlineData (-1, -1, new string [] { })] - [InlineData (1, 1, new [] { "top", "view" })] - [InlineData (1, 2, new [] { "top", "view" })] - [InlineData (2, 1, new [] { "top", "view" })] - [InlineData (2, 2, new [] { "top", "view", "popover" })] - [InlineData (3, 3, new [] { "top" })] // clipped - [InlineData (2, 3, new [] { "top" })] // clipped - public void Popover (int mouseX, int mouseY, string [] viewIdStrings) - { - // Arrange - Application.TopRunnable = new () - { - Frame = new (0, 0, 10, 10), - Id = "top" - }; - - var view = new View - { - Id = "view", - X = 1, - Y = 1, - Width = 2, - Height = 2, - Arrangement = ViewArrangement.Overlapped - }; // at 1,1 to 3,2 (screen) - - var popOver = new View - { - Id = "popover", - X = 1, - Y = 1, - Width = 2, - Height = 2, - Arrangement = ViewArrangement.Overlapped - }; // at 2,2 to 4,3 (screen) - - view.Add (popOver); - Application.TopRunnable.Add (view); - - List found = Application.TopRunnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); - - string [] foundIds = found.Select (v => v!.Id).ToArray (); - - Assert.Equal (viewIdStrings, foundIds); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } - - [Fact] - public void Returns_TopToplevel_When_Point_Inside_Only_TopToplevel () - { - Application.ResetState (true); - - Toplevel topToplevel = new () - { - Id = "topToplevel", - Frame = new (0, 0, 20, 20) - }; - - Toplevel secondaryToplevel = new () - { - Id = "secondaryToplevel", - Frame = new (5, 5, 10, 10) - }; - secondaryToplevel.Margin!.Thickness = new (1); - secondaryToplevel.Layout (); - - Application.SessionStack.Clear (); - Application.SessionStack.Push (topToplevel); - Application.SessionStack.Push (secondaryToplevel); - Application.TopRunnable = secondaryToplevel; - - List found = Application.TopRunnable.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); - Assert.Contains (found, v => v?.Id == topToplevel.Id); - Assert.Contains (found, v => v == topToplevel); - - topToplevel.Dispose (); - secondaryToplevel.Dispose (); - Application.SessionStack.Clear (); - Application.ResetState (true); - } - - [Fact] - public void Returns_SecondaryToplevel_When_Point_Inside_Only_SecondaryToplevel () - { - Application.ResetState (true); - - Toplevel topToplevel = new () - { - Id = "topToplevel", - Frame = new (0, 0, 20, 20) - }; - - Toplevel secondaryToplevel = new () - { - Id = "secondaryToplevel", - Frame = new (5, 5, 10, 10) - }; - secondaryToplevel.Margin!.Thickness = new (1); - secondaryToplevel.Layout (); - - Application.SessionStack.Clear (); - Application.SessionStack.Push (topToplevel); - Application.SessionStack.Push (secondaryToplevel); - Application.TopRunnable = secondaryToplevel; - - List found = Application.TopRunnable.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); - Assert.Contains (found, v => v?.Id == secondaryToplevel.Id); - Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); - - topToplevel.Dispose (); - secondaryToplevel.Dispose (); - Application.SessionStack.Clear (); - Application.ResetState (true); - } - - [Fact] - public void Returns_Depends_On_Margin_ViewportSettings_When_Point_In_Margin_Of_SecondaryToplevel () - { - Application.ResetState (true); - - Toplevel topToplevel = new () - { - Id = "topToplevel", - Frame = new (0, 0, 20, 20) - }; - - Toplevel secondaryToplevel = new () - { - Id = "secondaryToplevel", - Frame = new (5, 5, 10, 10) - }; - secondaryToplevel.Margin!.Thickness = new (1); - - Application.SessionStack.Clear (); - Application.SessionStack.Push (topToplevel); - Application.SessionStack.Push (secondaryToplevel); - Application.TopRunnable = secondaryToplevel; - - secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.None; - - List found = Application.TopRunnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); - Assert.Contains (found, v => v == secondaryToplevel); - Assert.Contains (found, v => v == secondaryToplevel.Margin); - Assert.DoesNotContain (found, v => v?.Id == topToplevel.Id); - - secondaryToplevel.Margin!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; - found = Application.TopRunnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); - Assert.DoesNotContain (found, v => v == secondaryToplevel); - Assert.DoesNotContain (found, v => v == secondaryToplevel.Margin); - Assert.Contains (found, v => v?.Id == topToplevel.Id); - - topToplevel.Dispose (); - secondaryToplevel.Dispose (); - Application.SessionStack.Clear (); - Application.ResetState (true); - } - - [Fact] - public void Returns_Empty_When_Point_Outside_All_Toplevels () - { - Application.ResetState (true); - - Toplevel topToplevel = new () - { - Id = "topToplevel", - Frame = new (0, 0, 20, 20) - }; - - Toplevel secondaryToplevel = new () - { - Id = "secondaryToplevel", - Frame = new (5, 5, 10, 10) - }; - secondaryToplevel.Margin!.Thickness = new (1); - secondaryToplevel.Layout (); - - Application.SessionStack.Clear (); - Application.SessionStack.Push (topToplevel); - Application.SessionStack.Push (secondaryToplevel); - Application.TopRunnable = secondaryToplevel; - - List found = Application.TopRunnable.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); - Assert.Empty (found); - - topToplevel.Dispose (); - secondaryToplevel.Dispose (); - Application.SessionStack.Clear (); - Application.ResetState (true); - } -} diff --git a/Tests/UnitTests/View/Layout/Pos.AnchorEndTests.cs b/Tests/UnitTests/View/Layout/Pos.AnchorEndTests.cs deleted file mode 100644 index 39c96c707..000000000 --- a/Tests/UnitTests/View/Layout/Pos.AnchorEndTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using UnitTests; - -namespace UnitTests.LayoutTests; - -public class PosAnchorEndTests () -{ - // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved - // TODO: A new test that calls SetRelativeLayout directly is needed. - [Fact] - [AutoInitShutdown] - public void PosAnchorEnd_Equal_Inside_Window () - { - var viewWidth = 10; - var viewHeight = 1; - - var tv = new TextView - { - X = Pos.AnchorEnd (viewWidth), Y = Pos.AnchorEnd (viewHeight), Width = viewWidth, Height = viewHeight - }; - - var win = new Window (); - - win.Add (tv); - - Toplevel top = new (); - top.Add (win); - SessionToken rs = Application.Begin (top); - - Application.Driver!.SetScreenSize (80,25); - - Assert.Equal (new (0, 0, 80, 25), top.Frame); - Assert.Equal (new (0, 0, 80, 25), win.Frame); - Assert.Equal (new (68, 22, 10, 1), tv.Frame); - Application.End (rs); - top.Dispose (); - } - - //// TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved - //// TODO: A new test that calls SetRelativeLayout directly is needed. - //[Fact] - //[AutoInitShutdown] - //public void PosAnchorEnd_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel () - //{ - // var viewWidth = 10; - // var viewHeight = 1; - - // var tv = new TextView - // { - // X = Pos.AnchorEnd (viewWidth), Y = Pos.AnchorEnd (viewHeight), Width = viewWidth, Height = viewHeight - // }; - - // var win = new Window (); - - // win.Add (tv); - - // var menu = new MenuBar (); - // var status = new StatusBar (); - // Toplevel top = new (); - // top.Add (win, menu, status); - // SessionToken rs = Application.Begin (top); - - // Assert.Equal (new (0, 0, 80, 25), top.Frame); - // Assert.Equal (new (0, 0, 80, 1), menu.Frame); - // Assert.Equal (new (0, 24, 80, 1), status.Frame); - // Assert.Equal (new (0, 1, 80, 23), win.Frame); - // Assert.Equal (new (68, 20, 10, 1), tv.Frame); - - // Application.End (rs); - // top.Dispose (); - //} -} diff --git a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs b/Tests/UnitTests/View/Layout/Pos.CombineTests.cs deleted file mode 100644 index 6a495968d..000000000 --- a/Tests/UnitTests/View/Layout/Pos.CombineTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.VisualStudio.TestPlatform.Utilities; -using UnitTests; -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; -using static Terminal.Gui.ViewBase.Pos; - -namespace UnitTests.LayoutTests; - -public class PosCombineTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved - // TODO: A new test that calls SetRelativeLayout directly is needed. - [Fact] - [SetupFakeApplication] - public void PosCombine_Will_Throws () - { - Toplevel t = new (); - - var w = new Window { X = Pos.Left (t) + 2, Y = Pos.Top (t) + 2 }; - var f = new FrameView (); - var v1 = new View { X = Pos.Left (w) + 2, Y = Pos.Top (w) + 2 }; - var v2 = new View { X = Pos.Left (v1) + 2, Y = Pos.Top (v1) + 2 }; - - f.Add (v1); // v2 not added - w.Add (f); - t.Add (w); - - f.X = Pos.X (v2) - Pos.X (v1); - f.Y = Pos.Y (v2) - Pos.Y (v1); - - Assert.Throws (() => Application.Run (t)); - t.Dispose (); - v2.Dispose (); - } - - - [Fact] - [SetupFakeApplication] - public void PosCombine_DimCombine_View_With_SubViews () - { - Application.TopRunnable = new Toplevel () { Width = 80, Height = 25 }; - var win1 = new Window { Id = "win1", Width = 20, Height = 10 }; - var view1 = new View - { - Text = "view1", - Width = Auto (DimAutoStyle.Text), - Height = Auto (DimAutoStyle.Text) - - }; - var win2 = new Window { Id = "win2", Y = Pos.Bottom (view1) + 1, Width = 10, Height = 3 }; - var view2 = new View { Id = "view2", Width = Dim.Fill (), Height = 1, CanFocus = true }; - - //var clicked = false; - //view2.MouseClick += (sender, e) => clicked = true; - var view3 = new View { Id = "view3", Width = Dim.Fill (1), Height = 1, CanFocus = true }; - - view2.Add (view3); - win2.Add (view2); - win1.Add (view1, win2); - Application.TopRunnable.Add (win1); - Application.TopRunnable.Layout (); - - Assert.Equal (new Rectangle (0, 0, 80, 25), Application.TopRunnable.Frame); - Assert.Equal (new Rectangle (0, 0, 5, 1), view1.Frame); - Assert.Equal (new Rectangle (0, 0, 20, 10), win1.Frame); - Assert.Equal (new Rectangle (0, 2, 10, 3), win2.Frame); - Assert.Equal (new Rectangle (0, 0, 8, 1), view2.Frame); - Assert.Equal (new Rectangle (0, 0, 7, 1), view3.Frame); - var foundView = Application.TopRunnable.GetViewsUnderLocation (new Point(9, 4), ViewportSettingsFlags.None).LastOrDefault (); - Assert.Equal (foundView, view2); - Application.TopRunnable.Dispose (); - } - - [Fact] - public void PosCombine_Refs_SuperView_Throws () - { - Application.Init ("fake"); - - var top = new Toplevel (); - var w = new Window { X = Pos.Left (top) + 2, Y = Pos.Top (top) + 2 }; - var f = new FrameView (); - var v1 = new View { X = Pos.Left (w) + 2, Y = Pos.Top (w) + 2 }; - var v2 = new View { X = Pos.Left (v1) + 2, Y = Pos.Top (v1) + 2 }; - - f.Add (v1, v2); - w.Add (f); - top.Add (w); - Application.Begin (top); - - f.X = Pos.X (Application.TopRunnable) + Pos.X (v2) - Pos.X (v1); - f.Y = Pos.Y (Application.TopRunnable) + Pos.Y (v2) - Pos.Y (v1); - - Application.TopRunnable.SubViewsLaidOut += (s, e) => - { - Assert.Equal (0, Application.TopRunnable.Frame.X); - Assert.Equal (0, Application.TopRunnable.Frame.Y); - Assert.Equal (2, w.Frame.X); - Assert.Equal (2, w.Frame.Y); - Assert.Equal (2, f.Frame.X); - Assert.Equal (2, f.Frame.Y); - Assert.Equal (4, v1.Frame.X); - Assert.Equal (4, v1.Frame.Y); - Assert.Equal (6, v2.Frame.X); - Assert.Equal (6, v2.Frame.Y); - }; - - Application.StopAfterFirstIteration = true; - - Assert.Throws (() => Application.Run ()); - Application.TopRunnable.Dispose (); - top.Dispose (); - Application.Shutdown (); - } -} diff --git a/Tests/UnitTests/View/Layout/Pos.Tests.cs b/Tests/UnitTests/View/Layout/Pos.Tests.cs index 4fbe3f906..1b81e9c49 100644 --- a/Tests/UnitTests/View/Layout/Pos.Tests.cs +++ b/Tests/UnitTests/View/Layout/Pos.Tests.cs @@ -10,7 +10,7 @@ public class PosTests () { Application.Init ("fake"); - Toplevel t = new (); + Runnable t = new (); var w = new Window { X = Pos.Left (t) + 2, Y = Pos.Absolute (2) }; @@ -19,7 +19,7 @@ public class PosTests () w.Add (v); t.Add (w); - t.Ready += (s, e) => + t.IsModalChanged += (s, e) => { v.Frame = new Rectangle (2, 2, 10, 10); Assert.Equal (2, v.X = 2); @@ -37,12 +37,11 @@ public class PosTests () // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved // TODO: A new test that calls SetRelativeLayout directly is needed. [Fact] - [TestRespondersDisposed] public void PosCombine_WHY_Throws () { Application.Init ("fake"); - Toplevel t = new Toplevel (); + Runnable t = new Runnable (); var w = new Window { X = Pos.Left (t) + 2, Y = Pos.Top (t) + 2 }; var f = new FrameView (); @@ -70,7 +69,7 @@ public class PosTests () [SetupFakeApplication] public void Pos_Add_Operator () { - Toplevel top = new (); + Runnable top = new (); var view = new View { X = 0, Y = 0, Width = 20, Height = 20 }; var field = new TextField { X = 0, Y = 0, Width = 20 }; @@ -127,12 +126,11 @@ public class PosTests () // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved // TODO: A new test that calls SetRelativeLayout directly is needed. [Fact] - [TestRespondersDisposed] public void Pos_Subtract_Operator () { Application.Init ("fake"); - Toplevel top = new (); + Runnable top = new (); var view = new View { X = 0, Y = 0, Width = 20, Height = 20 }; var field = new TextField { X = 0, Y = 0, Width = 20 }; @@ -206,12 +204,12 @@ public class PosTests () { Application.Init ("fake"); - Toplevel t = new (); + Runnable t = new (); var w = new Window { X = 1, Y = 2, Width = 3, Height = 5 }; t.Add (w); - t.Ready += (s, e) => + t.IsModalChanged += (s, e) => { Assert.Equal (2, w.X = 2); Assert.Equal (2, w.Y = 2); @@ -232,12 +230,12 @@ public class PosTests () { Application.Init ("fake"); - Toplevel t = new Toplevel (); + Runnable t = new Runnable (); var w = new Window { X = 1, Y = 2, Width = 3, Height = 5 }; t.Add (w); - t.Ready += (s, e) => + t.IsModalChanged += (s, e) => { Assert.Equal (2, w.X = 2); Assert.Equal (2, w.Y = 2); diff --git a/Tests/UnitTests/View/Layout/Pos.ViewTests.cs b/Tests/UnitTests/View/Layout/Pos.ViewTests.cs index 2ceb85789..3edf2733c 100644 --- a/Tests/UnitTests/View/Layout/Pos.ViewTests.cs +++ b/Tests/UnitTests/View/Layout/Pos.ViewTests.cs @@ -14,7 +14,7 @@ public class PosViewTests (ITestOutputHelper output) [SetupFakeApplication] public void Subtract_Operator () { - var top = new Toplevel (); + var top = new Runnable (); var view = new View { X = 0, Y = 0, Width = 20, Height = 20 }; var field = new TextField { X = 0, Y = 0, Width = 20 }; diff --git a/Tests/UnitTests/View/Layout/SetLayoutTests.cs b/Tests/UnitTests/View/Layout/SetLayoutTests.cs deleted file mode 100644 index e5eb6b027..000000000 --- a/Tests/UnitTests/View/Layout/SetLayoutTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.LayoutTests; - -public class SetLayoutTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - - [Fact] - [AutoInitShutdown] - public void Screen_Size_Change_Causes_Layout () - { - Application.TopRunnable = new (); - - var view = new View - { - X = 3, - Y = 2, - Width = 10, - Height = 1, - Text = "0123456789" - }; - Application.TopRunnable.Add (view); - - var rs = Application.Begin (Application.TopRunnable); - Application.Driver!.SetScreenSize (80, 25); - - Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, Application.Screen.Width, Application.Screen.Height)); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.TopRunnable.Frame); - Assert.Equal (new (0, 0, 80, 25), Application.TopRunnable.Frame); - - Application.Driver!.SetScreenSize (20, 10); - Assert.Equal (new (0, 0, Application.Screen.Width, Application.Screen.Height), Application.TopRunnable.Frame); - - Assert.Equal (new (0, 0, 20, 10), Application.TopRunnable.Frame); - - Application.End (rs); - Application.TopRunnable.Dispose (); - } -} diff --git a/Tests/UnitTests/View/Mouse/MouseTests.cs b/Tests/UnitTests/View/Mouse/MouseTests.cs index ec1215934..05326f7ea 100644 --- a/Tests/UnitTests/View/Mouse/MouseTests.cs +++ b/Tests/UnitTests/View/Mouse/MouseTests.cs @@ -1,6 +1,6 @@ using Timeout = Terminal.Gui.App.Timeout; -namespace UnitTests.ViewMouseTests; +namespace UnitTests.ViewBaseTests.MouseTests; [Trait ("Category", "Input")] public class MouseTests : TestsAllViews @@ -38,7 +38,7 @@ public class MouseTests : TestsAllViews testView.Border!.Thickness = new (borderThickness); testView.Padding!.Thickness = new (paddingThickness); - var top = new Toplevel (); + var top = new Runnable (); top.Add (testView); SessionToken rs = Application.Begin (top); diff --git a/Tests/UnitTests/View/Navigation/CanFocusTests.cs b/Tests/UnitTests/View/Navigation/CanFocusTests.cs deleted file mode 100644 index 5ad4611b1..000000000 --- a/Tests/UnitTests/View/Navigation/CanFocusTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using UnitTests; - -namespace UnitTests.ViewTests; - -public class CanFocusTests -{ - // TODO: Figure out what this test is supposed to be testing - [Fact] - [AutoInitShutdown] - public void CanFocus_Faced_With_Container_Before_Run () - { - using Toplevel t = new (); - - var w = new Window (); - var f = new FrameView (); - var v = new View { CanFocus = true }; - f.Add (v); - w.Add (f); - t.Add (w); - - Assert.True (t.CanFocus); - Assert.True (w.CanFocus); - Assert.True (f.CanFocus); - Assert.True (v.CanFocus); - - f.CanFocus = false; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - - v.CanFocus = false; - Assert.False (f.CanFocus); - Assert.False (v.CanFocus); - - v.CanFocus = true; - Assert.False (f.CanFocus); - Assert.True (v.CanFocus); - - Application.StopAfterFirstIteration = true; - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } - - //[Fact] - //public void CanFocus_Set_Changes_TabIndex_And_TabStop () - //{ - // var r = new View (); - // var v1 = new View { Text = "1" }; - // var v2 = new View { Text = "2" }; - // var v3 = new View { Text = "3" }; - - // r.Add (v1, v2, v3); - - // v2.CanFocus = true; - // Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); - // Assert.Equal (0, v2.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v2.TabStop); - - // v1.CanFocus = true; - // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - // Assert.Equal (1, v1.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v1.TabStop); - - // v1.TabIndex = 2; - // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - // Assert.Equal (1, v1.TabIndex); - // v3.CanFocus = true; - // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - // Assert.Equal (1, v1.TabIndex); - // Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); - // Assert.Equal (2, v3.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v3.TabStop); - - // v2.CanFocus = false; - // Assert.Equal (r.TabIndexes.IndexOf (v1), v1.TabIndex); - // Assert.Equal (1, v1.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v1.TabStop); - // Assert.Equal (r.TabIndexes.IndexOf (v2), v2.TabIndex); // TabIndex is not changed - // Assert.NotEqual (-1, v2.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v2.TabStop); // TabStop is not changed - // Assert.Equal (r.TabIndexes.IndexOf (v3), v3.TabIndex); - // Assert.Equal (2, v3.TabIndex); - // Assert.Equal (TabBehavior.TabStop, v3.TabStop); - // r.Dispose (); - //} - - [Fact] - public void CanFocus_Set_True_Get_AdvanceFocus_Works () - { - IApplication app = ApplicationImpl.Instance; // Force legacy - app.TopRunnable = new () { App = app }; - - Label label = new () { Text = "label" }; - View view = new () { Text = "view", CanFocus = true }; - app.TopRunnable.Add (label, view); - - app.TopRunnable.SetFocus (); - Assert.Equal (view, app.Navigation!.GetFocused ()); - Assert.False (label.CanFocus); - Assert.False (label.HasFocus); - Assert.True (view.CanFocus); - Assert.True (view.HasFocus); - - Assert.False (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); - - // Set label CanFocus to true - label.CanFocus = true; - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); - - // label can now be focused, so AdvanceFocus should move to it. - Assert.True (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); - Assert.True (label.HasFocus); - Assert.False (view.HasFocus); - - // Move back to view - view.SetFocus (); - Assert.False (label.HasFocus); - Assert.True (view.HasFocus); - - Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); - Assert.True (label.HasFocus); - Assert.False (view.HasFocus); - - app.TopRunnable.Dispose (); - app.ResetState (); - } -} diff --git a/Tests/UnitTests/View/Navigation/EnabledTests.cs b/Tests/UnitTests/View/Navigation/EnabledTests.cs index 27e9cc69e..67f5f52bc 100644 --- a/Tests/UnitTests/View/Navigation/EnabledTests.cs +++ b/Tests/UnitTests/View/Navigation/EnabledTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class EnabledTests { @@ -14,7 +14,7 @@ public class EnabledTests button.Accepting += (s, e) => wasClicked = !wasClicked; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (button); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); var iterations = 0; diff --git a/Tests/UnitTests/View/SchemeTests.cs b/Tests/UnitTests/View/SchemeTests.cs index 6b16dcb39..9660a853b 100644 --- a/Tests/UnitTests/View/SchemeTests.cs +++ b/Tests/UnitTests/View/SchemeTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; [Trait ("Category", "View.Scheme")] public class SchemeTests diff --git a/Tests/UnitTests/View/SubviewTests.cs b/Tests/UnitTests/View/SubviewTests.cs index 6bee76a5c..a5241638d 100644 --- a/Tests/UnitTests/View/SubviewTests.cs +++ b/Tests/UnitTests/View/SubviewTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class SubViewTests { @@ -12,7 +12,7 @@ public class SubViewTests //[AutoInitShutdown] //public void Initialized_Event_Will_Be_Invoked_When_Added_Dynamically () //{ - // var t = new Toplevel { Id = "0" }; + // var t = new Runnable { Id = "0" }; // var w = new Window { Id = "t", Width = Dim.Fill (), Height = Dim.Fill () }; // var v1 = new View { Id = "v1", Width = Dim.Fill (), Height = Dim.Fill () }; diff --git a/Tests/UnitTests/View/TextTests.cs b/Tests/UnitTests/View/TextTests.cs index 53d882756..f63d219e2 100644 --- a/Tests/UnitTests/View/TextTests.cs +++ b/Tests/UnitTests/View/TextTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; /// /// Tests of the and properties. @@ -65,7 +65,7 @@ Y var viewX = new View { Text = "X", X = Pos.Right (label), Width = 1, Height = 1 }; var viewY = new View { Text = "Y", Y = Pos.Bottom (label), Width = 1, Height = 1 }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (label, viewX, viewY); SessionToken rs = Application.Begin (top); @@ -119,11 +119,12 @@ Y var view = new View (); win.Add (view); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (15, 15); + Application.LayoutAndDraw (); Assert.Equal (new (0, 0, 15, 15), win.Frame); Assert.Equal (new (0, 0, 15, 15), win.Margin!.Frame); @@ -385,10 +386,11 @@ Y var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (view); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (4, 10); + Application.LayoutAndDraw (); Assert.Equal (5, text.Length); @@ -396,7 +398,7 @@ Y Assert.Equal (new (1, 5), view.TextFormatter.ConstrainToSize); Assert.Equal (new () { "Views" }, view.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 4, 10), win.Frame); - Assert.Equal (new (0, 0, 4, 10), Application.TopRunnable.Frame); + Assert.Equal (new (0, 0, 4, 10), Application.TopRunnableView.Frame); var expected = @" ┌──┐ @@ -511,10 +513,11 @@ w "; Text = "Window" }; win.Add (horizontalView, verticalView); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (20, 20); + Application.LayoutAndDraw (); Assert.Equal (new (0, 0, 11, 2), horizontalView.Frame); Assert.Equal (new (0, 3, 2, 11), verticalView.Frame); @@ -599,10 +602,11 @@ w "; }; var win = new Window { Id = "win", Width = Dim.Fill (), Height = Dim.Fill (), Text = "Window" }; win.Add (horizontalView, verticalView); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (22, 22); + Application.LayoutAndDraw (); Assert.Equal (new (text.GetColumns (), 1), horizontalView.TextFormatter.ConstrainToSize); Assert.Equal (new (2, 8), verticalView.TextFormatter.ConstrainToSize); @@ -678,7 +682,7 @@ w "; public void Excess_Text_Is_Erased_When_The_Width_Is_Reduced () { var lbl = new Label { Text = "123" }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (lbl); SessionToken rs = Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -783,10 +787,11 @@ w "; var frame = new FrameView { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; frame.Add (lblLeft, lblCenter, lblRight, lblJust); - var top = new Toplevel (); + var top = new Runnable (); top.Add (frame); Application.Begin (top); Application.Driver!.SetScreenSize (width + 2, 6); + Application.LayoutAndDraw (); // frame.Width is width + border wide (20 + 2) and 6 high @@ -913,10 +918,11 @@ w "; var frame = new FrameView { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; frame.Add (lblLeft, lblCenter, lblRight, lblJust); - var top = new Toplevel (); + var top = new Runnable (); top.Add (frame); Application.Begin (top); Application.Driver!.SetScreenSize (9, height + 2); + Application.LayoutAndDraw (); if (autoSize) { diff --git a/Tests/UnitTests/View/ViewCommandTests.cs b/Tests/UnitTests/View/ViewCommandTests.cs index bb611ac03..f77f36af1 100644 --- a/Tests/UnitTests/View/ViewCommandTests.cs +++ b/Tests/UnitTests/View/ViewCommandTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class ViewCommandTests { @@ -46,9 +46,8 @@ public class ViewCommandTests w.LayoutSubViews (); - Application.TopRunnable = w; - Application.SessionStack.Push (w); - Assert.Same (Application.TopRunnable, w); + Application.Begin (w); + Assert.Same (Application.TopRunnableView, w); // Click button 2 Rectangle btn2Frame = btnB.FrameToScreen (); @@ -81,7 +80,7 @@ public class ViewCommandTests } // See: https://github.com/gui-cs/Terminal.Gui/issues/3905 - [Fact]// (Skip = "Failing as part of ##4270. Disabling temporarily.")] + [Fact] [SetupFakeApplication] public void Button_CanFocus_False_Raises_Accepted_Correctly () { @@ -121,9 +120,8 @@ public class ViewCommandTests w.Add (btn); - Application.TopRunnable = w; - Application.SessionStack.Push (w); - Assert.Same (Application.TopRunnable, w); + Application.Begin (w); + Assert.Same (Application.TopRunnableView, w); w.LayoutSubViews (); @@ -154,6 +152,7 @@ public class ViewCommandTests Assert.Equal (1, btnAcceptedCount); Assert.Equal (0, wAcceptedCount); + w.Dispose (); Application.ResetState (true); } } diff --git a/Tests/UnitTests/View/ViewTests.cs b/Tests/UnitTests/View/ViewTests.cs index 894fd4355..0a4e0f1aa 100644 --- a/Tests/UnitTests/View/ViewTests.cs +++ b/Tests/UnitTests/View/ViewTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace UnitTests.ViewBaseTests; public class ViewTests { @@ -448,7 +448,7 @@ public class ViewTests Assert.Equal (0, view.Height); var win = new Window (); win.Add (view); - Toplevel top = new (); + Runnable top = new (); top.Add (win); SessionToken rs = Application.Begin (top); @@ -458,6 +458,7 @@ public class ViewTests Assert.Equal ("Testing visibility.".Length, view.Frame.Width); Assert.True (view.Visible); Application.Driver!.SetScreenSize (30, 5); + Application.LayoutAndDraw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -495,7 +496,7 @@ public class ViewTests var button = new Button { Text = "Click Me" }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (button); - Toplevel top = new (); + Runnable top = new (); top.Add (win); var iterations = 0; diff --git a/Tests/UnitTests/Views/AppendAutocompleteTests.cs b/Tests/UnitTests/Views/AppendAutocompleteTests.cs index ceb4f883a..ee38be859 100644 --- a/Tests/UnitTests/Views/AppendAutocompleteTests.cs +++ b/Tests/UnitTests/Views/AppendAutocompleteTests.cs @@ -30,12 +30,12 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("f", tf.Text); // Still has focus though - Assert.Same (tf, Application.TopRunnable.Focused); + Assert.Same (tf, Application.TopRunnableView.Focused); // But can tab away Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.TopRunnable.Focused); - Application.TopRunnable.Dispose (); + Assert.NotSame (tf, Application.TopRunnableView.Focused); + Application.TopRunnableView.Dispose (); } [Fact] @@ -69,7 +69,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("fi", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Theory] @@ -107,7 +107,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre ("fish", output); Assert.Equal ("f", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -133,7 +133,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("f", output); Assert.Equal ("f ", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -157,7 +157,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("fx", output); Assert.Equal ("fx", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -195,7 +195,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("my FISH", output); Assert.Equal ("my FISH", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -231,12 +231,12 @@ public class AppendAutocompleteTests (ITestOutputHelper output) Assert.Equal ("fish", tf.Text); // Tab should autcomplete but not move focus - Assert.Same (tf, Application.TopRunnable.Focused); + Assert.Same (tf, Application.TopRunnableView.Focused); // Second tab should move focus (nothing to autocomplete) Application.RaiseKeyDownEvent ('\t'); - Assert.NotSame (tf, Application.TopRunnable.Focused); - Application.TopRunnable.Dispose (); + Assert.NotSame (tf, Application.TopRunnableView.Focused); + Application.TopRunnableView.Dispose (); } [Theory] @@ -256,7 +256,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) tf.PositionCursor (); DriverAssert.AssertDriverContentsAre (expectRender, output); Assert.Equal ("f", tf.Text); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } private TextField GetTextFieldsInView () @@ -264,7 +264,7 @@ public class AppendAutocompleteTests (ITestOutputHelper output) var tf = new TextField { Width = 10 }; var tf2 = new TextField { Y = 1, Width = 10 }; - Toplevel top = new (); + Runnable top = new (); top.Add (tf); top.Add (tf2); diff --git a/Tests/UnitTests/Views/ButtonTests.cs b/Tests/UnitTests/Views/ButtonTests.cs index 1f97316d7..6e3690eee 100644 --- a/Tests/UnitTests/Views/ButtonTests.cs +++ b/Tests/UnitTests/Views/ButtonTests.cs @@ -127,7 +127,7 @@ public class ButtonTests (ITestOutputHelper output) Assert.Contains (Command.HotKey, btn.GetSupportedCommands ()); Assert.Contains (Command.Accept, btn.GetSupportedCommands ()); - var top = new Toplevel (); + var top = new Runnable (); top.Add (btn); Application.Begin (top); @@ -173,7 +173,7 @@ public class ButtonTests (ITestOutputHelper output) var clicked = false; var btn = new Button { Text = "_Test" }; btn.Accepting += (s, e) => clicked = true; - var top = new Toplevel (); + var top = new Runnable (); top.Add (btn); Application.Begin (top); @@ -208,8 +208,8 @@ public class ButtonTests (ITestOutputHelper output) Assert.True (clicked); clicked = false; - // Toplevel does not handle Enter, so it should get passed on to button - Assert.False (Application.TopRunnable.NewKeyDownEvent (Key.Enter)); + // Runnable does not handle Enter, so it should get passed on to button + Assert.False (Application.TopRunnableView.NewKeyDownEvent (Key.Enter)); Assert.True (clicked); clicked = false; @@ -243,14 +243,14 @@ public class ButtonTests (ITestOutputHelper output) var btn = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Say Hello 你" }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (btn); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Assert.False (btn.IsInitialized); Application.Begin (top); Application.Driver?.SetScreenSize (30, 5); - + Application.LayoutAndDraw(); Assert.True (btn.IsInitialized); Assert.Equal ("Say Hello 你", btn.Text); Assert.Equal ($"{Glyphs.LeftBracket} {btn.Text} {Glyphs.RightBracket}", btn.TextFormatter.Text); diff --git a/Tests/UnitTests/Views/CheckBoxTests.cs b/Tests/UnitTests/Views/CheckBoxTests.cs index 0324e0fde..a14d1c45d 100644 --- a/Tests/UnitTests/Views/CheckBoxTests.cs +++ b/Tests/UnitTests/Views/CheckBoxTests.cs @@ -12,63 +12,6 @@ public class CheckBoxTests (ITestOutputHelper output) - [Fact] - public void Commands_Select () - { - Application.TopRunnable = new (); - View otherView = new () { CanFocus = true }; - var ckb = new CheckBox (); - Application.TopRunnable.Add (ckb, otherView); - Application.TopRunnable.SetFocus (); - Assert.True (ckb.HasFocus); - - var checkedStateChangingCount = 0; - ckb.CheckedStateChanging += (s, e) => checkedStateChangingCount++; - - var selectCount = 0; - ckb.Selecting += (s, e) => selectCount++; - - var acceptCount = 0; - ckb.Accepting += (s, e) => acceptCount++; - - Assert.Equal (CheckState.UnChecked, ckb.CheckedState); - Assert.Equal (0, checkedStateChangingCount); - Assert.Equal (0, selectCount); - Assert.Equal (0, acceptCount); - Assert.Equal (Key.Empty, ckb.HotKey); - - // Test while focused - ckb.Text = "_Test"; - Assert.Equal (Key.T, ckb.HotKey); - ckb.NewKeyDownEvent (Key.T); - Assert.Equal (CheckState.Checked, ckb.CheckedState); - Assert.Equal (1, checkedStateChangingCount); - Assert.Equal (1, selectCount); - Assert.Equal (0, acceptCount); - - ckb.Text = "T_est"; - Assert.Equal (Key.E, ckb.HotKey); - ckb.NewKeyDownEvent (Key.E.WithAlt); - Assert.Equal (2, checkedStateChangingCount); - Assert.Equal (2, selectCount); - Assert.Equal (0, acceptCount); - - ckb.NewKeyDownEvent (Key.Space); - Assert.Equal (3, checkedStateChangingCount); - Assert.Equal (3, selectCount); - Assert.Equal (0, acceptCount); - - ckb.NewKeyDownEvent (Key.Enter); - Assert.Equal (3, checkedStateChangingCount); - Assert.Equal (3, selectCount); - Assert.Equal (1, acceptCount); - - Application.TopRunnable.Dispose (); - Application.ResetState (); - } - - - #region Mouse Tests @@ -91,11 +34,12 @@ public class CheckBoxTests (ITestOutputHelper output) }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill (), Title = "Test Demo 你" }; win.Add (checkBox); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver?.SetScreenSize (30, 5); + Application.LayoutAndDraw (); Assert.Equal (Alignment.Center, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); @@ -151,12 +95,12 @@ public class CheckBoxTests (ITestOutputHelper output) }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill (), Title = "Test Demo 你" }; win.Add (checkBox1, checkBox2); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (30, 6); - + Application.LayoutAndDraw (); Assert.Equal (Alignment.Fill, checkBox1.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox1.Frame); Assert.Equal (Alignment.Fill, checkBox2.TextAlignment); @@ -212,12 +156,12 @@ public class CheckBoxTests (ITestOutputHelper output) }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill (), Title = "Test Demo 你" }; win.Add (checkBox); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (30, 5); - + Application.LayoutAndDraw (); Assert.Equal (Alignment.Start, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); Assert.Equal (_size25x1, checkBox.TextFormatter.ConstrainToSize); @@ -263,12 +207,12 @@ public class CheckBoxTests (ITestOutputHelper output) }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill (), Title = "Test Demo 你" }; win.Add (checkBox); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (30, 5); - + Application.LayoutAndDraw (); Assert.Equal (Alignment.End, checkBox.TextAlignment); Assert.Equal (new (1, 1, 25, 1), checkBox.Frame); Assert.Equal (_size25x1, checkBox.TextFormatter.ConstrainToSize); diff --git a/Tests/UnitTests/Views/ColorPicker16Tests.cs b/Tests/UnitTests/Views/ColorPicker16Tests.cs index be2b376cb..14a089b0c 100644 --- a/Tests/UnitTests/Views/ColorPicker16Tests.cs +++ b/Tests/UnitTests/Views/ColorPicker16Tests.cs @@ -49,7 +49,7 @@ public class ColorPicker16Tests { var colorPicker = new ColorPicker16 { X = 0, Y = 0, Height = 4, Width = 32 }; Assert.Equal (ColorName16.Black, colorPicker.SelectedColor); - var top = new Toplevel (); + var top = new Runnable (); top.Add (colorPicker); Application.Begin (top); diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs index 7c960a18b..3ed1358ef 100644 --- a/Tests/UnitTests/Views/ColorPickerTests.cs +++ b/Tests/UnitTests/Views/ColorPickerTests.cs @@ -1,826 +1,9 @@ -using UnitTests; +#nullable enable +using UnitTests; -namespace UnitTests.ViewsTests; +namespace UnitTests_Parallelizable.ViewsTests; public class ColorPickerTests { - [Fact] - [SetupFakeApplication] - public void ColorPicker_ChangeValueOnUI_UpdatesAllUIElements () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true); - - var otherView = new View { CanFocus = true }; - - Application.TopRunnable?.Add (otherView); // thi sets focus to otherView - Assert.True (otherView.HasFocus); - - cp.SetFocus (); - Assert.False (otherView.HasFocus); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); - TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); - TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); - - Assert.Equal ("R:", r.Text); - Assert.Equal (2, r.TrianglePosition); - Assert.Equal ("0", rTextField.Text); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("0", gTextField.Text); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("0", bTextField.Text); - Assert.Equal ("#000000", hex.Text); - - // Change value using text field - TextField rBarTextField = cp.SubViews.OfType ().First (tf => tf.Text == "0"); - - rBarTextField.SetFocus (); - rBarTextField.Text = "128"; - - otherView.SetFocus (); - Assert.True (otherView.HasFocus); - - cp.Draw (); - - Assert.Equal ("R:", r.Text); - Assert.Equal (9, r.TrianglePosition); - Assert.Equal ("128", rTextField.Text); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("0", gTextField.Text); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("0", bTextField.Text); - Assert.Equal ("#800000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_ClickingAtEndOfBar_SetsMaxValue () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - - cp.Draw (); - - // Click at the end of the Red bar - cp.Focused.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (19, 0) // Assuming 0-based indexing - }); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("R:", r.Text); - Assert.Equal (19, r.TrianglePosition); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("#FF0000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_ClickingBeyondBar_ChangesToMaxValue () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - - cp.Draw (); - - // Click beyond the bar - cp.Focused.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (21, 0) // Beyond the bar - }); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("R:", r.Text); - Assert.Equal (19, r.TrianglePosition); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("#FF0000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_ClickingDifferentBars_ChangesFocus () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - - cp.Draw (); - - // Click on Green bar - Application.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - ScreenPosition = new (0, 1) - }); - - //cp.SubViews.OfType () - // .Single () - // .OnMouseEvent ( - // new () - // { - // Flags = MouseFlags.Button1Pressed, - // Position = new (0, 1) - // }); - - cp.Draw (); - - Assert.IsAssignableFrom (cp.Focused); - - // Click on Blue bar - Application.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - ScreenPosition = new (0, 2) - }); - - //cp.SubViews.OfType () - // .Single () - // .OnMouseEvent ( - // new () - // { - // Flags = MouseFlags.Button1Pressed, - // Position = new (0, 2) - // }); - - cp.Draw (); - - Assert.IsAssignableFrom (cp.Focused); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_Construct_DefaultValue () - { - ColorPicker cp = GetColorPicker (ColorModel.HSV, false); - - // Should be only a single text field (Hex) because ShowTextFields is false - Assert.Single (cp.SubViews.OfType ()); - - cp.Draw (); - - // All bars should be at 0 with the triangle at 0 (+2 because of "H:", "S:" etc) - ColorBar h = GetColorBar (cp, ColorPickerPart.Bar1); - Assert.Equal ("H:", h.Text); - Assert.Equal (2, h.TrianglePosition); - Assert.IsType (h); - - ColorBar s = GetColorBar (cp, ColorPickerPart.Bar2); - Assert.Equal ("S:", s.Text); - Assert.Equal (2, s.TrianglePosition); - Assert.IsType (s); - - ColorBar v = GetColorBar (cp, ColorPickerPart.Bar3); - Assert.Equal ("V:", v.Text); - Assert.Equal (2, v.TrianglePosition); - Assert.IsType (v); - - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - Assert.Equal ("#000000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_DisposesOldViews_OnModelChange () - { - ColorPicker cp = GetColorPicker (ColorModel.HSL, true); - - ColorBar b1 = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar b2 = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b3 = GetColorBar (cp, ColorPickerPart.Bar3); - - TextField tf1 = GetTextField (cp, ColorPickerPart.Bar1); - TextField tf2 = GetTextField (cp, ColorPickerPart.Bar2); - TextField tf3 = GetTextField (cp, ColorPickerPart.Bar3); - - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - -#if DEBUG_IDISPOSABLE - Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.False (b.WasDisposed)); -#endif - cp.Style.ColorModel = ColorModel.RGB; - cp.ApplyStyleChanges (); - - ColorBar b1After = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar b2After = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b3After = GetColorBar (cp, ColorPickerPart.Bar3); - - TextField tf1After = GetTextField (cp, ColorPickerPart.Bar1); - TextField tf2After = GetTextField (cp, ColorPickerPart.Bar2); - TextField tf3After = GetTextField (cp, ColorPickerPart.Bar3); - - TextField hexAfter = GetTextField (cp, ColorPickerPart.Hex); - - // Old bars should be disposed -#if DEBUG_IDISPOSABLE - Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.True (b.WasDisposed)); -#endif - Assert.NotSame (hex, hexAfter); - - Assert.NotSame (b1, b1After); - Assert.NotSame (b2, b2After); - Assert.NotSame (b3, b3After); - - Assert.NotSame (tf1, tf1After); - Assert.NotSame (tf2, tf2After); - Assert.NotSame (tf3, tf3After); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_EnterHexFor_ColorName () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); - - cp.Draw (); - - TextField name = GetTextField (cp, ColorPickerPart.ColorName); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - hex.SetFocus (); - - Assert.True (hex.HasFocus); - Assert.Same (hex, cp.Focused); - - hex.Text = ""; - name.Text = ""; - - Assert.Empty (hex.Text); - Assert.Empty (name.Text); - - Application.RaiseKeyDownEvent ('#'); - Assert.Empty (name.Text); - - //7FFFD4 - - Assert.Equal ("#", hex.Text); - Application.RaiseKeyDownEvent ('7'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('D'); - Assert.Empty (name.Text); - - Application.RaiseKeyDownEvent ('4'); - - Assert.True (hex.HasFocus); - - // Tab out of the hex field - should wrap to first focusable subview - Application.RaiseKeyDownEvent (Key.Tab); - Assert.False (hex.HasFocus); - Assert.NotSame (hex, cp.Focused); - - // Color name should be recognised as a known string and populated - Assert.Equal ("#7FFFD4", hex.Text); - Assert.Equal ("Aquamarine", name.Text); - } - - /// - /// In this version we use the Enter button to accept the typed text instead - /// of tabbing to the next view. - /// - [Fact] - [SetupFakeApplication] - public void ColorPicker_EnterHexFor_ColorName_AcceptVariation () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); - - cp.Draw (); - - TextField name = GetTextField (cp, ColorPickerPart.ColorName); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - hex.SetFocus (); - - Assert.True (hex.HasFocus); - Assert.Same (hex, cp.Focused); - - hex.Text = ""; - name.Text = ""; - - Assert.Empty (hex.Text); - Assert.Empty (name.Text); - - Application.RaiseKeyDownEvent ('#'); - Assert.Empty (name.Text); - - //7FFFD4 - - Assert.Equal ("#", hex.Text); - Application.RaiseKeyDownEvent ('7'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('F'); - Application.RaiseKeyDownEvent ('D'); - Assert.Empty (name.Text); - - Application.RaiseKeyDownEvent ('4'); - - Assert.True (hex.HasFocus); - - // Should stay in the hex field (because accept not tab) - Application.RaiseKeyDownEvent (Key.Enter); - Assert.True (hex.HasFocus); - Assert.Same (hex, cp.Focused); - - // But still, Color name should be recognised as a known string and populated - Assert.Equal ("#7FFFD4", hex.Text); - Assert.Equal ("Aquamarine", name.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_InvalidHexInput_DoesNotChangeColor () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true); - - cp.Draw (); - - // Enter invalid hex value - TextField hexField = cp.SubViews.OfType ().First (tf => tf.Text == "#000000"); - hexField.SetFocus (); - hexField.Text = "#ZZZZZZ"; - Assert.True (hexField.HasFocus); - Assert.Equal ("#ZZZZZZ", hexField.Text); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("#ZZZZZZ", hex.Text); - - // Advance away from hexField to cause validation - cp.AdvanceFocus (NavigationDirection.Forward, null); - - cp.Draw (); - - Assert.Equal ("R:", r.Text); - Assert.Equal (2, r.TrianglePosition); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("#000000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_RGB_KeyboardNavigation () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("R:", r.Text); - Assert.Equal (2, r.TrianglePosition); - Assert.IsType (r); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.IsType (g); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.IsType (b); - Assert.Equal ("#000000", hex.Text); - - Assert.IsAssignableFrom (cp.Focused); - - cp.Draw (); - - Application.RaiseKeyDownEvent (Key.CursorRight); - - cp.Draw (); - - Assert.Equal (3, r.TrianglePosition); - Assert.Equal ("#0F0000", hex.Text); - - Application.RaiseKeyDownEvent (Key.CursorRight); - - cp.Draw (); - - Assert.Equal (4, r.TrianglePosition); - Assert.Equal ("#1E0000", hex.Text); - - // Use cursor to move the triangle all the way to the right - for (var i = 0; i < 1000; i++) - { - Application.RaiseKeyDownEvent (Key.CursorRight); - } - - cp.Draw (); - - // 20 width and TrianglePosition is 0 indexed - // Meaning we are asserting that triangle is at end - Assert.Equal (19, r.TrianglePosition); - Assert.Equal ("#FF0000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_RGB_MouseNavigation () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("R:", r.Text); - Assert.Equal (2, r.TrianglePosition); - Assert.IsType (r); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.IsType (g); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.IsType (b); - Assert.Equal ("#000000", hex.Text); - - Assert.IsAssignableFrom (cp.Focused); - - cp.Focused.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (3, 0) - }); - - cp.Draw (); - - Assert.Equal (3, r.TrianglePosition); - Assert.Equal ("#0F0000", hex.Text); - - cp.Focused.RaiseMouseEvent ( - new () - { - Flags = MouseFlags.Button1Pressed, - Position = new (4, 0) - }); - - cp.Draw (); - - Assert.Equal (4, r.TrianglePosition); - Assert.Equal ("#1E0000", hex.Text); - } - - [Theory] - [SetupFakeApplication] - [MemberData (nameof (ColorPickerTestData))] - public void ColorPicker_RGB_NoText ( - Color c, - string expectedR, - int expectedRTriangle, - string expectedG, - int expectedGTriangle, - string expectedB, - int expectedBTriangle, - string expectedHex - ) - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - cp.SelectedColor = c; - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal (expectedR, r.Text); - Assert.Equal (expectedRTriangle, r.TrianglePosition); - Assert.Equal (expectedG, g.Text); - Assert.Equal (expectedGTriangle, g.TrianglePosition); - Assert.Equal (expectedB, b.Text); - Assert.Equal (expectedBTriangle, b.TrianglePosition); - Assert.Equal (expectedHex, hex.Text); - } - - [Theory] - [SetupFakeApplication] - [MemberData (nameof (ColorPickerTestData_WithTextFields))] - public void ColorPicker_RGB_NoText_WithTextFields ( - Color c, - string expectedR, - int expectedRTriangle, - int expectedRValue, - string expectedG, - int expectedGTriangle, - int expectedGValue, - string expectedB, - int expectedBTriangle, - int expectedBValue, - string expectedHex - ) - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true); - cp.SelectedColor = c; - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); - TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); - TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); - - Assert.Equal (expectedR, r.Text); - Assert.Equal (expectedRTriangle, r.TrianglePosition); - Assert.Equal (expectedRValue.ToString (), rTextField.Text); - Assert.Equal (expectedG, g.Text); - Assert.Equal (expectedGTriangle, g.TrianglePosition); - Assert.Equal (expectedGValue.ToString (), gTextField.Text); - Assert.Equal (expectedB, b.Text); - Assert.Equal (expectedBTriangle, b.TrianglePosition); - Assert.Equal (expectedBValue.ToString (), bTextField.Text); - Assert.Equal (expectedHex, hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_SwitchingColorModels_ResetsBars () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, false); - cp.BeginInit (); - cp.EndInit (); - cp.SelectedColor = new (255, 0); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - Assert.Equal ("R:", r.Text); - Assert.Equal (19, r.TrianglePosition); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("#FF0000", hex.Text); - - // Switch to HSV - cp.Style.ColorModel = ColorModel.HSV; - cp.ApplyStyleChanges (); - - cp.Draw (); - - ColorBar h = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar s = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar v = GetColorBar (cp, ColorPickerPart.Bar3); - - Assert.Equal ("H:", h.Text); - Assert.Equal (2, h.TrianglePosition); - Assert.Equal ("S:", s.Text); - Assert.Equal (19, s.TrianglePosition); - Assert.Equal ("V:", v.Text); - Assert.Equal (19, v.TrianglePosition); - Assert.Equal ("#FF0000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_SyncBetweenTextFieldAndBars () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true); - - cp.Draw (); - - // Change value using the bar - RBar rBar = cp.SubViews.OfType ().First (); - rBar.Value = 128; - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); - TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); - TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); - - Assert.Equal ("R:", r.Text); - Assert.Equal (9, r.TrianglePosition); - Assert.Equal ("128", rTextField.Text); - Assert.Equal ("G:", g.Text); - Assert.Equal (2, g.TrianglePosition); - Assert.Equal ("0", gTextField.Text); - Assert.Equal ("B:", b.Text); - Assert.Equal (2, b.TrianglePosition); - Assert.Equal ("0", bTextField.Text); - Assert.Equal ("#800000", hex.Text); - } - - [Fact] - [SetupFakeApplication] - public void ColorPicker_TabCompleteColorName () - { - ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); - - cp.Draw (); - - ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); - ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); - ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); - TextField name = GetTextField (cp, ColorPickerPart.ColorName); - TextField hex = GetTextField (cp, ColorPickerPart.Hex); - - name.SetFocus (); - - Assert.True (name.HasFocus); - Assert.Same (name, cp.Focused); - - name.Text = ""; - Assert.Empty (name.Text); - - Application.RaiseKeyDownEvent (Key.A); - Application.RaiseKeyDownEvent (Key.Q); - - Assert.Equal ("aq", name.Text); - - // Auto complete the color name - Application.RaiseKeyDownEvent (Key.Tab); - - // Match cyan alternative name - Assert.Equal ("Aqua", name.Text); - - Assert.True (name.HasFocus); - - Application.RaiseKeyDownEvent (Key.Tab); - - // Resolves to cyan color - Assert.Equal ("Aqua", name.Text); - - // Tab out of the text field - Application.RaiseKeyDownEvent (Key.Tab); - - Assert.False (name.HasFocus); - Assert.NotSame (name, cp.Focused); - - Assert.Equal ("#00FFFF", hex.Text); - } - - public static IEnumerable ColorPickerTestData () - { - yield return new object [] - { - new Color (255, 0), - "R:", 19, "G:", 2, "B:", 2, "#FF0000" - }; - - yield return new object [] - { - new Color (0, 255), - "R:", 2, "G:", 19, "B:", 2, "#00FF00" - }; - - yield return new object [] - { - new Color (0, 0, 255), - "R:", 2, "G:", 2, "B:", 19, "#0000FF" - }; - - yield return new object [] - { - new Color (125, 125, 125), - "R:", 11, "G:", 11, "B:", 11, "#7D7D7D" - }; - } - - public static IEnumerable ColorPickerTestData_WithTextFields () - { - yield return new object [] - { - new Color (255, 0), - "R:", 15, 255, "G:", 2, 0, "B:", 2, 0, "#FF0000" - }; - - yield return new object [] - { - new Color (0, 255), - "R:", 2, 0, "G:", 15, 255, "B:", 2, 0, "#00FF00" - }; - - yield return new object [] - { - new Color (0, 0, 255), - "R:", 2, 0, "G:", 2, 0, "B:", 15, 255, "#0000FF" - }; - - yield return new object [] - { - new Color (125, 125, 125), - "R:", 9, 125, "G:", 9, 125, "B:", 9, 125, "#7D7D7D" - }; - } - - private ColorBar GetColorBar (ColorPicker cp, ColorPickerPart toGet) - { - if (toGet <= ColorPickerPart.Bar3) - { - return cp.SubViews.OfType ().ElementAt ((int)toGet); - } - - throw new NotSupportedException ("ColorPickerPart must be a bar"); - } - - private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, bool showName = false) - { - var cp = new ColorPicker { Width = 20, SelectedColor = new (0, 0) }; - cp.Style.ColorModel = colorModel; - cp.Style.ShowTextFields = showTextFields; - cp.Style.ShowColorName = showName; - cp.ApplyStyleChanges (); - - Application.TopRunnable = new () { Width = 20, Height = 5 }; - Application.TopRunnable.Add (cp); - - Application.TopRunnable.LayoutSubViews (); - Application.TopRunnable.SetFocus (); - - return cp; - } - - private TextField GetTextField (ColorPicker cp, ColorPickerPart toGet) - { - bool hasBarValueTextFields = cp.Style.ShowTextFields; - bool hasColorNameTextField = cp.Style.ShowColorName; - - switch (toGet) - { - case ColorPickerPart.Bar1: - case ColorPickerPart.Bar2: - case ColorPickerPart.Bar3: - if (!hasBarValueTextFields) - { - throw new NotSupportedException ("Corresponding Style option is not enabled"); - } - - return cp.SubViews.OfType ().ElementAt ((int)toGet); - case ColorPickerPart.ColorName: - if (!hasColorNameTextField) - { - throw new NotSupportedException ("Corresponding Style option is not enabled"); - } - - return cp.SubViews.OfType ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet - 3); - case ColorPickerPart.Hex: - - int offset = hasBarValueTextFields ? 0 : 3; - offset += hasColorNameTextField ? 0 : 1; - - return cp.SubViews.OfType ().ElementAt ((int)toGet - offset); - default: - throw new ArgumentOutOfRangeException (nameof (toGet), toGet, null); - } - } - - private enum ColorPickerPart - { - Bar1 = 0, - Bar2 = 1, - Bar3 = 2, - ColorName = 3, - Hex = 4 - } + } diff --git a/Tests/UnitTests/Views/ComboBoxTests.cs b/Tests/UnitTests/Views/ComboBoxTests.cs index 43059f823..338a871d6 100644 --- a/Tests/UnitTests/Views/ComboBoxTests.cs +++ b/Tests/UnitTests/Views/ComboBoxTests.cs @@ -77,7 +77,7 @@ public class ComboBoxTests (ITestOutputHelper output) string [] source = Enumerable.Range (0, 15).Select (x => x.ToString ()).ToArray (); comboBox.SetSource (new ObservableCollection (source.ToList ())); - var top = new Toplevel (); + var top = new Runnable (); top.Add (comboBox); foreach (KeyCode key in (KeyCode [])Enum.GetValues (typeof (KeyCode))) @@ -96,7 +96,7 @@ public class ComboBoxTests (ITestOutputHelper output) cb.Expanded += (s, e) => cb.SetSource (list); cb.Collapsed += (s, e) => cb.Source = null; - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -127,7 +127,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = false }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -182,7 +182,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = false }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -219,7 +219,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = false, ReadOnly = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -278,7 +278,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Height = 4, Width = 5 }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -382,7 +382,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -501,7 +501,7 @@ public class ComboBoxTests (ITestOutputHelper output) var cb = new ComboBox { Width = 6, Height = 4, HideDropdownListOnClick = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); var otherView = new View { CanFocus = true }; @@ -677,7 +677,7 @@ Three ", var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -740,7 +740,7 @@ Three ", var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -798,7 +798,7 @@ Three ", var cb = new ComboBox { Height = 4, Width = 5, HideDropdownListOnClick = true }; cb.SetSource (["One", "Two", "Three"]); cb.OpenSelectedItem += (s, e) => selected = e.Value.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); Application.Begin (top); @@ -832,7 +832,7 @@ Three ", { ObservableCollection source = ["One", "Two", "Three"]; var cb = new ComboBox { Width = 10 }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (cb); @@ -1009,52 +1009,4 @@ Three Assert.Equal (3, cb.Source.Count); top.Dispose (); } - - [Fact] - public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Minus_One () - { - var cb = new ComboBox (); - var top = new Toplevel (); - Application.TopRunnable = top; - - top.Add (cb); - top.FocusDeepest (NavigationDirection.Forward, null); - Assert.Null (cb.Source); - Assert.Equal (-1, cb.SelectedItem); - ObservableCollection source = []; - cb.SetSource (source); - Assert.NotNull (cb.Source); - Assert.Equal (0, cb.Source.Count); - Assert.Equal (-1, cb.SelectedItem); - source.Add ("One"); - Assert.Equal (1, cb.Source.Count); - Assert.Equal (-1, cb.SelectedItem); - Assert.True (Application.RaiseKeyDownEvent (Key.F4)); - Assert.True (cb.IsShow); - Assert.Equal (0, cb.SelectedItem); - Assert.Equal ("One", cb.Text); - source.Add ("Two"); - Assert.Equal (0, cb.SelectedItem); - Assert.Equal ("One", cb.Text); - cb.Text = "T"; - Assert.True (cb.IsShow); - Assert.Equal (-1, cb.SelectedItem); - Assert.Equal ("T", cb.Text); - Assert.True (Application.RaiseKeyDownEvent (Key.Enter)); - Assert.False (cb.IsShow); - Assert.Equal (2, cb.Source.Count); - Assert.Equal (-1, cb.SelectedItem); - Assert.Equal ("T", cb.Text); - Assert.True (Application.RaiseKeyDownEvent (Key.Esc)); - Assert.False (cb.IsShow); - Assert.Equal (-1, cb.SelectedItem); // retains last accept selected item - Assert.Equal ("", cb.Text); // clear text - cb.SetSource (new ObservableCollection ()); - Assert.Equal (0, cb.Source.Count); - Assert.Equal (-1, cb.SelectedItem); - Assert.Equal ("", cb.Text); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } } diff --git a/Tests/UnitTests/Views/DatePickerTests.cs b/Tests/UnitTests/Views/DatePickerTests.cs index 4dd616494..414bddd9c 100644 --- a/Tests/UnitTests/Views/DatePickerTests.cs +++ b/Tests/UnitTests/Views/DatePickerTests.cs @@ -12,7 +12,7 @@ public class DatePickerTests var date = new DateTime (9999, 11, 15); var datePicker = new DatePicker (date); - var top = new Toplevel (); + var top = new Runnable (); top.Add (datePicker); Application.Begin (top); @@ -42,7 +42,7 @@ public class DatePickerTests { var date = new DateTime (1, 2, 15); var datePicker = new DatePicker (date); - var top = new Toplevel (); + var top = new Runnable (); // Move focus to previous month button top.Add (datePicker); diff --git a/Tests/UnitTests/Views/FrameViewTests.cs b/Tests/UnitTests/Views/FrameViewTests.cs index 2b78d216b..9e59b48c4 100644 --- a/Tests/UnitTests/Views/FrameViewTests.cs +++ b/Tests/UnitTests/Views/FrameViewTests.cs @@ -42,7 +42,7 @@ public class FrameViewTests (ITestOutputHelper output) var fv = new FrameView () { BorderStyle = LineStyle.Single }; Assert.Equal (string.Empty, fv.Title); Assert.Equal (string.Empty, fv.Text); - var top = new Toplevel (); + var top = new Runnable (); top.Add (fv); Application.Begin (top); Assert.Equal (new (0, 0, 0, 0), fv.Frame); diff --git a/Tests/UnitTests/Views/GraphViewTests.cs b/Tests/UnitTests/Views/GraphViewTests.cs index fbe2ca8ee..8f3d878fd 100644 --- a/Tests/UnitTests/Views/GraphViewTests.cs +++ b/Tests/UnitTests/Views/GraphViewTests.cs @@ -1499,7 +1499,7 @@ public class PathAnnotationTests { // create a wide window var mount = new View { Width = 100, Height = 100 }; - var top = new Toplevel (); + var top = new Runnable (); try { @@ -1519,7 +1519,7 @@ public class PathAnnotationTests //put label into view mount.Add (view); - //putting mount into Toplevel since changing size + //putting mount into Runnable since changing size top.Add (mount); Application.Begin (top); diff --git a/Tests/UnitTests/Views/LabelTests.cs b/Tests/UnitTests/Views/LabelTests.cs index 1128520ff..d3a9efe50 100644 --- a/Tests/UnitTests/Views/LabelTests.cs +++ b/Tests/UnitTests/Views/LabelTests.cs @@ -1,4 +1,5 @@ -using Xunit.Abstractions; +#nullable enable +using Xunit.Abstractions; namespace UnitTests.ViewsTests; @@ -13,7 +14,7 @@ public class LabelTests (ITestOutputHelper output) var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); @@ -54,12 +55,12 @@ public class LabelTests (ITestOutputHelper output) var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (30, 5); - + Application.LayoutAndDraw (); var expected = @" ┌────────────────────────────┐ │ │ @@ -100,7 +101,7 @@ public class LabelTests (ITestOutputHelper output) TextFormatter tf2 = new () { Direction = TextDirection.LeftRight_TopBottom, ConstrainToSize = tfSize, FillRemaining = true }; tf2.Text = "This TextFormatter (tf2) with fill will be cleared on rewritten."; - Toplevel top = new (); + Runnable top = new (); top.Add (label); SessionToken sessionToken = Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -172,7 +173,7 @@ This TextFormatter (tf2) is rewritten. ", public void Label_Draw_Horizontal_Simple_Runes () { var label = new Label { Text = "Demo Simple Text" }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (label); Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -193,7 +194,7 @@ Demo Simple Text public void Label_Draw_Vertical_Simple_Text () { var label = new Label { TextDirection = TextDirection.TopBottom_LeftRight, Text = "Demo Simple Text" }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (label); Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -229,7 +230,7 @@ t public void Label_Draw_Vertical_Wide_Runes () { var label = new Label { TextDirection = TextDirection.TopBottom_LeftRight, Text = "デモエムポンズ" }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (label); Application.Begin (top); AutoInitShutdownAttribute.RunIteration (); @@ -256,14 +257,14 @@ t var label = new Label { X = Pos.Center (), Y = Pos.Center (), Text = "Say Hello 你" }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Assert.False (label.IsInitialized); Application.Begin (top); Application.Driver!.SetScreenSize (30, 5); - + Application.LayoutAndDraw (); Assert.True (label.IsInitialized); Assert.Equal ("Say Hello 你", label.Text); Assert.Equal ("Say Hello 你", label.TextFormatter.Text); @@ -288,14 +289,14 @@ t var label = new Label { X = Pos.Center (), Y = Pos.Center (), Text = "Say Hello 你" }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Assert.False (label.IsInitialized); Application.Begin (top); Application.Driver!.SetScreenSize (30, 5); - + Application.LayoutAndDraw (); Assert.True (label.IsInitialized); Assert.Equal ("Say Hello 你", label.Text); Assert.Equal ("Say Hello 你", label.TextFormatter.Text); @@ -342,54 +343,6 @@ t label.Dispose (); } - [Fact] - [AutoInitShutdown] - public void With_Top_Margin_Without_Top_Border () - { - var label = new Label { Text = "Test", /*Width = 6, Height = 3,*/ BorderStyle = LineStyle.Single }; - label.Margin!.Thickness = new (0, 1, 0, 0); - label.Border!.Thickness = new (1, 0, 1, 1); - var top = new Toplevel (); - top.Add (label); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (new (0, 0, 6, 3), label.Frame); - Assert.Equal (new (0, 0, 4, 1), label.Viewport); - Application.Begin (top); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -│Test│ -└────┘", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Without_Top_Border () - { - var label = new Label { Text = "Test", /* Width = 6, Height = 3, */BorderStyle = LineStyle.Single }; - label.Border!.Thickness = new (1, 0, 1, 1); - var top = new Toplevel (); - top.Add (label); - Application.Begin (top); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 6, 2), label.Frame); - Assert.Equal (new (0, 0, 4, 1), label.Viewport); - Application.Begin (top); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -│Test│ -└────┘", - output - ); - top.Dispose (); - } - // These tests were formally in AutoSizetrue.cs. They are (poor) Label tests. private readonly string [] _expecteds = new string [21] { @@ -728,11 +681,11 @@ t win.Add (label); - Toplevel top = new (); + Runnable top = new (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (40, 10); - + Application.LayoutAndDraw (); Assert.Equal (29, label.Text.Length); Assert.Equal (new (0, 0, 40, 10), top.Frame); Assert.Equal (new (0, 0, 40, 10), win.Frame); @@ -776,11 +729,11 @@ t win.Add (label); - Toplevel top = new (); + Runnable top = new (); top.Add (win); SessionToken rs = Application.Begin (top); Application.Driver!.SetScreenSize (40, 10); - + Application.LayoutAndDraw (); Assert.Equal (new (0, 0, 40, 10), top.Frame); Assert.Equal (new (0, 0, 40, 10), win.Frame); Assert.Equal (new (0, 7, 29, 1), label.Frame); @@ -809,7 +762,7 @@ t [AutoInitShutdown] public void Dim_Subtract_Operator_With_Text () { - Toplevel top = new (); + Runnable top = new (); var view = new View { @@ -852,6 +805,7 @@ t if (k.KeyCode == KeyCode.Enter) { Application.Driver!.SetScreenSize (22, count + 4); + Application.LayoutAndDraw (); Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (_expecteds [count], output); Assert.Equal (new (0, 0, 22, count + 4), pos); @@ -896,7 +850,7 @@ t return; - void OnApplicationOnIteration (object s, EventArgs a) + void OnApplicationOnIteration (object? s, EventArgs a) { while (count > -1) { @@ -992,7 +946,7 @@ t [AutoInitShutdown] public void Dim_Add_Operator_With_Text () { - Toplevel top = new (); + Runnable top = new (); var view = new View { @@ -1012,6 +966,7 @@ t if (k.KeyCode == KeyCode.Enter) { Application.Driver!.SetScreenSize (22, count + 4); + Application.LayoutAndDraw (); Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (_expecteds [count], output); Assert.Equal (new (0, 0, 22, count + 4), pos); @@ -1057,7 +1012,7 @@ t return; - void OnApplicationOnIteration (object s, EventArgs a) + void OnApplicationOnIteration (object? s, EventArgs a) { while (count < 21) { @@ -1088,17 +1043,17 @@ t }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (10, 4); - + Application.LayoutAndDraw (); Assert.Equal (5, text.Length); Assert.Equal (new (0, 0, 5, 1), label.Frame); Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.TopRunnable.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.TopRunnableView!.Frame); var expected = @" ┌────────┐ @@ -1147,17 +1102,17 @@ t }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (label); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (10, 4); - + Application.LayoutAndDraw (); Assert.Equal (5, text.Length); Assert.Equal (new (0, 0, 5, 1), label.Frame); Assert.Equal (new (5, 1), label.TextFormatter.ConstrainToSize); Assert.Equal (["Label"], label.TextFormatter.GetLines ()); Assert.Equal (new (0, 0, 10, 4), win.Frame); - Assert.Equal (new (0, 0, 10, 4), Application.TopRunnable.Frame); + Assert.Equal (new (0, 0, 10, 4), Application.TopRunnableView!.Frame); var expected = @" ┌────────┐ @@ -1210,147 +1165,6 @@ t super.Dispose (); } - [Fact] - public void CanFocus_False_HotKey_SetsFocus_Next () - { - View otherView = new () - { - Text = "otherView", - CanFocus = true - }; - - Label label = new () - { - Text = "_label" - }; - - View nextView = new () - { - Text = "nextView", - CanFocus = true - }; - - Application.TopRunnable = new (); - Application.TopRunnable.Add (otherView, label, nextView); - - Application.TopRunnable.SetFocus (); - Assert.True (otherView.HasFocus); - - Assert.True (Application.RaiseKeyDownEvent (label.HotKey)); - Assert.False (otherView.HasFocus); - Assert.False (label.HasFocus); - Assert.True (nextView.HasFocus); - - Application.TopRunnable.Dispose (); - Application.ResetState (); - } - - [Fact] - public void CanFocus_False_MouseClick_SetsFocus_Next () - { - View otherView = new () { X = 0, Y = 0, Width = 1, Height = 1, Id = "otherView", CanFocus = true }; - Label label = new () { X = 0, Y = 1, Text = "_label" }; - View nextView = new () { X = Pos.Right (label), Y = Pos.Top (label), Width = 1, Height = 1, Id = "nextView", CanFocus = true }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (otherView, label, nextView); - Application.TopRunnable.Layout (); - - Application.TopRunnable.SetFocus (); - - // click on label - Application.RaiseMouseEvent (new () { ScreenPosition = label.Frame.Location, Flags = MouseFlags.Button1Clicked }); - Assert.False (label.HasFocus); - Assert.True (nextView.HasFocus); - - Application.TopRunnable.Dispose (); - Application.ResetState (); - } - - [Fact] - public void CanFocus_True_HotKey_SetsFocus () - { - Label label = new () - { - Text = "_label", - CanFocus = true - }; - - View view = new () - { - Text = "view", - CanFocus = true - }; - - Application.TopRunnable = new (); - Application.TopRunnable.Add (label, view); - - view.SetFocus (); - Assert.True (label.CanFocus); - Assert.False (label.HasFocus); - Assert.True (view.CanFocus); - Assert.True (view.HasFocus); - - // No focused view accepts Tab, and there's no other view to focus, so OnKeyDown returns false - Assert.True (Application.RaiseKeyDownEvent (label.HotKey)); - Assert.True (label.HasFocus); - Assert.False (view.HasFocus); - - Application.TopRunnable.Dispose (); - Application.ResetState (); - } - - [Fact] - public void CanFocus_True_MouseClick_Focuses () - { - Label label = new () - { - Text = "label", - X = 0, - Y = 0, - CanFocus = true - }; - - View otherView = new () - { - Text = "view", - X = 0, - Y = 1, - Width = 4, - Height = 1, - CanFocus = true - }; - - Application.TopRunnable = new () - { - Width = 10, - Height = 10 - }; - Application.TopRunnable.Add (label, otherView); - Application.TopRunnable.SetFocus (); - Application.TopRunnable.Layout (); - - Assert.True (label.CanFocus); - Assert.True (label.HasFocus); - Assert.True (otherView.CanFocus); - Assert.False (otherView.HasFocus); - - otherView.SetFocus (); - Assert.True (otherView.HasFocus); - - // label can focus, so clicking on it set focus - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked }); - Assert.True (label.HasFocus); - Assert.False (otherView.HasFocus); - - // click on view - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 1), Flags = MouseFlags.Button1Clicked }); - Assert.False (label.HasFocus); - Assert.True (otherView.HasFocus); - - Application.TopRunnable.Dispose (); - Application.ResetState (); - } - // https://github.com/gui-cs/Terminal.Gui/issues/3893 [Fact] [SetupFakeApplication] diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index b15501a07..9657a3b44 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -10,7 +10,7 @@ public class MenuBarTests () public void DefaultKey_Activates_And_Opens () { // Arrange - var top = new Toplevel () + var top = new Runnable () { App = ApplicationImpl.Instance }; @@ -58,7 +58,7 @@ public class MenuBarTests () public void DefaultKey_Deactivates () { // Arrange - var top = new Toplevel () { App = ApplicationImpl.Instance }; + var top = new Runnable () { App = ApplicationImpl.Instance }; MenuBar menuBar = new MenuBar () { App = ApplicationImpl.Instance }; menuBar.EnableForDesign (ref top); @@ -95,7 +95,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); @@ -133,7 +133,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -165,7 +165,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -204,7 +204,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -237,7 +237,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -274,7 +274,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -319,7 +319,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); menuBar.Add (menuBarItem2); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -356,7 +356,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -382,7 +382,7 @@ public class MenuBarTests () public void Mouse_Click_Activates_And_Opens () { // Arrange - var top = new Toplevel () { App = ApplicationImpl.Instance }; + var top = new Runnable () { App = ApplicationImpl.Instance }; MenuBar menuBar = new MenuBar () { App = ApplicationImpl.Instance }; menuBar.EnableForDesign (ref top); @@ -424,7 +424,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -471,7 +471,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); @@ -513,7 +513,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -544,7 +544,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -576,7 +576,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -614,7 +614,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -663,7 +663,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); @@ -704,7 +704,7 @@ public class MenuBarTests () menuBar.Add (menuBarItem); Assert.Single (menuBar.SubViews); Assert.Single (menuBarItem.SubViews); - var top = new Toplevel (); + var top = new Runnable (); top.Add (menuBar); SessionToken rs = Application.Begin (top); Assert.False (menuBar.Active); diff --git a/Tests/UnitTests/Views/ScrollBarTests.cs b/Tests/UnitTests/Views/ScrollBarTests.cs index 4a5cdf7ad..d2cdb838b 100644 --- a/Tests/UnitTests/Views/ScrollBarTests.cs +++ b/Tests/UnitTests/Views/ScrollBarTests.cs @@ -536,7 +536,7 @@ public class ScrollBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Mouse_Click_DecrementButton_Decrements ([CombinatorialRange (1, 3, 1)] int increment, Orientation orientation) { - var top = new Toplevel () + var top = new Runnable () { Id = "top", Width = 10, @@ -585,7 +585,7 @@ public class ScrollBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Mouse_Click_IncrementButton_Increments ([CombinatorialRange (1, 3, 1)] int increment, Orientation orientation) { - var top = new Toplevel () + var top = new Runnable () { Id = "top", Width = 10, diff --git a/Tests/UnitTests/Views/ShortcutTests.cs b/Tests/UnitTests/Views/ShortcutTests.cs index 412e41c33..c517afc96 100644 --- a/Tests/UnitTests/Views/ShortcutTests.cs +++ b/Tests/UnitTests/Views/ShortcutTests.cs @@ -23,7 +23,7 @@ public class ShortcutTests [InlineData (9, 0)] public void MouseClick_Raises_Accepted (int x, int expectedAccepted) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -31,8 +31,8 @@ public class ShortcutTests Text = "0", Title = "C" }; - Application.TopRunnable.Add (shortcut); - Application.TopRunnable.Layout (); + Application.TopRunnableView.Add (shortcut); + Application.TopRunnableView.Layout (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -46,7 +46,7 @@ public class ShortcutTests Assert.Equal (expectedAccepted, accepted); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -74,7 +74,7 @@ public class ShortcutTests int expectedShortcutSelected ) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -93,9 +93,9 @@ public class ShortcutTests var shortcutSelectCount = 0; shortcut.Selecting += (s, e) => { shortcutSelectCount++; }; - Application.TopRunnable.Add (shortcut); - Application.TopRunnable.SetRelativeLayout (new (100, 100)); - Application.TopRunnable.LayoutSubViews (); + Application.TopRunnableView.Add (shortcut); + Application.TopRunnableView.SetRelativeLayout (new (100, 100)); + Application.TopRunnableView.LayoutSubViews (); Application.RaiseMouseEvent ( new () @@ -109,7 +109,7 @@ public class ShortcutTests Assert.Equal (expectedCommandViewAccepted, commandViewAcceptCount); Assert.Equal (expectedCommandViewSelected, commandViewSelectCount); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -130,7 +130,7 @@ public class ShortcutTests [InlineData (9, 0, 0)] public void MouseClick_Button_CommandView_Raises_Shortcut_Accepted (int mouseX, int expectedAccept, int expectedButtonAccept) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -147,9 +147,9 @@ public class ShortcutTests }; var buttonAccepted = 0; shortcut.CommandView.Accepting += (s, e) => { buttonAccepted++; }; - Application.TopRunnable.Add (shortcut); - Application.TopRunnable.SetRelativeLayout (new (100, 100)); - Application.TopRunnable.LayoutSubViews (); + Application.TopRunnableView.Add (shortcut); + Application.TopRunnableView.SetRelativeLayout (new (100, 100)); + Application.TopRunnableView.LayoutSubViews (); var accepted = 0; shortcut.Accepting += (s, e) => { accepted++; }; @@ -164,7 +164,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedButtonAccept, buttonAccepted); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -186,7 +186,7 @@ public class ShortcutTests [InlineData (10, 1, 0)] public void MouseClick_CheckBox_CommandView_Raises_Shortcut_Accepted_Selected_Correctly (int mouseX, int expectedAccepted, int expectedCheckboxAccepted) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -212,9 +212,9 @@ public class ShortcutTests checkboxSelected++; }; - Application.TopRunnable.Add (shortcut); - Application.TopRunnable.SetRelativeLayout (new (100, 100)); - Application.TopRunnable.LayoutSubViews (); + Application.TopRunnableView.Add (shortcut); + Application.TopRunnableView.SetRelativeLayout (new (100, 100)); + Application.TopRunnableView.LayoutSubViews (); var selected = 0; shortcut.Selecting += (s, e) => @@ -241,7 +241,7 @@ public class ShortcutTests Assert.Equal (expectedCheckboxAccepted, checkboxAccepted); Assert.Equal (expectedCheckboxAccepted, checkboxSelected); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -260,7 +260,7 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0, 0)] public void KeyDown_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -269,7 +269,7 @@ public class ShortcutTests Title = "_C", CanFocus = canFocus }; - Application.TopRunnable.Add (shortcut); + Application.TopRunnableView.Add (shortcut); shortcut.SetFocus (); Assert.Equal (canFocus, shortcut.HasFocus); @@ -285,61 +285,10 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); Assert.Equal (expectedSelect, selected); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } - - [Theory] - [InlineData (true, KeyCode.A, 1, 1)] - [InlineData (true, KeyCode.C, 1, 1)] - [InlineData (true, KeyCode.C | KeyCode.AltMask, 1, 1)] - [InlineData (true, KeyCode.Enter, 1, 1)] - [InlineData (true, KeyCode.Space, 1, 1)] - [InlineData (true, KeyCode.F1, 0, 0)] - [InlineData (false, KeyCode.A, 1, 1)] - [InlineData (false, KeyCode.C, 1, 1)] - [InlineData (false, KeyCode.C | KeyCode.AltMask, 1, 1)] - [InlineData (false, KeyCode.Enter, 0, 0)] - [InlineData (false, KeyCode.Space, 0, 0)] - [InlineData (false, KeyCode.F1, 0, 0)] - public void KeyDown_CheckBox_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) - { - Application.TopRunnable = new (); - - var shortcut = new Shortcut - { - Key = Key.A, - Text = "0", - CommandView = new CheckBox () - { - Title = "_C" - }, - CanFocus = canFocus - }; - Application.TopRunnable.Add (shortcut); - shortcut.SetFocus (); - - Assert.Equal (canFocus, shortcut.HasFocus); - - var accepted = 0; - shortcut.Accepting += (s, e) => - { - accepted++; - e.Handled = true; - }; - - var selected = 0; - shortcut.Selecting += (s, e) => selected++; - - Application.RaiseKeyDownEvent (key); - - Assert.Equal (expectedAccept, accepted); - Assert.Equal (expectedSelect, selected); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); - } [Theory] [InlineData (KeyCode.A, 1)] [InlineData (KeyCode.C, 1)] @@ -349,7 +298,7 @@ public class ShortcutTests [InlineData (KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Accept (KeyCode key, int expectedAccept) { - Application.TopRunnable = new () { App = ApplicationImpl.Instance }; + Application.Begin (new Runnable ()); var shortcut = new Shortcut { @@ -357,9 +306,9 @@ public class ShortcutTests Text = "0", Title = "_C" }; - Application.TopRunnable.Add (shortcut); + Application.TopRunnableView.Add (shortcut); shortcut.BindKeyToApplication = true; - Application.TopRunnable.SetFocus (); + Application.TopRunnableView.SetFocus (); var accepted = 0; shortcut.Accepting += (s, e) => accepted++; @@ -368,7 +317,7 @@ public class ShortcutTests Assert.Equal (expectedAccept, accepted); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -388,7 +337,7 @@ public class ShortcutTests [AutoInitShutdown] public void KeyDown_Invokes_Action (bool canFocus, KeyCode key, int expectedAction) { - var current = new Toplevel (); + var current = new Runnable (); var shortcut = new Shortcut { @@ -427,24 +376,21 @@ public class ShortcutTests [InlineData (false, KeyCode.F1, 0)] public void KeyDown_App_Scope_Invokes_Action (bool canFocus, KeyCode key, int expectedAction) { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut { + App = ApplicationImpl.Instance, // HACK: Move to Parallel and get rid of this BindKeyToApplication = true, Key = Key.A, Text = "0", Title = "_C", - CanFocus = canFocus + CanFocus = canFocus, }; - Application.TopRunnable.Add (shortcut); + Application.TopRunnableView.Add (shortcut); - // Shortcut requires Init for App scoped hotkeys to work - Application.TopRunnable.BeginInit (); - Application.TopRunnable.EndInit (); - - Application.TopRunnable.SetFocus (); + Application.TopRunnableView.SetFocus (); var action = 0; shortcut.Action += () => action++; @@ -453,7 +399,7 @@ public class ShortcutTests Assert.Equal (expectedAction, action); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (true); } @@ -461,11 +407,11 @@ public class ShortcutTests [Fact] public void Scheme_SetScheme_Does_Not_Fault_3664 () { - Application.TopRunnable = new (); + Application.Begin (new Runnable ()); var shortcut = new Shortcut (); - Application.TopRunnable.SetScheme (null); + Application.TopRunnableView.SetScheme (null); Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); @@ -475,7 +421,7 @@ public class ShortcutTests Assert.False (shortcut.HasScheme); Assert.NotNull (shortcut.GetScheme ()); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); Application.ResetState (); } } diff --git a/Tests/UnitTests/Views/SpinnerViewTests.cs b/Tests/UnitTests/Views/SpinnerViewTests.cs index 1a74d5514..abcecf470 100644 --- a/Tests/UnitTests/Views/SpinnerViewTests.cs +++ b/Tests/UnitTests/Views/SpinnerViewTests.cs @@ -40,7 +40,7 @@ public class SpinnerViewTests (ITestOutputHelper output) // Dispose clears timeout view.Dispose (); Assert.Empty (Application.TimedEvents.Timeouts); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -62,7 +62,7 @@ public class SpinnerViewTests (ITestOutputHelper output) expected = "/"; DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -95,14 +95,14 @@ public class SpinnerViewTests (ITestOutputHelper output) //expected = "|"; //DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } private SpinnerView GetSpinnerView () { var view = new SpinnerView (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); diff --git a/Tests/UnitTests/Views/StatusBarTests.cs b/Tests/UnitTests/Views/StatusBarTests.cs index 987d79a7a..9e39707e5 100644 --- a/Tests/UnitTests/Views/StatusBarTests.cs +++ b/Tests/UnitTests/Views/StatusBarTests.cs @@ -56,7 +56,7 @@ public class StatusBarTests // ) // } // ); - // Toplevel top = new (); + // Runnable top = new (); // top.Add (statusBar); // bool CanExecuteNew () { return win == null; } @@ -100,7 +100,7 @@ public class StatusBarTests var iteration = 0; Application.Iteration += OnApplicationOnIteration; - Application.Run ().Dispose (); + Application.Run (); Application.Iteration -= OnApplicationOnIteration; return; diff --git a/Tests/UnitTests/Views/TabViewTests.cs b/Tests/UnitTests/Views/TabViewTests.cs index db11ed0eb..f4cf528db 100644 --- a/Tests/UnitTests/Views/TabViewTests.cs +++ b/Tests/UnitTests/Views/TabViewTests.cs @@ -119,7 +119,7 @@ public class TabViewTests (ITestOutputHelper output) tv.TabClicked += (s, e) => { clicked = e.Tab; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -211,7 +211,7 @@ public class TabViewTests (ITestOutputHelper output) newChanged = e.NewTab; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -301,7 +301,7 @@ public class TabViewTests (ITestOutputHelper output) newChanged = e.NewTab; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -369,7 +369,7 @@ public class TabViewTests (ITestOutputHelper output) Text = "Ok" }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv, btn); Application.Begin (top); @@ -406,7 +406,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.Equal (tv, top.Focused); Assert.Equal (tv.MostFocused, top.Focused.MostFocused); - // Press the cursor down key. Since the selected tab has no focusable views, the focus should move to the next view in the toplevel + // Press the cursor down key. Since the selected tab has no focusable views, the focus should move to the next view in the runnable Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (btn, top.MostFocused); @@ -448,7 +448,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (btn, top.MostFocused); - // Press the cursor down key again will focus next view in the toplevel, which is the TabView + // Press the cursor down key again will focus next view in the runnable, which is the TabView Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (tv, top.Focused); @@ -1445,7 +1445,7 @@ public class TabViewTests (ITestOutputHelper output) tv.Width = 20; tv.Height = 5; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -1465,7 +1465,7 @@ public class TabViewTests (ITestOutputHelper output) tv.Width = 20; tv.Height = 5; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); diff --git a/Tests/UnitTests/Views/TableViewTests.cs b/Tests/UnitTests/Views/TableViewTests.cs index 5d45ae33b..791501a08 100644 --- a/Tests/UnitTests/Views/TableViewTests.cs +++ b/Tests/UnitTests/Views/TableViewTests.cs @@ -25,33 +25,33 @@ public class TableViewTests (ITestOutputHelper output) public static DataTableSource BuildTable (int cols, int rows) { return BuildTable (cols, rows, out _); } - /// Builds a simple table of string columns with the requested number of columns and rows - /// - /// - /// - public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) - { - dt = new (); - - for (var c = 0; c < cols; c++) + /// Builds a simple table of string columns with the requested number of columns and rows + /// + /// + /// + public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) { - dt.Columns.Add ("Col" + c); - } - - for (var r = 0; r < rows; r++) - { - DataRow newRow = dt.NewRow (); + dt = new (); for (var c = 0; c < cols; c++) { - newRow [c] = $"R{r}C{c}"; + dt.Columns.Add ("Col" + c); } - dt.Rows.Add (newRow); - } + for (var r = 0; r < rows; r++) + { + DataRow newRow = dt.NewRow (); - return new (dt); - } + for (var c = 0; c < cols; c++) + { + newRow [c] = $"R{r}C{c}"; + } + + dt.Rows.Add (newRow); + } + + return new (dt); + } [Fact] [AutoInitShutdown] @@ -419,11 +419,11 @@ public class TableViewTests (ITestOutputHelper output) { var tableView = new TableView (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tableView); SessionToken rs = Application.Begin (top); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 25 characters can be printed into table tableView.Viewport = new (0, 0, 25, 5); @@ -610,7 +610,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.Style.AlwaysShowHeaders = false; // ensure that TableView has the input focus - var top = new Toplevel (); + var top = new Runnable (); top.Add (tableView); Application.Begin (top); @@ -688,7 +688,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.BeginInit (); tableView.EndInit (); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); @@ -770,7 +770,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.BeginInit (); tableView.EndInit (); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; tableView.LayoutSubViews (); // 3 columns are visibile @@ -835,7 +835,7 @@ public class TableViewTests (ITestOutputHelper output) tableView.BeginInit (); tableView.EndInit (); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); @@ -949,7 +949,7 @@ public class TableViewTests (ITestOutputHelper output) { TableView tableView = GetABCDEFTableView (out _); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); @@ -980,7 +980,7 @@ public class TableViewTests (ITestOutputHelper output) { TableView tableView = GetABCDEFTableView (out _); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); @@ -1012,7 +1012,7 @@ public class TableViewTests (ITestOutputHelper output) var tv = new TableView (BuildTable (1, 1)); tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row, c.Col].ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -1073,7 +1073,7 @@ public class TableViewTests (ITestOutputHelper output) bStyle.ColorGetter = a => Convert.ToInt32 (a.CellValue) == 2 ? cellHighlight : null; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); @@ -1169,7 +1169,7 @@ public class TableViewTests (ITestOutputHelper output) // when B is 2 use the custom highlight color for the row tv.Style.RowColorGetter += e => Convert.ToInt32 (e.Table [e.RowIndex, 1]) == 2 ? rowHighlight : null; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); @@ -1250,7 +1250,7 @@ public class TableViewTests (ITestOutputHelper output) // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -1581,7 +1581,7 @@ public class TableViewTests (ITestOutputHelper output) { App = ApplicationImpl.Instance }; - tv.SchemeName = "TopLevel"; + tv.SchemeName = "Runnable"; tv.Viewport = new (0, 0, 50, 7); tv.Table = new EnumerableTableSource ( @@ -1619,7 +1619,7 @@ public class TableViewTests (ITestOutputHelper output) Assert.Equal (0, tv.SelectedRow); // ensure that TableView has the input focus - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -2228,7 +2228,7 @@ public class TableViewTests (ITestOutputHelper output) { App = ApplicationImpl.Instance }; - tv.SchemeName = "TopLevel"; + tv.SchemeName = "Runnable"; tv.Viewport = new (0, 0, 50, 6); tv.Table = new EnumerableTableSource ( @@ -2437,7 +2437,7 @@ A B C }; //tv.BeginInit (); tv.EndInit (); - tv.SchemeName = "TopLevel"; + tv.SchemeName = "Runnable"; tv.Viewport = new (0, 0, 25, 4); tv.Style = new () @@ -3247,141 +3247,6 @@ A B C Assert.Equal ("Column Name 2", cn [1]); } - [Fact] - public void CanTabOutOfTableViewUsingCursor_Left () - { - GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); - - // Make the selected cell one in - tableView.SelectedColumn = 1; - - // Pressing left should move us to the first column without changing focus - Application.RaiseKeyDownEvent (Key.CursorLeft); - Assert.Same (tableView, Application.TopRunnable!.MostFocused); - Assert.True (tableView.HasFocus); - - // Because we are now on the leftmost cell a further left press should move focus - Application.RaiseKeyDownEvent (Key.CursorLeft); - - Assert.NotSame (tableView, Application.TopRunnable.MostFocused); - Assert.False (tableView.HasFocus); - - Assert.Same (tf1, Application.TopRunnable.MostFocused); - Assert.True (tf1.HasFocus); - - Application.TopRunnable.Dispose (); - } - - [Fact] - public void CanTabOutOfTableViewUsingCursor_Up () - { - GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); - - // Make the selected cell one in - tableView.SelectedRow = 1; - - // First press should move us up - Application.RaiseKeyDownEvent (Key.CursorUp); - Assert.Same (tableView, Application.TopRunnable!.MostFocused); - Assert.True (tableView.HasFocus); - - // Because we are now on the top row a further press should move focus - Application.RaiseKeyDownEvent (Key.CursorUp); - - Assert.NotSame (tableView, Application.TopRunnable.MostFocused); - Assert.False (tableView.HasFocus); - - Assert.Same (tf1, Application.TopRunnable.MostFocused); - Assert.True (tf1.HasFocus); - - Application.TopRunnable.Dispose (); - } - - [Fact] - public void CanTabOutOfTableViewUsingCursor_Right () - { - GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); - - // Make the selected cell one in from the rightmost column - tableView.SelectedColumn = tableView.Table.Columns - 2; - - // First press should move us to the rightmost column without changing focus - Application.RaiseKeyDownEvent (Key.CursorRight); - Assert.Same (tableView, Application.TopRunnable!.MostFocused); - Assert.True (tableView.HasFocus); - - // Because we are now on the rightmost cell, a further right press should move focus - Application.RaiseKeyDownEvent (Key.CursorRight); - - Assert.NotSame (tableView, Application.TopRunnable.MostFocused); - Assert.False (tableView.HasFocus); - - Assert.Same (tf2, Application.TopRunnable.MostFocused); - Assert.True (tf2.HasFocus); - - Application.TopRunnable.Dispose (); - } - - [Fact] - public void CanTabOutOfTableViewUsingCursor_Down () - { - GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); - - // Make the selected cell one in from the bottommost row - tableView.SelectedRow = tableView.Table.Rows - 2; - - // First press should move us to the bottommost row without changing focus - Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.Same (tableView, Application.TopRunnable!.MostFocused); - Assert.True (tableView.HasFocus); - - // Because we are now on the bottommost cell, a further down press should move focus - Application.RaiseKeyDownEvent (Key.CursorDown); - - Assert.NotSame (tableView, Application.TopRunnable.MostFocused); - Assert.False (tableView.HasFocus); - - Assert.Same (tf2, Application.TopRunnable.MostFocused); - Assert.True (tf2.HasFocus); - - Application.TopRunnable.Dispose (); - } - - [Fact] - public void CanTabOutOfTableViewUsingCursor_Left_ClearsSelectionFirst () - { - GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); - - // Make the selected cell one in - tableView.SelectedColumn = 1; - - // Pressing shift-left should give us a multi selection - Application.RaiseKeyDownEvent (Key.CursorLeft.WithShift); - Assert.Same (tableView, Application.TopRunnable!.MostFocused); - Assert.True (tableView.HasFocus); - Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); - - // Because we are now on the leftmost cell a further left press would normally move focus - // However there is an ongoing selection so instead the operation clears the selection and - // gets swallowed (not resulting in a focus change) - Application.RaiseKeyDownEvent (Key.CursorLeft); - - // Selection 'clears' just to the single cell and we remain focused - Assert.Single (tableView.GetAllSelectedCells ()); - Assert.Same (tableView, Application.TopRunnable.MostFocused); - Assert.True (tableView.HasFocus); - - // A further left will switch focus - Application.RaiseKeyDownEvent (Key.CursorLeft); - - Assert.NotSame (tableView, Application.TopRunnable.MostFocused); - Assert.False (tableView.HasFocus); - - Assert.Same (tf1, Application.TopRunnable.MostFocused); - Assert.True (tf1.HasFocus); - - Application.TopRunnable.Dispose (); - } [Theory] [InlineData (true, 0, 1)] @@ -3406,37 +3271,6 @@ A B C Assert.Equal (expectedRow, tableView.CollectionNavigator.GetNextMatchingItem (0, "3".ToCharArray () [0])); } - /// - /// Creates 3 views on with the focus in the - /// . This is a helper method to setup tests that want to - /// explore moving input focus out of a tableview. - /// - /// - /// - /// - private void GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2) - { - tableView = new (); - tableView.BeginInit (); - tableView.EndInit (); - - - Application.TopRunnable = new (); - tf1 = new (); - tf2 = new (); - Application.TopRunnable.Add (tf1); - Application.TopRunnable.Add (tableView); - Application.TopRunnable.Add (tf2); - - tableView.SetFocus (); - - Assert.Same (tableView, Application.TopRunnable.MostFocused); - Assert.True (tableView.HasFocus); - - // Set big table - tableView.Table = BuildTable (25, 50); - } - private TableView GetABCDEFTableView (out DataTable dt) { var tableView = new TableView () @@ -3446,7 +3280,7 @@ A B C tableView.BeginInit (); tableView.EndInit (); - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visible tableView.Viewport = new (0, 0, 7, 5); @@ -3474,7 +3308,7 @@ A B C var tv = new TableView () { App = ApplicationImpl.Instance, - SchemeName = "TopLevel", + SchemeName = "Runnable", Viewport = new (0, 0, 25, 6) }; @@ -3506,7 +3340,7 @@ A B C { App = ApplicationImpl.Instance }; - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; // 3 columns are visible tableView.Viewport = new (0, 0, 7, 5); diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 057ec6b02..1207a9ae0 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -15,7 +15,7 @@ public class TextFieldTests (ITestOutputHelper output) [TextFieldTestsAutoInitShutdown] public void CanFocus_False_Wont_Focus_With_Mouse () { - Toplevel top = new (); + Runnable top = new (); var tf = new TextField { Width = Dim.Fill (), CanFocus = false, ReadOnly = true, Text = "some text" }; var fv = new FrameView @@ -84,7 +84,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre (expectedRender, output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -105,7 +105,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.Draw (); DriverAssert.AssertDriverContentsAre ("Misérables", output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Theory (Skip = "Broke with ContextMenuv2")] @@ -133,7 +133,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre (content, output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -158,7 +158,7 @@ public class TextFieldTests (ITestOutputHelper output) tf.SetClipToScreen (); tf.Draw (); DriverAssert.AssertDriverContentsAre ("Enter txt", output); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -188,7 +188,7 @@ public class TextFieldTests (ITestOutputHelper output) // All characters in "Enter text" should have the caption attribute DriverAssert.AssertDriverAttributesAre ("0000000000", output, Application.Driver, captionAttr); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -222,7 +222,7 @@ public class TextFieldTests (ITestOutputHelper output) // F is underlined (index 1), remaining characters use normal caption attribute (index 0) DriverAssert.AssertDriverAttributesAre ("1000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -256,7 +256,7 @@ public class TextFieldTests (ITestOutputHelper output) // "Enter " (6 chars) + "T" (underlined) + "ext" (3 chars) DriverAssert.AssertDriverAttributesAre ("0000001000", output, Application.Driver, captionAttr, hotkeyAttr); - Application.TopRunnable.Dispose (); + Application.TopRunnableView.Dispose (); } [Fact] @@ -452,7 +452,7 @@ public class TextFieldTests (ITestOutputHelper output) oldText = tf.Text; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tf); Application.Begin (top); @@ -836,7 +836,7 @@ public class TextFieldTests (ITestOutputHelper output) // Proves #3022 is fixed (TextField selected text does not show in v2) _textField.CursorPosition = 0; - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textField); SessionToken rs = Application.Begin (top); @@ -919,7 +919,7 @@ public class TextFieldTests (ITestOutputHelper output) var clickCounter = 0; tf.MouseClick += (s, m) => { clickCounter++; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tf); Application.Begin (top); @@ -1661,7 +1661,7 @@ Les Miśerables", var tf = new TextField { Width = 10 }; var tf2 = new TextField { Y = 1, Width = 10 }; - Toplevel top = new (); + Runnable top = new (); top.Add (tf); top.Add (tf2); diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index c11227c35..ece72d13d 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -60,7 +60,7 @@ public class TextViewTests [TextViewTestsSetupFakeApplication] public void BackTab_Test_Follow_By_Tab () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -109,7 +109,7 @@ public class TextViewTests Assert.Equal (leftCol, _textView.LeftColumn); } - Application.TopRunnable.Remove (_textView); + Application.TopRunnableView.Remove (_textView); Application.RequestStop (); } } @@ -118,7 +118,7 @@ public class TextViewTests [TextViewTestsSetupFakeApplication] public void CanFocus_False_Wont_Focus_With_Mouse () { - Toplevel top = new (); + Runnable top = new (); var tv = new TextView { Width = Dim.Fill (), CanFocus = false, ReadOnly = true, Text = "some text" }; var fv = new FrameView @@ -200,7 +200,7 @@ public class TextViewTests Assert.Equal (expectedCol, e.Col); }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); Assert.Equal (1, eventcount); @@ -266,7 +266,7 @@ public class TextViewTests Assert.Equal ("abc", tv.Text); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); Assert.Equal (1, eventcount); // for Initialize @@ -296,7 +296,7 @@ public class TextViewTests Assert.Equal (expectedCol, e.Col); }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); Assert.Equal (1, eventcount); // for Initialize @@ -649,7 +649,7 @@ public class TextViewTests const string text = "This is the first line.\nThis is the second line.\n"; var tv = new TextView { Width = Dim.Fill (), Height = Dim.Fill (), Text = text }; string envText = tv.Text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -723,7 +723,7 @@ This is the second line. const string text = "This is the first line.\nThis is the second line.\n"; var tv = new TextView { Width = Dim.Fill (), Height = Dim.Fill (), Text = text, WordWrap = true }; string envText = tv.Text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -798,7 +798,7 @@ This is the second line. const string text = "This is the first line.\nThis is the second line.\n"; var tv = new TextView { Width = Dim.Fill (), Height = Dim.Fill (), Text = text }; string envText = tv.Text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -871,7 +871,7 @@ This is the second line. const string text = "This is the first line.\nThis is the second line.\n"; var tv = new TextView { Width = Dim.Fill (), Height = Dim.Fill (), Text = text, WordWrap = true }; string envText = tv.Text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -952,7 +952,7 @@ This is the second line. var tv = new TextView { Width = 10, Height = 10 }; tv.Text = text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -1005,7 +1005,7 @@ This is the second line. var tv = new TextView { Width = 10, Height = 10 }; tv.Text = text; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); @@ -1461,7 +1461,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -1679,7 +1679,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -1813,7 +1813,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -2049,7 +2049,7 @@ This is the second line. var text = $"This is the first line.{Environment.NewLine}This is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -2218,7 +2218,7 @@ This is the second line. { var text = "One\nTwo\nThree"; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -2282,7 +2282,7 @@ This is the second line. { var text = "One\nTwo\nThree\n"; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -2345,7 +2345,7 @@ This is the second line. public void HistoryText_Undo_Redo_Multi_Line_Selected_With_Empty_Text () { var tv = new TextView { Width = 10, Height = 2 }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -2701,7 +2701,7 @@ This is the second line. public void HistoryText_Undo_Redo_Multi_Line_With_Empty_Text () { var tv = new TextView { Width = 10, Height = 2 }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3077,7 +3077,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3135,7 +3135,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3193,7 +3193,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3247,7 +3247,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3317,7 +3317,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3387,7 +3387,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -3453,7 +3453,7 @@ This is the second line. { var text = "This is the first line.\nThis is the second line.\nThis is the third line."; var tv = new TextView { Width = 10, Height = 2, Text = text }; - Toplevel top = new (); + Runnable top = new (); top.Add (tv); Application.Begin (top); @@ -4559,7 +4559,7 @@ This is the second line. } } - [Fact (Skip = "Fake Clipboard is broken")] + [Fact] [TextViewTestsSetupFakeApplication] public void Kill_To_End_Delete_Forwards_Copy_To_The_Clipboard_And_Paste () { @@ -4621,7 +4621,7 @@ This is the second line. } } - [Fact (Skip = "FakeClipboard is broken in some way, causing this unit test to fail intermittently.")] + [Fact] [TextViewTestsSetupFakeApplication] public void Kill_To_Start_Delete_Backwards_Copy_To_The_Clipboard_And_Paste () { @@ -4858,7 +4858,7 @@ This is the second line. public void Selected_Text_Shows () { // Proves #3022 is fixed (TextField selected text does not show in v2) - Toplevel top = new (); + Runnable top = new (); top.Add (_textView); SessionToken rs = Application.Begin (top); @@ -4945,7 +4945,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void Tab_Test_Follow_By_BackTab () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -4993,7 +4993,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void Tab_Test_Follow_By_BackTab_With_Text () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -5041,7 +5041,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -5098,7 +5098,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight_With_Text () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -5157,7 +5157,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void Tab_Test_Follow_By_Home_And_Then_Follow_By_End_And_Then_Follow_By_BackTab_With_Text () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Iteration += OnApplicationOnIteration; @@ -5232,7 +5232,7 @@ This is the second line. [TextViewTestsSetupFakeApplication] public void TabWidth_Setting_To_Zero_Keeps_AllowsTab () { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Begin (top); @@ -5329,7 +5329,7 @@ TAB to jump between text field", var win = new Window (); win.Add (tv); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (15, 15); @@ -5407,7 +5407,7 @@ TAB to jump between text field", var win = new Window (); win.Add (tv); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); Application.Begin (top); Application.Driver!.SetScreenSize (15, 15); @@ -5492,7 +5492,7 @@ TAB to jump between text field", Width = Dim.Fill (), Height = Dim.Fill (), Text = "This is the first line.\nThis is the second line.\n" }; tv.UnwrappedCursorPosition += (s, e) => { cp = e; }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -5524,7 +5524,7 @@ This is the second line. ); Application.Driver!.SetScreenSize (6, 25); - tv.SetRelativeLayout (Application.Screen.Size); + Application.LayoutAndDraw (); tv.Draw (); Assert.Equal (new (4, 2), tv.CursorPosition); Assert.Equal (new (12, 0), cp); @@ -6697,7 +6697,7 @@ line. public void WordWrap_Deleting_Backwards () { var tv = new TextView { Width = 5, Height = 2, WordWrap = true, Text = "aaaa" }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -6775,7 +6775,7 @@ a [InlineData (KeyCode.Delete)] public void WordWrap_Draw_Typed_Keys_After_Text_Is_Deleted (KeyCode del) { - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); _textView.Text = "Line 1.\nLine 2."; _textView.WordWrap = true; @@ -6850,7 +6850,7 @@ Line 2.", ); tv.WordWrap = true; - var top = new Toplevel () + var top = new Runnable () { Driver = ApplicationImpl.Instance.Driver, }; @@ -6939,7 +6939,7 @@ line. ); tv.WordWrap = true; - var top = new Toplevel () + var top = new Runnable () { Driver = ApplicationImpl.Instance.Driver, }; @@ -7104,7 +7104,7 @@ line. tv.Text = $"{Cell.ToString (text [0])}\n{Cell.ToString (text [1])}\n"; Assert.False (tv.WordWrap); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -7157,7 +7157,7 @@ line. ", TextView tv = CreateTextView (); tv.Load (cells); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); SessionToken rs = Application.Begin (top); SetupFakeApplicationAttribute.RunIteration (); @@ -7165,7 +7165,7 @@ line. ", Assert.True (tv.InheritsPreviousAttribute); var expectedText = @" -TopLevel +Runnable Base Dialog Menu @@ -7175,7 +7175,7 @@ Error "; Attribute [] attributes = { // 0 - SchemeManager.GetSchemes () ["TopLevel"].Normal, + SchemeManager.GetSchemes () ["Runnable"].Normal, // 1 SchemeManager.GetSchemes () ["Base"].Normal, @@ -7209,7 +7209,7 @@ Error "; tv.CursorPosition = new (6, 2); tv.SelectionStartColumn = 0; tv.SelectionStartRow = 0; - Assert.Equal ($"TopLevel{Environment.NewLine}Base{Environment.NewLine}Dialog", tv.SelectedText); + Assert.Equal ($"Runnable{Environment.NewLine}Base{Environment.NewLine}Dialog", tv.SelectedText); tv.Copy (); tv.IsSelecting = false; tv.CursorPosition = new (2, 4); @@ -7217,11 +7217,11 @@ Error "; SetupFakeApplicationAttribute.RunIteration (); expectedText = @" -TopLevel +Runnable Base Dialog Menu -ErTopLevel +ErRunnable Base Dialogror "; DriverAssert.AssertDriverContentsWithFrameAre (expectedText, _output); @@ -7242,7 +7242,7 @@ Dialogror "; tv.SelectionStartRow = 0; Assert.Equal ( - $"TopLevel{Environment.NewLine}Base{Environment.NewLine}Dialog{Environment.NewLine}", + $"Runnable{Environment.NewLine}Base{Environment.NewLine}Dialog{Environment.NewLine}", tv.SelectedText ); tv.Copy (); @@ -7252,11 +7252,11 @@ Dialogror "; SetupFakeApplicationAttribute.RunIteration (); expectedText = @" -TopLevel +Runnable Base Dialog Menu -ErTopLevel +ErRunnable Base Dialog ror "; @@ -7282,7 +7282,7 @@ ror "; public void IsSelecting_False_If_SelectedLength_Is_Zero_On_Mouse_Click () { _textView.Text = "This is the first line."; - var top = new Toplevel (); + var top = new Runnable (); top.Add (_textView); Application.Begin (top); diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs deleted file mode 100644 index 2de19854d..000000000 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ /dev/null @@ -1,694 +0,0 @@ -namespace UnitTests.ViewsTests; - -public class ToplevelTests -{ - [Fact] - public void Constructor_Default () - { - var top = new Toplevel (); - - Assert.Equal ("Toplevel", top.SchemeName); - Assert.Equal ("Fill(Absolute(0))", top.Width.ToString ()); - Assert.Equal ("Fill(Absolute(0))", top.Height.ToString ()); - Assert.False (top.Running); - Assert.False (top.Modal); - - //Assert.Null (top.StatusBar); - } - - [Fact] - public void Arrangement_Default_Is_Overlapped () - { - var top = new Toplevel (); - Assert.Equal (ViewArrangement.Overlapped, top.Arrangement); - } - - [Fact] - [AutoInitShutdown] - public void Internal_Tests () - { - var top = new Toplevel (); - - var eventInvoked = ""; - - top.Loaded += (s, e) => eventInvoked = "Loaded"; - top.OnLoaded (); - Assert.Equal ("Loaded", eventInvoked); - top.Ready += (s, e) => eventInvoked = "Ready"; - top.OnReady (); - Assert.Equal ("Ready", eventInvoked); - top.Unloaded += (s, e) => eventInvoked = "Unloaded"; - top.OnUnloaded (); - Assert.Equal ("Unloaded", eventInvoked); - - Application.Begin (top); - Assert.Equal (top, Application.TopRunnable); - - // Application.TopRunnable without menu and status bar. - View supView = View.GetLocationEnsuringFullVisibility (top, 2, 2, out int nx, out int ny /*, out StatusBar sb*/); - Assert.Equal (Application.TopRunnable, supView); - Assert.Equal (0, nx); - Assert.Equal (0, ny); - // Application.Current with a menu and without status bar. - View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - Assert.Equal (0, ny); - // Application.TopRunnable with a menu and status bar. - View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - - // Application.TopRunnable without a menu and with a status bar. - View.GetLocationEnsuringFullVisibility (top, 2, 2, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - - - var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; - top.Add (win); - top.LayoutSubViews (); - - // The SuperView is always the same regardless of the caller. - supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.TopRunnable, supView); - supView = View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (Application.TopRunnable, supView); - - // Application.TopRunnable without menu and status bar. - View.GetLocationEnsuringFullVisibility (win, 0, 0, out nx, out ny /*, out sb*/); - Assert.Equal (0, nx); - Assert.Equal (0, ny); - top.Remove (win); - - win = new () { Width = 60, Height = 15 }; - top.Add (win); - } - - [Fact] - public void SuperViewChanged_Should_Not_Be_Used_To_Initialize_Toplevel_Events () - { - var wasAdded = false; - - var view = new View (); - view.SuperViewChanged += SuperViewChanged; - - var win = new Window (); - win.Add (view); - Application.Init ("fake"); - Toplevel top = new (); - top.Add (win); - - Assert.True (wasAdded); - - Application.Shutdown (); - - return; - - void SuperViewChanged (object sender, SuperViewChangedEventArgs _) - { - Assert.False (wasAdded); - wasAdded = true; - view.SuperViewChanged -= SuperViewChanged; - } - } - - [Fact] - [AutoInitShutdown] - public void Mouse_Drag_On_Top_With_Superview_Null () - { - var win = new Window (); - Toplevel top = new (); - top.Add (win); - int iterations = -1; - Window testWindow; - - Application.Iteration += OnApplicationOnIteration; - - Application.Run (top); - Application.Iteration -= OnApplicationOnIteration; - top.Dispose (); - - return; - - void OnApplicationOnIteration (object s, EventArgs a) - { - iterations++; - - if (iterations == 0) - { - Application.Driver?.SetScreenSize (15, 7); - - // Don't use MessageBox here; it's too complicated for this unit test; just use Window - testWindow = new () - { - Text = "Hello", - X = 2, - Y = 2, - Width = 10, - Height = 3, - Arrangement = ViewArrangement.Movable - }; - Application.Run (testWindow); - } - else if (iterations == 1) - { - Assert.Equal (new (2, 2), Application.TopRunnable!.Frame.Location); - } - else if (iterations == 2) - { - Assert.Null (Application.Mouse.MouseGrabView); - - // Grab the mouse - Application.RaiseMouseEvent (new () { ScreenPosition = new (3, 2), Flags = MouseFlags.Button1Pressed }); - - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (2, 2, 10, 3), Application.TopRunnable.Frame); - } - else if (iterations == 3) - { - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - - // Drag to left - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (2, 2), - Flags = MouseFlags.Button1Pressed - | MouseFlags.ReportMousePosition - }); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2, 10, 3), Application.TopRunnable.Frame); - } - else if (iterations == 4) - { - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 2), Application.TopRunnable.Frame.Location); - - Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); - } - else if (iterations == 5) - { - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - - // Drag up - Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.TopRunnable.Frame); - } - else if (iterations == 6) - { - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1), Application.TopRunnable.Frame.Location); - - Assert.Equal (Application.TopRunnable.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), Application.TopRunnable.Frame); - } - else if (iterations == 7) - { - Assert.Equal (Application.TopRunnable!.Border, Application.Mouse.MouseGrabView); - - // Ungrab the mouse - Application.RaiseMouseEvent (new () { ScreenPosition = new (2, 1), Flags = MouseFlags.Button1Released }); - AutoInitShutdownAttribute.RunIteration (); - - Assert.Null (Application.Mouse.MouseGrabView); - } - else if (iterations == 8) - { - Application.RequestStop (); - } - else if (iterations == 9) - { - Application.RequestStop (); - } - } - } - - [Fact] - [AutoInitShutdown] - public void Mouse_Drag_On_Top_With_Superview_Not_Null () - { - var win = new Window { X = 3, Y = 2, Width = 10, Height = 5, Arrangement = ViewArrangement.Movable }; - Toplevel top = new (); - top.Add (win); - - int iterations = -1; - - var movex = 0; - var movey = 0; - - var location = new Rectangle (win.Frame.X, win.Frame.Y, 7, 3); - - Application.Iteration += OnApplicationOnIteration; - - Application.Run (top); - Application.Iteration -= OnApplicationOnIteration; - top.Dispose (); - - return; - - void OnApplicationOnIteration (object s, EventArgs a) - { - iterations++; - - if (iterations == 0) - { - Application.Driver?.SetScreenSize (30, 10); - } - else if (iterations == 1) - { - location = win.Frame; - - Assert.Null (Application.Mouse.MouseGrabView); - - // Grab the mouse - Application.RaiseMouseEvent (new () { ScreenPosition = new (win.Frame.X, win.Frame.Y), Flags = MouseFlags.Button1Pressed }); - - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - } - else if (iterations == 2) - { - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - - // Drag to left - movex = 1; - movey = 0; - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (win.Frame.X + movex, win.Frame.Y + movey), - Flags = MouseFlags.Button1Pressed - | MouseFlags.ReportMousePosition - }); - - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - } - else if (iterations == 3) - { - // we should have moved +1, +0 - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - location.Offset (movex, movey); - } - else if (iterations == 4) - { - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - - // Drag up - movex = 0; - movey = -1; - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (win.Frame.X + movex, win.Frame.Y + movey), - Flags = MouseFlags.Button1Pressed - | MouseFlags.ReportMousePosition - }); - - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - } - else if (iterations == 5) - { - // we should have moved +0, -1 - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - location.Offset (movex, movey); - Assert.Equal (location, win.Frame); - } - else if (iterations == 6) - { - Assert.Equal (win.Border, Application.Mouse.MouseGrabView); - - // Ungrab the mouse - movex = 0; - movey = 0; - - Application.RaiseMouseEvent (new () { ScreenPosition = new (win.Frame.X + movex, win.Frame.Y + movey), Flags = MouseFlags.Button1Released }); - - Assert.Null (Application.Mouse.MouseGrabView); - } - else if (iterations == 7) - { - Application.RequestStop (); - } - } - } - - [Fact] - [SetupFakeApplication] - public void GetLocationThatFits_With_Border_Null_Not_Throws () - { - var top = new Toplevel (); - top.BeginInit (); - top.EndInit (); - - Exception exception = Record.Exception (() => Application.Driver!.SetScreenSize (0, 10)); - Assert.Null (exception); - - exception = Record.Exception (() => Application.Driver!.SetScreenSize (10, 0)); - Assert.Null (exception); - } - - [Fact] - [AutoInitShutdown] - public void PositionCursor_SetCursorVisibility_To_Invisible_If_Focused_Is_Null () - { - var tf = new TextField { Width = 5, Text = "test" }; - var view = new View { Width = 10, Height = 10, CanFocus = true }; - view.Add (tf); - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Assert.True (tf.HasFocus); - Application.PositionCursor (); - Application.Driver!.GetCursorVisibility (out CursorVisibility cursor); - Assert.Equal (CursorVisibility.Default, cursor); - - view.Enabled = false; - Assert.False (tf.HasFocus); - Application.PositionCursor (); - Application.Driver!.GetCursorVisibility (out cursor); - Assert.Equal (CursorVisibility.Invisible, cursor); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void IsLoaded_Application_Begin () - { - Toplevel top = new (); - Assert.False (top.IsLoaded); - - Application.Begin (top); - Assert.True (top.IsLoaded); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void IsLoaded_With_Sub_Toplevel_Application_Begin_NeedDisplay () - { - Toplevel top = new (); - var subTop = new Toplevel (); - var view = new View { Frame = new (0, 0, 20, 10) }; - subTop.Add (view); - top.Add (subTop); - - Assert.False (top.IsLoaded); - Assert.False (subTop.IsLoaded); - Assert.Equal (new (0, 0, 20, 10), view.Frame); - - view.SubViewLayout += ViewLayoutStarted; - - void ViewLayoutStarted (object sender, LayoutEventArgs e) - { - Assert.Equal (new (0, 0, 20, 10), view.NeedsDrawRect); - view.SubViewLayout -= ViewLayoutStarted; - } - - Application.Begin (top); - - Assert.True (top.IsLoaded); - Assert.True (subTop.IsLoaded); - Assert.Equal (new (0, 0, 20, 10), view.Frame); - - view.Frame = new (1, 3, 10, 5); - Assert.Equal (new (1, 3, 10, 5), view.Frame); - Assert.Equal (new (0, 0, 10, 5), view.NeedsDrawRect); - - view.Frame = new (1, 3, 10, 5); - top.Layout (); - Assert.Equal (new (1, 3, 10, 5), view.Frame); - Assert.Equal (new (0, 0, 10, 5), view.NeedsDrawRect); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Window_Viewport_Bigger_Than_Driver_Cols_And_Rows_Allow_Drag_Beyond_Left_Right_And_Bottom () - { - Toplevel top = new (); - var window = new Window { Width = 20, Height = 3, Arrangement = ViewArrangement.Movable }; - SessionToken rsTop = Application.Begin (top); - Application.Driver?.SetScreenSize (40, 10); - - SessionToken rsWindow = Application.Begin (window); - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 40, 10), top.Frame); - Assert.Equal (new (0, 0, 20, 3), window.Frame); - - Assert.Null (Application.Mouse.MouseGrabView); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - - Assert.Equal (window.Border, Application.Mouse.MouseGrabView); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (-11, -4), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 40, 10), top.Frame); - Assert.Equal (new (-11, -4, 20, 3), window.Frame); - - // Changes Top size to same size as Dialog more menu and scroll bar - Application.Driver?.SetScreenSize (20, 3); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (-1, -1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 20, 3), top.Frame); - Assert.Equal (new (-1, -1, 20, 3), window.Frame); - - // Changes Top size smaller than Dialog size - Application.Driver?.SetScreenSize (19, 2); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (-1, -1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 19, 2), top.Frame); - Assert.Equal (new (-1, -1, 20, 3), window.Frame); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (18, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 19, 2), top.Frame); - Assert.Equal (new (18, 1, 20, 3), window.Frame); - - // On a real app we can't go beyond the SuperView bounds - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (19, 2), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (new (0, 0, 19, 2), top.Frame); - Assert.Equal (new (19, 2, 20, 3), window.Frame); - - //DriverAsserts.AssertDriverContentsWithFrameAre (@"", output); - - Application.End (rsWindow); - Application.End (rsTop); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Modal_As_Top_Will_Drag_Cleanly () - { - // Don't use Dialog as a Top, use a Window instead - dialog has complex layout behavior that is not needed here. - var window = new Window { Width = 10, Height = 3, Arrangement = ViewArrangement.Movable }; - - window.Add ( - new Label - { - X = Pos.Center (), - Y = Pos.Center (), - Width = Dim.Fill (), - Height = Dim.Fill (), - TextAlignment = Alignment.Center, - VerticalTextAlignment = Alignment.Center, - Text = "Test" - } - ); - - SessionToken rs = Application.Begin (window); - - Assert.Null (Application.Mouse.MouseGrabView); - Assert.Equal (new (0, 0, 10, 3), window.Frame); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (window.Border, Application.Mouse.MouseGrabView); - - Assert.Equal (new (0, 0, 10, 3), window.Frame); - - Application.RaiseMouseEvent ( - new () - { - ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition - }); - - AutoInitShutdownAttribute.RunIteration (); - Assert.Equal (window.Border, Application.Mouse.MouseGrabView); - Assert.Equal (new (1, 1, 10, 3), window.Frame); - - Application.End (rs); - window.Dispose (); - } - - [Fact] - public void Multi_Thread_Toplevels () - { - Application.Init ("fake"); - - Toplevel t = new (); - var w = new Window (); - t.Add (w); - - int count = 0, count1 = 0, count2 = 0; - bool log = false, log1 = false, log2 = false; - var fromTopStillKnowFirstIsRunning = false; - var fromTopStillKnowSecondIsRunning = false; - var fromFirstStillKnowSecondIsRunning = false; - - Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), - () => - { - count++; - - if (count1 == 5) - { - log1 = true; - } - - if (count1 == 14 && count2 == 10 && count == 15) - { - // count2 is already stopped - fromTopStillKnowFirstIsRunning = true; - } - - if (count1 == 7 && count2 == 7 && count == 8) - { - fromTopStillKnowSecondIsRunning = true; - } - - if (count == 30) - { - Assert.Equal (30, count); - Assert.Equal (20, count1); - Assert.Equal (10, count2); - - Assert.True (log); - Assert.True (log1); - Assert.True (log2); - - Assert.True (fromTopStillKnowFirstIsRunning); - Assert.True (fromTopStillKnowSecondIsRunning); - Assert.True (fromFirstStillKnowSecondIsRunning); - - Application.RequestStop (); - - return false; - } - - return true; - } - ); - - t.Ready += FirstWindow; - - void FirstWindow (object sender, EventArgs args) - { - var firstWindow = new Window (); - firstWindow.Ready += SecondWindow; - - Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), - () => - { - count1++; - - if (count2 == 5) - { - log2 = true; - } - - if (count2 == 4 && count1 == 5 && count == 5) - { - fromFirstStillKnowSecondIsRunning = true; - } - - if (count1 == 20) - { - Assert.Equal (20, count1); - Application.RequestStop (); - - return false; - } - - return true; - } - ); - - Application.Run (firstWindow); - firstWindow.Dispose (); - } - - void SecondWindow (object sender, EventArgs args) - { - var testWindow = new Window (); - - Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), - () => - { - count2++; - - if (count < 30) - { - log = true; - } - - if (count2 == 10) - { - Assert.Equal (10, count2); - Application.RequestStop (); - - return false; - } - - return true; - } - ); - - Application.Run (testWindow); - testWindow.Dispose (); - } - - Application.Run (t); - t.Dispose (); - Application.Shutdown (); - } -} diff --git a/Tests/UnitTests/Views/TreeTableSourceTests.cs b/Tests/UnitTests/Views/TreeTableSourceTests.cs index 0f5fd42c2..e8b884c7e 100644 --- a/Tests/UnitTests/Views/TreeTableSourceTests.cs +++ b/Tests/UnitTests/Views/TreeTableSourceTests.cs @@ -159,7 +159,7 @@ public class TreeTableSourceTests : IDisposable [AutoInitShutdown] public void TestTreeTableSource_CombinedWithCheckboxes () { - Toplevel top = new (); + Runnable top = new (); TableView tv = GetTreeTable (out TreeView treeSource); CheckBoxTableSourceWrapperByIndex checkSource; @@ -243,7 +243,7 @@ public class TreeTableSourceTests : IDisposable { Driver = ApplicationImpl.Instance.Driver, }; - tableView.SchemeName = "TopLevel"; + tableView.SchemeName = "Runnable"; tableView.Viewport = new Rectangle (0, 0, 40, 6); tableView.Style.ShowHorizontalHeaderUnderline = true; @@ -297,7 +297,7 @@ public class TreeTableSourceTests : IDisposable tableView.EndInit (); tableView.LayoutSubViews (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tableView); top.SetFocus (); Assert.Equal (tableView, top.MostFocused); diff --git a/Tests/UnitTests/Views/TreeViewTests.cs b/Tests/UnitTests/Views/TreeViewTests.cs index 0e115751b..af95f268e 100644 --- a/Tests/UnitTests/Views/TreeViewTests.cs +++ b/Tests/UnitTests/Views/TreeViewTests.cs @@ -100,7 +100,7 @@ public class TreeViewTests (ITestOutputHelper output) tv.AddObject (n1); tv.AddObject (n2); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); Application.Begin (top); diff --git a/Tests/UnitTests/Views/ViewDisposalTest.cs b/Tests/UnitTests/Views/ViewDisposalTest.cs index dc2bc58ea..0d47d6a5a 100644 --- a/Tests/UnitTests/Views/ViewDisposalTest.cs +++ b/Tests/UnitTests/Views/ViewDisposalTest.cs @@ -34,7 +34,7 @@ public class ViewDisposalTest (ITestOutputHelper output) { GetSpecialParams (); var container = new View () { Id = "container" }; - Toplevel top = new () { Id = "top" }; + Runnable top = new () { Id = "top" }; List views = GetViews (); foreach (Type view in views) diff --git a/Tests/UnitTests/Views/WindowTests.cs b/Tests/UnitTests/Views/WindowTests.cs index 9ad07b547..26abfd270 100644 --- a/Tests/UnitTests/Views/WindowTests.cs +++ b/Tests/UnitTests/Views/WindowTests.cs @@ -14,7 +14,7 @@ public class WindowTests () Assert.NotNull (defaultWindow); Assert.Equal (string.Empty, defaultWindow.Title); - // Toplevels have Width/Height set to Dim.Fill + // Runnables have Width/Height set to Dim.Fill // If there's no SuperView, Top, or Driver, the default Fill width is int.MaxValue Assert.Equal ($"Window(){defaultWindow.Frame}", defaultWindow.ToString ()); diff --git a/Tests/UnitTests/Application/Application.NavigationTests.cs b/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs similarity index 50% rename from Tests/UnitTests/Application/Application.NavigationTests.cs rename to Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs index 29adca085..6d25610bd 100644 --- a/Tests/UnitTests/Application/Application.NavigationTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Application.NavigationTests.cs @@ -1,32 +1,31 @@ -using UnitTests; -using Xunit.Abstractions; +using Xunit.Abstractions; -namespace UnitTests.ApplicationTests; +namespace ApplicationTests; public class ApplicationNavigationTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; - - [AutoInitShutdown] [Theory] [InlineData (TabBehavior.NoStop)] [InlineData (TabBehavior.TabStop)] [InlineData (TabBehavior.TabGroup)] public void Begin_SetsFocus_On_Deepest_Focusable_View (TabBehavior behavior) { - var top = new Toplevel + using IApplication? application = Application.Create (); + + Runnable runnable = new() { - TabStop = behavior + TabStop = behavior, + CanFocus = true }; - Assert.False (top.HasFocus); View subView = new () { CanFocus = true, TabStop = behavior }; - top.Add (subView); + runnable.Add (subView); View subSubView = new () { @@ -35,59 +34,60 @@ public class ApplicationNavigationTests (ITestOutputHelper output) }; subView.Add (subSubView); - SessionToken rs = Application.Begin (top); - Assert.True (top.HasFocus); + SessionToken? rs = application.Begin (runnable); + Assert.True (runnable.HasFocus); Assert.True (subView.HasFocus); Assert.True (subSubView.HasFocus); - top.Dispose (); + runnable.Dispose (); } [Fact] - [AutoInitShutdown] public void Begin_SetsFocus_On_Top () { - var top = new Toplevel (); - Assert.False (top.HasFocus); + using IApplication? application = Application.Create (); - SessionToken rs = Application.Begin (top); - Assert.True (top.HasFocus); + Runnable runnable = new () { CanFocus = true }; + Assert.False (runnable.HasFocus); - top.Dispose (); + application.Begin (runnable); + Assert.True (runnable.HasFocus); + + runnable.Dispose (); } [Fact] public void Focused_Change_Raises_FocusedChanged () { + using IApplication? application = Application.Create (); + var raised = false; - Application.Navigation!.FocusedChanged += ApplicationNavigationOnFocusedChanged; + application.Navigation!.FocusedChanged += ApplicationNavigationOnFocusedChanged; - Application.Navigation.SetFocused (new () { CanFocus = true, HasFocus = true }); + application.Navigation.SetFocused (new () { CanFocus = true, HasFocus = true }); Assert.True (raised); - Application.Navigation.GetFocused ().Dispose (); - Application.Navigation.SetFocused (null); - - Application.Navigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged; + application.Navigation.FocusedChanged -= ApplicationNavigationOnFocusedChanged; return; - void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e) { raised = true; } + void ApplicationNavigationOnFocusedChanged (object? sender, EventArgs e) { raised = true; } } [Fact] public void GetFocused_Returns_Focused_View () { - IApplication app = ApplicationImpl.Instance; + using IApplication app = Application.Create (); - app.TopRunnable = new () - { - Id = "top", - CanFocus = true, - App = app - }; + app.Begin ( + new Runnable + { + Id = "top", + CanFocus = true, + App = app + }); var subView1 = new View { @@ -101,10 +101,10 @@ public class ApplicationNavigationTests (ITestOutputHelper output) CanFocus = true }; - app.TopRunnable?.Add (subView1, subView2); - Assert.False (app.TopRunnable?.HasFocus); + app.TopRunnableView?.Add (subView1, subView2); + subView1.SetFocus (); - app.TopRunnable?.SetFocus (); + //app.TopRunnableView?.SetFocus (); Assert.True (subView1.HasFocus); Assert.Equal (subView1, app.Navigation?.GetFocused ()); @@ -115,14 +115,15 @@ public class ApplicationNavigationTests (ITestOutputHelper output) [Fact] public void GetFocused_Returns_Null_If_No_Focused_View () { - IApplication app = ApplicationImpl.Instance; // Force legacy + using IApplication app = Application.Create (); - app.TopRunnable = new () - { - Id = "top", - CanFocus = true, - App = app - }; + app.Begin ( + new Runnable + { + Id = "top", + CanFocus = true, + App = app + }); var subView1 = new View { @@ -130,21 +131,19 @@ public class ApplicationNavigationTests (ITestOutputHelper output) CanFocus = true }; - app!.TopRunnable.Add (subView1); - Assert.False (app.TopRunnable.HasFocus); + app.TopRunnableView!.Add (subView1); - app.TopRunnable.SetFocus (); + app.TopRunnableView.SetFocus (); Assert.True (subView1.HasFocus); Assert.Equal (subView1, app.Navigation!.GetFocused ()); subView1.HasFocus = false; Assert.False (subView1.HasFocus); - Assert.True (app.TopRunnable.HasFocus); - Assert.Equal (app.TopRunnable, app.Navigation.GetFocused ()); + Assert.True (app.TopRunnableView.HasFocus); + Assert.Equal (app.TopRunnableView, app.Navigation.GetFocused ()); - app.TopRunnable.HasFocus = false; - Assert.False (app.TopRunnable.HasFocus); + app.TopRunnableView.HasFocus = false; + Assert.False (app.TopRunnableView.HasFocus); Assert.Null (app.Navigation.GetFocused ()); - } } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplBeginEndTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplBeginEndTests.cs new file mode 100644 index 000000000..c331e7a15 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplBeginEndTests.cs @@ -0,0 +1,407 @@ +using Xunit.Abstractions; + +namespace ApplicationTests; + +/// +/// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack. +/// These tests ensure the fragile state management logic is robust and catches regressions. +/// Tests work directly with ApplicationImpl instances to avoid global Application state issues. +/// +public class ApplicationImplBeginEndTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void Begin_WithNullRunnable_ThrowsArgumentNullException () + { + IApplication app = Application.Create (); + + try + { + Assert.Throws (() => app.Begin (null!)); + } + finally + { + app.Dispose (); + } + } + + [Fact] + public void Begin_SetsCurrent_WhenCurrentIsNull () + { + IApplication app = Application.Create (); + Runnable? runnable = null; + + try + { + runnable = new (); + Assert.Null (app.TopRunnableView); + + app.Begin (runnable); + + Assert.NotNull (app.TopRunnableView); + Assert.Same (runnable, app.TopRunnableView); + Assert.Single (app.SessionStack!); + } + finally + { + runnable?.Dispose (); + app.Dispose (); + } + } + + [Fact] + public void Begin_PushesToSessionStack () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + + app.Begin (runnable1); + Assert.Single (app.SessionStack!); + Assert.Same (runnable1, app.TopRunnableView); + + app.Begin (runnable2); + Assert.Equal (2, app.SessionStack!.Count); + Assert.Same (runnable2, app.TopRunnableView); + } + finally + { + runnable1?.Dispose (); + runnable2?.Dispose (); + app.Dispose (); + } + } + + [Fact] + public void End_WithNullSessionToken_ThrowsArgumentNullException () + { + IApplication app = Application.Create (); + + try + { + Assert.Throws (() => app.End (null!)); + } + finally + { + app.Dispose (); + } + } + + [Fact] + public void End_PopsSessionStack () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + + SessionToken token1 = app.Begin (runnable1)!; + SessionToken token2 = app.Begin (runnable2)!; + + Assert.Equal (2, app.SessionStack!.Count); + + app.End (token2); + + Assert.Single (app.SessionStack!); + Assert.Same (runnable1, app.TopRunnableView); + + app.End (token1); + + Assert.Empty (app.SessionStack!); + } + finally + { + runnable1?.Dispose (); + runnable2?.Dispose (); + app.Dispose (); + } + } + + [Fact (Skip = "This test may be bogus. What's wrong with ending a non-top session?")] + public void End_ThrowsArgumentException_WhenNotBalanced () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + + SessionToken? token1 = app.Begin (runnable1); + SessionToken? token2 = app.Begin (runnable2); + + // Trying to end token1 when token2 is on top should throw + // NOTE: This throws but has the side effect of popping token2 from the stack + Assert.Throws (() => app.End (token1!)); + + // Don't try to clean up with more End calls - the state is now inconsistent + // Let Shutdown/ResetState handle cleanup + } + finally + { + // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + runnable1?.Dispose (); + runnable2?.Dispose (); + + // Shutdown will call ResetState which clears any remaining state + app.Dispose (); + } + } + + [Fact] + public void End_RestoresCurrentToPreviousRunnable () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + Runnable? runnable3 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + runnable3 = new () { Id = "3" }; + + SessionToken? token1 = app.Begin (runnable1); + SessionToken? token2 = app.Begin (runnable2); + SessionToken? token3 = app.Begin (runnable3); + + Assert.Same (runnable3, app.TopRunnableView); + + app.End (token3!); + Assert.Same (runnable2, app.TopRunnableView); + + app.End (token2!); + Assert.Same (runnable1, app.TopRunnableView); + + app.End (token1!); + } + finally + { + runnable1?.Dispose (); + runnable2?.Dispose (); + runnable3?.Dispose (); + app.Dispose (); + } + } + + [Fact] + public void MultipleBeginEnd_MaintainsStackIntegrity () + { + IApplication app = Application.Create (); + List runnables = new (); + List tokens = new (); + + try + { + // Begin multiple runnables + for (var i = 0; i < 5; i++) + { + var runnable = new Runnable { Id = $"runnable-{i}" }; + runnables.Add (runnable); + SessionToken? token = app.Begin (runnable); + tokens.Add (token!); + } + + Assert.Equal (5, app.SessionStack!.Count); + Assert.Same (runnables [4], app.TopRunnableView); + + // End them in reverse order (LIFO) + for (var i = 4; i >= 0; i--) + { + app.End (tokens [i]); + + if (i > 0) + { + Assert.Equal (i, app.SessionStack.Count); + Assert.Same (runnables [i - 1], app.TopRunnableView); + } + else + { + Assert.Empty (app.SessionStack); + } + } + } + finally + { + foreach (Runnable runnable in runnables) + { + runnable.Dispose (); + } + + app.Dispose (); + } + } + + [Fact] + public void End_NullsSessionTokenRunnable () + { + IApplication app = Application.Create (); + Runnable? runnable = null; + + try + { + runnable = new (); + + SessionToken? token = app.Begin (runnable); + Assert.Same (runnable, token!.Runnable); + + app.End (token); + + Assert.Null (token.Runnable); + } + finally + { + runnable?.Dispose (); + app.Dispose (); + } + } + + [Fact] + public void ResetState_ClearsSessionStack () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + + app.Begin (runnable1); + app.Begin (runnable2); + + Assert.Equal (2, app.SessionStack!.Count); + Assert.NotNull (app.TopRunnableView); + } + finally + { + // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + runnable1?.Dispose (); + runnable2?.Dispose (); + + // Shutdown calls ResetState, which will clear SessionStack and set Current to null + app.Dispose (); + + // Verify cleanup happened + Assert.Empty (app.SessionStack!); + Assert.Null (app.TopRunnableView); + } + } + + [Fact] + public void ResetState_StopsAllRunningRunnables () + { + IApplication app = Application.Create (); + Runnable? runnable1 = null; + Runnable? runnable2 = null; + + try + { + runnable1 = new () { Id = "1" }; + runnable2 = new () { Id = "2" }; + + app.Begin (runnable1); + app.Begin (runnable2); + + Assert.True (runnable1.IsRunning); + Assert.True (runnable2.IsRunning); + } + finally + { + // Dispose runnables BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions + runnable1?.Dispose (); + runnable2?.Dispose (); + + // Shutdown calls ResetState, which will stop all running runnables + app.Dispose (); + + // Verify runnables were stopped + Assert.False (runnable1!.IsRunning); + Assert.False (runnable2!.IsRunning); + } + } + + //[Fact] + //public void Begin_ActivatesNewRunnable_WhenCurrentExists () + //{ + // IApplication app = Application.Create (); + // Runnable? runnable1 = null; + // Runnable? runnable2 = null; + + // try + // { + // runnable1 = new () { Id = "1" }; + // runnable2 = new () { Id = "2" }; + + // var runnable1Deactivated = false; + // var runnable2Activated = false; + + // runnable1.Deactivate += (s, e) => runnable1Deactivated = true; + // runnable2.Activate += (s, e) => runnable2Activated = true; + + // app.Begin (runnable1); + // app.Begin (runnable2); + + // Assert.True (runnable1Deactivated); + // Assert.True (runnable2Activated); + // Assert.Same (runnable2, app.TopRunnable); + // } + // finally + // { + // runnable1?.Dispose (); + // runnable2?.Dispose (); + // app.Dispose (); + // } + //} + + [Fact] + public void SessionStack_ContainsAllBegunRunnables () + { + IApplication app = Application.Create (); + List runnables = new (); + + try + { + for (var i = 0; i < 10; i++) + { + var runnable = new Runnable { Id = $"runnable-{i}" }; + runnables.Add (runnable); + app.Begin (runnable); + } + + // All runnables should be in the stack + Assert.Equal (10, app.SessionStack!.Count); + + // Verify stack contains all runnables + List stackList = app.SessionStack.ToList (); + + foreach (Runnable runnable in runnables) + { + Assert.Contains (runnable, stackList.Select (r => r.Runnable)); + } + } + finally + { + foreach (Runnable runnable in runnables) + { + runnable.Dispose (); + } + + app.Dispose (); + } + } +} diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs index dd59e2eb5..69478dbca 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs @@ -1,15 +1,14 @@ -#nullable enable -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Moq; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class ApplicationImplTests { /// /// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked. /// - private IApplication? NewMockedApplicationImpl () + private IApplication NewMockedApplicationImpl () { Mock netInput = new (); SetupRunInputMockMethodToBlock (netInput); @@ -45,66 +44,63 @@ public class ApplicationImplTests .Verifiable (Times.Once); } - [Fact] public void Init_CreatesKeybindings () { - IApplication? app = NewMockedApplicationImpl (); + IApplication app = NewMockedApplicationImpl (); - app?.Keyboard.KeyBindings.Clear (); + app.Keyboard.KeyBindings.Clear (); - Assert.Empty (app?.Keyboard?.KeyBindings.GetBindings ()!); + Assert.Empty (app.Keyboard.KeyBindings.GetBindings ()); - app?.Init ("fake"); + app.Init ("fake"); - Assert.NotEmpty (app?.Keyboard?.KeyBindings.GetBindings ()!); + Assert.NotEmpty (app.Keyboard.KeyBindings.GetBindings ()); - app?.Shutdown (); + app.Dispose (); } [Fact] public void NoInitThrowOnRun () { - IApplication? app = NewMockedApplicationImpl (); - var ex = Assert.Throws (() => app?.Run (new Window ())); - Assert.Equal ("Run cannot be accessed before Initialization", ex.Message); - app?.Shutdown (); + IApplication app = NewMockedApplicationImpl (); + var ex = Assert.Throws (() => app.Run (new Window ())); + app.Dispose (); } [Fact] public void InitRunShutdown_Top_Set_To_Null_After_Shutdown () { - IApplication? app = NewMockedApplicationImpl (); + IApplication app = NewMockedApplicationImpl (); - app?.Init ("fake"); + app.Init ("fake"); - object? timeoutToken = app?.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - if (app.TopRunnable is { }) - { - app.RequestStop (); + object? timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + if (app.TopRunnableView is { }) + { + app.RequestStop (); - return false; - } + return false; + } - return false; - } - ); - Assert.Null (app?.TopRunnable); + return false; + } + ); + Assert.Null (app.TopRunnableView); // Blocks until the timeout call is hit - app?.Run (new Window ()); + app.Run (new Window ()); // We returned false above, so we should not have to remove the timeout - Assert.False (app?.RemoveTimeout (timeoutToken!)); + Assert.False (app.RemoveTimeout (timeoutToken!)); - Assert.NotNull (app?.TopRunnable); - app.TopRunnable?.Dispose (); - app.Shutdown (); - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); + app.Dispose (); + Assert.Null (app.TopRunnableView); } [Fact] @@ -114,42 +110,42 @@ public class ApplicationImplTests app.Init ("fake"); - Toplevel top = new Window + IRunnable top = new Window { Title = "InitRunShutdown_Running_Set_To_False" }; - object timeoutToken = app.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object? timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.IsRunning); - if (app.TopRunnable != null) - { - app.RequestStop (); + if (app.TopRunnableView != null) + { + app.RequestStop (); - return false; - } + return false; + } - return false; - } - ); + return false; + } + ); - Assert.False (top!.Running); + Assert.False (top.IsRunning); // Blocks until the timeout call is hit app.Run (top); // We returned false above, so we should not have to remove the timeout - Assert.False (app.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken!)); - Assert.False (top!.Running); + Assert.False (top.IsRunning); // BUGBUG: Shutdown sets Top to null, not End. //Assert.Null (Application.TopRunnable); - app.TopRunnable?.Dispose (); - app.Shutdown (); + app.TopRunnableView?.Dispose (); + app.Dispose (); } [Fact] @@ -157,47 +153,45 @@ public class ApplicationImplTests { IApplication app = NewMockedApplicationImpl ()!; - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); Assert.Null (app.Driver); app.Init ("fake"); - Toplevel top = new Window (); - app.TopRunnable = top; + IRunnable top = new Window (); + var isIsModalChanged = 0; - var closedCount = 0; + top.IsModalChanged + += (_, a) => { isIsModalChanged++; }; - top.Closed - += (_, a) => { closedCount++; }; + var isRunningChangedCount = 0; - var unloadedCount = 0; + top.IsRunningChanged + += (_, a) => { isRunningChangedCount++; }; - top.Unloaded - += (_, a) => { unloadedCount++; }; + object? timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + //Assert.Fail (@"Didn't stop after first iteration."); - object timeoutToken = app.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.Fail (@"Didn't stop after first iteration."); + return false; + } + ); - return false; - } - ); - - Assert.Equal (0, closedCount); - Assert.Equal (0, unloadedCount); + Assert.Equal (0, isIsModalChanged); + Assert.Equal (0, isRunningChangedCount); app.StopAfterFirstIteration = true; app.Run (top); - Assert.Equal (1, closedCount); - Assert.Equal (1, unloadedCount); + Assert.Equal (2, isIsModalChanged); + Assert.Equal (2, isRunningChangedCount); - app.TopRunnable?.Dispose (); - app.Shutdown (); - Assert.Equal (1, closedCount); - Assert.Equal (1, unloadedCount); + app.TopRunnableView?.Dispose (); + app.Dispose (); + Assert.Equal (2, isIsModalChanged); + Assert.Equal (2, isRunningChangedCount); } [Fact] @@ -205,57 +199,56 @@ public class ApplicationImplTests { IApplication app = NewMockedApplicationImpl ()!; - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); Assert.Null (app.Driver); app.Init ("fake"); - Toplevel top = new Window (); + IRunnable top = new Window (); - // BUGBUG: Both Closed and Unloaded are called from End; what's the difference? - var closedCount = 0; + var isIsModalChanged = 0; - top.Closed - += (_, a) => { closedCount++; }; + top.IsModalChanged + += (_, a) => { isIsModalChanged++; }; - var unloadedCount = 0; + var isRunningChangedCount = 0; - top.Unloaded - += (_, a) => { unloadedCount++; }; + top.IsRunningChanged + += (_, a) => { isRunningChangedCount++; }; - object timeoutToken = app.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object? timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.IsRunning); - if (app.TopRunnable != null) - { - app.RequestStop (); + if (app.TopRunnableView != null) + { + app.RequestStop (); - return false; - } + return false; + } - return false; - } - ); + return false; + } + ); - Assert.Equal (0, closedCount); - Assert.Equal (0, unloadedCount); + Assert.Equal (0, isIsModalChanged); + Assert.Equal (0, isRunningChangedCount); // Blocks until the timeout call is hit app.Run (top); - Assert.Equal (1, closedCount); - Assert.Equal (1, unloadedCount); + Assert.Equal (2, isIsModalChanged); + Assert.Equal (2, isRunningChangedCount); // We returned false above, so we should not have to remove the timeout - Assert.False (app.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken!)); - app.TopRunnable?.Dispose (); - app.Shutdown (); - Assert.Equal (1, closedCount); - Assert.Equal (1, unloadedCount); + app.TopRunnableView?.Dispose (); + app.Dispose (); + Assert.Equal (2, isIsModalChanged); + Assert.Equal (2, isRunningChangedCount); } [Fact] @@ -265,40 +258,40 @@ public class ApplicationImplTests app.Init ("fake"); - Toplevel top = new Window + IRunnable top = new Window { Title = "InitRunShutdown_QuitKey_Quits" }; - object timeoutToken = app.AddTimeout ( - TimeSpan.FromMilliseconds (150), - () => - { - Assert.True (top!.Running); + object? timeoutToken = app.AddTimeout ( + TimeSpan.FromMilliseconds (150), + () => + { + Assert.True (top!.IsRunning); - if (app.TopRunnable != null) - { - app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey); - } + if (app.TopRunnableView != null) + { + app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey); + } - return false; - } - ); + return false; + } + ); - Assert.False (top!.Running); + Assert.False (top!.IsRunning); // Blocks until the timeout call is hit app.Run (top); // We returned false above, so we should not have to remove the timeout - Assert.False (app.RemoveTimeout (timeoutToken)); + Assert.False (app.RemoveTimeout (timeoutToken!)); - Assert.False (top!.Running); + Assert.False (top!.IsRunning); - Assert.NotNull (app.TopRunnable); - top.Dispose (); - app.Shutdown (); - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); + ((top as Window)!).Dispose (); + app.Dispose (); + Assert.Null (app.TopRunnableView); } [Fact] @@ -309,48 +302,68 @@ public class ApplicationImplTests app.Init ("fake"); app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); // Blocks until the timeout call is hit app.Run (); - Assert.NotNull (app.TopRunnable); - app.TopRunnable?.Dispose (); - app.Shutdown (); - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); + app.Dispose (); + Assert.Null (app.TopRunnableView); } [Fact] - public void Shutdown_Closing_Closed_Raised () + public void Run_IsRunningChanging_And_IsRunningChanged_Raised () { IApplication app = NewMockedApplicationImpl ()!; app.Init ("fake"); - var closing = 0; - var closed = 0; - var t = new Toplevel (); + var isRunningChanging = 0; + var isRunningChanged = 0; + Runnable t = new (); - t.Closing + t.IsRunningChanging + += (_, a) => { isRunningChanging++; }; + + t.IsRunningChanged + += (_, a) => { isRunningChanged++; }; + + app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); + + // Blocks until the timeout call is hit + app.Run (t); + + Assert.Equal (2, isRunningChanging); + Assert.Equal (2, isRunningChanged); + } + + [Fact] + public void Run_IsRunningChanging_Cancel_IsRunningChanged_Not_Raised () + { + IApplication app = NewMockedApplicationImpl ()!; + + app.Init ("fake"); + + var isRunningChanging = 0; + var isRunningChanged = 0; + Runnable t = new (); + + t.IsRunningChanging += (_, a) => { // Cancel the first time - if (closing == 0) + if (isRunningChanging == 0) { a.Cancel = true; } - closing++; - Assert.Same (t, a.RequestingTop); + isRunningChanging++; }; - t.Closed - += (_, a) => - { - closed++; - Assert.Same (t, a.Toplevel); - }; + t.IsRunningChanged + += (_, a) => { isRunningChanged++; }; app.AddTimeout (TimeSpan.Zero, () => IdleExit (app)); @@ -358,16 +371,13 @@ public class ApplicationImplTests app.Run (t); - app.TopRunnable?.Dispose (); - app.Shutdown (); - - Assert.Equal (2, closing); - Assert.Equal (1, closed); + Assert.Equal (1, isRunningChanging); + Assert.Equal (0, isRunningChanged); } private bool IdleExit (IApplication app) { - if (app.TopRunnable != null) + if (app.TopRunnableView != null) { app.RequestStop (); @@ -409,7 +419,7 @@ public class ApplicationImplTests () => { // Run asynchronous logic inside Task.Run - if (app.TopRunnable != null) + if (app.TopRunnableView != null) { b.NewKeyDownEvent (Key.Enter); b.NewKeyUpEvent (Key.Enter); @@ -418,7 +428,7 @@ public class ApplicationImplTests return false; }); - Assert.Null (app.TopRunnable); + Assert.Null (app.TopRunnableView); var w = new Window { @@ -429,10 +439,8 @@ public class ApplicationImplTests // Blocks until the timeout call is hit app.Run (w); - Assert.NotNull (app.TopRunnable); - app.TopRunnable?.Dispose (); - app.Shutdown (); - Assert.Null (app.TopRunnable); + w?.Dispose (); + app.Dispose (); Assert.True (result); } @@ -449,8 +457,8 @@ public class ApplicationImplTests //Assert.Null (v2.Popover); //Assert.Null (v2.Navigation); - Assert.Null (v2.TopRunnable); - Assert.Empty (v2.SessionStack); + Assert.Null (v2.TopRunnableView); + Assert.Empty (v2.SessionStack!); // Init should populate instance fields v2.Init ("fake"); @@ -460,17 +468,94 @@ public class ApplicationImplTests Assert.True (v2.Initialized); Assert.NotNull (v2.Popover); Assert.NotNull (v2.Navigation); - Assert.Null (v2.TopRunnable); // Top is still null until Run + Assert.Null (v2.TopRunnableView); // Top is still null until Run // Shutdown should clean up instance fields - v2.Shutdown (); + v2.Dispose (); Assert.Null (v2.Driver); Assert.False (v2.Initialized); //Assert.Null (v2.Popover); //Assert.Null (v2.Navigation); - Assert.Null (v2.TopRunnable); - Assert.Empty (v2.SessionStack); + Assert.Null (v2.TopRunnableView); + Assert.Empty (v2.SessionStack!); + } + + [Fact] + public void Init_Begin_End_Cleans_Up () + { + IApplication? app = Application.Create (); + + SessionToken? newSessionToken = null; + + EventHandler newSessionTokenFn = (s, e) => + { + Assert.NotNull (e.State); + newSessionToken = e.State; + }; + app.SessionBegun += newSessionTokenFn; + + Runnable runnable = new (); + SessionToken sessionToken = app.Begin (runnable)!; + Assert.NotNull (sessionToken); + Assert.NotNull (newSessionToken); + Assert.Equal (sessionToken, newSessionToken); + + // Assert.Equal (runnable, Application.TopRunnable); + + app.SessionBegun -= newSessionTokenFn; + app.End (newSessionToken); + + Assert.Null (app.TopRunnable); + Assert.Null (app.Driver); + + runnable.Dispose (); + } + + [Fact] + public void Run_RequestStop_Stops () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + var top = new Runnable (); + SessionToken? sessionToken = app.Begin (top); + Assert.NotNull (sessionToken); + + app.Iteration += OnApplicationOnIteration; + app.Run (top); + app.Iteration -= OnApplicationOnIteration; + + top.Dispose (); + + return; + + void OnApplicationOnIteration (object? s, EventArgs a) { app.RequestStop (); } + } + + [Fact] + public void Run_T_Init_Driver_Cleared_with_Runnable_Throws () + { + IApplication? app = Application.Create (); + + app.Init ("fake"); + app.Driver = null; + + app.StopAfterFirstIteration = true; + + // Init has been called, but Driver has been set to null. Bad. + Assert.Throws (() => app.Run ()); + } + + [Fact] + public void Init_Unbalanced_Throws () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + + Assert.Throws (() => + app.Init ("fake") + ); } } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationMouseEnterLeaveTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationMouseEnterLeaveTests.cs new file mode 100644 index 000000000..c03a1d477 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationMouseEnterLeaveTests.cs @@ -0,0 +1,454 @@ +#nullable enable +using System.ComponentModel; + +namespace ApplicationTests; + +[Trait ("Category", "Input")] +public class ApplicationMouseEnterLeaveTests +{ + private class TestView : View + { + public TestView () + { + X = 1; + Y = 1; + Width = 1; + Height = 1; + } + + // public bool CancelOnEnter { get; } + public int OnMouseEnterCalled { get; private set; } + public int OnMouseLeaveCalled { get; private set; } + + protected override bool OnMouseEnter (CancelEventArgs eventArgs) + { + OnMouseEnterCalled++; + // eventArgs.Cancel = CancelOnEnter; + + base.OnMouseEnter (eventArgs); + + return eventArgs.Cancel; + } + + protected override void OnMouseLeave () + { + OnMouseLeaveCalled++; + + base.OnMouseLeave (); + } + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_MouseEntersView_CallsOnMouseEnter () + { + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + + // Arrange + var view = new TestView (); + runnable.Add (view); + var mousePosition = new Point (1, 1); + List currentViewsUnderMouse = [view]; + + var mouseEvent = new MouseEventArgs + { + Position = mousePosition, + ScreenPosition = mousePosition + }; + + app.Mouse.CachedViewsUnderMouse.Clear (); + + try + { + // Act + app.Mouse.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); + + // Assert + Assert.Equal (1, view.OnMouseEnterCalled); + } + finally + { + // Cleanup + app.Mouse.ResetState (); + } + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_MouseLeavesView_CallsOnMouseLeave () + { + // Arrange + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + + var view = new TestView (); + runnable.Add (view); + var mousePosition = new Point (0, 0); + List currentViewsUnderMouse = new (); + var mouseEvent = new MouseEventArgs (); + + app.Mouse.CachedViewsUnderMouse.Clear (); + app.Mouse.CachedViewsUnderMouse.Add (view); + + // Act + app.Mouse.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); + + // Assert + Assert.Equal (0, view.OnMouseEnterCalled); + Assert.Equal (1, view.OnMouseLeaveCalled); + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenAdjacentViews_CallsOnMouseEnterAndLeave () + { + // Arrange + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + var view1 = new TestView (); // at 1,1 to 2,2 + + var view2 = new TestView () // at 2,2 to 3,3 + { + X = 2, + Y = 2 + }; + runnable.Add (view1); + runnable.Add (view2); + + app.Mouse.CachedViewsUnderMouse.Clear (); + + // Act + var mousePosition = new Point (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (0, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (1, 1); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (3, 3); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (1, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (1, view2.OnMouseLeaveCalled); + + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_NoViewsUnderMouse_DoesNotCallOnMouseEnterOrLeave () + { + // Arrange + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + var view = new TestView (); + runnable.Add (view); + var mousePosition = new Point (0, 0); + List currentViewsUnderMouse = new (); + var mouseEvent = new MouseEventArgs (); + + app.Mouse.CachedViewsUnderMouse.Clear (); + + // Act + app.Mouse.RaiseMouseEnterLeaveEvents (mousePosition, currentViewsUnderMouse); + + // Assert + Assert.Equal (0, view.OnMouseEnterCalled); + Assert.Equal (0, view.OnMouseLeaveCalled); + + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingPeerViews_CallsOnMouseEnterAndLeave () + { + // Arrange + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + + var view1 = new TestView + { + Width = 2 + }; // at 1,1 to 3,2 + + var view2 = new TestView () // at 2,2 to 4,3 + { + Width = 2, + X = 2, + Y = 2 + }; + runnable.Add (view1); + runnable.Add (view2); + + app.Mouse.CachedViewsUnderMouse.Clear (); + + // Act + var mousePosition = new Point (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (0, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (1, 1); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (0, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (3, 3); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (1, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, view2.OnMouseEnterCalled); + Assert.Equal (1, view2.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (2, view2.OnMouseEnterCalled); + Assert.Equal (1, view2.OnMouseLeaveCalled); + + } + + [Fact] + public void RaiseMouseEnterLeaveEvents_MouseMovesBetweenOverlappingSubViews_CallsOnMouseEnterAndLeave () + { + // Arrange + IApplication? app = Application.Create (); + Runnable runnable = new () { Frame = new (0, 0, 10, 10) }; + app.Begin (runnable); + + var view1 = new TestView + { + Id = "view1", + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 1,1 to 3,3 (screen) + + var subView = new TestView + { + Id = "subView", + Width = 2, + Height = 2, + X = 1, + Y = 1, + Arrangement = ViewArrangement.Overlapped + }; // at 2,2 to 4,4 (screen) + view1.Add (subView); + runnable.Add (view1); + + app.Mouse.CachedViewsUnderMouse.Clear (); + + Assert.Equal (1, view1.FrameToScreen ().X); + Assert.Equal (2, subView.FrameToScreen ().X); + + // Act + var mousePosition = new Point (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (0, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, subView.OnMouseEnterCalled); + Assert.Equal (0, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (1, 1); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (0, subView.OnMouseEnterCalled); + Assert.Equal (0, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (0, view1.OnMouseLeaveCalled); + Assert.Equal (1, subView.OnMouseEnterCalled); + Assert.Equal (0, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (1, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (1, subView.OnMouseEnterCalled); + Assert.Equal (1, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (2, view1.OnMouseEnterCalled); + Assert.Equal (1, view1.OnMouseLeaveCalled); + Assert.Equal (2, subView.OnMouseEnterCalled); + Assert.Equal (1, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (3, 3); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (2, view1.OnMouseEnterCalled); + Assert.Equal (2, view1.OnMouseLeaveCalled); + Assert.Equal (2, subView.OnMouseEnterCalled); + Assert.Equal (2, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (0, 0); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (2, view1.OnMouseEnterCalled); + Assert.Equal (2, view1.OnMouseLeaveCalled); + Assert.Equal (2, subView.OnMouseEnterCalled); + Assert.Equal (2, subView.OnMouseLeaveCalled); + + // Act + mousePosition = new (2, 2); + + app.Mouse.RaiseMouseEnterLeaveEvents ( + mousePosition, + runnable.GetViewsUnderLocation (mousePosition, ViewportSettingsFlags.TransparentMouse)); + + // Assert + Assert.Equal (3, view1.OnMouseEnterCalled); + Assert.Equal (2, view1.OnMouseLeaveCalled); + Assert.Equal (3, subView.OnMouseEnterCalled); + Assert.Equal (2, subView.OnMouseLeaveCalled); + + } +} diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs index 3ea1d796e..67cda869d 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -2,7 +2,7 @@ using Moq; using Terminal.Gui.App; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class ApplicationPopoverTests { @@ -191,6 +191,6 @@ public class ApplicationPopoverTests } /// - public Toplevel? Current { get; set; } + public IRunnable? Current { get; set; } } } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs new file mode 100644 index 000000000..a2608b703 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs @@ -0,0 +1,550 @@ +#nullable enable +using Xunit.Abstractions; + +namespace ApplicationTests; + +/// +/// Parallelizable tests for IApplication that don't require the main event loop. +/// Tests using the modern non-static IApplication API. +/// +public class ApplicationTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + [Fact] + public void AddTimeout_Fires () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + uint timeoutTime = 100; + var timeoutFired = false; + + // Setup a timeout that will fire + app.AddTimeout ( + TimeSpan.FromMilliseconds (timeoutTime), + () => + { + timeoutFired = true; + + // Return false so the timer does not repeat + return false; + } + ); + + // The timeout has not fired yet + Assert.False (timeoutFired); + + // Block the thread to prove the timeout does not fire on a background thread + Thread.Sleep ((int)timeoutTime * 2); + Assert.False (timeoutFired); + + app.StopAfterFirstIteration = true; + app.Run (); + + // The timeout should have fired + Assert.True (timeoutFired); + + app.Dispose (); + } + + [Fact] + public void Begin_Null_Runnable_Throws () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + // Test null Runnable + Assert.Throws (() => app.Begin (null!)); + + app.Dispose (); + } + + [Fact] + public void Begin_Sets_Application_Top_To_Console_Size () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + Assert.Null (app.TopRunnableView); + app.Driver!.SetScreenSize (80, 25); + Runnable top = new (); + SessionToken? token = app.Begin (top); + Assert.Equal (new (0, 0, 80, 25), app.TopRunnableView!.Frame); + app.Driver!.SetScreenSize (5, 5); + app.LayoutAndDraw (); + Assert.Equal (new (0, 0, 5, 5), app.TopRunnableView!.Frame); + + if (token is { }) + { + app.End (token); + } + top.Dispose (); + + app.Dispose (); + } + + [Fact] + public void Init_Null_Driver_Should_Pick_A_Driver () + { + IApplication app = Application.Create (); + app.Init (); + + Assert.NotNull (app.Driver); + + app.Dispose (); + } + + [Fact] + public void Init_Dispose_Cleans_Up () + { + IApplication app = Application.Create (); + + app.Init ("fake"); + + app.Dispose (); + +#if DEBUG_IDISPOSABLE + // Validate there are no outstanding Responder-based instances + // after cleanup + // Note: We can't check View.Instances in parallel tests as it's a static field + // that would be shared across parallel test runs +#endif + } + + [Fact] + public void Init_Dispose_Fire_InitializedChanged () + { + var initialized = false; + var Dispose = false; + + IApplication app = Application.Create (); + + app.InitializedChanged += OnApplicationOnInitializedChanged; + + app.Init (driverName: "fake"); + Assert.True (initialized); + Assert.False (Dispose); + + app.Dispose (); + Assert.True (initialized); + Assert.True (Dispose); + + app.InitializedChanged -= OnApplicationOnInitializedChanged; + + return; + + void OnApplicationOnInitializedChanged (object? s, EventArgs a) + { + if (a.Value) + { + initialized = true; + } + else + { + Dispose = true; + } + } + } + + [Fact] + public void Init_KeyBindings_Are_Not_Reset () + { + IApplication app = Application.Create (); + + // Set via Keyboard property (modern API) + app.Keyboard.QuitKey = Key.Q; + Assert.Equal (Key.Q, app.Keyboard.QuitKey); + + app.Init ("fake"); + + Assert.Equal (Key.Q, app.Keyboard.QuitKey); + + app.Dispose (); + } + + [Fact] + public void Init_NoParam_ForceDriver_Works () + { + using IApplication app = Application.Create (); + + app.ForceDriver = "fake"; + // Note: Init() without params picks up driver configuration + app.Init (); + + Assert.Equal ("fake", app.Driver!.GetName ()); + } + + [Fact] + public void Init_Dispose_Resets_Instance_Properties () + { + IApplication app = Application.Create (); + + // Init the app + app.Init (driverName: "fake"); + + // Verify initialized + Assert.True (app.Initialized); + Assert.NotNull (app.Driver); + + // Dispose cleans up + app.Dispose (); + + // Check reset state on the instance + CheckReset (app); + + // Create a new instance and set values + app = Application.Create (); + app.Init ("fake"); + + app.StopAfterFirstIteration = true; + app.Keyboard.PrevTabGroupKey = Key.A; + app.Keyboard.NextTabGroupKey = Key.B; + app.Keyboard.QuitKey = Key.C; + app.Keyboard.KeyBindings.Add (Key.D, Command.Cancel); + + app.Mouse.CachedViewsUnderMouse.Clear (); + app.Mouse.LastMousePosition = new Point (1, 1); + + // Dispose and check reset + app.Dispose (); + CheckReset (app); + + return; + + void CheckReset (IApplication application) + { + // Check that all fields and properties are reset on the instance + + // Public Properties + Assert.Null (application.TopRunnableView); + Assert.Null (application.Mouse.MouseGrabView); + Assert.Null (application.Driver); + Assert.False (application.StopAfterFirstIteration); + + // Internal properties + Assert.False (application.Initialized); + Assert.Null (application.MainThreadId); + Assert.Empty (application.Mouse.CachedViewsUnderMouse); + } + } + + [Fact] + public void Internal_Properties_Correct () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + Assert.True (app.Initialized); + Assert.Null (app.TopRunnableView); + SessionToken? rs = app.Begin (new Runnable ()); + Assert.Equal (app.TopRunnable, rs!.Runnable); + Assert.Null (app.Mouse.MouseGrabView); // public + + app.Dispose (); + } + + [Fact] + public void Invoke_Adds_Idle () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + Runnable top = new (); + SessionToken? rs = app.Begin (top); + + var actionCalled = 0; + app.Invoke ((_) => { actionCalled++; }); + app.TimedEvents!.RunTimers (); + Assert.Equal (1, actionCalled); + top.Dispose (); + + app.Dispose (); + } + + [Fact] + public void Run_Iteration_Fires () + { + var iteration = 0; + + IApplication app = Application.Create (); + app.Init ("fake"); + + app.Iteration += Application_Iteration; + app.Run (); + app.Iteration -= Application_Iteration; + + Assert.Equal (1, iteration); + app.Dispose (); + + return; + + void Application_Iteration (object? sender, EventArgs e) + { + if (iteration > 0) + { + Assert.Fail (); + } + + iteration++; + app.RequestStop (); + } + } + + [Fact] + public void Screen_Size_Changes () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + IDriver? driver = app.Driver; + + app.Driver!.SetScreenSize (80, 25); + + Assert.Equal (new (0, 0, 80, 25), driver!.Screen); + Assert.Equal (new (0, 0, 80, 25), app.Screen); + + // TODO: Should not be possible to manually change these at whim! + driver.Cols = 100; + driver.Rows = 30; + + app.Driver!.SetScreenSize (100, 30); + + Assert.Equal (new (0, 0, 100, 30), driver.Screen); + + app.Screen = new (0, 0, driver.Cols, driver.Rows); + Assert.Equal (new (0, 0, 100, 30), driver.Screen); + + app.Dispose (); + } + + [Fact] + public void Dispose_Alone_Does_Nothing () + { + IApplication app = Application.Create (); + app.Dispose (); + } + + #region RunTests + + [Fact] + public void Run_T_After_InitWithDriver_with_Runnable_and_Driver_Does_Not_Throw () + { + IApplication app = Application.Create (); + app.StopAfterFirstIteration = true; + + // Run> when already initialized or not with a Driver will not throw (because Window is derived from Runnable) + // Using another type not derived from Runnable will throws at compile time + app.Run (null, "fake"); + + // Run> when already initialized or not with a Driver will not throw (because Dialog is derived from Runnable) + app.Run (null, "fake"); + + app.Dispose (); + } + + [Fact] + public void Run_T_After_Init_Does_Not_Disposes_Application_Top () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + // Init doesn't create a Runnable and assigned it to app.TopRunnable + // but Begin does + var initTop = new Runnable (); + + app.Iteration += OnApplicationOnIteration; + + app.Run (); + app.Iteration -= OnApplicationOnIteration; + +#if DEBUG_IDISPOSABLE + Assert.False (initTop.WasDisposed); + initTop.Dispose (); + Assert.True (initTop.WasDisposed); +#endif + initTop.Dispose (); + + app.Dispose (); + + return; + + void OnApplicationOnIteration (object? s, EventArgs a) + { + Assert.NotEqual (initTop, app.TopRunnableView); +#if DEBUG_IDISPOSABLE + Assert.False (initTop.WasDisposed); +#endif + app.RequestStop (); + } + } + + [Fact] + public void Run_T_After_InitWithDriver_with_TestRunnable_DoesNotThrow () + { + IApplication app = Application.Create (); + app.Init ("fake"); + app.StopAfterFirstIteration = true; + + // Init has been called and we're passing no driver to Run. This is ok. + app.Run (); + + app.Dispose (); + } + + [Fact] + public void Run_T_After_InitNullDriver_with_TestRunnable_DoesNotThrow () + { + IApplication app = Application.Create (); + app.Init ("fake"); + app.StopAfterFirstIteration = true; + + // Init has been called, selecting FakeDriver; we're passing no driver to Run. Should be fine. + app.Run (); + + app.Dispose (); + } + + [Fact] + public void Run_T_NoInit_DoesNotThrow () + { + IApplication app = Application.Create (); + app.StopAfterFirstIteration = true; + + app.Run (); + + app.Dispose (); + } + + [Fact] + public void Run_T_NoInit_WithDriver_DoesNotThrow () + { + IApplication app = Application.Create (); + app.StopAfterFirstIteration = true; + + // Init has NOT been called and we're passing a valid driver to Run. This is ok. + app.Run (null, "fake"); + + app.Dispose (); + } + + [Fact] + public void Run_Sets_Running_True () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + var top = new Runnable (); + SessionToken? rs = app.Begin (top); + Assert.NotNull (rs); + + app.Iteration += OnApplicationOnIteration; + app.Run (top); + app.Iteration -= OnApplicationOnIteration; + + top.Dispose (); + + app.Dispose (); + + return; + + void OnApplicationOnIteration (object? s, EventArgs a) + { + Assert.True (top.IsRunning); + top.RequestStop (); + } + } + + [Fact] + public void Run_A_Modal_Runnable_Refresh_Background_On_Moving () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + // Don't use Dialog here as it has more layout logic. Use Window instead. + var w = new Window + { + Width = 5, Height = 5, + Arrangement = ViewArrangement.Movable + }; + app.Driver!.SetScreenSize (10, 10); + SessionToken? rs = app.Begin (w); + + // Don't use visuals to test as style of border can change over time. + Assert.Equal (new (0, 0), w.Frame.Location); + + app.Mouse.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + Assert.Equal (w.Border, app.Mouse.MouseGrabView); + Assert.Equal (new (0, 0), w.Frame.Location); + + // Move down and to the right. + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition }); + Assert.Equal (new (1, 1), w.Frame.Location); + + app.End (rs!); + w.Dispose (); + + app.Dispose (); + } + + [Fact] + public void Run_T_Creates_Top_Without_Init () + { + IApplication app = Application.Create (); + app.StopAfterFirstIteration = true; + + app.SessionEnded += OnApplicationOnSessionEnded; + + app.Run (null, "fake"); + + Assert.Null (app.TopRunnableView); + + app.Dispose (); + Assert.Null (app.TopRunnableView); + + return; + + void OnApplicationOnSessionEnded (object? sender, SessionTokenEventArgs e) + { + app.SessionEnded -= OnApplicationOnSessionEnded; + e.State.Result = (e.State.Runnable as IRunnable)?.Result; + } + } + + #endregion + + #region DisposeTests + + [Fact] + public async Task Dispose_Allows_Async () + { + var isCompletedSuccessfully = false; + + async Task TaskWithAsyncContinuation () + { + await Task.Yield (); + await Task.Yield (); + + isCompletedSuccessfully = true; + } + + IApplication app = Application.Create (); + app.Dispose (); + + Assert.False (isCompletedSuccessfully); + await TaskWithAsyncContinuation (); + Thread.Sleep (100); + Assert.True (isCompletedSuccessfully); + } + + [Fact] + public void Dispose_Resets_SyncContext () + { + IApplication app = Application.Create (); + app.Dispose (); + Assert.Null (SynchronizationContext.Current); + } + + #endregion + +} diff --git a/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs b/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs index 11e33fa44..2a6d038d1 100644 --- a/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs +++ b/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs @@ -2,7 +2,7 @@ using System; using Terminal.Gui.App; using Xunit; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class ResultEventArgsTests { diff --git a/Tests/UnitTestsParallelizable/Application/IApplicationScreenChangedTests.cs b/Tests/UnitTestsParallelizable/Application/IApplicationScreenChangedTests.cs new file mode 100644 index 000000000..112860cd0 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/IApplicationScreenChangedTests.cs @@ -0,0 +1,453 @@ +using Xunit.Abstractions; + +namespace ApplicationTests; + +/// +/// Parallelizable tests for IApplication.ScreenChanged event and Screen property. +/// Tests using the modern instance-based IApplication API. +/// +public class IApplicationScreenChangedTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + #region ScreenChanged Event Tests + + [Fact] + public void ScreenChanged_Event_Fires_When_Driver_Size_Changes () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventFired = false; + Rectangle? newScreen = null; + + EventHandler> handler = (sender, args) => + { + eventFired = true; + newScreen = args.Value; + }; + + app.ScreenChanged += handler; + + try + { + // Act + app.Driver!.SetScreenSize (100, 40); + + // Assert + Assert.True (eventFired); + Assert.NotNull (newScreen); + Assert.Equal (new (0, 0, 100, 40), newScreen.Value); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void ScreenChanged_Event_Updates_Application_Screen_Property () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + Rectangle initialScreen = app.Screen; + Assert.Equal (new (0, 0, 80, 25), initialScreen); + + // Act + app.Driver!.SetScreenSize (120, 50); + + // Assert + Assert.Equal (new (0, 0, 120, 50), app.Screen); + } + + [Fact] + public void ScreenChanged_Event_Sender_Is_IApplication () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + object? eventSender = null; + + EventHandler> handler = (sender, args) => { eventSender = sender; }; + + app.ScreenChanged += handler; + + try + { + // Act + app.Driver!.SetScreenSize (100, 30); + + // Assert + Assert.NotNull (eventSender); + Assert.IsAssignableFrom (eventSender); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void ScreenChanged_Event_Provides_Correct_Rectangle_In_EventArgs () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + Rectangle? capturedRectangle = null; + + EventHandler> handler = (sender, args) => { capturedRectangle = args.Value; }; + + app.ScreenChanged += handler; + + try + { + // Act + app.Driver!.SetScreenSize (200, 60); + + // Assert + Assert.NotNull (capturedRectangle); + Assert.Equal (0, capturedRectangle.Value.X); + Assert.Equal (0, capturedRectangle.Value.Y); + Assert.Equal (200, capturedRectangle.Value.Width); + Assert.Equal (60, capturedRectangle.Value.Height); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void ScreenChanged_Event_Fires_Multiple_Times_For_Multiple_Resizes () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventCount = 0; + List sizes = new (); + + EventHandler> handler = (sender, args) => + { + eventCount++; + sizes.Add (args.Value.Size); + }; + + app.ScreenChanged += handler; + + try + { + // Act + app.Driver!.SetScreenSize (100, 30); + app.Driver!.SetScreenSize (120, 40); + app.Driver!.SetScreenSize (80, 25); + + // Assert + Assert.Equal (3, eventCount); + Assert.Equal (3, sizes.Count); + Assert.Equal (new (100, 30), sizes [0]); + Assert.Equal (new (120, 40), sizes [1]); + Assert.Equal (new (80, 25), sizes [2]); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void ScreenChanged_Event_Does_Not_Fire_When_No_Resize_Occurs () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventFired = false; + + EventHandler> handler = (sender, args) => { eventFired = true; }; + + app.ScreenChanged += handler; + + try + { + // Act - Don't resize, just access Screen property + Rectangle screen = app.Screen; + + // Assert + Assert.False (eventFired); + Assert.Equal (new (0, 0, 80, 25), screen); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void ScreenChanged_Event_Can_Be_Unsubscribed () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventCount = 0; + + EventHandler> handler = (sender, args) => { eventCount++; }; + + app.ScreenChanged += handler; + + // Act - First resize should fire + app.Driver!.SetScreenSize (100, 30); + Assert.Equal (1, eventCount); + + // Unsubscribe + app.ScreenChanged -= handler; + + // Second resize should not fire + app.Driver!.SetScreenSize (120, 40); + + // Assert + Assert.Equal (1, eventCount); + } + + [Fact] + public void ScreenChanged_Event_Sets_Runnables_To_NeedsLayout () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + using var runnable = new Runnable (); + SessionToken? token = app.Begin (runnable); + + Assert.NotNull (app.TopRunnableView); + app.LayoutAndDraw (); + + // Clear the NeedsLayout flag + Assert.False (app.TopRunnableView.NeedsLayout); + + try + { + // Act + app.Driver!.SetScreenSize (100, 30); + + // Assert + Assert.True (app.TopRunnableView.NeedsLayout); + } + finally + { + // Cleanup + if (token is { }) + { + app.End (token); + } + } + } + + [Fact] + public void ScreenChanged_Event_Handles_Multiple_Runnables_In_Session_Stack () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + using var runnable1 = new Runnable (); + SessionToken? token1 = app.Begin (runnable1); + app.LayoutAndDraw (); + + using var runnable2 = new Runnable (); + SessionToken? token2 = app.Begin (runnable2); + app.LayoutAndDraw (); + + // Both should not need layout after drawing + Assert.False (runnable1.NeedsLayout); + Assert.False (runnable2.NeedsLayout); + + try + { + // Act - Resize should mark both as needing layout + app.Driver!.SetScreenSize (100, 30); + + // Assert + Assert.True (runnable1.NeedsLayout); + Assert.True (runnable2.NeedsLayout); + } + finally + { + // Cleanup + if (token2 is { }) + { + app.End (token2); + } + + if (token1 is { }) + { + app.End (token1); + } + } + } + + [Fact] + public void ScreenChanged_Event_With_No_Active_Runnables_Does_Not_Throw () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventFired = false; + + EventHandler> handler = (sender, args) => { eventFired = true; }; + + app.ScreenChanged += handler; + + try + { + // Act - Resize with no runnables + Exception? exception = Record.Exception (() => app.Driver!.SetScreenSize (100, 30)); + + // Assert + Assert.Null (exception); + Assert.True (eventFired); + } + finally + { + app.ScreenChanged -= handler; + } + } + + #endregion ScreenChanged Event Tests + + #region Screen Property Tests + + [Fact] + public void Screen_Property_Returns_Driver_Screen_When_Not_Set () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + // Act + Rectangle screen = app.Screen; + + // Assert + Assert.Equal (app.Driver!.Screen, screen); + Assert.Equal (new (0, 0, 80, 25), screen); + } + + [Fact] + public void Screen_Property_Returns_Default_Size_When_Driver_Not_Initialized () + { + // Arrange + using IApplication app = Application.Create (); + + // Act - Don't call Init + Rectangle screen = app.Screen; + + // Assert - Should return default size + Assert.Equal (new (0, 0, 2048, 2048), screen); + } + + [Fact] + public void Screen_Property_Throws_When_Setting_Non_Zero_Origin () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + // Act & Assert + var exception = Assert.Throws (() => + app.Screen = new (10, 10, 80, 25)); + + Assert.Contains ("Screen locations other than 0, 0", exception.Message); + } + + [Fact] + public void Screen_Property_Allows_Setting_With_Zero_Origin () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + // Act + Exception? exception = Record.Exception (() => + app.Screen = new (0, 0, 100, 50)); + + // Assert + Assert.Null (exception); + Assert.Equal (new (0, 0, 100, 50), app.Screen); + } + + [Fact] + public void Screen_Property_Setting_Does_Not_Fire_ScreenChanged_Event () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + var eventFired = false; + + EventHandler> handler = (sender, args) => { eventFired = true; }; + + app.ScreenChanged += handler; + + try + { + // Act - Manually set Screen property (not via driver resize) + app.Screen = new (0, 0, 100, 50); + + // Assert - Event should not fire for manual property setting + Assert.False (eventFired); + Assert.Equal (new (0, 0, 100, 50), app.Screen); + } + finally + { + app.ScreenChanged -= handler; + } + } + + [Fact] + public void Screen_Property_Thread_Safe_Access () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + List exceptions = new (); + List tasks = new (); + + // Act - Access Screen property from multiple threads + for (var i = 0; i < 10; i++) + { + tasks.Add ( + Task.Run (() => + { + try + { + Rectangle screen = app.Screen; + Assert.NotEqual (Rectangle.Empty, screen); + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add (ex); + } + } + })); + } + +#pragma warning disable xUnit1031 + Task.WaitAll (tasks.ToArray ()); +#pragma warning restore xUnit1031 + + // Assert - No exceptions should occur + Assert.Empty (exceptions); + } + + #endregion Screen Property Tests +} diff --git a/Tests/UnitTestsParallelizable/Application/KeyboardImplThreadSafetyTests.cs b/Tests/UnitTestsParallelizable/Application/KeyboardImplThreadSafetyTests.cs index 66f77dd84..4b74a2424 100644 --- a/Tests/UnitTestsParallelizable/Application/KeyboardImplThreadSafetyTests.cs +++ b/Tests/UnitTestsParallelizable/Application/KeyboardImplThreadSafetyTests.cs @@ -1,7 +1,7 @@ // ReSharper disable AccessToDisposedClosure #nullable enable -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Tests to verify that KeyboardImpl is thread-safe for concurrent access scenarios. @@ -102,7 +102,7 @@ public class KeyboardImplThreadSafetyTests // Assert Assert.Empty (exceptions); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -147,7 +147,7 @@ public class KeyboardImplThreadSafetyTests // Assert Assert.Empty (exceptions); keyboard.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -191,7 +191,7 @@ public class KeyboardImplThreadSafetyTests // Assert Assert.Empty (exceptions); keyboard.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -471,7 +471,7 @@ public class KeyboardImplThreadSafetyTests // Assert Assert.Empty (exceptions); keyboard.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -515,6 +515,6 @@ public class KeyboardImplThreadSafetyTests // Assert Assert.Empty (exceptions); keyboard.Dispose (); - app.Shutdown (); + app.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs b/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs index 45b094419..f275e96da 100644 --- a/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs +++ b/Tests/UnitTestsParallelizable/Application/KeyboardTests.cs @@ -1,7 +1,7 @@ #nullable enable using Terminal.Gui.App; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Parallelizable tests for keyboard handling. diff --git a/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs b/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs index 00d7b245d..fba071860 100644 --- a/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs +++ b/Tests/UnitTestsParallelizable/Application/LogarithmicTimeoutTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class LogarithmicTimeoutTests { diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs new file mode 100644 index 000000000..ea9ef3d12 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs @@ -0,0 +1,212 @@ +#nullable enable +using System.Collections.Concurrent; +using System.Diagnostics; +// ReSharper disable AccessToDisposedClosure +#pragma warning disable xUnit1031 + +namespace ApplicationTests; + +/// +/// Tests for to verify input loop lifecycle. +/// These tests ensure that the input thread starts, runs, and stops correctly when applications +/// are created, initialized, and disposed. +/// +public class MainLoopCoordinatorTests : IDisposable +{ + private readonly List _createdApps = new (); + + public void Dispose () + { + // Cleanup any apps that weren't disposed in tests + foreach (IApplication app in _createdApps) + { + try + { + app.Dispose (); + } + catch + { + // Ignore cleanup errors + } + } + + _createdApps.Clear (); + } + + private IApplication CreateApp () + { + IApplication app = Application.Create (); + _createdApps.Add (app); + + return app; + } + + /// + /// Verifies that Dispose() stops the input loop when using Application.Create(). + /// This is the key test that proves the input thread respects cancellation. + /// + [Fact] + public void Application_Dispose_Stops_Input_Loop () + { + // Arrange + IApplication app = CreateApp (); + app.Init ("fake"); + + // The input thread should now be running + Assert.NotNull (app.Driver); + Assert.True (app.Initialized); + + // Act - Dispose the application + var sw = Stopwatch.StartNew (); + app.Dispose (); + sw.Stop (); + + // Assert - Dispose should complete quickly (within 1 second) + // If the input thread doesn't stop, this will hang and the test will timeout + Assert.True (sw.ElapsedMilliseconds < 1000, $"Dispose() took {sw.ElapsedMilliseconds}ms - input thread may not have stopped"); + + // Verify the application is properly disposed + Assert.Null (app.Driver); + Assert.False (app.Initialized); + + _createdApps.Remove (app); + } + + /// + /// Verifies that calling Dispose() multiple times doesn't cause issues. + /// + [Fact] + public void Dispose_Called_Multiple_Times_Does_Not_Throw () + { + // Arrange + IApplication app = CreateApp (); + app.Init ("fake"); + + // Act - Call Dispose() multiple times + Exception? exception = Record.Exception (() => + { + app.Dispose (); + app.Dispose (); + app.Dispose (); + }); + + // Assert - Should not throw + Assert.Null (exception); + + _createdApps.Remove (app); + } + + /// + /// Verifies that multiple applications can be created and disposed without thread leaks. + /// This simulates the ColorPicker test scenario where multiple ApplicationImpl instances + /// are created in parallel tests and must all be properly cleaned up. + /// + [Fact] + public void Multiple_Applications_Dispose_Without_Thread_Leaks () + { + const int COUNT = 5; + IApplication [] apps = new IApplication [COUNT]; + + // Arrange - Create multiple applications (simulating parallel test scenario) + for (var i = 0; i < COUNT; i++) + { + apps [i] = Application.Create (); + apps [i].Init ("fake"); + } + + // Act - Dispose all applications + var sw = Stopwatch.StartNew (); + + for (var i = 0; i < COUNT; i++) + { + apps [i].Dispose (); + } + + sw.Stop (); + + // Assert - All disposals should complete quickly + // If input threads don't stop, this will hang or take a very long time + Assert.True (sw.ElapsedMilliseconds < 5000, $"Disposing {COUNT} apps took {sw.ElapsedMilliseconds}ms - input threads may not have stopped"); + } + + /// + /// Verifies that the 20ms throttle limits the input loop poll rate to prevent CPU spinning. + /// This test proves throttling exists by verifying the poll rate is bounded (not millions of calls). + /// The test uses an upper bound approach to avoid timing sensitivity issues during parallel execution. + /// + [Fact (Skip = "Can't get this to run reliably.")] + public void InputLoop_Throttle_Limits_Poll_Rate () + { + // Arrange - Create a FakeInput and manually run it with throttling + FakeInput input = new FakeInput (); + ConcurrentQueue queue = new ConcurrentQueue (); + input.Initialize (queue); + + CancellationTokenSource cts = new CancellationTokenSource (); + + // Act - Run the input loop for 500ms + // Short duration reduces test time while still proving throttle exists + Task inputTask = Task.Run (() => input.Run (cts.Token), cts.Token); + + Thread.Sleep (500); + + int peekCount = input.PeekCallCount; + cts.Cancel (); + + // Wait for task to complete + bool completed = inputTask.Wait (TimeSpan.FromSeconds (10)); + Assert.True (completed, "Input task did not complete within timeout"); + + // Assert - The key insight: throttle prevents CPU spinning + // With 20ms throttle: ~25 calls in 500ms (but can be much less under load) + // WITHOUT throttle: Would be 10,000+ calls minimum (tight spin loop) + // + // We use an upper bound test: verify it's NOT spinning wildly + // This is much more reliable than testing exact timing under parallel load + // + // Max 500 calls = average 1ms between polls (still proves 20ms throttle exists) + // Without throttle = millions of calls (tight loop) + Assert.True (peekCount < 500, $"Poll count {peekCount} suggests no throttling (expected <500 with 20ms throttle)"); + + // Also verify the thread actually ran (not immediately cancelled) + Assert.True (peekCount > 0, $"Poll count was {peekCount} - thread may not have started"); + + input.Dispose (); + } + + /// + /// Verifies that the 20ms throttle prevents CPU spinning even with many leaked applications. + /// Before the throttle fix, 10+ leaked apps would saturate the CPU with tight spin loops. + /// + [Fact] + public void Throttle_Prevents_CPU_Saturation_With_Leaked_Apps () + { + const int COUNT = 10; + IApplication [] apps = new IApplication [COUNT]; + + // Arrange - Create multiple applications WITHOUT disposing them (simulating the leak) + for (var i = 0; i < COUNT; i++) + { + apps [i] = Application.Create (); + apps [i].Init ("fake"); + } + + // Let them run for a moment + Thread.Sleep (100); + + // Act - Now dispose them all and measure how long it takes + var sw = Stopwatch.StartNew (); + + for (var i = 0; i < COUNT; i++) + { + apps [i].Dispose (); + } + + sw.Stop (); + + // Assert - Even with 10 leaked apps, disposal should be fast + // Before the throttle fix, this would take many seconds due to CPU saturation + // With the throttle, each thread does Task.Delay(20ms) and exits within ~20-40ms + Assert.True (sw.ElapsedMilliseconds < 2000, $"Disposing {COUNT} apps took {sw.ElapsedMilliseconds}ms - CPU may be saturated"); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs index 22421f90b..1ebda8b62 100644 --- a/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MouseInterfaceTests.cs @@ -2,7 +2,7 @@ using Terminal.Gui.App; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Parallelizable tests for IMouse interface. diff --git a/Tests/UnitTestsParallelizable/Application/MouseTests.cs b/Tests/UnitTestsParallelizable/Application/MouseTests.cs index d5dba4dd4..71dbd6b09 100644 --- a/Tests/UnitTestsParallelizable/Application/MouseTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MouseTests.cs @@ -1,16 +1,13 @@ -using Terminal.Gui.App; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Tests for the interface and implementation. /// These tests demonstrate the decoupled mouse handling that enables parallel test execution. /// -public class MouseTests (ITestOutputHelper output) +public class MouseTests { - private readonly ITestOutputHelper _output = output; - [Fact] public void Mouse_Instance_CreatedSuccessfully () { @@ -76,7 +73,7 @@ public class MouseTests (ITestOutputHelper output) // Assert - CachedViewsUnderMouse should be cleared Assert.Empty (mouse.CachedViewsUnderMouse); - + // Event handlers should be cleared MouseEventArgs mouseEvent = new () { ScreenPosition = new Point (0, 0), Flags = MouseFlags.Button1Pressed }; mouse.RaiseMouseEvent (mouseEvent); @@ -122,4 +119,103 @@ public class MouseTests (ITestOutputHelper output) // Assert - Event count unchanged Assert.Equal (1, eventCount); } + + + /// + /// Tests that the mouse coordinates passed to the focused view are correct when the mouse is clicked. With + /// Frames; Frame != Viewport + /// + [Theory] + + // click on border + [InlineData (0, 0, 0, 0, 0, false)] + [InlineData (0, 1, 0, 0, 0, false)] + [InlineData (0, 0, 1, 0, 0, false)] + [InlineData (0, 9, 0, 0, 0, false)] + [InlineData (0, 0, 9, 0, 0, false)] + + // outside border + [InlineData (0, 10, 0, 0, 0, false)] + [InlineData (0, 0, 10, 0, 0, false)] + + // view is offset from origin ; click is on border + [InlineData (1, 1, 1, 0, 0, false)] + [InlineData (1, 2, 1, 0, 0, false)] + [InlineData (1, 1, 2, 0, 0, false)] + [InlineData (1, 10, 1, 0, 0, false)] + [InlineData (1, 1, 10, 0, 0, false)] + + // outside border + [InlineData (1, -1, 0, 0, 0, false)] + [InlineData (1, 0, -1, 0, 0, false)] + [InlineData (1, 10, 10, 0, 0, false)] + [InlineData (1, 11, 11, 0, 0, false)] + + // view is at origin, click is inside border + [InlineData (0, 1, 1, 0, 0, true)] + [InlineData (0, 2, 1, 1, 0, true)] + [InlineData (0, 1, 2, 0, 1, true)] + [InlineData (0, 8, 1, 7, 0, true)] + [InlineData (0, 1, 8, 0, 7, true)] + [InlineData (0, 8, 8, 7, 7, true)] + + // view is offset from origin ; click inside border + // our view is 10x10, but has a border, so it's bounds is 8x8 + [InlineData (1, 2, 2, 0, 0, true)] + [InlineData (1, 3, 2, 1, 0, true)] + [InlineData (1, 2, 3, 0, 1, true)] + [InlineData (1, 9, 2, 7, 0, true)] + [InlineData (1, 2, 9, 0, 7, true)] + [InlineData (1, 9, 9, 7, 7, true)] + [InlineData (1, 10, 10, 7, 7, false)] + + //01234567890123456789 + // |12345678| + // |xxxxxxxx + public void MouseCoordinatesTest_Border ( + int offset, + int clickX, + int clickY, + int expectedX, + int expectedY, + bool expectedClicked + ) + { + Size size = new (10, 10); + Point pos = new (offset, offset); + + var clicked = false; + + using IApplication? application = Application.Create (); + + application.Begin (new Window () + { + Id = "top", + }); + application.TopRunnableView!.X = 0; + application.TopRunnableView.Y = 0; + application.TopRunnableView.Width = size.Width * 2; + application.TopRunnableView.Height = size.Height * 2; + application.TopRunnableView.BorderStyle = LineStyle.None; + + var view = new View { Id = "view", X = pos.X, Y = pos.Y, Width = size.Width, Height = size.Height }; + + // Give the view a border. With PR #2920, mouse clicks are only passed if they are inside the view's Viewport. + view.BorderStyle = LineStyle.Single; + view.CanFocus = true; + + application.TopRunnableView.Add (view); + + var mouseEvent = new MouseEventArgs { Position = new (clickX, clickY), ScreenPosition = new (clickX, clickY), Flags = MouseFlags.Button1Clicked }; + + view.MouseClick += (s, e) => + { + Assert.Equal (expectedX, e.Position.X); + Assert.Equal (expectedY, e.Position.Y); + clicked = true; + }; + + application.Mouse.RaiseMouseEvent (mouseEvent); + Assert.Equal (expectedClicked, clicked); + } } diff --git a/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs b/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs index 7d2d27d50..0e1917303 100644 --- a/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/PopoverBaseImplTests.cs @@ -2,7 +2,7 @@ using System; using Terminal.Gui; using Terminal.Gui.App; using Xunit; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class PopoverBaseImplTests { @@ -23,10 +23,10 @@ public class PopoverBaseImplTests } [Fact] - public void Toplevel_Property_CanBeSetAndGet () + public void Runnable_Property_CanBeSetAndGet () { var popover = new TestPopover (); - var top = new Toplevel (); + var top = new Runnable (); popover.Current = top; Assert.Same (top, popover.Current); } diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs index 2c0671c36..6fcbd47c5 100644 --- a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableEdgeCasesTests.cs @@ -1,7 +1,7 @@ #nullable enable using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; +namespace ApplicationTests; /// /// Tests for edge cases and error conditions in IRunnable implementation. @@ -9,31 +9,7 @@ namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; public class RunnableEdgeCasesTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; - - [Fact] - public void RunnableSessionToken_CannotDisposeWithRunnableSet () - { - // Arrange - Runnable runnable = new (); - RunnableSessionToken token = new (runnable); - - // Act & Assert - var ex = Assert.Throws (() => token.Dispose ()); - Assert.Contains ("Runnable must be null", ex.Message); - } - - [Fact] - public void RunnableSessionToken_CanDisposeAfterClearingRunnable () - { - // Arrange - Runnable runnable = new (); - RunnableSessionToken token = new (runnable); - token.Runnable = null; - - // Act & Assert - Should not throw - token.Dispose (); - } - + [Fact] public void Runnable_MultipleEventSubscribers_AllInvoked () { @@ -187,13 +163,11 @@ public class RunnableEdgeCasesTests (ITestOutputHelper output) // Act bool canceledRunning = runnable.RaiseIsRunningChanging (false, true); runnable.RaiseIsRunningChangedEvent (true); - bool canceledModal = runnable.RaiseIsModalChanging (false, true); runnable.RaiseIsModalChangedEvent (true); // Assert Assert.True (runnable.OnIsRunningChangingCalled); Assert.True (runnable.OnIsRunningChangedCalled); - Assert.True (runnable.OnIsModalChangingCalled); Assert.True (runnable.OnIsModalChangedCalled); } @@ -219,7 +193,7 @@ public class RunnableEdgeCasesTests (ITestOutputHelper output) Runnable runnable = new (); // Act - RunnableSessionToken token = new (runnable); + SessionToken token = new (runnable); // Assert Assert.NotNull (token.Runnable); @@ -296,7 +270,6 @@ public class RunnableEdgeCasesTests (ITestOutputHelper output) { public bool OnIsRunningChangingCalled { get; private set; } public bool OnIsRunningChangedCalled { get; private set; } - public bool OnIsModalChangingCalled { get; private set; } public bool OnIsModalChangedCalled { get; private set; } protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) @@ -312,13 +285,6 @@ public class RunnableEdgeCasesTests (ITestOutputHelper output) base.OnIsRunningChanged (newIsRunning); } - protected override bool OnIsModalChanging (bool oldIsModal, bool newIsModal) - { - OnIsModalChangingCalled = true; - - return base.OnIsModalChanging (oldIsModal, newIsModal); - } - protected override void OnIsModalChanged (bool newIsModal) { OnIsModalChangedCalled = true; diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs index 3aa75af6e..e38a77cb1 100644 --- a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableIntegrationTests.cs @@ -1,7 +1,7 @@ #nullable enable using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Integration tests for IApplication's IRunnable support. @@ -14,7 +14,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID public void Dispose () { - _app?.Shutdown (); + _app?.Dispose (); _app = null; } @@ -24,19 +24,19 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - int stackCountBefore = app.RunnableSessionStack?.Count ?? 0; + int stackCountBefore = app.SessionStack?.Count ?? 0; // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.NotNull (token); Assert.NotNull (token.Runnable); Assert.Same (runnable, token.Runnable); - Assert.Equal (stackCountBefore + 1, app.RunnableSessionStack?.Count ?? 0); + Assert.Equal (stackCountBefore + 1, app.SessionStack?.Count ?? 0); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -47,13 +47,13 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID CancelableRunnable runnable = new () { CancelStart = true }; // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert - Should not be added to stack if canceled Assert.False (runnable.IsRunning); - // Token is still created but runnable not added to stack - Assert.NotNull (token); + // Token not created + Assert.Null (token); } [Fact] @@ -72,43 +72,14 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID }; // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.True (isModalChangedRaised); Assert.True (receivedValue); // Cleanup - app.End (token); - } - - [Fact] - public void Begin_RaisesIsModalChangingEvent () - { - // Arrange - IApplication app = GetApp (); - Runnable runnable = new (); - var isModalChangingRaised = false; - bool? oldValue = null; - bool? newValue = null; - - runnable.IsModalChanging += (s, e) => - { - isModalChangingRaised = true; - oldValue = e.CurrentValue; - newValue = e.NewValue; - }; - - // Act - RunnableSessionToken token = app.Begin (runnable); - - // Assert - Assert.True (isModalChangingRaised); - Assert.False (oldValue); - Assert.True (newValue); - - // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -127,14 +98,14 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID }; // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.True (isRunningChangedRaised); Assert.True (receivedValue); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -155,7 +126,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID }; // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.True (isRunningChangingRaised); @@ -163,7 +134,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Assert.True (newValue); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -174,13 +145,13 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Runnable runnable = new (); // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.True (runnable.IsModal); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -191,13 +162,13 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Runnable runnable = new (); // Act - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Assert Assert.True (runnable.IsRunning); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -216,18 +187,18 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); CancelableRunnable runnable = new () { CancelStop = true }; - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); runnable.CancelStop = true; // Enable cancellation // Act - app.End (token); + app.End (token!); // Assert - Should still be running if canceled Assert.True (runnable.IsRunning); // Force end by disabling cancellation runnable.CancelStop = false; - app.End (token); + app.End (token!); } [Fact] @@ -236,13 +207,13 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Act - app.End (token); + app.End (token!); // Assert - Assert.Null (token.Runnable); + Assert.Null (token!.Runnable); } [Fact] @@ -251,7 +222,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); var isRunningChangedRaised = false; bool? receivedValue = null; @@ -262,7 +233,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID }; // Act - app.End (token); + app.End (token!); // Assert Assert.True (isRunningChangedRaised); @@ -275,7 +246,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); var isRunningChangingRaised = false; bool? oldValue = null; bool? newValue = null; @@ -288,7 +259,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID }; // Act - app.End (token); + app.End (token!); // Assert Assert.True (isRunningChangingRaised); @@ -302,14 +273,14 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); - int stackCountBefore = app.RunnableSessionStack?.Count ?? 0; + SessionToken? token = app.Begin (runnable); + int stackCountBefore = app.SessionStack?.Count ?? 0; // Act - app.End (token); + app.End (token!); // Assert - Assert.Equal (stackCountBefore - 1, app.RunnableSessionStack?.Count ?? 0); + Assert.Equal (stackCountBefore - 1, app.SessionStack?.Count ?? 0); } [Fact] @@ -318,10 +289,10 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Act - app.End (token); + app.End (token!); // Assert Assert.False (runnable.IsModal); @@ -333,10 +304,10 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); Runnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Act - app.End (token); + app.End (token!); // Assert Assert.False (runnable.IsRunning); @@ -349,7 +320,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID IApplication app = GetApp (); // Act & Assert - Assert.Throws (() => app.End ((RunnableSessionToken)null!)); + Assert.Throws (() => app.End ((SessionToken)null!)); } [Fact] @@ -378,8 +349,8 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Runnable runnable2 = new () { Id = "2" }; // Act - RunnableSessionToken token1 = app.Begin (runnable1); - RunnableSessionToken token2 = app.Begin (runnable2); + SessionToken token1 = app.Begin (runnable1)!; + SessionToken token2 = app.Begin (runnable2)!; // Assert - runnable2 should be on top Assert.True (runnable2.IsModal); @@ -399,8 +370,8 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID IApplication app = GetApp (); Runnable runnable1 = new () { Id = "1" }; Runnable runnable2 = new () { Id = "2" }; - RunnableSessionToken token1 = app.Begin (runnable1); - RunnableSessionToken token2 = app.Begin (runnable2); + SessionToken token1 = app.Begin (runnable1)!; + SessionToken token2 = app.Begin (runnable2)!; // Act - End the top runnable app.End (token2); @@ -421,7 +392,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); StoppableRunnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Act app.RequestStop (runnable); @@ -431,7 +402,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Assert.NotNull (runnable); // Cleanup - app.End (token); + app.End (token!); } [Fact] @@ -440,7 +411,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID // Arrange IApplication app = GetApp (); StoppableRunnable runnable = new (); - RunnableSessionToken token = app.Begin (runnable); + SessionToken? token = app.Begin (runnable); // Act app.RequestStop ((IRunnable?)null); @@ -449,7 +420,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Assert.NotNull (runnable); // Cleanup - app.End (token); + app.End (token!); } [Fact (Skip = "Run methods with main loop are not suitable for parallel tests - use non-parallel UnitTests instead")] @@ -467,7 +438,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Assert.Same (app, result); // Fluent API returns this // Note: Run blocks until stopped, but StopAfterFirstIteration makes it return immediately - // The runnable is automatically disposed by Shutdown() + // The runnable is automatically disposed by Dispose() } [Fact (Skip = "Run methods with main loop are not suitable for parallel tests - use non-parallel UnitTests instead")] @@ -482,7 +453,7 @@ public class ApplicationRunnableIntegrationTests (ITestOutputHelper output) : ID Assert.Throws (() => app.Run ()); // Cleanup - app.Shutdown (); + app.Dispose (); } private IApplication GetApp () diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs index 04ddaad51..0b05fb53a 100644 --- a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableLifecycleTests.cs @@ -1,7 +1,7 @@ #nullable enable using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; +namespace ViewsTests; /// /// Tests for IRunnable lifecycle behavior. diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs index 82439cd5d..194f2cbb8 100644 --- a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableSessionTokenTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; +namespace ApplicationTests.RunnableTests; /// /// Tests for RunnableSessionToken class. @@ -16,7 +16,7 @@ public class RunnableSessionTokenTests (ITestOutputHelper output) Runnable runnable = new (); // Act - RunnableSessionToken token = new (runnable); + SessionToken token = new (runnable); // Assert Assert.NotNull (token.Runnable); @@ -28,7 +28,7 @@ public class RunnableSessionTokenTests (ITestOutputHelper output) { // Arrange Runnable runnable = new (); - RunnableSessionToken token = new (runnable); + SessionToken token = new (runnable); // Act token.Runnable = null; @@ -36,27 +36,4 @@ public class RunnableSessionTokenTests (ITestOutputHelper output) // Assert Assert.Null (token.Runnable); } - - [Fact] - public void RunnableSessionToken_Dispose_ThrowsIfRunnableNotNull () - { - // Arrange - Runnable runnable = new (); - RunnableSessionToken token = new (runnable); - - // Act & Assert - Assert.Throws (() => token.Dispose ()); - } - - [Fact] - public void RunnableSessionToken_Dispose_SucceedsIfRunnableIsNull () - { - // Arrange - Runnable runnable = new (); - RunnableSessionToken token = new (runnable); - token.Runnable = null; - - // Act & Assert - should not throw - token.Dispose (); - } } diff --git a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs index 1d9b71ca8..250f7d07b 100644 --- a/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs +++ b/Tests/UnitTestsParallelizable/Application/Runnable/RunnableTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests.RunnableTests; +namespace ApplicationTests.RunnableTests; /// /// Tests for IRunnable interface and Runnable base class. @@ -68,7 +68,7 @@ public class RunnableTests (ITestOutputHelper output) Assert.False (runnable.IsRunning); // Cleanup - app.Shutdown (); + app.Dispose (); } [Fact] @@ -152,40 +152,6 @@ public class RunnableTests (ITestOutputHelper output) Assert.True (receivedValue); } - [Fact] - public void RaiseIsModalChanging_CanBeCanceled_ByVirtualMethod () - { - // Arrange - CancelableRunnable runnable = new () { CancelModalChange = true }; - - // Act - bool canceled = runnable.RaiseIsModalChanging (false, true); - - // Assert - Assert.True (canceled); - } - - [Fact] - public void RaiseIsModalChanging_CanBeCanceled_ByEvent () - { - // Arrange - Runnable runnable = new (); - var eventRaised = false; - - runnable.IsModalChanging += (s, e) => - { - eventRaised = true; - e.Cancel = true; - }; - - // Act - bool canceled = runnable.RaiseIsModalChanging (false, true); - - // Assert - Assert.True (eventRaised); - Assert.True (canceled); - } - [Fact] public void RaiseIsModalChanged_RaisesEvent () { @@ -213,10 +179,6 @@ public class RunnableTests (ITestOutputHelper output) /// private class CancelableRunnable : Runnable { - public bool CancelModalChange { get; set; } - protected override bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning) => true; // Always cancel - - protected override bool OnIsModalChanging (bool oldIsModal, bool newIsModal) => CancelModalChange; } } diff --git a/Tests/UnitTestsParallelizable/Application/SessionTokenTests.cs b/Tests/UnitTestsParallelizable/Application/SessionTokenTests.cs new file mode 100644 index 000000000..1f3ddf7a9 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/SessionTokenTests.cs @@ -0,0 +1,44 @@ + +#nullable enable +namespace ApplicationTests; + +/// These tests focus on Application.SessionToken and the various ways it can be changed. +public class SessionTokenTests +{ + [Fact] + public void Begin_Throws_On_Null () + { + IApplication? app = Application.Create (); + // Test null Runnable + Assert.Throws (() => app.Begin (null!)); + } + + [Fact] + public void Begin_End_Cleans_Up_SessionToken () + { + IApplication? app = Application.Create (); + + Runnable top = new Runnable (); + SessionToken? sessionToken = app.Begin (top); + Assert.NotNull (sessionToken); + app.End (sessionToken); + + Assert.Null (app.TopRunnableView); + + Assert.DoesNotContain(sessionToken, app.SessionStack!); + + top.Dispose (); + + } + + [Fact] + public void New_Creates_SessionToken () + { + var rs = new SessionToken (null!); + Assert.Null (rs.Runnable); + + var top = new Runnable (); + rs = new (top); + Assert.Equal (top, rs.Runnable); + } +} diff --git a/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs b/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs index eac966801..9c484cd67 100644 --- a/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs +++ b/Tests/UnitTestsParallelizable/Application/SmoothAcceleratingTimeoutTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; public class SmoothAcceleratingTimeoutTests diff --git a/Tests/UnitTestsParallelizable/Application/StackExtensionsTests.cs b/Tests/UnitTestsParallelizable/Application/StackExtensionsTests.cs deleted file mode 100644 index e11be2a43..000000000 --- a/Tests/UnitTestsParallelizable/Application/StackExtensionsTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using UnitTests; - -namespace UnitTests_Parallelizable.ApplicationTests; - -public class StackExtensionsTests : FakeDriverBase -{ - [Fact] - public void Stack_topLevels_Contains () - { - Stack topLevels = CreatetopLevels (); - var comparer = new ToplevelEqualityComparer (); - - Assert.True (topLevels.Contains (new Window { Id = "w2" }, comparer)); - Assert.False (topLevels.Contains (new Toplevel { Id = "top2" }, comparer)); - } - - [Fact] - public void Stack_topLevels_CreatetopLevels () - { - Stack topLevels = CreatetopLevels (); - - int index = topLevels.Count - 1; - - foreach (Toplevel top in topLevels) - { - if (top.GetType () == typeof (Toplevel)) - { - Assert.Equal ("Top", top.Id); - } - else - { - Assert.Equal ($"w{index}", top.Id); - } - - index--; - } - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("w4", tops [0].Id); - Assert.Equal ("w3", tops [1].Id); - Assert.Equal ("w2", tops [2].Id); - Assert.Equal ("w1", tops [3].Id); - Assert.Equal ("Top", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_FindDuplicates () - { - Stack topLevels = CreatetopLevels (); - var comparer = new ToplevelEqualityComparer (); - - topLevels.Push (new Toplevel { Id = "w4" }); - topLevels.Push (new Toplevel { Id = "w1" }); - - Toplevel [] dup = topLevels.FindDuplicates (comparer).ToArray (); - - Assert.Equal ("w4", dup [0].Id); - Assert.Equal ("w1", dup [^1].Id); - } - - [Fact] - public void Stack_topLevels_MoveNext () - { - Stack topLevels = CreatetopLevels (); - - topLevels.MoveNext (); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("w3", tops [0].Id); - Assert.Equal ("w2", tops [1].Id); - Assert.Equal ("w1", tops [2].Id); - Assert.Equal ("Top", tops [3].Id); - Assert.Equal ("w4", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_MovePrevious () - { - Stack topLevels = CreatetopLevels (); - - topLevels.MovePrevious (); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("Top", tops [0].Id); - Assert.Equal ("w4", tops [1].Id); - Assert.Equal ("w3", tops [2].Id); - Assert.Equal ("w2", tops [3].Id); - Assert.Equal ("w1", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_MoveTo () - { - Stack topLevels = CreatetopLevels (); - - var valueToMove = new Window { Id = "w1" }; - var comparer = new ToplevelEqualityComparer (); - - topLevels.MoveTo (valueToMove, 1, comparer); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("w4", tops [0].Id); - Assert.Equal ("w1", tops [1].Id); - Assert.Equal ("w3", tops [2].Id); - Assert.Equal ("w2", tops [3].Id); - Assert.Equal ("Top", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_MoveTo_From_Last_To_Top () - { - Stack topLevels = CreatetopLevels (); - - var valueToMove = new Window { Id = "Top" }; - var comparer = new ToplevelEqualityComparer (); - - topLevels.MoveTo (valueToMove, 0, comparer); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("Top", tops [0].Id); - Assert.Equal ("w4", tops [1].Id); - Assert.Equal ("w3", tops [2].Id); - Assert.Equal ("w2", tops [3].Id); - Assert.Equal ("w1", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_Replace () - { - Stack topLevels = CreatetopLevels (); - - var valueToReplace = new Window { Id = "w1" }; - var valueToReplaceWith = new Window { Id = "new" }; - var comparer = new ToplevelEqualityComparer (); - - topLevels.Replace (valueToReplace, valueToReplaceWith, comparer); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("w4", tops [0].Id); - Assert.Equal ("w3", tops [1].Id); - Assert.Equal ("w2", tops [2].Id); - Assert.Equal ("new", tops [3].Id); - Assert.Equal ("Top", tops [^1].Id); - } - - [Fact] - public void Stack_topLevels_Swap () - { - Stack topLevels = CreatetopLevels (); - - var valueToSwapFrom = new Window { Id = "w3" }; - var valueToSwapTo = new Window { Id = "w1" }; - var comparer = new ToplevelEqualityComparer (); - topLevels.Swap (valueToSwapFrom, valueToSwapTo, comparer); - - Toplevel [] tops = topLevels.ToArray (); - - Assert.Equal ("w4", tops [0].Id); - Assert.Equal ("w1", tops [1].Id); - Assert.Equal ("w2", tops [2].Id); - Assert.Equal ("w3", tops [3].Id); - Assert.Equal ("Top", tops [^1].Id); - } - - [Fact] - public void ToplevelEqualityComparer_GetHashCode () - { - Stack topLevels = CreatetopLevels (); - - // Only allows unique keys - HashSet hCodes = new (); - - foreach (Toplevel top in topLevels) - { - Assert.True (hCodes.Add (top.GetHashCode ())); - } - } - - private Stack CreatetopLevels () - { - Stack topLevels = new (); - - topLevels.Push (new Toplevel { Id = "Top" }); - topLevels.Push (new Window { Id = "w1" }); - topLevels.Push (new Window { Id = "w2" }); - topLevels.Push (new Window { Id = "w3" }); - topLevels.Push (new Window { Id = "w4" }); - - return topLevels; - } -} diff --git a/Tests/UnitTestsParallelizable/Configuration/AttributeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/AttributeJsonConverterTests.cs index 2fd24d34f..47ff59c5a 100644 --- a/Tests/UnitTestsParallelizable/Configuration/AttributeJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/AttributeJsonConverterTests.cs @@ -2,7 +2,7 @@ using Moq; using UnitTests; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class AttributeJsonConverterTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/ColorJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/ColorJsonConverterTests.cs index 732a6f6e4..f01cdc950 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ColorJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ColorJsonConverterTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ColorJsonConverterTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs index cbcb73653..48b493a47 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ConfigPropertyTests.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ConfigPropertyTests { @@ -62,7 +62,7 @@ public class ConfigPropertyTests { var clone = DeepCloner.DeepClone (configProperty); Assert.NotSame (configProperty, clone); - Assert.Equal ("DeepCloneValue", clone.PropertyValue); + Assert.Equal ("DeepCloneValue", clone!.PropertyValue); }); } diff --git a/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs index 99eab2dd7..21b383efa 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ConfigurationPropertyAttributeTests.cs @@ -4,7 +4,7 @@ using System.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ConfigurationPropertyAttributeTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs b/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs index 0f4c671a0..8af27c191 100644 --- a/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/DeepClonerTests.cs @@ -5,7 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Text; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; /// /// Unit tests for the class, ensuring robust deep cloning for diff --git a/Tests/UnitTestsParallelizable/Configuration/KeyCodeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/KeyCodeJsonConverterTests.cs index c3e0c13be..c22ee71bd 100644 --- a/Tests/UnitTestsParallelizable/Configuration/KeyCodeJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/KeyCodeJsonConverterTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class KeyCodeJsonConverterTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs index d3d7e1571..ce9151b7c 100644 --- a/Tests/UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/KeyJsonConverterTests.cs @@ -3,7 +3,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Unicode; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class KeyJsonConverterTests { @@ -49,7 +49,7 @@ public class KeyJsonConverterTests var deserializedKey = JsonSerializer.Deserialize (json, ConfigurationManager.SerializerContext.Options); // Assert - Assert.Equal (expectedStringTo, deserializedKey.ToString ()); + Assert.Equal (expectedStringTo, deserializedKey!.ToString ()); } [Fact] @@ -60,7 +60,7 @@ public class KeyJsonConverterTests // Act string json = "\"Ctrl+Q\""; - Key deserializedKey = JsonSerializer.Deserialize (json, ConfigurationManager.SerializerContext.Options); + Key? deserializedKey = JsonSerializer.Deserialize (json, ConfigurationManager.SerializerContext.Options); // Assert Assert.Equal (key, deserializedKey); diff --git a/Tests/UnitTestsParallelizable/Configuration/MemorySizeEstimator.cs b/Tests/UnitTestsParallelizable/Configuration/MemorySizeEstimator.cs index 8d042533e..701a40e69 100644 --- a/Tests/UnitTestsParallelizable/Configuration/MemorySizeEstimator.cs +++ b/Tests/UnitTestsParallelizable/Configuration/MemorySizeEstimator.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; using System; using System.Collections; @@ -122,11 +122,11 @@ public static class MemorySizeEstimator return false; } - private static long EstimateSimpleTypeSize (object source, Type type) + private static long EstimateSimpleTypeSize (object? source, Type type) { if (type == typeof (string)) { - string str = (string)source; + string str = (string)source!; // Header + length (4) + char array ref + chars (2 bytes each) return OBJECT_HEADER_SIZE + 4 + POINTER_SIZE + (str.Length * 2); } @@ -142,9 +142,9 @@ public static class MemorySizeEstimator } } - private static long EstimateArraySize (object source, ConcurrentDictionary visited) + private static long EstimateArraySize (object? source, ConcurrentDictionary visited) { - Array array = (Array)source; + Array array = (Array)source!; long size = OBJECT_HEADER_SIZE + 4 + POINTER_SIZE; // Header + length + padding foreach (object? element in array) @@ -155,9 +155,9 @@ public static class MemorySizeEstimator return size; } - private static long EstimateDictionarySize (object source, ConcurrentDictionary visited) + private static long EstimateDictionarySize (object? source, ConcurrentDictionary visited) { - IDictionary dict = (IDictionary)source; + IDictionary dict = (IDictionary)source!; long size = OBJECT_HEADER_SIZE + (POINTER_SIZE * 5); // Header + buckets, entries, comparer, fields size += dict.Count * 4; // Bucket array (~4 bytes per entry) size += dict.Count * (4 + 4 + POINTER_SIZE * 2); // Entry array: hashcode, next, key, value @@ -171,9 +171,9 @@ public static class MemorySizeEstimator return size; } - private static long EstimateCollectionSize (object source, ConcurrentDictionary visited) + private static long EstimateCollectionSize (object? source, ConcurrentDictionary visited) { - Type type = source.GetType (); + Type type = source!.GetType (); long size = OBJECT_HEADER_SIZE + (POINTER_SIZE * 3); // Header + internal array + fields if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (Dictionary<,>)) @@ -192,7 +192,7 @@ public static class MemorySizeEstimator return size; } - private static long EstimateObjectSize (object source, Type type, ConcurrentDictionary visited) + private static long EstimateObjectSize (object? source, Type type, ConcurrentDictionary visited) { long size = OBJECT_HEADER_SIZE; diff --git a/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs index e37b47972..c57189389 100644 --- a/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/RuneJsonConverterTests.cs @@ -1,7 +1,7 @@ using System.Text; using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class RuneJsonConverterTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs index b057084cc..e6f42ffe8 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SchemeJsonConverterTests.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class SchemeJsonConverterTests { @@ -78,7 +78,7 @@ public class SchemeJsonConverterTests }; string json = JsonSerializer.Serialize (expected, ConfigurationManager.SerializerContext.Options); - Scheme actual = JsonSerializer.Deserialize (json, ConfigurationManager.SerializerContext.Options); + Scheme? actual = JsonSerializer.Deserialize (json, ConfigurationManager.SerializerContext.Options); Assert.NotNull (actual); diff --git a/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs index c28993fe7..1365d63f0 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SchemeManagerTests.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class SchemeManagerTests { @@ -47,7 +47,7 @@ public class SchemeManagerTests Assert.Contains ("Base", names); Assert.Contains ("Menu", names); Assert.Contains ("Dialog", names); - Assert.Contains ("Toplevel", names); + Assert.Contains ("Runnable", names); Assert.Contains ("Error", names); } diff --git a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs b/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs index 290efeba1..52d50ecfa 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ScopeJsonConverterTests.cs @@ -1,7 +1,7 @@ #nullable enable using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ScopeJsonConverterTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs index 000744067..1db3b0fb7 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ScopeTests.cs @@ -1,7 +1,7 @@ #nullable enable using System.Reflection; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ScopeTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs index 24526de0f..fe1f4b6d8 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SettingsScopeTests.cs @@ -1,5 +1,5 @@  -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class SettingsScopeTests { diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs index 59d34a487..fd37edbd1 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs @@ -1,6 +1,6 @@ using System.Reflection; using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class SourcesManagerTests { @@ -262,7 +262,7 @@ public class SourcesManagerTests var location = ConfigLocations.AppResources; // Act - bool result = sourcesManager.Load (settingsScope, assembly, null, location); + bool result = sourcesManager.Load (settingsScope, assembly, string.Empty, location); // Assert Assert.False (result); diff --git a/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs b/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs index 4d8427e84..811010e43 100644 --- a/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/ThemeScopeTests.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Text.Json; -namespace UnitTests_Parallelizable.ConfigurationTests; +namespace ConfigurationTests; public class ThemeScopeTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/AlignerTests.cs b/Tests/UnitTestsParallelizable/Drawing/AlignerTests.cs index da50f9517..85e2fd055 100644 --- a/Tests/UnitTestsParallelizable/Drawing/AlignerTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/AlignerTests.cs @@ -2,7 +2,7 @@ using System.Text; using System.Text.Json; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class AlignerTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs b/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs index 311d0681d..9b6e7b547 100644 --- a/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs @@ -2,7 +2,7 @@ using UnitTests; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class AttributeTests : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs index b51ab37a9..f6da2e852 100644 --- a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs @@ -1,6 +1,6 @@ using System.Text; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class CellTests { @@ -23,10 +23,10 @@ public class CellTests [InlineData ("æ", new uint [] { 0x00E6 })] [InlineData ("a︠", new uint [] { 0x0061, 0xFE20 })] [InlineData ("e︡", new uint [] { 0x0065, 0xFE21 })] - public void Runes_From_Grapheme (string grapheme, uint [] expected) + public void Runes_From_Grapheme (string? grapheme, uint [] expected) { // Arrange - var c = new Cell { Grapheme = grapheme }; + var c = new Cell { Grapheme = grapheme! }; // Act Rune [] runes = expected.Select (u => new Rune (u)).ToArray (); @@ -72,7 +72,7 @@ public class CellTests Assert.Equal (expected, result); } - public static IEnumerable ToStringTestData () + public static IEnumerable ToStringTestData () { yield return ["", null, "[\"\":]"]; yield return ["a", null, "[\"a\":]"]; diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs index ee3ad49c0..bf40a17bc 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class AnsiColorNameResolverTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/ColorStandardColorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/ColorStandardColorTests.cs index 04666d2cd..f69ca4d41 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Color/ColorStandardColorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Color/ColorStandardColorTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class ColorStandardColorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs index f804e7b49..6614db6ff 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Xunit.Abstractions; using Terminal.Gui; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class MultiStandardColorNameResolverTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/StandardColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/StandardColorNameResolverTests.cs index 5b6c84eef..0f8391d3d 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Color/StandardColorNameResolverTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Color/StandardColorNameResolverTests.cs @@ -2,7 +2,7 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class StandardColorNameResolverTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drawing/ColorTests.Constructors.cs b/Tests/UnitTestsParallelizable/Drawing/ColorTests.Constructors.cs index 7b4122f61..531108ad2 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ColorTests.Constructors.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ColorTests.Constructors.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public partial class ColorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/ColorTests.Operators.cs b/Tests/UnitTestsParallelizable/Drawing/ColorTests.Operators.cs index 2eecafa12..de0cfeb66 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ColorTests.Operators.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ColorTests.Operators.cs @@ -1,7 +1,7 @@ using System.Numerics; using System.Reflection; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public partial class ColorTests { @@ -195,7 +195,7 @@ public static partial class ColorTestsTheoryDataGenerators public static TheoryData Fields_At_Expected_Offsets () { - TheoryData data = [] + TheoryData data = [] ; data.Add ( @@ -246,6 +246,6 @@ public static partial class ColorTestsTheoryDataGenerators 3 ); - return data; + return data!; } } diff --git a/Tests/UnitTestsParallelizable/Drawing/ColorTests.ParsingAndFormatting.cs b/Tests/UnitTestsParallelizable/Drawing/ColorTests.ParsingAndFormatting.cs index 1967f64b5..0ac9be122 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ColorTests.ParsingAndFormatting.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ColorTests.ParsingAndFormatting.cs @@ -2,7 +2,7 @@ using System.Buffers.Binary; using System.Globalization; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public partial class ColorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/ColorTests.TypeChecks.cs b/Tests/UnitTestsParallelizable/Drawing/ColorTests.TypeChecks.cs index 07d574191..5ad8f7e37 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ColorTests.TypeChecks.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ColorTests.TypeChecks.cs @@ -1,7 +1,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public partial class ColorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/ColorTests.cs b/Tests/UnitTestsParallelizable/Drawing/ColorTests.cs index 396b64769..c1d26d5a2 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ColorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ColorTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public partial class ColorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs b/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs index c6b5c06f7..d33aa0cd3 100644 --- a/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class DrawContextTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/FillPairTests.cs b/Tests/UnitTestsParallelizable/Drawing/FillPairTests.cs index 125913706..fe1992003 100644 --- a/Tests/UnitTestsParallelizable/Drawing/FillPairTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/FillPairTests.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class FillPairTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/GradientFillTests.cs b/Tests/UnitTestsParallelizable/Drawing/GradientFillTests.cs index ffceb13bf..f2e10e8e0 100644 --- a/Tests/UnitTestsParallelizable/Drawing/GradientFillTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/GradientFillTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class GradientFillTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/GradientTests.cs b/Tests/UnitTestsParallelizable/Drawing/GradientTests.cs index 75fcdd397..8a8b69bc2 100644 --- a/Tests/UnitTestsParallelizable/Drawing/GradientTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/GradientTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class GradientTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/LineCanvasTests.cs b/Tests/UnitTestsParallelizable/Drawing/LineCanvasTests.cs index 91530891c..7178ea897 100644 --- a/Tests/UnitTestsParallelizable/Drawing/LineCanvasTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/LineCanvasTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; /// /// Pure unit tests for that don't require Application.Driver or View context. @@ -1410,7 +1410,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase foreach (Cell? cell in cellMap.Values) { Assert.NotNull (cell); - Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground); + Assert.Equal (foregroundColor, cell.Value.Attribute!.Value.Foreground); Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background); } } @@ -1439,7 +1439,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase foreach (Cell? cell in cellMap.Values) { Assert.NotNull (cell); - Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground); + Assert.Equal (foregroundColor, cell.Value.Attribute!.Value.Foreground); Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background); } } @@ -1468,7 +1468,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase foreach (Cell? cell in cellMap.Values) { Assert.NotNull (cell); - Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground); + Assert.Equal (foregroundColor, cell.Value.Attribute!.Value.Foreground); Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background); } } diff --git a/Tests/UnitTestsParallelizable/Drawing/PopularityPaletteWithThresholdTests.cs b/Tests/UnitTestsParallelizable/Drawing/PopularityPaletteWithThresholdTests.cs index 3a8132874..95d772f9d 100644 --- a/Tests/UnitTestsParallelizable/Drawing/PopularityPaletteWithThresholdTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/PopularityPaletteWithThresholdTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class PopularityPaletteWithThresholdTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/DifferenceTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/DifferenceTests.cs index 683b3adb9..d229190f5 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/DifferenceTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/DifferenceTests.cs @@ -1,6 +1,6 @@ using Xunit.Sdk; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class DifferenceTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs index 43fdb1976..bad986817 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/DrawOuterBoundaryTests.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; /// /// Tests for . diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/MergeRectanglesTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/MergeRectanglesTests.cs index f2bcb695a..b893b029d 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/MergeRectanglesTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/MergeRectanglesTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class MergeRectanglesTests diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs index 6827d011c..c51e82a1d 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class RegionTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/SubtractRectangleTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/SubtractRectangleTests.cs index 5568fd3fb..23b9bd5aa 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/SubtractRectangleTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/SubtractRectangleTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; using Xunit; diff --git a/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs b/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs index 9e2d9d320..6e305438b 100644 --- a/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/RulerTests.cs @@ -2,7 +2,7 @@ using Microsoft.VisualStudio.TestPlatform.Utilities; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; /// /// Pure unit tests for that don't require Application.Driver or View context. diff --git a/Tests/UnitTestsParallelizable/Drawing/SchemeTests.GetAttributeForRoleAlgorithmTests.cs b/Tests/UnitTestsParallelizable/Drawing/SchemeTests.GetAttributeForRoleAlgorithmTests.cs index ad231702d..48d466fb6 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SchemeTests.GetAttributeForRoleAlgorithmTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/SchemeTests.GetAttributeForRoleAlgorithmTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SchemeGetAttributeForRoleAlgorithmTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/SchemeTests.cs b/Tests/UnitTestsParallelizable/Drawing/SchemeTests.cs index 73cfd3099..f8cf86d37 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SchemeTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/SchemeTests.cs @@ -1,7 +1,7 @@ #nullable enable using System.Reflection; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SchemeTests { @@ -36,7 +36,7 @@ public class SchemeTests Assert.True (schemes.ContainsKey ("Dialog")); Assert.True (schemes.ContainsKey ("Error")); Assert.True (schemes.ContainsKey ("Menu")); - Assert.True (schemes.ContainsKey ("TopLevel")); + Assert.True (schemes.ContainsKey ("Runnable")); } @@ -66,10 +66,10 @@ public class SchemeTests Assert.NotNull (menuScheme); Assert.Equal (new Attribute (StandardColor.Charcoal, StandardColor.LightBlue, TextStyle.Bold), menuScheme!.Normal); - // Toplevel - var toplevelScheme = schemes ["Toplevel"]; - Assert.NotNull (toplevelScheme); - Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), toplevelScheme!.Normal.ToString ()); + // Runnable + var runnableScheme = schemes ["Runnable"]; + Assert.NotNull (runnableScheme); + Assert.Equal (new Attribute (StandardColor.CadetBlue, StandardColor.Charcoal).ToString (), runnableScheme!.Normal.ToString ()); } diff --git a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs b/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs index ab97eed73..3a1ed881a 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs @@ -1,5 +1,5 @@  -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SixelEncoderTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/SolidFillTests.cs b/Tests/UnitTestsParallelizable/Drawing/SolidFillTests.cs index c335a793f..d1d604141 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SolidFillTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/SolidFillTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SolidFillTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/StraightLineExtensionsTests.cs b/Tests/UnitTestsParallelizable/Drawing/StraightLineExtensionsTests.cs index 0275e8a23..663a96c31 100644 --- a/Tests/UnitTestsParallelizable/Drawing/StraightLineExtensionsTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/StraightLineExtensionsTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class StraightLineExtensionsTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drawing/StraightLineTests.cs b/Tests/UnitTestsParallelizable/Drawing/StraightLineTests.cs index 7cf11cf4e..b39246e0b 100644 --- a/Tests/UnitTestsParallelizable/Drawing/StraightLineTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/StraightLineTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class StraightLineTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs b/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs index 65105cb89..c00732549 100644 --- a/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/ThicknessTests.cs @@ -3,7 +3,7 @@ using Terminal.Gui.Drivers; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase { @@ -745,7 +745,7 @@ public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase f.Driver = driver; driver.SetScreenSize (45, 20); - var top = new Toplevel () { Width = driver.Cols, Height = driver.Rows }; + var top = new Runnable () { Width = driver.Cols, Height = driver.Rows }; top.Driver = driver; top.Add (f); diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs index 4b4205791..f0596fc83 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs @@ -5,7 +5,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { @@ -19,7 +19,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase driver.Rows = 25; driver.Cols = 80; driver.AddRune (new Rune ('a')); - Assert.Equal ("a", driver.Contents [0, 0].Grapheme); + Assert.Equal ("a", driver.Contents? [0, 0].Grapheme); driver.End (); } @@ -33,7 +33,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase var text = "\u1eaf"; driver.AddStr (text); - Assert.Equal (expected, driver.Contents [0, 0].Grapheme); + Assert.Equal (expected, driver.Contents! [0, 0].Grapheme); Assert.Equal (" ", driver.Contents [0, 1].Grapheme); driver.ClearContents (); @@ -88,7 +88,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { for (var row = 0; row < driver.Rows; row++) { - Assert.Equal (" ", driver.Contents [row, col].Grapheme); + Assert.Equal (" ", driver.Contents? [row, col].Grapheme); } } @@ -101,12 +101,12 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase IDriver driver = CreateFakeDriver (); driver.AddRune ('a'); - Assert.Equal ("a", driver.Contents [0, 0].Grapheme); + Assert.Equal ("a", driver.Contents? [0, 0].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (1, driver.Col); driver.AddRune ('b'); - Assert.Equal ("b", driver.Contents [0, 1].Grapheme); + Assert.Equal ("b", driver.Contents? [0, 1].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (2, driver.Col); @@ -118,7 +118,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase // Add a rune to the last column of the first row; should increment the row or col even though it's now invalid driver.AddRune ('c'); - Assert.Equal ("c", driver.Contents [0, lastCol].Grapheme); + Assert.Equal ("c", driver.Contents? [0, lastCol].Grapheme); Assert.Equal (lastCol + 1, driver.Col); // Add a rune; should succeed but do nothing as it's outside of Contents @@ -129,7 +129,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { for (var row = 0; row < driver.Rows; row++) { - Assert.NotEqual ("d", driver.Contents [row, col].Grapheme); + Assert.NotEqual ("d", driver.Contents? [row, col].Grapheme); } } @@ -148,7 +148,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase Assert.Equal (2, rune.GetColumns ()); driver.AddRune (rune); - Assert.Equal (rune.ToString (), driver.Contents [0, 0].Grapheme); + Assert.Equal (rune.ToString (), driver.Contents? [0, 0].Grapheme); Assert.Equal (0, driver.Row); Assert.Equal (2, driver.Col); diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiKeyboardParserTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiKeyboardParserTests.cs index e1f8e93a8..ab8e3de3d 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiKeyboardParserTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiKeyboardParserTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class AnsiKeyboardParserTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiMouseParserTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiMouseParserTests.cs index 38c42ec9f..621396098 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiMouseParserTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiMouseParserTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class AnsiMouseParserTests { @@ -23,7 +23,7 @@ public class AnsiMouseParserTests public void ProcessMouseInput_ReturnsCorrectFlags (string input, int expectedX, int expectedY, MouseFlags expectedFlags) { // Act - MouseEventArgs result = _parser.ProcessMouseInput (input); + MouseEventArgs? result = _parser.ProcessMouseInput (input); // Assert if (expectedFlags == MouseFlags.None) diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs index aafa8243f..f5ce41d7b 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiRequestSchedulerTests.cs @@ -1,6 +1,6 @@ using Moq; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class AnsiRequestSchedulerTests { @@ -31,7 +31,7 @@ public class AnsiRequestSchedulerTests _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); // then we should execute our request - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> ()!, null, false)).Verifiable (Times.Once); // Act bool result = _scheduler.SendOrSchedule (null, request); @@ -78,7 +78,7 @@ public class AnsiRequestSchedulerTests // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> ()!, null, false)).Verifiable (Times.Exactly (2)); _scheduler.SendOrSchedule (null, request); @@ -109,7 +109,7 @@ public class AnsiRequestSchedulerTests // Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2)); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> ()!, null, false)).Verifiable (Times.Exactly (2)); _scheduler.SendOrSchedule (null, request); @@ -154,7 +154,7 @@ public class AnsiRequestSchedulerTests // Send _parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once); - _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> (), null, false)).Verifiable (Times.Exactly (2)); + _parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny> ()!, null, false)).Verifiable (Times.Exactly (2)); Assert.True (_scheduler.SendOrSchedule (null, request1)); @@ -210,7 +210,7 @@ public class AnsiRequestSchedulerTests // 'x' is free _parserMock.Setup (p => p.IsExpecting ("x")).Returns (false).Verifiable (Times.Once); - _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> (), null, false)).Verifiable (Times.Once); + _parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny> ()!, null, false)).Verifiable (Times.Once); // Act bool a = _scheduler.SendOrSchedule (null, request1); diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiResponseParserTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiResponseParserTests.cs index 506edf93a..ebe92cbe4 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiResponseParserTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiResponseParserTests.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Text; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; // BUGBUG: These tests use TInputRecord of `int`, but that's not a realistic type for keyboard input. public class AnsiResponseParserTests (ITestOutputHelper output) diff --git a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs index 1b70a4a83..a8a2a5531 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class ClipRegionTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Drivers/ConsoleKeyMappingTests.cs b/Tests/UnitTestsParallelizable/Drivers/ConsoleKeyMappingTests.cs index 3f16aea96..850c4fa02 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ConsoleKeyMappingTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ConsoleKeyMappingTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class ConsoleKeyMappingTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs index b900ad206..917f36f4f 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs @@ -4,7 +4,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class ContentsTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs index c9ff9584f..fbee752fb 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputProcessorTests.cs @@ -1,7 +1,7 @@ using System.Collections.Concurrent; using System.Text; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class NetInputProcessorTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs index 29b5ecb19..83d133a93 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs @@ -2,7 +2,7 @@ using UnitTests; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class DriverColorTests : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 254396df8..928dd923b 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class DriverTests (ITestOutputHelper output) : FakeDriverBase { @@ -55,11 +55,11 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase [InlineData ("windows")] [InlineData ("dotnet")] [InlineData ("unix")] - public void All_Drivers_Init_Shutdown_Cross_Platform (string driverName) + public void All_Drivers_Init_Dispose_Cross_Platform (string driverName) { IApplication? app = Application.Create (); app.Init (driverName); - app.Shutdown (); + app.Dispose (); } [Theory] @@ -72,8 +72,8 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase IApplication? app = Application.Create (); app.Init (driverName); app.StopAfterFirstIteration = true; - app.Run ().Dispose (); - app.Shutdown (); + app.Run> (); + app.Dispose (); } [Theory] @@ -86,15 +86,15 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase IApplication? app = Application.Create (); app.Init (driverName); app.StopAfterFirstIteration = true; - app.Run ().Dispose (); + app.Run (); DriverAssert.AssertDriverContentsWithFrameAre (driverName!, output, app.Driver); - app.Shutdown (); + app.Dispose (); } } -public class TestTop : Toplevel +public class TestTop : Runnable { /// public override void BeginInit () diff --git a/Tests/UnitTestsParallelizable/Drivers/EscSeqRequestsTests.cs b/Tests/UnitTestsParallelizable/Drivers/EscSeqRequestsTests.cs index f0c4990c1..077860c3d 100644 --- a/Tests/UnitTestsParallelizable/Drivers/EscSeqRequestsTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/EscSeqRequestsTests.cs @@ -1,6 +1,6 @@ using UnitTests; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class EscSeqRequestsTests : FakeDriverBase { @@ -82,7 +82,7 @@ public class EscSeqRequestsTests : FakeDriverBase [Theory] [InlineData (null)] [InlineData ("")] - public void Add_Null_Or_Empty_Terminator_Throws (string terminator) + public void Add_Null_Or_Empty_Terminator_Throws (string? terminator) { if (terminator is null) { @@ -95,7 +95,6 @@ public class EscSeqRequestsTests : FakeDriverBase } [Theory] - [InlineData (null)] [InlineData ("")] public void HasResponse_Null_Or_Empty_Terminator_Does_Not_Throws (string terminator) { @@ -107,20 +106,12 @@ public class EscSeqRequestsTests : FakeDriverBase } [Theory] - [InlineData (null)] [InlineData ("")] public void Remove_Null_Or_Empty_Terminator_Throws (string terminator) { EscSeqRequests.Add ("t"); - if (terminator is null) - { - Assert.Throws (() => EscSeqRequests.Remove (terminator)); - } - else - { - Assert.Throws (() => EscSeqRequests.Remove (terminator)); - } + Assert.Throws (() => EscSeqRequests.Remove (terminator)); EscSeqRequests.Clear (); } diff --git a/Tests/UnitTestsParallelizable/Drivers/EscSeqUtilsTests.cs b/Tests/UnitTestsParallelizable/Drivers/EscSeqUtilsTests.cs index 99537e799..cc3305c3e 100644 --- a/Tests/UnitTestsParallelizable/Drivers/EscSeqUtilsTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/EscSeqUtilsTests.cs @@ -2,7 +2,7 @@ // ReSharper disable HeuristicUnreachableCode -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class EscSeqUtilsTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs index 30ba27be9..bb9d6b52e 100644 --- a/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/FakeDriverTests.cs @@ -2,7 +2,7 @@ using System.Text; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; /// /// Tests for the FakeDriver to ensure it works properly with the modern component factory architecture. @@ -49,7 +49,7 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase driver?.SetScreenSize (100, 30); // Verify new size - Assert.Equal (100, driver.Cols); + Assert.Equal (100, driver!.Cols); Assert.Equal (30, driver.Rows); Assert.Equal (new (0, 0, 100, 30), driver.Screen); } @@ -177,7 +177,7 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase driver?.SetScreenSize (100, 30); // Verify new size - Assert.Equal (100, driver.Cols); + Assert.Equal (100, driver!.Cols); Assert.Equal (30, driver.Rows); // Verify buffer is clean (no stale runes from previous size) @@ -192,7 +192,7 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase driver?.SetScreenSize (80, 25); // Verify size is back - Assert.Equal (80, driver.Cols); + Assert.Equal (80, driver!.Cols); Assert.Equal (25, driver.Rows); // Verify buffer dimensions match @@ -254,7 +254,7 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase Assert.Equal (40, eventSize.Value.Height); // Verify driver.Screen was updated - Assert.Equal (new (0, 0, 120, 40), driver.Screen); + Assert.Equal (new (0, 0, 120, 40), driver!.Screen); Assert.Equal (120, driver.Cols); Assert.Equal (40, driver.Rows); } @@ -266,20 +266,13 @@ public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase IDriver driver = CreateFakeDriver (); var sizeChangedFired = false; - var screenChangedFired = false; -#pragma warning disable CS0618 // Type or member is obsolete driver.SizeChanged += (sender, args) => { sizeChangedFired = true; }; -#pragma warning restore CS0618 // Type or member is obsolete - - driver.SizeChanged += (sender, args) => { screenChangedFired = true; }; // Trigger resize using FakeResize driver?.SetScreenSize (90, 35); - // Both events should fire for compatibility Assert.True (sizeChangedFired); - Assert.True (screenChangedFired); } #endregion diff --git a/Tests/UnitTestsParallelizable/Drivers/KeyCodeTests.cs b/Tests/UnitTestsParallelizable/Drivers/KeyCodeTests.cs index 1dd14e9c1..0a84b1236 100644 --- a/Tests/UnitTestsParallelizable/Drivers/KeyCodeTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/KeyCodeTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class KeyCodeTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/LowLevel/IInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/LowLevel/IInputOutputTests.cs index 231786279..1ad380981 100644 --- a/Tests/UnitTestsParallelizable/Drivers/LowLevel/IInputOutputTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/LowLevel/IInputOutputTests.cs @@ -3,7 +3,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; using Xunit.Sdk; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; /// /// Low-level tests for IInput and IOutput implementations across all drivers. diff --git a/Tests/UnitTestsParallelizable/Drivers/MouseInterpreterTests.cs b/Tests/UnitTestsParallelizable/Drivers/MouseInterpreterTests.cs index 71f92d10b..022c44f4d 100644 --- a/Tests/UnitTestsParallelizable/Drivers/MouseInterpreterTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/MouseInterpreterTests.cs @@ -1,4 +1,5 @@ -namespace UnitTests_Parallelizable.DriverTests; +#nullable disable +namespace DriverTests; public class MouseInterpreterTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs index c78585e49..7db07eeca 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs @@ -2,7 +2,7 @@ using System.Text; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; /// /// Tests for the ToAnsi functionality that generates ANSI escape sequences from buffer contents. diff --git a/Tests/UnitTestsParallelizable/Drivers/UrlHyperlinkerTests.cs b/Tests/UnitTestsParallelizable/Drivers/UrlHyperlinkerTests.cs index 309528ea2..86fbf66c0 100644 --- a/Tests/UnitTestsParallelizable/Drivers/UrlHyperlinkerTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/UrlHyperlinkerTests.cs @@ -2,7 +2,7 @@ using System.Text; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class Osc8UrlLinkerTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs index 4906f30cd..1038cd0c4 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowSizeMonitorTests.cs @@ -1,6 +1,6 @@ using Moq; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class WindowSizeMonitorTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs index 1145fc90a..4701d4963 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputProcessorTests.cs @@ -5,7 +5,7 @@ using EventFlags = Terminal.Gui.Drivers.WindowsConsole.EventFlags; using ControlKeyState = Terminal.Gui.Drivers.WindowsConsole.ControlKeyState; using MouseEventRecord = Terminal.Gui.Drivers.WindowsConsole.MouseEventRecord; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class WindowsInputProcessorTests { diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs index f50ee8809..2cad545da 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; [Collection ("Global Test Setup")] [Trait ("Platform", "Windows")] diff --git a/Tests/UnitTestsParallelizable/FileServices/FileSystemColorProviderTests.cs b/Tests/UnitTestsParallelizable/FileServices/FileSystemColorProviderTests.cs index 4c01895df..8e6ea7f1d 100644 --- a/Tests/UnitTestsParallelizable/FileServices/FileSystemColorProviderTests.cs +++ b/Tests/UnitTestsParallelizable/FileServices/FileSystemColorProviderTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.FileServicesTests; +namespace FileServicesTests; public class FileSystemColorProviderTests { diff --git a/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs b/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs index 37bd3acde..edd507830 100644 --- a/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs +++ b/Tests/UnitTestsParallelizable/FileServices/FileSystemIconProviderTests.cs @@ -3,7 +3,7 @@ using System.IO.Abstractions.TestingHelpers; using System.Runtime.InteropServices; using System.Text; -namespace UnitTests_Parallelizable.FileServicesTests; +namespace FileServicesTests; public class FileSystemIconProviderTests { diff --git a/Tests/UnitTestsParallelizable/FileServices/NerdFontsTests.cs b/Tests/UnitTestsParallelizable/FileServices/NerdFontsTests.cs index 580df348e..eced78aef 100644 --- a/Tests/UnitTestsParallelizable/FileServices/NerdFontsTests.cs +++ b/Tests/UnitTestsParallelizable/FileServices/NerdFontsTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.FileServicesTests; +namespace FileServicesTests; public class NerdFontTests { diff --git a/Tests/UnitTestsParallelizable/Input/EnqueueKeyEventTests.cs b/Tests/UnitTestsParallelizable/Input/EnqueueKeyEventTests.cs index 123f1ffcd..af071adb8 100644 --- a/Tests/UnitTestsParallelizable/Input/EnqueueKeyEventTests.cs +++ b/Tests/UnitTestsParallelizable/Input/EnqueueKeyEventTests.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; /// /// Parallelizable unit tests for IInput.EnqueueKeyDownEvent and InputProcessor.EnqueueKeyDownEvent. diff --git a/Tests/UnitTestsParallelizable/Input/EnqueueMouseEventTests.cs b/Tests/UnitTestsParallelizable/Input/EnqueueMouseEventTests.cs index cfe478d1f..a01a3eec9 100644 --- a/Tests/UnitTestsParallelizable/Input/EnqueueMouseEventTests.cs +++ b/Tests/UnitTestsParallelizable/Input/EnqueueMouseEventTests.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; /// /// Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent. diff --git a/Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs b/Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs index 5c22a96ea..1e20f4a60 100644 --- a/Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs +++ b/Tests/UnitTestsParallelizable/Input/InputBindingsThreadSafetyTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; /// /// Tests to verify that InputBindings (KeyBindings and MouseBindings) are thread-safe diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingTests.cs index 413c3a413..081c19fc6 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class KeyBindingTests () { diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingsTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingsTests.cs index 4aaeecfc8..4beaa53b1 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyBindingsTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class KeyBindingsTests { @@ -210,7 +210,7 @@ public class KeyBindingsTests Command [] commands2 = { Command.Up, Command.Down }; keyBindings.Add (Key.B, commands2); - Key key = keyBindings.GetFirstFromCommands (commands1); + Key? key = keyBindings.GetFirstFromCommands (commands1); Assert.Equal (Key.A, key); key = keyBindings.GetFirstFromCommands (commands2); @@ -223,7 +223,7 @@ public class KeyBindingsTests var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.Right); - Key key = keyBindings.GetFirstFromCommands (Command.Right); + Key? key = keyBindings.GetFirstFromCommands (Command.Right); Assert.Equal (Key.A, key); } @@ -240,7 +240,7 @@ public class KeyBindingsTests { var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); - Key resultKey = keyBindings.GetFirstFromCommands (Command.HotKey); + Key? resultKey = keyBindings.GetFirstFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); } @@ -298,7 +298,7 @@ public class KeyBindingsTests keyBindings.Add (Key.A, Command.Accept); keyBindings.Add (Key.B, Command.HotKey); - keyBindings.Replace (keyBindings.GetFirstFromCommands (Command.Accept), Key.C); + keyBindings.Replace (keyBindings.GetFirstFromCommands (Command.Accept)!, Key.C); Assert.Empty (keyBindings.GetCommands (Key.A)); Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); } diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs index c62aea0f3..a532814e2 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs @@ -1,7 +1,7 @@ using System.Text; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class KeyTests { diff --git a/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingTests.cs b/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingTests.cs index 65ad17d07..b92d16c78 100644 --- a/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class MouseBindingTests { diff --git a/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingsTests.cs b/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingsTests.cs index 94e5c14f5..501d97f9a 100644 --- a/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Mouse/MouseBindingsTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class MouseBindingsTests { diff --git a/Tests/UnitTestsParallelizable/Input/Mouse/MouseEventArgsTest.cs b/Tests/UnitTestsParallelizable/Input/Mouse/MouseEventArgsTest.cs index 483a89d7b..331a7e198 100644 --- a/Tests/UnitTestsParallelizable/Input/Mouse/MouseEventArgsTest.cs +++ b/Tests/UnitTestsParallelizable/Input/Mouse/MouseEventArgsTest.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.InputTests; +namespace InputTests; public class MouseEventArgsTests { diff --git a/Tests/UnitTestsParallelizable/LocalPackagesTests.cs b/Tests/UnitTestsParallelizable/LocalPackagesTests.cs index 8076f726f..84608b798 100644 --- a/Tests/UnitTestsParallelizable/LocalPackagesTests.cs +++ b/Tests/UnitTestsParallelizable/LocalPackagesTests.cs @@ -1,5 +1,5 @@  -namespace UnitTests_Parallelizable.BuildAndDeployTests; +namespace BuildAndDeployTests; public class LocalPackagesTests { diff --git a/Tests/UnitTestsParallelizable/README.md b/Tests/UnitTestsParallelizable/README.md index 2de5c1be7..e3e2818b9 100644 --- a/Tests/UnitTestsParallelizable/README.md +++ b/Tests/UnitTestsParallelizable/README.md @@ -2,42 +2,12 @@ This project contains unit tests that can run in parallel without interference. Tests here must not depend on global state or static Application infrastructure. -## Migration Rules - -### Tests CAN be parallelized if they: -- ✅ Test properties, constructors, and basic operations -- ✅ Use `[SetupFakeDriver]` without Application statics -- ✅ Call `View.Draw()`, `LayoutAndDraw()` without Application statics -- ✅ Verify visual output with `DriverAssert` (when using `[SetupFakeDriver]`) -- ✅ Create View hierarchies without `Application.Top` -- ✅ Test events and behavior without global state -- ✅ Use `View.BeginInit()` / `View.EndInit()` for initialization - -### Tests CANNOT be parallelized if they: -- ❌ Use `[AutoInitShutdown]` or `[SetupFakeApplication]`- requires `Application.Init/Shutdown` which creates global state -- ❌ Set `Application.Driver` (global singleton) -- ❌ Call `Application.Init()`, `Application.Run/Run()`, or `Application.Begin()` -- ❌ Enable `ConfigurationManager` (Enable/Load/Apply/Disable) -- ❌ Access `ConfigurationManager` including `ThemeManager` and `SchemeManager` - these rely on global state -- ❌ Modify static properties like `Key.Separator`, `CultureInfo.CurrentCulture`, etc. -- ❌ Set static members on View subclasses (e.g., configuration properties like `Dialog.DefaultButtonAlignment`) or any static fields/properties - these are shared across all parallel tests -- ❌ Are true integration tests that test multiple components working together - ### Important Notes -- Many tests in `UnitTests` blindly use the above patterns when they don't actually need them +- Many tests in `UnitTests` blindly use the the legacy model they don't actually need to - These tests CAN be rewritten to remove unnecessary dependencies and migrated here - Many tests APPEAR to be integration tests but are just poorly written and cover multiple surface areas - these can be split into focused unit tests - When in doubt, analyze if the test truly needs global state or can be refactored -## How to Migrate Tests - -1. **Identify** tests in `UnitTests` that don't actually need Application statics -2. **Rewrite** tests to remove `[AutoInitShutdown]` or `[SetupFakeApplication]`, `Application.Begin()`, etc. if not needed -3. **Move** the test to the equivalent file in `UnitTests.Parallelizable` -4. **Delete** the old test from `UnitTests` to avoid duplicates -5. **Verify** no duplicate test names exist (CI will check this) -6. **Test** to ensure the migrated test passes - ## Example Migrations ### Simple Property Test (no changes needed) @@ -93,7 +63,7 @@ public void Event_Fires_When_Property_Changes () public void Focus_Test () { var view = new Button (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (view); Application.Begin (top); view.SetFocus (); diff --git a/Tests/UnitTestsParallelizable/Resources/ResourceManagerTests.cs b/Tests/UnitTestsParallelizable/Resources/ResourceManagerTests.cs index be18dbdb7..8cff633c7 100644 --- a/Tests/UnitTestsParallelizable/Resources/ResourceManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Resources/ResourceManagerTests.cs @@ -6,7 +6,7 @@ using System.Resources; using System.Runtime.CompilerServices; using UnitTests; -namespace UnitTests_Parallelizable.ResourcesTests; +namespace ResourcesTests; public class ResourceManagerTests : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Text/AutocompleteTests.cs b/Tests/UnitTestsParallelizable/Text/AutocompleteTests.cs index 58eb364d8..d27709977 100644 --- a/Tests/UnitTestsParallelizable/Text/AutocompleteTests.cs +++ b/Tests/UnitTestsParallelizable/Text/AutocompleteTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; /// /// Pure unit tests for Autocomplete functionality that don't require Application or Driver. diff --git a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs index f393b43d4..f998f9ba8 100644 --- a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs +++ b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs @@ -3,7 +3,7 @@ using System.Collections.Concurrent; using Moq; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; public class CollectionNavigatorTests { diff --git a/Tests/UnitTestsParallelizable/Text/RuneTests.cs b/Tests/UnitTestsParallelizable/Text/RuneTests.cs index 4e03e8048..a2ec053f9 100644 --- a/Tests/UnitTestsParallelizable/Text/RuneTests.cs +++ b/Tests/UnitTestsParallelizable/Text/RuneTests.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Text; -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; public class RuneTests { @@ -733,16 +733,16 @@ public class RuneTests [Theory] [InlineData ('\uea85', null, "", false)] // Private Use Area [InlineData (0x1F356, new [] { '\ud83c', '\udf56' }, "🍖", true)] // 🍖 Meat On Bone - public void Test_DecodeSurrogatePair (int code, char [] charsValue, string runeString, bool isSurrogatePair) + public void Test_DecodeSurrogatePair (int code, char []? charsValue, string runeString, bool isSurrogatePair) { var rune = new Rune (code); - char [] chars; + char []? chars; if (isSurrogatePair) { Assert.True (rune.DecodeSurrogatePair (out chars)); - Assert.Equal (2, chars.Length); - Assert.Equal (charsValue [0], chars [0]); + Assert.Equal (2, chars!.Length); + Assert.Equal (charsValue! [0], chars [0]); Assert.Equal (charsValue [1], chars [1]); Assert.Equal (runeString, new Rune (chars [0], chars [1]).ToString ()); } diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index a3c4e52ba..1c6e848cd 100644 --- a/Tests/UnitTestsParallelizable/Text/StringTests.cs +++ b/Tests/UnitTestsParallelizable/Text/StringTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; #nullable enable diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs index fb15c0f6b..222d58c7f 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterDrawTests.cs @@ -6,7 +6,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase { @@ -427,7 +427,7 @@ Nice Work")] TextFormatter tf = new () { - Text = UICatalogTop.GetAboutBoxMessage (), + Text = UICatalogRunnable.GetAboutBoxMessage (), Alignment = Alignment.Center, VerticalAlignment = Alignment.Start, WordWrap = false, diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs index e01f995ae..2d95e48b1 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterJustificationTests.cs @@ -4,7 +4,7 @@ using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs index 0ec012292..b7f580cf9 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs @@ -1,8 +1,9 @@ -using System.Text; +#nullable disable +using System.Text; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.TextTests; +namespace TextTests; public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj index 5024415aa..5bd16391b 100644 --- a/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj +++ b/Tests/UnitTestsParallelizable/UnitTests.Parallelizable.csproj @@ -1,77 +1,73 @@  - - - - - 2.0 - 2.0 - 2.0 - 2.0 - - - false - - true - true - portable - $(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL - enable - true - true - + + + + + 2.0 + 2.0 + 2.0 + 2.0 + + + enable + false + + true + true + portable + $(DefineConstants);JETBRAINS_ANNOTATIONS;CONTRACTS_FULL + enable + true + true + - - true - $(DefineConstants);DEBUG_IDISPOSABLE - - - true - - - - - - + + true + $(DefineConstants);DEBUG_IDISPOSABLE + + + true + - - - - + + + + - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - - - + + + + + + + - - - PreserveNewest - - - - - - - + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentSubViewTests.cs b/Tests/UnitTestsParallelizable/View/Adornment/AdornmentSubViewTests.cs deleted file mode 100644 index 3876b618c..000000000 --- a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentSubViewTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Xunit.Abstractions; - -namespace UnitTests_Parallelizable.ViewTests; - -[Collection ("Global Test Setup")] -public class AdornmentSubViewTests () -{ - [Fact] - public void Setting_Thickness_Causes_Adornment_SubView_Layout () - { - var view = new View (); - var subView = new View (); - view.Margin.Add (subView); - view.BeginInit (); - view.EndInit (); - var raised = false; - - subView.SubViewLayout += LayoutStarted; - view.Margin.Thickness = new Thickness (1, 2, 3, 4); - view.Layout (); - Assert.True (raised); - - return; - void LayoutStarted (object sender, LayoutEventArgs e) - { - raised = true; - } - } -} diff --git a/Tests/UnitTestsParallelizable/View/Adornment/MarginTests.cs b/Tests/UnitTestsParallelizable/View/Adornment/MarginTests.cs deleted file mode 100644 index d7cc2ed0d..000000000 --- a/Tests/UnitTestsParallelizable/View/Adornment/MarginTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests_Parallelizable.ViewTests; - -public class MarginTests -{ - [Fact] - public void Is_Visually_Transparent () - { - var view = new View { Height = 3, Width = 3 }; - Assert.True(view.Margin!.ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent), "Margin should be transparent by default."); - } - - [Fact] - public void Is_Transparent_To_Mouse () - { - var view = new View { Height = 3, Width = 3 }; - Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse by default."); - } - - [Fact] - public void When_Not_Visually_Transparent () - { - var view = new View { Height = 3, Width = 3 }; - - // Give the Margin some size - view.Margin!.Thickness = new Thickness (1, 1, 1, 1); - - // Give it Text - view.Margin!.Text = "Test"; - - // Strip off ViewportSettings.Transparent - view.Margin!.ViewportSettings &= ~ViewportSettingsFlags.Transparent; - - // - - } - - [Fact] - public void Thickness_Is_Empty_By_Default () - { - var view = new View { Height = 3, Width = 3 }; - Assert.Equal (Thickness.Empty, view.Margin!.Thickness); - } - - // ShadowStyle - [Fact] - public void Margin_Uses_ShadowStyle_Transparent () - { - var view = new View { Height = 3, Width = 3, ShadowStyle = ShadowStyle.Transparent }; - Assert.Equal (ShadowStyle.Transparent, view.Margin!.ShadowStyle); - Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse when ShadowStyle is Transparent."); - Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Transparent.."); - } - - [Fact] - public void Margin_Uses_ShadowStyle_Opaque () - { - var view = new View { Height = 3, Width = 3, ShadowStyle = ShadowStyle.Opaque }; - Assert.Equal (ShadowStyle.Opaque, view.Margin!.ShadowStyle); - Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse when ShadowStyle is Opaque."); - Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Opaque.."); - } - -} diff --git a/Tests/UnitTestsParallelizable/View/Layout/GetViewsAtLocationTests.cs b/Tests/UnitTestsParallelizable/View/Layout/GetViewsAtLocationTests.cs deleted file mode 100644 index d2b2e8484..000000000 --- a/Tests/UnitTestsParallelizable/View/Layout/GetViewsAtLocationTests.cs +++ /dev/null @@ -1,407 +0,0 @@ -#nullable enable - -namespace UnitTests_Parallelizable.ViewMouseTests; - -[Trait ("Category", "Layout")] -public class GetViewsAtLocationTests -{ - private class TestView : View - { - public TestView (int x, int y, int w, int h, bool visible = true) - { - X = x; - Y = y; - Width = w; - Height = h; - base.Visible = visible; - } - } - - [Fact] - public void ReturnsEmpty_WhenRootIsNull () - { - var result = View.GetViewsAtLocation (null, new Point (0, 0)); - Assert.Empty (result); - } - - [Fact] - public void ReturnsEmpty_WhenRootIsNotVisible () - { - TestView root = new (0, 0, 10, 10, visible: false); - var result = View.GetViewsAtLocation (root, new Point (5, 5)); - Assert.Empty (result); - } - - [Fact] - public void ReturnsEmpty_WhenPointOutsideRoot () - { - TestView root = new (0, 0, 10, 10); - var result = View.GetViewsAtLocation (root, new Point (20, 20)); - Assert.Empty (result); - } - - - [Fact] - public void ReturnsEmpty_WhenPointOutsideRoot_AndSubview () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (5, 5, 2, 2); - root.Add (sub); - var result = View.GetViewsAtLocation (root, new Point (20, 20)); - Assert.Empty (result); - } - - [Fact] - public void ReturnsRoot_WhenPointInsideRoot_NoSubviews () - { - TestView root = new (0, 0, 10, 10); - var result = View.GetViewsAtLocation (root, new Point (5, 5)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } - - - [Fact] - public void ReturnsRoot_And_Subview_WhenPointInsideRootMargin () - { - TestView root = new (0, 0, 10, 10); - root.Margin!.Thickness = new (1); - TestView sub = new (2, 2, 5, 5); - root.Add (sub); - var result = View.GetViewsAtLocation (root, new Point (3, 3)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - } - - [Fact] - public void ReturnsRoot_And_Subview_Border_WhenPointInsideRootMargin () - { - TestView root = new (0, 0, 10, 10); - root.Margin!.Thickness = new (1); - TestView sub = new (2, 2, 5, 5); - sub.BorderStyle = LineStyle.Dotted; - root.Add (sub); - var result = View.GetViewsAtLocation (root, new Point (3, 3)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Border, result [2]); - } - - - [Fact] - public void ReturnsRoot_And_Margin_WhenPointInside_With_Margin () - { - TestView root = new (0, 0, 10, 10); - root.Margin!.Thickness = new (1); - var result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Margin, result [1]); - } - - [Fact] - public void ReturnsRoot_WhenPointOutsideSubview_With_Margin () - { - TestView root = new (0, 0, 10, 10); - root.Margin!.Thickness = new (1); - TestView sub = new (2, 2, 5, 5); - root.Add (sub); - List result = View.GetViewsAtLocation (root, new Point (2, 2)); - Assert.Single (result); - Assert.Equal (root, result [0]); - - result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Margin, result [1]); - - result = View.GetViewsAtLocation (root, new Point (1, 1)); - Assert.Single (result); - Assert.Equal (root, result [0]); - - result = View.GetViewsAtLocation (root, new Point (8, 8)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } - - - [Fact] - public void ReturnsRoot_And_Border_WhenPointInside_With_Border () - { - TestView root = new (0, 0, 10, 10); - root.Border!.Thickness = new (1); - var result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Border, result [1]); - } - - [Fact] - public void ReturnsRoot_WhenPointOutsideSubview_With_Border () - { - TestView root = new (0, 0, 10, 10); - root.Border!.Thickness = new (1); - TestView sub = new (2, 2, 5, 5); - root.Add (sub); - var result = View.GetViewsAtLocation (root, new Point (2, 2)); - Assert.Single (result); - Assert.Equal (root, result [0]); - - result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Border, result [1]); - - result = View.GetViewsAtLocation (root, new Point (1, 1)); - Assert.Single (result); - Assert.Equal (root, result [0]); - - result = View.GetViewsAtLocation (root, new Point (8, 8)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } - - [Fact] - public void ReturnsRoot_And_Border_WhenPointInsideRootBorder () - { - TestView root = new (0, 0, 10, 10); - root.Border!.Thickness = new (1); - var result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Border, result [1]); - } - - [Fact] - public void ReturnsRoot_And_Padding_WhenPointInsideRootPadding () - { - TestView root = new (0, 0, 10, 10); - root.Padding!.Thickness = new (1); - var result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (root.Padding, result [1]); - } - - [Fact] - public void ReturnsRootAndSubview_WhenPointInsideSubview () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - root.Add (sub); - - var result = View.GetViewsAtLocation (root, new Point (3, 3)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - } - - [Fact] - public void ReturnsRootAndSubviewAndMargin_WhenPointInsideSubviewMargin () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.Margin!.Thickness = new (1); - root.Add (sub); - - var result = View.GetViewsAtLocation (root, new Point (6, 6)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Margin, result [2]); - } - - [Fact] - public void ReturnsRootAndSubviewAndBorder_WhenPointInsideSubviewBorder () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.Border!.Thickness = new (1); - root.Add (sub); - - var result = View.GetViewsAtLocation (root, new Point (2, 2)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Border, result [2]); - } - - [Fact] - public void ReturnsRootAndSubviewAndSubviewAndBorder_WhenPointInsideSubviewBorder () - { - TestView root = new (2, 2, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.Border!.Thickness = new (1); - root.Add (sub); - - var result = View.GetViewsAtLocation (root, new Point (4, 4)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Border, result [2]); - } - - [Fact] - public void ReturnsRootAndSubviewAndBorder_WhenPointInsideSubviewPadding () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.Padding!.Thickness = new (1); - root.Add (sub); - - var result = View.GetViewsAtLocation (root, new Point (2, 2)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Padding, result [2]); - } - - [Fact] - public void ReturnsRootAndSubviewAndMarginAndShadowView_WhenPointInsideSubviewMargin () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.ShadowStyle = ShadowStyle.Opaque; - root.Add (sub); - - root.Layout (); - - var result = View.GetViewsAtLocation (root, new Point (6, 6)); - Assert.Equal (5, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Margin, result [2]); - Assert.Equal (sub.Margin!.SubViews.ElementAt (0), result [3]); - Assert.Equal (sub.Margin!.SubViews.ElementAt (1), result [4]); - } - - [Fact] - public void ReturnsRootAndSubviewAndBorderAndButton_WhenPointInsideSubviewBorder () - { - TestView root = new (0, 0, 10, 10); - TestView sub = new (2, 2, 5, 5); - sub.Border!.Thickness = new (1); - - Button closeButton = new Button () - { - NoDecorations = true, - NoPadding = true, - Title = "X", - Width = 1, - Height = 1, - X = Pos.AnchorEnd (), - Y= 0, - ShadowStyle = ShadowStyle.None - }; - sub.Border!.Add (closeButton); - root.Add (sub); - - root.Layout (); - - var result = View.GetViewsAtLocation (root, new Point (6, 2)); - Assert.Equal (4, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub, result [1]); - Assert.Equal (sub.Border, result [2]); - Assert.Equal (closeButton, result [3]); - } - - [Fact] - public void ReturnsDeepestSubview_WhenNested () - { - TestView root = new (0, 0, 20, 20); - var sub1 = new TestView (2, 2, 16, 16); - var sub2 = new TestView (3, 3, 10, 10); - var sub3 = new TestView (1, 1, 5, 5); - root.Add (sub1); - sub1.Add (sub2); - sub2.Add (sub3); - - // Point inside all - var result = View.GetViewsAtLocation (root, new Point (7, 7)); - Assert.Equal (4, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub1, result [1]); - Assert.Equal (sub2, result [2]); - Assert.Equal (sub3, result [3]); - } - - [Fact] - public void ReturnsTopmostSubview_WhenOverlapping () - { - TestView root = new (0, 0, 10, 10); - var sub1 = new TestView (2, 2, 6, 6); - var sub2 = new TestView (4, 4, 6, 6); - root.Add (sub1); - root.Add (sub2); // sub2 is on top - - var result = View.GetViewsAtLocation (root, new Point (5, 5)); - Assert.Equal (3, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub1, result [1]); - Assert.Equal (sub2, result [2]); - } - - [Fact] - public void ReturnsTopmostSubview_WhenNotOverlapping () - { - TestView root = new (0, 0, 10, 10);// under 5,5, - var sub1 = new TestView (10, 10, 6, 6); // not under location 5,5 - var sub2 = new TestView (4, 4, 6, 6); // under 5,5, - root.Add (sub1); - root.Add (sub2); // sub2 is on top - - var result = View.GetViewsAtLocation (root, new Point (5, 5)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub2, result [1]); - } - - [Fact] - public void SkipsInvisibleSubviews () - { - TestView root = new (0, 0, 10, 10); - var sub1 = new TestView (2, 2, 6, 6, visible: false); - var sub2 = new TestView (4, 4, 6, 6); - root.Add (sub1); - root.Add (sub2); - - var result = View.GetViewsAtLocation (root, new Point (5, 5)); - Assert.Equal (2, result.Count); - Assert.Equal (root, result [0]); - Assert.Equal (sub2, result [1]); - } - - [Fact] - public void ReturnsRoot_WhenPointOnEdge () - { - TestView root = new (0, 0, 10, 10); - var result = View.GetViewsAtLocation (root, new Point (0, 0)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } - - [Fact] - public void ReturnsRoot_WhenPointOnBottomRightCorner () - { - TestView root = new (0, 0, 10, 10); - var result = View.GetViewsAtLocation (root, new Point (9, 9)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } - - [Fact] - public void ReturnsEmpty_WhenAllSubviewsInvisible () - { - TestView root = new (0, 0, 10, 10); - var sub1 = new TestView (2, 2, 6, 6, visible: false); - root.Add (sub1); - - var result = View.GetViewsAtLocation (root, new Point (3, 3)); - Assert.Single (result); - Assert.Equal (root, result [0]); - } -} - diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.CenterTests.cs b/Tests/UnitTestsParallelizable/View/Layout/Pos.CenterTests.cs deleted file mode 100644 index d9367c927..000000000 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.CenterTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Pos; - -namespace UnitTests_Parallelizable.LayoutTests; - -public class PosCenterTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - [Fact] - public void PosCenter_Constructor () - { - var posCenter = new PosCenter (); - Assert.NotNull (posCenter); - } - - [Fact] - public void PosCenter_ToString () - { - var posCenter = new PosCenter (); - var expectedString = "Center"; - - Assert.Equal (expectedString, posCenter.ToString ()); - } - - [Fact] - public void PosCenter_GetAnchor () - { - var posCenter = new PosCenter (); - var width = 50; - int expectedAnchor = width / 2; - - Assert.Equal (expectedAnchor, posCenter.GetAnchor (width)); - } - - [Fact] - public void PosCenter_CreatesCorrectInstance () - { - Pos pos = Center (); - Assert.IsType (pos); - } - - [Theory] - [InlineData (10, 2, 4)] - [InlineData (10, 10, 0)] - [InlineData (10, 11, 0)] - [InlineData (10, 12, -1)] - [InlineData (19, 20, 0)] - public void PosCenter_Calculate_ReturnsExpectedValue (int superviewDimension, int width, int expectedX) - { - var posCenter = new PosCenter (); - int result = posCenter.Calculate (superviewDimension, new DimAbsolute (width), null!, Dimension.Width); - Assert.Equal (expectedX, result); - } - - [Fact] - public void PosCenter_Bigger_Than_SuperView () - { - var superView = new View { Width = 10, Height = 10 }; - var view = new View { X = Center (), Y = Center (), Width = 20, Height = 20 }; - superView.Add (view); - superView.LayoutSubViews (); - - Assert.Equal (-5, view.Frame.Left); - Assert.Equal (-5, view.Frame.Top); - } -} diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.CombineTests.cs b/Tests/UnitTestsParallelizable/View/Layout/Pos.CombineTests.cs deleted file mode 100644 index 1e2d4968c..000000000 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.CombineTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.VisualStudio.TestPlatform.Utilities; -using UnitTests; -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; -using static Terminal.Gui.ViewBase.Pos; - -namespace UnitTests_Parallelizable.LayoutTests; - -public class PosCombineTests (ITestOutputHelper output) -{ - private readonly ITestOutputHelper _output = output; - - // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved - // TODO: A new test that calls SetRelativeLayout directly is needed. - [Fact] - public void PosCombine_Referencing_Same_View () - { - var super = new View { Width = 10, Height = 10, Text = "super" }; - var view1 = new View { Width = 2, Height = 2, Text = "view1" }; - var view2 = new View { Width = 2, Height = 2, Text = "view2" }; - view2.X = Pos.AnchorEnd (0) - (Pos.Right (view2) - Pos.Left (view2)); - - super.Add (view1, view2); - super.BeginInit (); - super.EndInit (); - - Exception exception = Record.Exception (super.LayoutSubViews); - Assert.Null (exception); - Assert.Equal (new (0, 0, 10, 10), super.Frame); - Assert.Equal (new (0, 0, 2, 2), view1.Frame); - Assert.Equal (new (8, 0, 2, 2), view2.Frame); - - super.Dispose (); - } - -} diff --git a/Tests/UnitTestsParallelizable/View/Navigation/NavigationTests.cs b/Tests/UnitTestsParallelizable/View/Navigation/NavigationTests.cs deleted file mode 100644 index bed43ef50..000000000 --- a/Tests/UnitTestsParallelizable/View/Navigation/NavigationTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests_Parallelizable.ViewTests; - -public class NavigationTests -{ - - // View.Focused & View.MostFocused tests - - // View.Focused - No subviews - [Fact] - public void Focused_NoSubViews () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - view.SetFocus (); - } - - [Fact] - public void GetMostFocused_NoSubViews_Returns_Null () - { - var view = new View (); - Assert.Null (view.Focused); - - view.CanFocus = true; - Assert.False (view.HasFocus); - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.Null (view.MostFocused); - } - - [Fact] - public void GetMostFocused_Returns_Most () - { - var view = new View - { - Id = "view", - CanFocus = true - }; - - var subview = new View - { - Id = "subview", - CanFocus = true - }; - - view.Add (subview); - - view.SetFocus (); - Assert.True (view.HasFocus); - Assert.True (subview.HasFocus); - Assert.Equal (subview, view.MostFocused); - - var subview2 = new View - { - Id = "subview2", - CanFocus = true - }; - - view.Add (subview2); - Assert.Equal (subview2, view.MostFocused); - } -} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentSubViewTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentSubViewTests.cs new file mode 100644 index 000000000..eb95f093a --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentSubViewTests.cs @@ -0,0 +1,95 @@ +using Xunit.Abstractions; + +namespace ViewBaseTests.Adornments; + +public class AdornmentSubViewTests () +{ + [Fact] + public void Setting_Thickness_Causes_Adornment_SubView_Layout () + { + var view = new View (); + var subView = new View (); + view.Margin!.Add (subView); + view.BeginInit (); + view.EndInit (); + var raised = false; + + subView.SubViewLayout += LayoutStarted; + view.Margin.Thickness = new Thickness (1, 2, 3, 4); + view.Layout (); + Assert.True (raised); + + return; + void LayoutStarted (object? sender, LayoutEventArgs e) + { + raised = true; + } + } + + [Theory] + [InlineData (0, 0, false)] // Margin has no thickness, so false + [InlineData (0, 1, false)] // Margin has no thickness, so false + [InlineData (1, 0, true)] + [InlineData (1, 1, true)] + [InlineData (2, 1, true)] + public void Adornment_WithSubView_Finds (int viewMargin, int subViewMargin, bool expectedFound) + { + IApplication? app = Application.Create (); + Runnable runnable = new () + { + Width = 10, + Height = 10 + }; + app.Begin (runnable); + + runnable.Margin!.Thickness = new Thickness (viewMargin); + // Turn of TransparentMouse for the test + runnable.Margin!.ViewportSettings = ViewportSettingsFlags.None; + + var subView = new View () + { + X = 0, + Y = 0, + Width = 5, + Height = 5 + }; + subView.Margin!.Thickness = new Thickness (subViewMargin); + // Turn of TransparentMouse for the test + subView.Margin!.ViewportSettings = ViewportSettingsFlags.None; + + runnable.Margin!.Add (subView); + runnable.Layout (); + + var foundView = runnable.GetViewsUnderLocation (new Point (0, 0), ViewportSettingsFlags.None).LastOrDefault (); + + bool found = foundView == subView || foundView == subView.Margin; + Assert.Equal (expectedFound, found); + } + + [Fact] + public void Adornment_WithNonVisibleSubView_Finds_Adornment () + { + IApplication? app = Application.Create (); + Runnable runnable = new () + { + Width = 10, + Height = 10 + }; + app.Begin (runnable); + runnable.Padding!.Thickness = new Thickness (1); + + var subView = new View () + { + X = 0, + Y = 0, + Width = 1, + Height = 1, + Visible = false + }; + runnable.Padding.Add (subView); + runnable.Layout (); + + Assert.Equal (runnable.Padding, runnable.GetViewsUnderLocation (new Point (0, 0), ViewportSettingsFlags.None).LastOrDefault ()); + + } +} diff --git a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentTests.cs index bbaad3a2b..ea04decd7 100644 --- a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Adornments; [Collection ("Global Test Setup")] public class AdornmentTests diff --git a/Tests/UnitTestsParallelizable/View/Adornment/BorderArrangementTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs similarity index 100% rename from Tests/UnitTestsParallelizable/View/Adornment/BorderArrangementTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs new file mode 100644 index 000000000..482b2519e --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs @@ -0,0 +1,136 @@ +#nullable enable +using UnitTests; +using Xunit.Abstractions; + +namespace ViewBaseTests.Adornments; + +public class MarginTests (ITestOutputHelper output) +{ + [Fact] + public void Margin_Is_Transparent () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + app.Driver!.SetScreenSize (5, 5); + + var view = new View { Height = 3, Width = 3 }; + view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; + view.Margin.Thickness = new (1); + + Runnable runnable = new (); + app.Begin (runnable); + + runnable.SetScheme (new () + { + Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) + }); + + runnable.Add (view); + Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, runnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsAre ( + @"", + output, + app.Driver + ); + DriverAssert.AssertDriverAttributesAre ("0", output, app.Driver, runnable.GetAttributeForRole (VisualRole.Normal)); + } + + [Fact] + public void Margin_ViewPortSettings_Not_Transparent_Is_NotTransparent () + { + IApplication? app = Application.Create (); + app.Init ("fake"); + app.Driver!.SetScreenSize (5, 5); + + var view = new View { Height = 3, Width = 3 }; + view.Margin!.Diagnostics = ViewDiagnosticFlags.Thickness; + view.Margin.Thickness = new (1); + view.Margin.ViewportSettings = ViewportSettingsFlags.None; + + Runnable runnable = new (); + app.Begin (runnable); + + runnable.SetScheme (new () + { + Normal = new (Color.Red, Color.Green), Focus = new (Color.Green, Color.Red) + }); + + runnable.Add (view); + Assert.Equal (ColorName16.Red, view.Margin.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + Assert.Equal (ColorName16.Red, runnable.GetAttributeForRole (VisualRole.Normal).Foreground.GetClosestNamedColor16 ()); + + app.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsAre ( + @" +MMM +M M +MMM", + output, + app.Driver + ); + DriverAssert.AssertDriverAttributesAre ("0", output, app.Driver, runnable.GetAttributeForRole (VisualRole.Normal)); + } + [Fact] + public void Is_Visually_Transparent () + { + var view = new View { Height = 3, Width = 3 }; + Assert.True(view.Margin!.ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent), "Margin should be transparent by default."); + } + + [Fact] + public void Is_Transparent_To_Mouse () + { + var view = new View { Height = 3, Width = 3 }; + Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse by default."); + } + + [Fact] + public void When_Not_Visually_Transparent () + { + var view = new View { Height = 3, Width = 3 }; + + // Give the Margin some size + view.Margin!.Thickness = new Thickness (1, 1, 1, 1); + + // Give it Text + view.Margin!.Text = "Test"; + + // Strip off ViewportSettings.Transparent + view.Margin!.ViewportSettings &= ~ViewportSettingsFlags.Transparent; + + // + + } + + [Fact] + public void Thickness_Is_Empty_By_Default () + { + var view = new View { Height = 3, Width = 3 }; + Assert.Equal (Thickness.Empty, view.Margin!.Thickness); + } + + // ShadowStyle + [Fact] + public void Margin_Uses_ShadowStyle_Transparent () + { + var view = new View { Height = 3, Width = 3, ShadowStyle = ShadowStyle.Transparent }; + Assert.Equal (ShadowStyle.Transparent, view.Margin!.ShadowStyle); + Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse when ShadowStyle is Transparent."); + Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Transparent.."); + } + + [Fact] + public void Margin_Uses_ShadowStyle_Opaque () + { + var view = new View { Height = 3, Width = 3, ShadowStyle = ShadowStyle.Opaque }; + Assert.Equal (ShadowStyle.Opaque, view.Margin!.ShadowStyle); + Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.TransparentMouse), "Margin should be transparent to mouse when ShadowStyle is Opaque."); + Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Opaque.."); + } + +} diff --git a/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs similarity index 95% rename from Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs index a3add070b..49bb7f0e7 100644 --- a/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Adornments; [Collection ("Global Test Setup")] @@ -53,7 +53,7 @@ public class ShadowStyleTests superView.BeginInit (); superView.EndInit (); - Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin.Thickness); + Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin!.Thickness); } diff --git a/Tests/UnitTestsParallelizable/View/Adornment/ToScreenTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ToScreenTests.cs similarity index 89% rename from Tests/UnitTestsParallelizable/View/Adornment/ToScreenTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Adornment/ToScreenTests.cs index cbea7cca8..e3601920f 100644 --- a/Tests/UnitTestsParallelizable/View/Adornment/ToScreenTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ToScreenTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; /// /// Test the and methods. diff --git a/Tests/UnitTestsParallelizable/View/ArrangementTests.cs b/Tests/UnitTestsParallelizable/ViewBase/ArrangementTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/ArrangementTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/ArrangementTests.cs index cc156b796..19aac38a8 100644 --- a/Tests/UnitTestsParallelizable/View/ArrangementTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/ArrangementTests.cs @@ -1,6 +1,7 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Arrangement; + public class ArrangementTests (ITestOutputHelper output) { @@ -223,13 +224,24 @@ public class ArrangementTests (ITestOutputHelper output) // Verify other directions are not set if (arrangement != ViewArrangement.LeftResizable) + { Assert.False (view.Arrangement.HasFlag (ViewArrangement.LeftResizable)); + } + if (arrangement != ViewArrangement.RightResizable) + { Assert.False (view.Arrangement.HasFlag (ViewArrangement.RightResizable)); + } + if (arrangement != ViewArrangement.TopResizable) + { Assert.False (view.Arrangement.HasFlag (ViewArrangement.TopResizable)); + } + if (arrangement != ViewArrangement.BottomResizable) + { Assert.False (view.Arrangement.HasFlag (ViewArrangement.BottomResizable)); + } } #endregion @@ -669,10 +681,10 @@ public class ArrangementTests (ITestOutputHelper output) #region View-Specific Arrangement Tests [Fact] - public void Toplevel_DefaultsToOverlapped () + public void Runnable_DefaultsToOverlapped () { - var toplevel = new Toplevel (); - Assert.True (toplevel.Arrangement.HasFlag (ViewArrangement.Overlapped)); + var runnable = new Runnable (); + Assert.True (runnable.Arrangement.HasFlag (ViewArrangement.Overlapped)); } [Fact] diff --git a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs index 780ffb94c..07f76890d 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/NeedsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs @@ -1,7 +1,7 @@ #nullable enable using UnitTests; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Drawing; [Trait ("Category", "Output")] public class NeedsDrawTests : FakeDriverBase diff --git a/Tests/UnitTestsParallelizable/View/SchemeTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/SchemeTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs index 57234c171..3642bed52 100644 --- a/Tests/UnitTestsParallelizable/View/SchemeTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/SchemeTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Drawing; [Trait ("Category", "View.Scheme")] public class SchemeTests : FakeDriverBase @@ -198,7 +198,7 @@ public class SchemeTests : FakeDriverBase Assert.Contains ("Dialog", schemes.Keys); Assert.Contains ("Error", schemes.Keys); Assert.Contains ("Menu", schemes.Keys); - Assert.Contains ("Toplevel", schemes.Keys); + Assert.Contains ("Runnable", schemes.Keys); } diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewClearViewportTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/ViewClearViewportTests.cs index 58ad1bf9c..0115ef52b 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewClearViewportTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewClearViewportTests.cs @@ -1,8 +1,8 @@ +#nullable disable using System.Text; using UnitTests; -using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Viewport; public class ViewClearViewportTests () : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs index 6a4c330e2..49dff4476 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawTextAndLineCanvasTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Drawing; public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase { @@ -80,7 +80,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase // Text should appear at the content location Point screenPos = view.ContentToScreen (Point.Empty); - Assert.Equal ("T", driver.Contents [screenPos.Y, screenPos.X].Grapheme); + Assert.Equal ("T", driver.Contents! [screenPos.Y, screenPos.X].Grapheme); Assert.Equal ("e", driver.Contents [screenPos.Y, screenPos.X + 1].Grapheme); Assert.Equal ("s", driver.Contents [screenPos.Y, screenPos.X + 2].Grapheme); Assert.Equal ("t", driver.Contents [screenPos.Y, screenPos.X + 3].Grapheme); @@ -113,7 +113,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase Point screenPos = view.ContentToScreen (Point.Empty); Attribute expectedAttr = view.GetAttributeForRole (VisualRole.Focus); - Assert.Equal (expectedAttr, driver.Contents [screenPos.Y, screenPos.X].Attribute); + Assert.Equal (expectedAttr, driver.Contents! [screenPos.Y, screenPos.X].Attribute); } [Fact] @@ -142,7 +142,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase Point screenPos = view.ContentToScreen (Point.Empty); Attribute expectedAttr = view.GetAttributeForRole (VisualRole.Normal); - Assert.Equal (expectedAttr, driver.Contents [screenPos.Y, screenPos.X].Attribute); + Assert.Equal (expectedAttr, driver.Contents! [screenPos.Y, screenPos.X].Attribute); } [Fact] @@ -273,7 +273,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase // Verify the line was drawn (check for horizontal line character) for (int i = 0; i < 5; i++) { - Assert.NotEqual (" ", driver.Contents [screenPos.Y, screenPos.X + i].Grapheme); + Assert.NotEqual (" ", driver.Contents! [screenPos.Y, screenPos.X + i].Grapheme); } } @@ -409,7 +409,7 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase bool lineRendered = true; for (int i = 0; i < 5; i++) { - if (driver.Contents [screenPos.Y, screenPos.X + i].Grapheme == " ") + if (driver.Contents! [screenPos.Y, screenPos.X + i].Grapheme == " ") { lineRendered = false; break; diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs index 1d7183857..220487dcd 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingClippingTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Drawing; public class ViewDrawingClippingTests () : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingFlowTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingFlowTests.cs index 1f646c292..7a429a042 100644 --- a/Tests/UnitTestsParallelizable/View/Draw/ViewDrawingFlowTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingFlowTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Drawing; public class ViewDrawingFlowTests () : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/View/InitTests.cs b/Tests/UnitTestsParallelizable/ViewBase/InitTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/InitTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/InitTests.cs index a6883ae7e..28a73ff5e 100644 --- a/Tests/UnitTestsParallelizable/View/InitTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/InitTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; /// Tests View BeginInit/EndInit/Initialized functionality. public class InitTests diff --git a/Tests/UnitTestsParallelizable/View/Keyboard/HotKeyTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/HotKeyTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Keyboard/HotKeyTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Keyboard/HotKeyTests.cs index 06a55a9bc..715733e81 100644 --- a/Tests/UnitTestsParallelizable/View/Keyboard/HotKeyTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/HotKeyTests.cs @@ -1,7 +1,7 @@ using System.Text; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; [Collection ("Global Test Setup")] public class HotKeyTests diff --git a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs similarity index 69% rename from Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs index eddee1293..588654222 100644 --- a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs @@ -1,38 +1,35 @@ -using System.Text; -using UnitTests; -using Xunit.Abstractions; - -namespace UnitTests.ViewTests; +#nullable enable +namespace ViewBaseTests.Keyboard; /// /// Tests for View.KeyBindings /// -public class KeyBindingsTests () +public class KeyBindingsTests { [Fact] - [AutoInitShutdown] public void Focused_HotKey_Application_All_Work () { + IApplication app = Application.Create (); + app.Begin (new Runnable { CanFocus = true }); + var view = new ScopedKeyBindingView (); var keyWasHandled = false; view.KeyDownNotHandled += (s, e) => keyWasHandled = true; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); + app!.TopRunnableView!.Add (view); - Application.RaiseKeyDownEvent (Key.A); + app.Keyboard.RaiseKeyDownEvent (Key.A); Assert.False (keyWasHandled); Assert.True (view.ApplicationCommand); keyWasHandled = false; - Application.RaiseKeyDownEvent (Key.H); + app.Keyboard.RaiseKeyDownEvent (Key.H); Assert.True (view.HotKeyCommand); Assert.False (keyWasHandled); keyWasHandled = false; Assert.False (view.HasFocus); - Application.RaiseKeyDownEvent (Key.F); + app.Keyboard.RaiseKeyDownEvent (Key.F); Assert.False (keyWasHandled); Assert.False (view.FocusedCommand); @@ -40,28 +37,25 @@ public class KeyBindingsTests () view.CanFocus = true; view.SetFocus (); Assert.True (view.HasFocus); - Application.RaiseKeyDownEvent (Key.F); + app.Keyboard.RaiseKeyDownEvent (Key.F); Assert.True (view.FocusedCommand); Assert.False (keyWasHandled); // Command was invoked, but wasn't handled Assert.True (view.ApplicationCommand); Assert.True (view.HotKeyCommand); - top.Dispose (); } [Fact] - [AutoInitShutdown] public void KeyBinding_Negative () { + IApplication? app = Application.Create (); + app.Begin (new Runnable { CanFocus = true }); + var view = new ScopedKeyBindingView (); var keyWasHandled = false; view.KeyDownNotHandled += (s, e) => keyWasHandled = true; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Application.RaiseKeyDownEvent (Key.Z); + app.Keyboard.RaiseKeyDownEvent (Key.Z); Assert.False (keyWasHandled); Assert.False (view.ApplicationCommand); Assert.False (view.HotKeyCommand); @@ -69,142 +63,137 @@ public class KeyBindingsTests () keyWasHandled = false; Assert.False (view.HasFocus); - Application.RaiseKeyDownEvent (Key.F); + app.Keyboard.RaiseKeyDownEvent (Key.F); Assert.False (keyWasHandled); Assert.False (view.ApplicationCommand); Assert.False (view.HotKeyCommand); Assert.False (view.FocusedCommand); - top.Dispose (); } [Fact] - [AutoInitShutdown] public void HotKey_KeyBinding () { + IApplication? app = Application.Create (); + app.Begin (new Runnable { CanFocus = true }); + var view = new ScopedKeyBindingView (); + app!.TopRunnableView!.Add (view); + var keyWasHandled = false; view.KeyDownNotHandled += (s, e) => keyWasHandled = true; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - keyWasHandled = false; - Application.RaiseKeyDownEvent (Key.H); + app.Keyboard.RaiseKeyDownEvent (Key.H); Assert.True (view.HotKeyCommand); Assert.False (keyWasHandled); view.HotKey = KeyCode.Z; keyWasHandled = false; view.HotKeyCommand = false; - Application.RaiseKeyDownEvent (Key.H); // old hot key + app.Keyboard.RaiseKeyDownEvent (Key.H); // old hot key Assert.False (keyWasHandled); Assert.False (view.HotKeyCommand); - Application.RaiseKeyDownEvent (Key.Z); // new hot key + app.Keyboard.RaiseKeyDownEvent (Key.Z); // new hot key Assert.True (view.HotKeyCommand); Assert.False (keyWasHandled); - - top.Dispose (); } [Fact] - [AutoInitShutdown] public void HotKey_KeyBinding_Negative () { + IApplication? app = Application.Create (); + app.Begin (new Runnable { CanFocus = true }); + var view = new ScopedKeyBindingView (); var keyWasHandled = false; view.KeyDownNotHandled += (s, e) => keyWasHandled = true; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Application.RaiseKeyDownEvent (Key.Z); + app.Keyboard.RaiseKeyDownEvent (Key.Z); Assert.False (keyWasHandled); Assert.False (view.HotKeyCommand); keyWasHandled = false; - Application.RaiseKeyDownEvent (Key.F); + app.Keyboard.RaiseKeyDownEvent (Key.F); Assert.False (view.HotKeyCommand); - top.Dispose (); } [Fact] - [AutoInitShutdown] public void HotKey_Enabled_False_Does_Not_Invoke () { + IApplication? app = Application.Create (); + app.Begin (new Runnable ()); + var view = new ScopedKeyBindingView (); var keyWasHandled = false; view.KeyDownNotHandled += (s, e) => keyWasHandled = true; - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); + app!.TopRunnableView!.Add (view); - Application.RaiseKeyDownEvent (Key.Z); + app.Keyboard.RaiseKeyDownEvent (Key.Z); Assert.False (keyWasHandled); Assert.False (view.HotKeyCommand); keyWasHandled = false; view.Enabled = false; - Application.RaiseKeyDownEvent (Key.F); + app.Keyboard.RaiseKeyDownEvent (Key.F); Assert.False (view.HotKeyCommand); - top.Dispose (); } - [Fact] public void HotKey_Raises_HotKeyCommand () { + IApplication? app = Application.Create (); + app.Begin (new Runnable ()); var hotKeyRaised = false; var acceptRaised = false; var selectRaised = false; - Application.TopRunnable = new Toplevel (); + var view = new View { CanFocus = true, - HotKeySpecifier = new Rune ('_'), + HotKeySpecifier = new ('_'), Title = "_Test" }; - Application.TopRunnable.Add (view); + app!.TopRunnableView!.Add (view); + view.HandlingHotKey += (s, e) => hotKeyRaised = true; view.Accepting += (s, e) => acceptRaised = true; view.Selecting += (s, e) => selectRaised = true; Assert.Equal (KeyCode.T, view.HotKey); - Assert.True (Application.RaiseKeyDownEvent (Key.T)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.T)); Assert.True (hotKeyRaised); Assert.False (acceptRaised); Assert.False (selectRaised); hotKeyRaised = false; - Assert.True (Application.RaiseKeyDownEvent (Key.T.WithAlt)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.T.WithAlt)); Assert.True (hotKeyRaised); Assert.False (acceptRaised); Assert.False (selectRaised); hotKeyRaised = false; view.HotKey = KeyCode.E; - Assert.True (Application.RaiseKeyDownEvent (Key.E.WithAlt)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.E.WithAlt)); Assert.True (hotKeyRaised); Assert.False (acceptRaised); Assert.False (selectRaised); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); } + // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) public class ScopedKeyBindingView : View { - public ScopedKeyBindingView () + /// + public override void EndInit () { + base.EndInit (); AddCommand (Command.Save, () => ApplicationCommand = true); AddCommand (Command.HotKey, () => HotKeyCommand = true); AddCommand (Command.Left, () => FocusedCommand = true); - Application.KeyBindings.Add (Key.A, this, Command.Save); + App!.Keyboard.KeyBindings.Add (Key.A, this, Command.Save); HotKey = KeyCode.H; KeyBindings.Add (Key.F, Command.Left); } diff --git a/Tests/UnitTestsParallelizable/View/Keyboard/KeyboardEventTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Keyboard/KeyboardEventTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs index 9dd6e0332..c894fe94f 100644 --- a/Tests/UnitTestsParallelizable/View/Keyboard/KeyboardEventTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyboardEventTests.cs @@ -1,9 +1,10 @@ -using UnitTests; +#nullable disable +using UnitTests; using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Keyboard; [Collection ("Global Test Setup")] public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.DimTypes.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.DimTypes.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.DimTypes.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.DimTypes.cs index 44c0add1d..157b72b03 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.DimTypes.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.DimTypes.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests; public partial class DimAutoTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.MinMax.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.MinMax.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.MinMax.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.MinMax.cs index 08532c876..ebbb99a6b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.MinMax.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.MinMax.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests; public partial class DimAutoTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.PosTypes.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.PosTypes.cs index c5a571c10..8a05f45ca 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.PosTypes.cs @@ -1,6 +1,6 @@ using UnitTests.Parallelizable; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public partial class DimAutoTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.cs index 3c785ae36..cedc97e95 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.AutoTests.cs @@ -2,7 +2,7 @@ using Xunit.Abstractions; using static Terminal.Gui.ViewBase.Dim; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; [Trait ("Category", "Layout")] public partial class DimAutoTests (ITestOutputHelper output) diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.CombineTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.CombineTests.cs similarity index 78% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.CombineTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.CombineTests.cs index a101bd714..9671b8f40 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.CombineTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.CombineTests.cs @@ -1,7 +1,7 @@ -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; +#nullable disable +using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class DimCombineTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.FillTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FillTests.cs similarity index 89% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.FillTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FillTests.cs index 356e3744b..8eccf808c 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.FillTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FillTests.cs @@ -1,7 +1,7 @@ -using UnitTests; +#nullable disable using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class DimFillTests (ITestOutputHelper output) { @@ -161,4 +161,19 @@ public class DimFillTests (ITestOutputHelper output) Assert.True (view.IsInitialized); Assert.Equal (expectedViewBounds, view.Viewport); } + + [Fact] + public void DimFill_SizedCorrectly () + { + var view = new View { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single }; + var top = new View { Width = 80, Height = 25 }; + top.Add (view); + + top.Layout (); + + view.SetRelativeLayout (new (32, 5)); + Assert.Equal (32, view.Frame.Width); + Assert.Equal (5, view.Frame.Height); + top.Dispose (); + } } diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.FuncTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FuncTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.FuncTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FuncTests.cs index c752619ad..e8ff416d3 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.FuncTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.FuncTests.cs @@ -1,7 +1,8 @@ -using Xunit.Abstractions; +#nullable disable +using Xunit.Abstractions; using static Terminal.Gui.ViewBase.Dim; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class DimFuncTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.PercentTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.PercentTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.PercentTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.PercentTests.cs index d2faa3af6..418a21547 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.PercentTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.PercentTests.cs @@ -1,9 +1,6 @@ -using System.Globalization; -using System.Text; -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; +#nullable disable -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class DimPercentTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.Tests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.Tests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.Tests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.Tests.cs index 81aace3fc..70587b225 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.Tests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.Tests.cs @@ -1,10 +1,8 @@ -using System.Globalization; -using System.Text; -using UnitTests; -using Xunit.Abstractions; +#nullable disable + using static Terminal.Gui.ViewBase.Dim; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; [Collection ("Global Test Setup")] public class DimTests diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.ViewTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.ViewTests.cs similarity index 93% rename from Tests/UnitTestsParallelizable/View/Layout/Dim.ViewTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.ViewTests.cs index 2bbbfa9f8..e78b53280 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.ViewTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Dim.ViewTests.cs @@ -1,6 +1,7 @@ -using static Terminal.Gui.ViewBase.Dim; +#nullable disable +using static Terminal.Gui.ViewBase.Dim; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class DimViewTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/FrameTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/FrameTests.cs index 1f9b24a90..7a416fb9b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/FrameTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class FrameTests { diff --git a/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsAtLocationTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsAtLocationTests.cs new file mode 100644 index 000000000..2abcd0c0d --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsAtLocationTests.cs @@ -0,0 +1,1223 @@ +#nullable enable + +namespace ViewBaseTests.Layout; + +[Trait ("Category", "Layout")] +public class GetViewsAtLocationTests +{ + private class TestView : View + { + public TestView (int x, int y, int w, int h, bool visible = true) + { + X = x; + Y = y; + Width = w; + Height = h; + base.Visible = visible; + } + } + + [Fact] + public void ReturnsEmpty_WhenRootIsNull () + { + List result = View.GetViewsAtLocation (null, new (0, 0)); + Assert.Empty (result); + } + + [Fact] + public void ReturnsEmpty_WhenRootIsNotVisible () + { + TestView root = new (0, 0, 10, 10, false); + List result = View.GetViewsAtLocation (root, new (5, 5)); + Assert.Empty (result); + } + + [Fact] + public void ReturnsEmpty_WhenPointOutsideRoot () + { + TestView root = new (0, 0, 10, 10); + List result = View.GetViewsAtLocation (root, new (20, 20)); + Assert.Empty (result); + } + + [Fact] + public void ReturnsEmpty_WhenPointOutsideRoot_AndSubview () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (5, 5, 2, 2); + root.Add (sub); + List result = View.GetViewsAtLocation (root, new (20, 20)); + Assert.Empty (result); + } + + [Fact] + public void ReturnsRoot_WhenPointInsideRoot_NoSubviews () + { + TestView root = new (0, 0, 10, 10); + List result = View.GetViewsAtLocation (root, new (5, 5)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Fact] + public void ReturnsRoot_And_Subview_WhenPointInsideRootMargin () + { + TestView root = new (0, 0, 10, 10); + root.Margin!.Thickness = new (1); + TestView sub = new (2, 2, 5, 5); + root.Add (sub); + List result = View.GetViewsAtLocation (root, new (3, 3)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + } + + [Fact] + public void ReturnsRoot_And_Subview_Border_WhenPointInsideRootMargin () + { + TestView root = new (0, 0, 10, 10); + root.Margin!.Thickness = new (1); + TestView sub = new (2, 2, 5, 5); + sub.BorderStyle = LineStyle.Dotted; + root.Add (sub); + List result = View.GetViewsAtLocation (root, new (3, 3)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Border, result [2]); + } + + [Fact] + public void ReturnsRoot_And_Margin_WhenPointInside_With_Margin () + { + TestView root = new (0, 0, 10, 10); + root.Margin!.Thickness = new (1); + List result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Margin, result [1]); + } + + [Fact] + public void ReturnsRoot_WhenPointOutsideSubview_With_Margin () + { + TestView root = new (0, 0, 10, 10); + root.Margin!.Thickness = new (1); + TestView sub = new (2, 2, 5, 5); + root.Add (sub); + List result = View.GetViewsAtLocation (root, new (2, 2)); + Assert.Single (result); + Assert.Equal (root, result [0]); + + result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Margin, result [1]); + + result = View.GetViewsAtLocation (root, new (1, 1)); + Assert.Single (result); + Assert.Equal (root, result [0]); + + result = View.GetViewsAtLocation (root, new (8, 8)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Fact] + public void ReturnsRoot_And_Border_WhenPointInside_With_Border () + { + TestView root = new (0, 0, 10, 10); + root.Border!.Thickness = new (1); + List result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Border, result [1]); + } + + [Fact] + public void ReturnsRoot_WhenPointOutsideSubview_With_Border () + { + TestView root = new (0, 0, 10, 10); + root.Border!.Thickness = new (1); + TestView sub = new (2, 2, 5, 5); + root.Add (sub); + List result = View.GetViewsAtLocation (root, new (2, 2)); + Assert.Single (result); + Assert.Equal (root, result [0]); + + result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Border, result [1]); + + result = View.GetViewsAtLocation (root, new (1, 1)); + Assert.Single (result); + Assert.Equal (root, result [0]); + + result = View.GetViewsAtLocation (root, new (8, 8)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Fact] + public void ReturnsRoot_And_Border_WhenPointInsideRootBorder () + { + TestView root = new (0, 0, 10, 10); + root.Border!.Thickness = new (1); + List result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Border, result [1]); + } + + [Fact] + public void ReturnsRoot_And_Padding_WhenPointInsideRootPadding () + { + TestView root = new (0, 0, 10, 10); + root.Padding!.Thickness = new (1); + List result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (root.Padding, result [1]); + } + + [Fact] + public void ReturnsRootAndSubview_WhenPointInsideSubview () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + root.Add (sub); + + List result = View.GetViewsAtLocation (root, new (3, 3)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + } + + [Fact] + public void ReturnsRootAndSubviewAndMargin_WhenPointInsideSubviewMargin () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.Margin!.Thickness = new (1); + root.Add (sub); + + List result = View.GetViewsAtLocation (root, new (6, 6)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Margin, result [2]); + } + + [Fact] + public void ReturnsRootAndSubviewAndBorder_WhenPointInsideSubviewBorder () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.Border!.Thickness = new (1); + root.Add (sub); + + List result = View.GetViewsAtLocation (root, new (2, 2)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Border, result [2]); + } + + [Fact] + public void ReturnsRootAndSubviewAndSubviewAndBorder_WhenPointInsideSubviewBorder () + { + TestView root = new (2, 2, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.Border!.Thickness = new (1); + root.Add (sub); + + List result = View.GetViewsAtLocation (root, new (4, 4)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Border, result [2]); + } + + [Fact] + public void ReturnsRootAndSubviewAndBorder_WhenPointInsideSubviewPadding () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.Padding!.Thickness = new (1); + root.Add (sub); + + List result = View.GetViewsAtLocation (root, new (2, 2)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Padding, result [2]); + } + + [Fact] + public void ReturnsRootAndSubviewAndMarginAndShadowView_WhenPointInsideSubviewMargin () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.ShadowStyle = ShadowStyle.Opaque; + root.Add (sub); + + root.Layout (); + + List result = View.GetViewsAtLocation (root, new (6, 6)); + Assert.Equal (5, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Margin, result [2]); + Assert.Equal (sub.Margin!.SubViews.ElementAt (0), result [3]); + Assert.Equal (sub.Margin!.SubViews.ElementAt (1), result [4]); + } + + [Fact] + public void ReturnsRootAndSubviewAndBorderAndButton_WhenPointInsideSubviewBorder () + { + TestView root = new (0, 0, 10, 10); + TestView sub = new (2, 2, 5, 5); + sub.Border!.Thickness = new (1); + + var closeButton = new Button + { + NoDecorations = true, + NoPadding = true, + Title = "X", + Width = 1, + Height = 1, + X = Pos.AnchorEnd (), + Y = 0, + ShadowStyle = ShadowStyle.None + }; + sub.Border!.Add (closeButton); + root.Add (sub); + + root.Layout (); + + List result = View.GetViewsAtLocation (root, new (6, 2)); + Assert.Equal (4, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub, result [1]); + Assert.Equal (sub.Border, result [2]); + Assert.Equal (closeButton, result [3]); + } + + [Fact] + public void ReturnsDeepestSubview_WhenNested () + { + TestView root = new (0, 0, 20, 20); + var sub1 = new TestView (2, 2, 16, 16); + var sub2 = new TestView (3, 3, 10, 10); + var sub3 = new TestView (1, 1, 5, 5); + root.Add (sub1); + sub1.Add (sub2); + sub2.Add (sub3); + + // Point inside all + List result = View.GetViewsAtLocation (root, new (7, 7)); + Assert.Equal (4, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub1, result [1]); + Assert.Equal (sub2, result [2]); + Assert.Equal (sub3, result [3]); + } + + [Fact] + public void ReturnsTopmostSubview_WhenOverlapping () + { + TestView root = new (0, 0, 10, 10); + var sub1 = new TestView (2, 2, 6, 6); + var sub2 = new TestView (4, 4, 6, 6); + root.Add (sub1); + root.Add (sub2); // sub2 is on top + + List result = View.GetViewsAtLocation (root, new (5, 5)); + Assert.Equal (3, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub1, result [1]); + Assert.Equal (sub2, result [2]); + } + + [Fact] + public void ReturnsTopmostSubview_WhenNotOverlapping () + { + TestView root = new (0, 0, 10, 10); // under 5,5, + var sub1 = new TestView (10, 10, 6, 6); // not under location 5,5 + var sub2 = new TestView (4, 4, 6, 6); // under 5,5, + root.Add (sub1); + root.Add (sub2); // sub2 is on top + + List result = View.GetViewsAtLocation (root, new (5, 5)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub2, result [1]); + } + + [Fact] + public void SkipsInvisibleSubviews () + { + TestView root = new (0, 0, 10, 10); + var sub1 = new TestView (2, 2, 6, 6, false); + var sub2 = new TestView (4, 4, 6, 6); + root.Add (sub1); + root.Add (sub2); + + List result = View.GetViewsAtLocation (root, new (5, 5)); + Assert.Equal (2, result.Count); + Assert.Equal (root, result [0]); + Assert.Equal (sub2, result [1]); + } + + [Fact] + public void ReturnsRoot_WhenPointOnEdge () + { + TestView root = new (0, 0, 10, 10); + List result = View.GetViewsAtLocation (root, new (0, 0)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Fact] + public void ReturnsRoot_WhenPointOnBottomRightCorner () + { + TestView root = new (0, 0, 10, 10); + List result = View.GetViewsAtLocation (root, new (9, 9)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Fact] + public void ReturnsEmpty_WhenAllSubviewsInvisible () + { + TestView root = new (0, 0, 10, 10); + var sub1 = new TestView (2, 2, 6, 6, false); + root.Add (sub1); + + List result = View.GetViewsAtLocation (root, new (3, 3)); + Assert.Single (result); + Assert.Equal (root, result [0]); + } + + [Theory] + [InlineData (0, 0, 0, 0, 0, -1, -1, new string [] { })] + [InlineData (0, 0, 0, 0, 0, 0, 0, new [] { "Top" })] + [InlineData (0, 0, 0, 0, 0, 1, 1, new [] { "Top" })] + [InlineData (0, 0, 0, 0, 0, 4, 4, new [] { "Top" })] + [InlineData (0, 0, 0, 0, 0, 9, 9, new [] { "Top" })] + [InlineData (0, 0, 0, 0, 0, 10, 10, new string [] { })] + [InlineData (1, 1, 0, 0, 0, -1, -1, new string [] { })] + [InlineData (1, 1, 0, 0, 0, 0, 0, new string [] { })] + [InlineData (1, 1, 0, 0, 0, 1, 1, new [] { "Top" })] + [InlineData (1, 1, 0, 0, 0, 4, 4, new [] { "Top" })] + [InlineData (1, 1, 0, 0, 0, 9, 9, new [] { "Top" })] + [InlineData (1, 1, 0, 0, 0, 10, 10, new [] { "Top" })] + [InlineData (0, 0, 1, 0, 0, -1, -1, new string [] { })] + [InlineData (0, 0, 1, 0, 0, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 0, 0, 1, 1, new [] { "Top" })] + [InlineData (0, 0, 1, 0, 0, 4, 4, new [] { "Top" })] + [InlineData (0, 0, 1, 0, 0, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 0, 0, 10, 10, new string [] { })] + [InlineData (0, 0, 1, 1, 0, -1, -1, new string [] { })] + [InlineData (0, 0, 1, 1, 0, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 1, 0, 1, 1, new [] { "Top", "Border" })] + [InlineData (0, 0, 1, 1, 0, 4, 4, new [] { "Top" })] + [InlineData (0, 0, 1, 1, 0, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 1, 0, 10, 10, new string [] { })] + [InlineData (0, 0, 1, 1, 1, -1, -1, new string [] { })] + [InlineData (0, 0, 1, 1, 1, 0, 0, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 1, 1, 1, 1, new [] { "Top", "Border" })] + [InlineData (0, 0, 1, 1, 1, 2, 2, new [] { "Top", "Padding" })] + [InlineData (0, 0, 1, 1, 1, 4, 4, new [] { "Top" })] + [InlineData (0, 0, 1, 1, 1, 9, 9, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (0, 0, 1, 1, 1, 10, 10, new string [] { })] + [InlineData (1, 1, 1, 0, 0, -1, -1, new string [] { })] + [InlineData (1, 1, 1, 0, 0, 0, 0, new string [] { })] + [InlineData (1, 1, 1, 0, 0, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (1, 1, 1, 0, 0, 4, 4, new [] { "Top" })] + [InlineData (1, 1, 1, 0, 0, 9, 9, new [] { "Top" })] + [InlineData (1, 1, 1, 0, 0, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (1, 1, 1, 1, 0, -1, -1, new string [] { })] + [InlineData (1, 1, 1, 1, 0, 0, 0, new string [] { })] + [InlineData (1, 1, 1, 1, 0, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (1, 1, 1, 1, 0, 4, 4, new [] { "Top" })] + [InlineData (1, 1, 1, 1, 0, 9, 9, new [] { "Top", "Border" })] + [InlineData (1, 1, 1, 1, 0, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (1, 1, 1, 1, 1, -1, -1, new string [] { })] + [InlineData (1, 1, 1, 1, 1, 0, 0, new string [] { })] + [InlineData (1, 1, 1, 1, 1, 1, 1, new string [] { })] //margin is ViewportSettings.TransparentToMouse + [InlineData (1, 1, 1, 1, 1, 2, 2, new [] { "Top", "Border" })] + [InlineData (1, 1, 1, 1, 1, 3, 3, new [] { "Top", "Padding" })] + [InlineData (1, 1, 1, 1, 1, 4, 4, new [] { "Top" })] + [InlineData (1, 1, 1, 1, 1, 8, 8, new [] { "Top", "Padding" })] + [InlineData (1, 1, 1, 1, 1, 9, 9, new [] { "Top", "Border" })] + [InlineData (1, 1, 1, 1, 1, 10, 10, new string [] { })] //margin is ViewportSettings.TransparentToMouse + public void Top_Adornments_Returns_Correct_View ( + int frameX, + int frameY, + int marginThickness, + int borderThickness, + int paddingThickness, + int testX, + int testY, + string [] expectedViewsFound + ) + { + // Arrange + Runnable? runnable = new () + { + Id = "Top", + Frame = new (frameX, frameY, 10, 10) + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + runnable.Margin!.Thickness = new (marginThickness); + runnable.Margin!.Id = "Margin"; + runnable.Border!.Thickness = new (borderThickness); + runnable.Border!.Id = "Border"; + runnable.Padding!.Thickness = new (paddingThickness); + runnable.Padding.Id = "Padding"; + + var location = new Point (testX, testY); + + // Act + List viewsUnderMouse = runnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + + // Assert + if (expectedViewsFound.Length == 0) + { + Assert.Empty (viewsUnderMouse); + } + else + { + string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); + Assert.Equal (expectedViewsFound, foundIds); + } + } + + [Theory] + [InlineData (0, 0)] + [InlineData (1, 1)] + [InlineData (2, 2)] + public void Returns_Top_If_No_SubViews (int testX, int testY) + { + // Arrange + Runnable? runnable = new () + { + Frame = new (0, 0, 10, 10) + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var location = new Point (testX, testY); + + // Act + List viewsUnderMouse = runnable.GetViewsUnderLocation (location, ViewportSettingsFlags.TransparentMouse); + + // Assert + Assert.Contains (viewsUnderMouse, v => v == runnable); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation returns the correct view if the start view has no subviews + [Theory] + [InlineData (0, 0)] + [InlineData (1, 1)] + [InlineData (2, 2)] + public void Returns_Start_If_No_SubViews (int testX, int testY) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + Assert.Same (runnable, runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault ()); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation returns the correct view if the start view has subviews + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (6, 7, false)] + [InlineData (1, 2, true)] + [InlineData (5, 6, true)] + public void Returns_Correct_If_SubViews (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var subview = new View + { + X = 1, Y = 2, + Width = 5, Height = 5 + }; + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (6, 7, false)] + [InlineData (1, 2, false)] + [InlineData (5, 6, false)] + public void Returns_Null_If_SubView_NotVisible (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + + var subview = new View + { + X = 1, Y = 2, + Width = 5, Height = 5, + Visible = false + }; + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (6, 7, false)] + [InlineData (1, 2, false)] + [InlineData (5, 6, false)] + public void Returns_Null_If_Not_Visible_And_SubView_Visible (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10, + Visible = false + }; + + var subview = new View + { + X = 1, Y = 2, + Width = 5, Height = 5 + }; + runnable.Add (subview); + subview.Visible = true; + Assert.True (subview.Visible); + Assert.False (runnable.Visible); + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation works if the start view has positive Adornments + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (7, 8, false)] + [InlineData (1, 2, false)] + [InlineData (2, 3, true)] + [InlineData (5, 6, true)] + [InlineData (6, 7, true)] + public void Returns_Correct_If_Start_Has_Adornments (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + runnable.Margin!.Thickness = new (1); + + var subview = new View + { + X = 1, Y = 2, + Width = 5, Height = 5 + }; + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation works if the start view has offset Viewport location + [Theory] + [InlineData (1, 0, 0, true)] + [InlineData (1, 1, 1, true)] + [InlineData (1, 2, 2, false)] + [InlineData (-1, 3, 3, true)] + [InlineData (-1, 2, 2, true)] + [InlineData (-1, 1, 1, false)] + [InlineData (-1, 0, 0, false)] + public void Returns_Correct_If_Start_Has_Offset_Viewport (int offset, int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10, + ViewportSettings = ViewportSettingsFlags.AllowNegativeLocation + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + runnable.Viewport = new (offset, offset, 10, 10); + + var subview = new View + { + X = 1, Y = 1, + Width = 2, Height = 2 + }; + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + [Theory] + [InlineData (9, 9, true)] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (10, 10, false)] + [InlineData (7, 8, false)] + [InlineData (1, 2, false)] + [InlineData (2, 3, false)] + [InlineData (5, 6, false)] + [InlineData (6, 7, false)] + public void Returns_Correct_If_Start_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + runnable.Padding!.Thickness = new (1); + + var subview = new View + { + X = Pos.AnchorEnd (1), Y = Pos.AnchorEnd (1), + Width = 1, Height = 1 + }; + runnable.Padding.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == subview); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, new string [] { })] + [InlineData (9, 9, new string [] { })] + [InlineData (1, 1, new [] { "Top", "Border" })] + [InlineData (8, 8, new [] { "Top", "Border" })] + [InlineData (2, 2, new [] { "Top", "Padding" })] + [InlineData (7, 7, new [] { "Top", "Padding" })] + [InlineData (5, 5, new [] { "Top" })] + public void Returns_Adornment_If_Start_Has_Adornments (int testX, int testY, string [] expectedViewsFound) + { + IApplication? app = Application.Create (); + + Runnable? runnable = new () + { + Id = "Top", + Width = 10, Height = 10 + }; + app.Begin (runnable); + + runnable.Margin!.Thickness = new (1); + runnable.Margin!.Id = "Margin"; + runnable.Border!.Thickness = new (1); + runnable.Border!.Id = "Border"; + runnable.Padding!.Thickness = new (1); + runnable.Padding.Id = "Padding"; + + var subview = new View + { + Id = "SubView", + X = 1, Y = 1, + Width = 1, Height = 1 + }; + runnable.Add (subview); + + List viewsUnderMouse = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); + + Assert.Equal (expectedViewsFound, foundIds); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation works if the subview has positive Adornments + [Theory] + [InlineData (0, 0, new [] { "Top" })] + [InlineData (1, 1, new [] { "Top" })] + [InlineData (9, 9, new [] { "Top" })] + [InlineData (10, 10, new string [] { })] + [InlineData (7, 8, new [] { "Top" })] + [InlineData (6, 7, new [] { "Top" })] + [InlineData (1, 2, new [] { "Top", "subview", "border" })] + [InlineData (5, 6, new [] { "Top", "subview", "border" })] + [InlineData (2, 3, new [] { "Top", "subview" })] + public void Returns_Correct_If_SubView_Has_Adornments (int testX, int testY, string [] expectedViewsFound) + { + Runnable? runnable = new () + { + Id = "Top", + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var subview = new View + { + Id = "subview", + X = 1, Y = 2, + Width = 5, Height = 5 + }; + subview.Border!.Thickness = new (1); + subview.Border!.Id = "border"; + runnable.Add (subview); + + List viewsUnderMouse = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); + + Assert.Equal (expectedViewsFound, foundIds); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation works if the subview has positive Adornments + [Theory] + [InlineData (0, 0, new [] { "Top" })] + [InlineData (1, 1, new [] { "Top" })] + [InlineData (9, 9, new [] { "Top" })] + [InlineData (10, 10, new string [] { })] + [InlineData (7, 8, new [] { "Top" })] + [InlineData (6, 7, new [] { "Top" })] + [InlineData (1, 2, new [] { "Top" })] + [InlineData (5, 6, new [] { "Top" })] + [InlineData (2, 3, new [] { "Top", "subview" })] + public void Returns_Correct_If_SubView_Has_Adornments_With_TransparentMouse (int testX, int testY, string [] expectedViewsFound) + { + Runnable? runnable = new () + { + Id = "Top", + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var subview = new View + { + Id = "subview", + X = 1, Y = 2, + Width = 5, Height = 5 + }; + subview.Border!.Thickness = new (1); + subview.Border!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; + subview.Border!.Id = "border"; + runnable.Add (subview); + + List viewsUnderMouse = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse); + string [] foundIds = viewsUnderMouse.Select (v => v!.Id).ToArray (); + + Assert.Equal (expectedViewsFound, foundIds); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (7, 8, false)] + [InlineData (6, 7, false)] + [InlineData (1, 2, false)] + [InlineData (5, 6, false)] + [InlineData (6, 5, false)] + [InlineData (5, 5, true)] + public void Returns_Correct_If_SubView_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + // A subview with + Padding + var subview = new View + { + X = 1, Y = 1, + Width = 5, Height = 5 + }; + subview.Padding!.Thickness = new (1); + + // This subview will be at the bottom-right-corner of subview + // So screen-relative location will be X + Width - 1 = 5 + var paddingSubView = new View + { + X = Pos.AnchorEnd (1), + Y = Pos.AnchorEnd (1), + Width = 1, + Height = 1 + }; + subview.Padding.Add (paddingSubView); + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == paddingSubView); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, false)] + [InlineData (1, 1, false)] + [InlineData (9, 9, false)] + [InlineData (10, 10, false)] + [InlineData (7, 8, false)] + [InlineData (6, 7, false)] + [InlineData (1, 2, false)] + [InlineData (5, 6, false)] + [InlineData (6, 5, false)] + [InlineData (5, 5, true)] + public void Returns_Correct_If_SubView_Is_Scrolled_And_Has_Adornment_WithSubView (int testX, int testY, bool expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + // A subview with + Padding + var subview = new View + { + X = 1, Y = 1, + Width = 5, Height = 5 + }; + subview.Padding!.Thickness = new (1); + + // Scroll the subview + subview.SetContentSize (new (10, 10)); + subview.Viewport = subview.Viewport with { Location = new (1, 1) }; + + // This subview will be at the bottom-right-corner of subview + // So screen-relative location will be X + Width - 1 = 5 + var paddingSubView = new View + { + X = Pos.AnchorEnd (1), + Y = Pos.AnchorEnd (1), + Width = 1, + Height = 1 + }; + subview.Padding.Add (paddingSubView); + runnable.Add (subview); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + + Assert.Equal (expectedSubViewFound, found == paddingSubView); + runnable.Dispose (); + } + + // Test that GetViewsUnderLocation works with nested subviews + [Theory] + [InlineData (0, 0, -1)] + [InlineData (9, 9, -1)] + [InlineData (10, 10, -1)] + [InlineData (1, 1, 0)] + [InlineData (1, 2, 0)] + [InlineData (2, 2, 1)] + [InlineData (3, 3, 2)] + [InlineData (5, 5, 2)] + public void Returns_Correct_With_NestedSubViews (int testX, int testY, int expectedSubViewFound) + { + Runnable? runnable = new () + { + Width = 10, Height = 10 + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var numSubViews = 3; + List subviews = new (); + + for (var i = 0; i < numSubViews; i++) + { + var subview = new View + { + X = 1, Y = 1, + Width = 5, Height = 5 + }; + subviews.Add (subview); + + if (i > 0) + { + subviews [i - 1].Add (subview); + } + } + + runnable.Add (subviews [0]); + + View? found = runnable.GetViewsUnderLocation (new (testX, testY), ViewportSettingsFlags.TransparentMouse).LastOrDefault (); + Assert.Equal (expectedSubViewFound, subviews.IndexOf (found!)); + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, new [] { "top" })] + [InlineData (9, 9, new [] { "top" })] + [InlineData (10, 10, new string [] { })] + [InlineData (1, 1, new [] { "top", "view" })] + [InlineData (1, 2, new [] { "top", "view" })] + [InlineData (2, 1, new [] { "top", "view" })] + [InlineData (2, 2, new [] { "top", "view", "subView" })] + [InlineData (3, 3, new [] { "top" })] // clipped + [InlineData (2, 3, new [] { "top" })] // clipped + public void Tiled_SubViews (int mouseX, int mouseY, string [] viewIdStrings) + { + // Arrange + Runnable? runnable = new () + { + Frame = new (0, 0, 10, 10), + Id = "top" + }; + IApplication? app = Application.Create (); + app.Begin (runnable); + + var view = new View + { + Id = "view", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 1,1 to 3,2 (screen) + + var subView = new View + { + Id = "subView", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 2,2 to 4,3 (screen) + view.Add (subView); + runnable.Add (view); + + List found = runnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + + string [] foundIds = found.Select (v => v!.Id).ToArray (); + + Assert.Equal (viewIdStrings, foundIds); + + runnable.Dispose (); + } + + [Theory] + [InlineData (0, 0, new [] { "top" })] + [InlineData (9, 9, new [] { "top" })] + [InlineData (10, 10, new string [] { })] + [InlineData (-1, -1, new string [] { })] + [InlineData (1, 1, new [] { "top", "view" })] + [InlineData (1, 2, new [] { "top", "view" })] + [InlineData (2, 1, new [] { "top", "view" })] + [InlineData (2, 2, new [] { "top", "view", "popover" })] + [InlineData (3, 3, new [] { "top" })] // clipped + [InlineData (2, 3, new [] { "top" })] // clipped + public void Popover (int mouseX, int mouseY, string [] viewIdStrings) + { + // Arrange + Runnable? runnable = new () + { + Frame = new (0, 0, 10, 10), + Id = "top" + }; + + IApplication? app = Application.Create (); + app.Begin (runnable); + + var view = new View + { + Id = "view", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 1,1 to 3,2 (screen) + + var popOver = new View + { + Id = "popover", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 2,2 to 4,3 (screen) + + view.Add (popOver); + runnable.Add (view); + + List found = runnable.GetViewsUnderLocation (new (mouseX, mouseY), ViewportSettingsFlags.TransparentMouse); + + string [] foundIds = found.Select (v => v!.Id).ToArray (); + + Assert.Equal (viewIdStrings, foundIds); + + runnable.Dispose (); + } + + [Fact] + public void Returns_TopRunnable_When_Point_Inside_Only_TopRunnable () + { + IApplication? app = Application.Create (); + + Runnable runnable = new () + { + Id = "topRunnable", + Frame = new (0, 0, 20, 20) + }; + + Runnable secondaryRunnable = new () + { + Id = "secondaryRunnable", + Frame = new (5, 5, 10, 10) + }; + secondaryRunnable.Margin!.Thickness = new (1); + secondaryRunnable.Layout (); + + app.Begin (runnable); + app.Begin (secondaryRunnable); + + List found = runnable.GetViewsUnderLocation (new (2, 2), ViewportSettingsFlags.TransparentMouse); + Assert.Contains (found, v => v?.Id == runnable.Id); + Assert.Contains (found, v => v == runnable); + + runnable.Dispose (); + secondaryRunnable.Dispose (); + } + + [Fact] + public void Returns_SecondaryRunnable_When_Point_Inside_Only_SecondaryRunnable () + { + IApplication? app = Application.Create (); + + Runnable runnable = new () + { + Id = "topRunnable", + Frame = new (0, 0, 20, 20) + }; + + Runnable secondaryRunnable = new () + { + Id = "secondaryRunnable", + Frame = new (5, 5, 10, 10) + }; + secondaryRunnable.Margin!.Thickness = new (1); + secondaryRunnable.Layout (); + + app.Begin (runnable); + app.Begin (secondaryRunnable); + + List found = runnable.GetViewsUnderLocation (new (7, 7), ViewportSettingsFlags.TransparentMouse); + Assert.Contains (found, v => v?.Id == secondaryRunnable.Id); + Assert.DoesNotContain (found, v => v?.Id == runnable.Id); + + runnable.Dispose (); + secondaryRunnable.Dispose (); + } + + [Fact] + public void Returns_Depends_On_Margin_ViewportSettings_When_Point_In_Margin_Of_SecondaryRunnable () + { + IApplication? app = Application.Create (); + + Runnable runnable = new () + { + Id = "topRunnable", + Frame = new (0, 0, 20, 20) + }; + + Runnable secondaryRunnable = new () + { + Id = "secondaryRunnable", + Frame = new (5, 5, 10, 10) + }; + secondaryRunnable.Margin!.Thickness = new (1); + + app.Begin (runnable); + app.Begin (secondaryRunnable); + + secondaryRunnable.Margin!.ViewportSettings = ViewportSettingsFlags.None; + + List found = runnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + Assert.Contains (found, v => v == secondaryRunnable); + Assert.Contains (found, v => v == secondaryRunnable.Margin); + Assert.DoesNotContain (found, v => v?.Id == runnable.Id); + + secondaryRunnable.Margin!.ViewportSettings = ViewportSettingsFlags.TransparentMouse; + found = runnable.GetViewsUnderLocation (new (5, 5), ViewportSettingsFlags.TransparentMouse); + Assert.DoesNotContain (found, v => v == secondaryRunnable); + Assert.DoesNotContain (found, v => v == secondaryRunnable.Margin); + Assert.Contains (found, v => v?.Id == runnable.Id); + + runnable.Dispose (); + secondaryRunnable.Dispose (); + } + + [Fact] + public void Returns_Empty_When_Point_Outside_All_Runnables () + { + IApplication? app = Application.Create (); + + Runnable runnable = new () + { + Id = "topRunnable", + Frame = new (0, 0, 20, 20) + }; + + Runnable secondaryRunnable = new () + { + Id = "secondaryRunnable", + Frame = new (5, 5, 10, 10) + }; + secondaryRunnable.Margin!.Thickness = new (1); + secondaryRunnable.Layout (); + + app.Begin (runnable); + app.Begin (secondaryRunnable); + + List found = runnable.GetViewsUnderLocation (new (20, 20), ViewportSettingsFlags.TransparentMouse); + Assert.Empty (found); + + runnable.Dispose (); + secondaryRunnable.Dispose (); + } +} diff --git a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationForRootTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationForRootTests.cs similarity index 95% rename from Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationForRootTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationForRootTests.cs index 34ef4d348..f8c467083 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationForRootTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationForRootTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewMouseTests; +namespace ViewBaseTests.Layout; [Trait ("Category", "Input")] public class GetViewsUnderLocationForRootTests @@ -8,7 +8,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsRoot_WhenPointInsideRoot_NoSubviews () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -19,7 +19,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsEmpty_WhenPointOutsideRoot () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -30,7 +30,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsSubview_WhenPointInsideSubview () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -49,7 +49,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsTop_WhenPointInsideSubview_With_TransparentMouse () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -73,7 +73,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsAdornment_WhenPointInMargin () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -87,7 +87,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void Returns_WhenPointIn_TransparentToMouseMargin_None () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -101,7 +101,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void Returns_WhenPointIn_NotTransparentToMouseMargin_Top_And_Margin () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -115,7 +115,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsAdornment_WhenPointInBorder () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -128,7 +128,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsAdornment_WhenPointInPadding () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -143,7 +143,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void HonorsIgnoreTransparentMouseParam () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10), ViewportSettings = ViewportSettingsFlags.TransparentMouse @@ -158,7 +158,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void ReturnsDeepestSubview_WhenNested () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -182,7 +182,7 @@ public class GetViewsUnderLocationForRootTests [Fact] public void Returns_Subview_WhenPointIn_TransparentToMouseMargin_Top () { - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 20, 20) }; @@ -216,7 +216,7 @@ public class GetViewsUnderLocationForRootTests public void Returns_Subview_Of_Adornment (string adornmentType) { // Arrange: top -> subView -> subView.[Adornment] -> adornmentSubView - Toplevel top = new () + Runnable top = new () { Frame = new (0, 0, 10, 10) }; @@ -277,7 +277,7 @@ public class GetViewsUnderLocationForRootTests public void Returns_OnlyParentsSuperView_Of_Adornment_If_TransparentMouse (string adornmentType) { // Arrange: top -> subView -> subView.[Adornment] -> adornmentSubView - Toplevel top = new () + Runnable top = new () { Id = "top", Frame = new (0, 0, 10, 10) diff --git a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationTests.cs index dd5cede33..6db388683 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/GetViewsUnderLocationTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/GetViewsUnderLocationTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewMouseTests; +namespace ViewBaseTests.Mouse; [Trait ("Category", "Input")] public class GetViewsUnderLocationTests diff --git a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/LayoutTests.cs similarity index 95% rename from Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/LayoutTests.cs index eccd03179..21ab20afc 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/LayoutTests.cs @@ -1,6 +1,5 @@ -using UnitTests.Parallelizable; - -namespace UnitTests_Parallelizable.LayoutTests; +#nullable enable +namespace ViewBaseTests.Layout; public class LayoutTests { @@ -36,6 +35,37 @@ public class LayoutTests #endregion Constructor Tests + + [Fact] + public void Screen_Size_Change_Causes_Layout () + { + IApplication? app = Application.Create (); + app.Init ("Fake"); + Runnable? runnable = new (); + app.Begin (runnable); + + var view = new View + { + X = 3, + Y = 2, + Width = 10, + Height = 1, + Text = "0123456789" + }; + runnable.Add (view); + + app.Driver!.SetScreenSize (80, 25); + + Assert.Equal (new (0, 0, 80, 25), new Rectangle (0, 0, app.Screen.Width, app.Screen.Height)); + Assert.Equal (new (0, 0, app.Screen.Width, app.Screen.Height), runnable.Frame); + Assert.Equal (new (0, 0, 80, 25), runnable.Frame); + + app.Driver!.SetScreenSize (20, 10); + app.LayoutAndDraw (); + Assert.Equal (new (0, 0, app.Screen.Width, app.Screen.Height), runnable.Frame); + + Assert.Equal (new (0, 0, 20, 10), runnable.Frame); + } [Fact] public void Set_All_Absolute_Sets_Correctly () { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.AbsoluteTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AbsoluteTests.cs similarity index 91% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.AbsoluteTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AbsoluteTests.cs index 9bdc21d1a..318a14408 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.AbsoluteTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AbsoluteTests.cs @@ -1,6 +1,7 @@ -using Xunit.Abstractions; +#nullable disable +using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosAbsoluteTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.AlignTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AlignTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.AlignTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AlignTests.cs index f0ef53409..ae67bbfff 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.AlignTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AlignTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosAlignTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.AnchorEndTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AnchorEndTests.cs similarity index 88% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.AnchorEndTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AnchorEndTests.cs index 767466209..a940a953c 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.AnchorEndTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.AnchorEndTests.cs @@ -1,7 +1,7 @@ -using Xunit.Abstractions; +#nullable disable using static Terminal.Gui.ViewBase.Pos; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosAnchorEndTests () { @@ -185,4 +185,28 @@ public class PosAnchorEndTests () Assert.Equal (7, result); } + + [Fact] + public void PosAnchorEnd_Equal_Inside_Window () + { + var viewWidth = 10; + var viewHeight = 1; + + var tv = new TextView + { + X = Pos.AnchorEnd (viewWidth), Y = Pos.AnchorEnd (viewHeight), Width = viewWidth, Height = viewHeight + }; + + var win = new Window { Width = 80, Height = 25 }; + + win.Add (tv); + + win.BeginInit (); + win.EndInit (); + win.Layout (); + + Assert.Equal (new (0, 0, 80, 25), win.Frame); + Assert.Equal (new (68, 22, 10, 1), tv.Frame); + win.Dispose (); + } } diff --git a/Tests/UnitTests/View/Layout/Pos.CenterTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs similarity index 74% rename from Tests/UnitTests/View/Layout/Pos.CenterTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs index 121df4da5..a7fc4783d 100644 --- a/Tests/UnitTests/View/Layout/Pos.CenterTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs @@ -1,16 +1,72 @@ using UnitTests; using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; using static Terminal.Gui.ViewBase.Pos; -namespace UnitTests.LayoutTests; +namespace ViewBaseTests.Layout; -public class PosCenterTests (ITestOutputHelper output) +public class PosCenterTests (ITestOutputHelper output) : FakeDriverBase { private readonly ITestOutputHelper _output = output; + [Fact] + public void PosCenter_Constructor () + { + var posCenter = new PosCenter (); + Assert.NotNull (posCenter); + } + + [Fact] + public void PosCenter_ToString () + { + var posCenter = new PosCenter (); + var expectedString = "Center"; + + Assert.Equal (expectedString, posCenter.ToString ()); + } + + [Fact] + public void PosCenter_GetAnchor () + { + var posCenter = new PosCenter (); + var width = 50; + int expectedAnchor = width / 2; + + Assert.Equal (expectedAnchor, posCenter.GetAnchor (width)); + } + + [Fact] + public void PosCenter_CreatesCorrectInstance () + { + Pos pos = Center (); + Assert.IsType (pos); + } + + [Theory] + [InlineData (10, 2, 4)] + [InlineData (10, 10, 0)] + [InlineData (10, 11, 0)] + [InlineData (10, 12, -1)] + [InlineData (19, 20, 0)] + public void PosCenter_Calculate_ReturnsExpectedValue (int superviewDimension, int width, int expectedX) + { + var posCenter = new PosCenter (); + int result = posCenter.Calculate (superviewDimension, new DimAbsolute (width), null!, Dimension.Width); + Assert.Equal (expectedX, result); + } + + [Fact] + public void PosCenter_Bigger_Than_SuperView () + { + var superView = new View { Width = 10, Height = 10 }; + var view = new View { X = Center (), Y = Center (), Width = 20, Height = 20 }; + superView.Add (view); + superView.LayoutSubViews (); + + Assert.Equal (-5, view.Frame.Left); + Assert.Equal (-5, view.Frame.Top); + } + [Theory] - [AutoInitShutdown] [InlineData (1)] [InlineData (2)] [InlineData (3)] @@ -23,7 +79,9 @@ public class PosCenterTests (ITestOutputHelper output) [InlineData (10)] public void PosCenter_SubView_85_Percent_Height (int height) { - var win = new Window { Width = Fill (), Height = Fill () }; + IDriver driver = CreateFakeDriver (20, height); + var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; + win.Driver = driver; var subview = new Window { @@ -31,23 +89,22 @@ public class PosCenterTests (ITestOutputHelper output) }; win.Add (subview); + win.BeginInit (); + win.EndInit (); + win.SetRelativeLayout (driver.Screen.Size); + win.LayoutSubViews (); + win.Draw (); - SessionToken rs = Application.Begin (win); - - Application.Driver!.SetScreenSize (20, height); - AutoInitShutdownAttribute.RunIteration (); var expected = string.Empty; switch (height) { case 1: - //Assert.Equal (new (0, 0, 17, 0), subview.Frame); expected = @" ────────────────────"; break; case 2: - //Assert.Equal (new (0, 0, 17, 1), subview.Frame); expected = @" ┌──────────────────┐ └──────────────────┘ @@ -55,7 +112,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 3: - //Assert.Equal (new (0, 0, 17, 2), subview.Frame); expected = @" ┌──────────────────┐ │ │ @@ -64,7 +120,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 4: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ ─────────────── │ @@ -73,7 +128,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 5: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ ┌─────────────┐ │ @@ -83,7 +137,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 6: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ ┌─────────────┐ │ @@ -94,7 +147,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 7: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ ┌─────────────┐ │ @@ -106,7 +158,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 8: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ ┌─────────────┐ │ @@ -119,7 +170,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 9: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ │ @@ -133,7 +183,6 @@ public class PosCenterTests (ITestOutputHelper output) break; case 10: - //Assert.Equal (new (0, 0, 17, 3), subview.Frame); expected = @" ┌──────────────────┐ │ │ @@ -150,13 +199,12 @@ public class PosCenterTests (ITestOutputHelper output) break; } - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output); - Application.End (rs); + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output, driver); win.Dispose (); + driver.End (); } [Theory] - [AutoInitShutdown] [InlineData (1)] [InlineData (2)] [InlineData (3)] @@ -169,7 +217,9 @@ public class PosCenterTests (ITestOutputHelper output) [InlineData (10)] public void PosCenter_SubView_85_Percent_Width (int width) { - var win = new Window { Width = Fill (), Height = Fill () }; + IDriver driver = CreateFakeDriver (width, 7); + var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; + win.Driver = driver; var subview = new Window { @@ -177,11 +227,12 @@ public class PosCenterTests (ITestOutputHelper output) }; win.Add (subview); + win.BeginInit (); + win.EndInit (); + win.SetRelativeLayout (driver.Screen.Size); + win.LayoutSubViews (); + win.Draw (); - SessionToken rs = Application.Begin (win); - - Application.Driver!.SetScreenSize (width, 7); - AutoInitShutdownAttribute.RunIteration (); var expected = string.Empty; switch (width) @@ -319,8 +370,8 @@ public class PosCenterTests (ITestOutputHelper output) break; } - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output); - Application.End (rs); + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output, driver); win.Dispose (); + driver.End (); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CombineTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CombineTests.cs new file mode 100644 index 000000000..f23a665f6 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CombineTests.cs @@ -0,0 +1,139 @@ +#nullable disable + +using Xunit.Abstractions; +using static Terminal.Gui.ViewBase.Dim; +using static Terminal.Gui.ViewBase.Pos; + +namespace ViewBaseTests.Layout; + +public class PosCombineTests (ITestOutputHelper output) +{ + private readonly ITestOutputHelper _output = output; + + // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved + // TODO: A new test that calls SetRelativeLayout directly is needed. + [Fact] + public void PosCombine_Referencing_Same_View () + { + var super = new View { Width = 10, Height = 10, Text = "super" }; + var view1 = new View { Width = 2, Height = 2, Text = "view1" }; + var view2 = new View { Width = 2, Height = 2, Text = "view2" }; + view2.X = AnchorEnd (0) - (Right (view2) - Left (view2)); + + super.Add (view1, view2); + super.BeginInit (); + super.EndInit (); + + Exception exception = Record.Exception (super.LayoutSubViews); + Assert.Null (exception); + Assert.Equal (new (0, 0, 10, 10), super.Frame); + Assert.Equal (new (0, 0, 2, 2), view1.Frame); + Assert.Equal (new (8, 0, 2, 2), view2.Frame); + + super.Dispose (); + } + + [Fact] + public void PosCombine_DimCombine_View_With_SubViews () + { + IApplication app = Application.Create (); + Runnable runnable = new () { Width = 80, Height = 25 }; + app.Begin (runnable); + var win1 = new Window { Id = "win1", Width = 20, Height = 10 }; + + var view1 = new View + { + Text = "view1", + Width = Auto (DimAutoStyle.Text), + Height = Auto (DimAutoStyle.Text) + }; + var win2 = new Window { Id = "win2", Y = Bottom (view1) + 1, Width = 10, Height = 3 }; + var view2 = new View { Id = "view2", Width = Fill (), Height = 1, CanFocus = true }; + + var view3 = new View { Id = "view3", Width = Fill (1), Height = 1, CanFocus = true }; + + view2.Add (view3); + win2.Add (view2); + win1.Add (view1, win2); + runnable.Add (win1); + + Assert.Equal (new (0, 0, 80, 25), runnable.Frame); + Assert.Equal (new (0, 0, 5, 1), view1.Frame); + Assert.Equal (new (0, 0, 20, 10), win1.Frame); + Assert.Equal (new (0, 2, 10, 3), win2.Frame); + Assert.Equal (new (0, 0, 8, 1), view2.Frame); + Assert.Equal (new (0, 0, 7, 1), view3.Frame); + View foundView = runnable.GetViewsUnderLocation (new (9, 4), ViewportSettingsFlags.None).LastOrDefault (); + Assert.Equal (foundView, view2); + runnable.Dispose (); + } + + [Fact] + public void PosCombine_Will_Throws () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + var t = new Runnable (); + + var w = new Window { X = Left (t) + 2, Y = Top (t) + 2 }; + var f = new FrameView (); + var v1 = new View { X = Left (w) + 2, Y = Top (w) + 2 }; + var v2 = new View { X = Left (v1) + 2, Y = Top (v1) + 2 }; + + f.Add (v1); // v2 not added + w.Add (f); + t.Add (w); + + f.X = X (v2) - X (v1); + f.Y = Y (v2) - Y (v1); + + app.StopAfterFirstIteration = true; + Assert.Throws (() => app.Run (t)); + t.Dispose (); + v2.Dispose (); + app.Dispose (); + } + + [Fact] + public void PosCombine_Refs_SuperView_Throws () + { + IApplication app = Application.Create (); + app.Init ("fake"); + + var top = new Runnable (); + var w = new Window { X = Left (top) + 2, Y = Top (top) + 2 }; + var f = new FrameView (); + var v1 = new View { X = Left (w) + 2, Y = Top (w) + 2 }; + var v2 = new View { X = Left (v1) + 2, Y = Top (v1) + 2 }; + + f.Add (v1, v2); + w.Add (f); + top.Add (w); + SessionToken token = app.Begin (top); + + f.X = X (app.TopRunnableView) + X (v2) - X (v1); + f.Y = Y (app.TopRunnableView) + Y (v2) - Y (v1); + + app.TopRunnableView!.SubViewsLaidOut += (s, e) => + { + Assert.Equal (0, app.TopRunnableView.Frame.X); + Assert.Equal (0, app.TopRunnableView.Frame.Y); + Assert.Equal (2, w.Frame.X); + Assert.Equal (2, w.Frame.Y); + Assert.Equal (2, f.Frame.X); + Assert.Equal (2, f.Frame.Y); + Assert.Equal (4, v1.Frame.X); + Assert.Equal (4, v1.Frame.Y); + Assert.Equal (6, v2.Frame.X); + Assert.Equal (6, v2.Frame.Y); + }; + + app.StopAfterFirstIteration = true; + + Assert.Throws (() => app.Run (top)); + app.TopRunnableView?.Dispose (); + top.Dispose (); + app.Dispose (); + } +} diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.FuncTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.FuncTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.FuncTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.FuncTests.cs index adc67dd60..082deac75 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.FuncTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.FuncTests.cs @@ -1,6 +1,7 @@ -using Xunit.Abstractions; +#nullable disable +using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosFuncTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.PercentTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.PercentTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.PercentTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.PercentTests.cs index 65df904ee..c3528ed9e 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.PercentTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.PercentTests.cs @@ -1,7 +1,7 @@ using Xunit.Abstractions; using static Terminal.Gui.ViewBase.Pos; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosPercentTests (ITestOutputHelper output) { @@ -39,7 +39,7 @@ public class PosPercentTests (ITestOutputHelper output) }; container.Add (view); - var top = new Toplevel (); + var top = new Runnable (); top.Add (container); top.LayoutSubViews (); diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.Tests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.Tests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.Tests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.Tests.cs index 392140f3e..dde6eaa4b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.Tests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.Tests.cs @@ -1,4 +1,5 @@ -namespace UnitTests_Parallelizable.LayoutTests; +#nullable disable +namespace ViewBaseTests.Layout; public class PosTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/Pos.ViewTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.ViewTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/Pos.ViewTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.ViewTests.cs index efac33e90..fc3e99508 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Pos.ViewTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.ViewTests.cs @@ -1,6 +1,6 @@ using static Terminal.Gui.ViewBase.Pos; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class PosViewTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/ScreenToTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/ScreenToTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/ScreenToTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/ScreenToTests.cs index 5b6c151b2..58f01b45b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ScreenToTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/ScreenToTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; /// Tests for view coordinate mapping (e.g. etc...). public class ScreenToTests diff --git a/Tests/UnitTestsParallelizable/View/Layout/SetRelativeLayoutTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/SetRelativeLayoutTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Layout/SetRelativeLayoutTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/SetRelativeLayoutTests.cs index a10f9d6df..b7e35fbcc 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/SetRelativeLayoutTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/SetRelativeLayoutTests.cs @@ -1,9 +1,6 @@ -using JetBrains.Annotations; -using UnitTests; -using Xunit.Abstractions; -using static Terminal.Gui.ViewBase.Dim; +using static Terminal.Gui.ViewBase.Dim; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class SetRelativeLayoutTests { @@ -404,7 +401,7 @@ public class SetRelativeLayoutTests }; view.X = Pos.AnchorEnd (0) - Pos.Func (GetViewWidth); - int GetViewWidth ([CanBeNull] View _) { return view.Frame.Width; } + int GetViewWidth (View? _) { return view.Frame.Width; } // view will be 3 chars wide. It's X will be 27 (30 - 3). // BUGBUG: IsInitialized need to be true before calculate diff --git a/Tests/UnitTestsParallelizable/View/Layout/ToScreenTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/ToScreenTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/ToScreenTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/ToScreenTests.cs index e005b7ca9..d426aa24e 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ToScreenTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/ToScreenTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; /// /// Test the and methods. diff --git a/Tests/UnitTestsParallelizable/View/Layout/TopologicalSortTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/TopologicalSortTests.cs similarity index 97% rename from Tests/UnitTestsParallelizable/View/Layout/TopologicalSortTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/TopologicalSortTests.cs index e2fe774a4..edae0dde7 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/TopologicalSortTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/TopologicalSortTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.LayoutTests; +namespace ViewBaseTests.Layout; public class TopologicalSortTests (ITestOutputHelper output) { diff --git a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewLayoutEventTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/ViewLayoutEventTests.cs index c606abbaa..d6b24bb39 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewLayoutEventTests.cs @@ -1,7 +1,7 @@ #nullable enable using UnitTests.Parallelizable; -namespace UnitTests_Parallelizable.ViewLayoutEventTests; +namespace ViewBaseTests.Layout; public class ViewLayoutEventTests { diff --git a/Tests/UnitTestsParallelizable/View/Layout/ViewportTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Layout/ViewportTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs index 123ff90c2..a60ba318b 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/ViewportTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs @@ -1,6 +1,6 @@ using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Viewport; /// /// Test the . diff --git a/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseDragTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseDragTests.cs new file mode 100644 index 000000000..8307ada94 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseDragTests.cs @@ -0,0 +1,667 @@ +namespace ViewBaseTests.Mouse; + +/// +/// Parallelizable tests for mouse drag functionality on movable and resizable views. +/// These tests validate mouse drag behavior without Application.Init or global state. +/// +[Trait ("Category", "Input")] +public class MouseDragTests +{ + #region Movable View Drag Tests + + [Fact] + public void MovableView_MouseDrag_UpdatesPosition () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50, + App = app + }; + + View movableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single, + App = app + }; + + superView.Add (movableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on border to start drag + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (10, 10), // Screen position + Flags = MouseFlags.Button1Pressed + }; + + // Act - Start drag + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (15, 15), // New screen position + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - View should have moved + Assert.Equal (15, movableView.Frame.X); + Assert.Equal (15, movableView.Frame.Y); + Assert.Equal (10, movableView.Frame.Width); + Assert.Equal (10, movableView.Frame.Height); + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void MovableView_MouseDrag_WithSuperview_UsesCorrectCoordinates () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + X = 5, + Y = 5, + Width = 50, + Height = 50 + }; + + View movableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single + }; + + superView.Add (movableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on border + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (15, 15), // 5+10 offset + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (18, 18), // Moved 3,3 + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - View should have moved relative to superview + Assert.Equal (13, movableView.Frame.X); // 10 + 3 + Assert.Equal (13, movableView.Frame.Y); // 10 + 3 + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void MovableView_MouseRelease_StopsDrag () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View movableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.Movable, + BorderStyle = LineStyle.Single + }; + + superView.Add (movableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Start drag + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (10, 10), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Drag + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (15, 15), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Release + MouseEventArgs releaseEvent = new () + { + ScreenPosition = new (15, 15), + Flags = MouseFlags.Button1Released + }; + + app.Mouse.RaiseMouseEvent (releaseEvent); + + // Assert - Position should remain at dragged location + Assert.Equal (15, movableView.Frame.X); + Assert.Equal (15, movableView.Frame.Y); + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + #endregion + + #region Resizable View Drag Tests + + [Fact] + public void ResizableView_RightResize_Drag_IncreasesWidth () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.RightResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on right border + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (19, 15), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag to the right + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (24, 15), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Width should have increased + Assert.Equal (10, resizableView.Frame.X); // X unchanged + Assert.Equal (10, resizableView.Frame.Y); // Y unchanged + Assert.Equal (15, resizableView.Frame.Width); // Width increased by 5 + Assert.Equal (10, resizableView.Frame.Height); // Height unchanged + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void ResizableView_BottomResize_Drag_IncreasesHeight () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.BottomResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on bottom border + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (15, 19), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag down + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (15, 24), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Height should have increased + Assert.Equal (10, resizableView.Frame.X); // X unchanged + Assert.Equal (10, resizableView.Frame.Y); // Y unchanged + Assert.Equal (10, resizableView.Frame.Width); // Width unchanged + Assert.Equal (15, resizableView.Frame.Height); // Height increased by 5 + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void ResizableView_LeftResize_Drag_MovesAndResizes () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.LeftResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on left border + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (10, 15), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag to the left + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (7, 15), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Should move left and resize + Assert.Equal (7, resizableView.Frame.X); // X moved left by 3 + Assert.Equal (10, resizableView.Frame.Y); // Y unchanged + Assert.Equal (13, resizableView.Frame.Width); // Width increased by 3 + Assert.Equal (10, resizableView.Frame.Height); // Height unchanged + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void ResizableView_TopResize_Drag_MovesAndResizes () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.TopResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on top border + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (15, 10), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag up + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (15, 8), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Should move up and resize + Assert.Equal (10, resizableView.Frame.X); // X unchanged + Assert.Equal (8, resizableView.Frame.Y); // Y moved up by 2 + Assert.Equal (10, resizableView.Frame.Width); // Width unchanged + Assert.Equal (12, resizableView.Frame.Height); // Height increased by 2 + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + #endregion + + #region Corner Resize Tests + + [Fact] + public void ResizableView_BottomRightCornerResize_Drag_ResizesBothDimensions () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.BottomResizable | ViewArrangement.RightResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on bottom-right corner + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (19, 19), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag diagonally + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (24, 24), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Both dimensions should increase + Assert.Equal (10, resizableView.Frame.X); // X unchanged + Assert.Equal (10, resizableView.Frame.Y); // Y unchanged + Assert.Equal (15, resizableView.Frame.Width); // Width increased by 5 + Assert.Equal (15, resizableView.Frame.Height); // Height increased by 5 + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void ResizableView_TopLeftCornerResize_Drag_MovesAndResizes () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.TopResizable | ViewArrangement.LeftResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Simulate mouse press on top-left corner + MouseEventArgs pressEvent = new () + { + ScreenPosition = new (10, 10), + Flags = MouseFlags.Button1Pressed + }; + + app.Mouse.RaiseMouseEvent (pressEvent); + + // Simulate mouse drag diagonally up and left + MouseEventArgs dragEvent = new () + { + ScreenPosition = new (7, 8), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + app.Mouse.RaiseMouseEvent (dragEvent); + + // Assert - Should move and resize + Assert.Equal (7, resizableView.Frame.X); // X moved left by 3 + Assert.Equal (8, resizableView.Frame.Y); // Y moved up by 2 + Assert.Equal (13, resizableView.Frame.Width); // Width increased by 3 + Assert.Equal (12, resizableView.Frame.Height); // Height increased by 2 + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + #endregion + + #region Minimum Size Constraints + + [Fact] + public void ResizableView_Drag_RespectsMinimumWidth () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.LeftResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Try to drag far to the right (making width very small) + MouseEventArgs dragEvent = new () + { + Position = new (8, 5), // Drag 8 units right, would make width 2 + ScreenPosition = new (18, 15), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + // Act + resizableView.Border!.HandleDragOperation (dragEvent); + + // Assert - Width should be constrained to minimum + // Minimum width = border thickness + margin right + int expectedMinWidth = resizableView.Border!.Thickness.Horizontal + resizableView.Margin!.Thickness.Right; + Assert.True (resizableView.Frame.Width >= expectedMinWidth); + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + [Fact] + public void ResizableView_Drag_RespectsMinimumHeight () + { + // Arrange + using IApplication app = Application.Create (); + app.Init ("fake"); + + View superView = new () + { + Width = 50, + Height = 50 + }; + + View resizableView = new () + { + X = 10, + Y = 10, + Width = 10, + Height = 10, + Arrangement = ViewArrangement.TopResizable, + BorderStyle = LineStyle.Single + }; + + superView.Add (resizableView); + + // Add to a runnable so the views are part of the application + var runnable = new Runnable { App = app, Frame = new (0, 0, 80, 25) }; + runnable.Add (superView); + app.Begin (runnable); + + // Try to drag far down (making height very small) + MouseEventArgs dragEvent = new () + { + Position = new (5, 8), // Drag 8 units down, would make height 2 + ScreenPosition = new (15, 18), + Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition + }; + + // Act + resizableView.Border!.HandleDragOperation (dragEvent); + + // Assert - Height should be constrained to minimum + int expectedMinHeight = resizableView.Border!.Thickness.Vertical + resizableView.Margin!.Thickness.Bottom; + Assert.True (resizableView.Frame.Height >= expectedMinHeight); + + app.End (app.SessionStack!.First ()); + runnable.Dispose (); + superView.Dispose (); + } + + #endregion +} diff --git a/Tests/UnitTestsParallelizable/View/Mouse/MouseEnterLeaveTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEnterLeaveTests.cs similarity index 97% rename from Tests/UnitTestsParallelizable/View/Mouse/MouseEnterLeaveTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEnterLeaveTests.cs index bd034084b..dabf62225 100644 --- a/Tests/UnitTestsParallelizable/View/Mouse/MouseEnterLeaveTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEnterLeaveTests.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace UnitTests_Parallelizable.ViewMouseTests; +namespace ViewBaseTests.Mouse; [Trait ("Category", "Input")] public class MouseEnterLeaveTests @@ -40,7 +40,7 @@ public class MouseEnterLeaveTests public bool MouseEnterRaised { get; private set; } public bool MouseLeaveRaised { get; private set; } - private void OnMouseEnterHandler (object s, CancelEventArgs e) + private void OnMouseEnterHandler (object? s, CancelEventArgs e) { MouseEnterRaised = true; @@ -50,7 +50,7 @@ public class MouseEnterLeaveTests } } - private void OnMouseLeaveHandler (object s, EventArgs e) { MouseLeaveRaised = true; } + private void OnMouseLeaveHandler (object? s, EventArgs e) { MouseLeaveRaised = true; } } [Fact] diff --git a/Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEventRoutingTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEventRoutingTests.cs index 8f9453832..5f8450efa 100644 --- a/Tests/UnitTestsParallelizable/View/Mouse/MouseEventRoutingTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseEventRoutingTests.cs @@ -1,7 +1,7 @@ using Terminal.Gui.App; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ApplicationTests; +namespace ApplicationTests; /// /// Parallelizable tests for mouse event routing and coordinate transformation. diff --git a/Tests/UnitTestsParallelizable/View/Mouse/MouseTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/Mouse/MouseTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseTests.cs index 9091312c4..9fa3d824f 100644 --- a/Tests/UnitTestsParallelizable/View/Mouse/MouseTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Mouse/MouseTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewMouseTests; +namespace ViewBaseTests.Mouse; [Collection ("Global Test Setup")] @@ -106,7 +106,7 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews [MemberData (nameof (AllViewTypes))] public void AllViews_NewMouseEvent_Enabled_False_Does_Not_Set_Handled (Type viewType) { - View view = CreateInstanceIfNotGeneric (viewType); + View? view = CreateInstanceIfNotGeneric (viewType); if (view == null) { @@ -126,7 +126,7 @@ public class MouseTests (ITestOutputHelper output) : TestsAllViews [MemberData (nameof (AllViewTypes))] public void AllViews_NewMouseEvent_Clicked_Enabled_False_Does_Not_Set_Handled (Type viewType) { - View view = CreateInstanceIfNotGeneric (viewType); + View? view = CreateInstanceIfNotGeneric (viewType); if (view == null) { diff --git a/Tests/UnitTestsParallelizable/View/Navigation/AddRemoveTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AddRemoveTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Navigation/AddRemoveTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/AddRemoveTests.cs index 03fe076a9..bc8e4b60d 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/AddRemoveTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AddRemoveTests.cs @@ -1,7 +1,6 @@ using UnitTests; -using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; [Collection ("Global Test Setup")] public class AddRemoveNavigationTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AdvanceFocusTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/AdvanceFocusTests.cs index 3c52f3ad9..bdb18b0f3 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AdvanceFocusTests.cs @@ -1,7 +1,4 @@ -using System.Runtime.Intrinsics; -using Xunit.Abstractions; - -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; public class AdvanceFocusTests () { diff --git a/Tests/UnitTests/View/Navigation/NavigationTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AllViewsNavigationTests.cs similarity index 64% rename from Tests/UnitTests/View/Navigation/NavigationTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/AllViewsNavigationTests.cs index d52d5a0ef..4549f79e6 100644 --- a/Tests/UnitTests/View/Navigation/NavigationTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/AllViewsNavigationTests.cs @@ -1,16 +1,16 @@ -using UnitTests; +#nullable enable +using UnitTests; using Xunit.Abstractions; -namespace UnitTests.ViewTests; +namespace ViewBaseTests.Navigation; -public class NavigationTests (ITestOutputHelper output) : TestsAllViews +public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews { [Theory] [MemberData (nameof (AllViewTypes))] - [SetupFakeApplication] // SetupFakeDriver resets app state; helps to avoid test pollution public void AllViews_AtLeastOneNavKey_Advances (Type viewType) { - View view = CreateInstanceIfNotGeneric (viewType); + View? view = CreateInstanceIfNotGeneric (viewType); if (view == null) { @@ -26,8 +26,8 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews return; } - Toplevel top = new (); - Application.TopRunnable = top; + IApplication app = Application.Create (); + app.Begin (new Runnable () { CanFocus = true }); View otherView = new () { @@ -36,7 +36,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews TabStop = view.TabStop == TabBehavior.NoStop ? TabBehavior.TabStop : view.TabStop }; - top.Add (view, otherView); + app.TopRunnableView!.Add (view, otherView); // Start with the focus on our test view view.SetFocus (); @@ -57,17 +57,17 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews case TabBehavior.TabStop: case TabBehavior.NoStop: case TabBehavior.TabGroup: - Application.RaiseKeyDownEvent (key); + app.Keyboard.RaiseKeyDownEvent (key); if (view.HasFocus) { // Try once more (HexView) - Application.RaiseKeyDownEvent (key); + app.Keyboard.RaiseKeyDownEvent (key); } break; default: - Application.RaiseKeyDownEvent (Key.Tab); + app.Keyboard.RaiseKeyDownEvent (Key.Tab); break; } @@ -83,17 +83,14 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews output.WriteLine ($"{view.GetType ().Name} - {key} did not Leave."); } - top.Dispose (); - Assert.True (left); } [Theory] [MemberData (nameof (AllViewTypes))] - [SetupFakeApplication] // SetupFakeDriver resets app state; helps to avoid test pollution public void AllViews_HasFocus_Changed_Event (Type viewType) { - View view = CreateInstanceIfNotGeneric (viewType); + View? view = CreateInstanceIfNotGeneric (viewType); if (view == null) { @@ -109,15 +106,15 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews return; } - if (view is Toplevel && ((Toplevel)view).Modal) + if (view is IRunnable) { - output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + output.WriteLine ($"Ignoring {viewType} - It's an IRunnable"); return; } - Toplevel top = new (); - Application.TopRunnable = top; + IApplication app = Application.Create (); + app.Begin (new Runnable () { CanFocus = true }); View otherView = new () { @@ -129,34 +126,38 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews var hasFocusTrue = 0; var hasFocusFalse = 0; - view.HasFocusChanged += (s, e) => - { - if (e.NewValue) - { - hasFocusTrue++; - } - else - { - hasFocusFalse++; - } - }; - - top.Add (view, otherView); - Assert.False (view.HasFocus); - Assert.False (otherView.HasFocus); - // Ensure the view is Visible view.Visible = true; + view.HasFocus = false; - Application.TopRunnable.SetFocus (); - Assert.True (Application.TopRunnable!.HasFocus); - Assert.True (top.HasFocus); + view.HasFocusChanged += (s, e) => + { + if (e.NewValue) + { + hasFocusTrue++; + } + else + { + hasFocusFalse++; + } + }; - // Start with the focus on our test view - Assert.True (view.HasFocus); + Assert.Equal (0, hasFocusTrue); + Assert.Equal (0, hasFocusFalse); + + app.TopRunnableView!.Add (view, otherView); + Assert.False (view.HasFocus); + Assert.True (otherView.HasFocus); Assert.Equal (1, hasFocusTrue); - Assert.Equal (0, hasFocusFalse); + Assert.Equal (1, hasFocusFalse); + + // Start with the focus on our test view + view.SetFocus (); + Assert.True (view.HasFocus); + + Assert.Equal (2, hasFocusTrue); + Assert.Equal (1, hasFocusFalse); // Use keyboard to navigate to next view (otherView). var tries = 0; @@ -173,21 +174,19 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews case null: case TabBehavior.NoStop: case TabBehavior.TabStop: - if (Application.RaiseKeyDownEvent (Key.Tab)) + if (app.Keyboard.RaiseKeyDownEvent (Key.Tab)) { if (view.HasFocus) { // Try another nav key (e.g. for TextView that eats Tab) - Application.RaiseKeyDownEvent (Key.CursorDown); + app.Keyboard.RaiseKeyDownEvent (Key.CursorDown); } - } - - ; + }; break; case TabBehavior.TabGroup: - Application.RaiseKeyDownEvent (Key.F6); + app.Keyboard.RaiseKeyDownEvent (Key.F6); break; default: @@ -195,8 +194,8 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews } } - Assert.Equal (1, hasFocusTrue); - Assert.Equal (1, hasFocusFalse); + Assert.Equal (2, hasFocusTrue); + Assert.Equal (2, hasFocusFalse); Assert.False (view.HasFocus); Assert.True (otherView.HasFocus); @@ -209,26 +208,26 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews break; case TabBehavior.TabStop: - Application.RaiseKeyDownEvent (Key.Tab); + app.Keyboard.RaiseKeyDownEvent (Key.Tab); break; case TabBehavior.TabGroup: - if (!Application.RaiseKeyDownEvent (Key.F6)) + if (!app.Keyboard.RaiseKeyDownEvent (Key.F6)) { view.SetFocus (); } break; case null: - Application.RaiseKeyDownEvent (Key.Tab); + app.Keyboard.RaiseKeyDownEvent (Key.Tab); break; default: throw new ArgumentOutOfRangeException (); } - Assert.Equal (2, hasFocusTrue); - Assert.Equal (1, hasFocusFalse); + Assert.Equal (3, hasFocusTrue); + Assert.Equal (2, hasFocusFalse); Assert.True (view.HasFocus); Assert.False (otherView.HasFocus); @@ -241,21 +240,18 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews int enterCount = hasFocusTrue; int leaveCount = hasFocusFalse; - top.Dispose (); - Assert.False (otherViewHasFocus); Assert.True (viewHasFocus); - Assert.Equal (2, enterCount); - Assert.Equal (1, leaveCount); + Assert.Equal (3, enterCount); + Assert.Equal (2, leaveCount); } [Theory] [MemberData (nameof (AllViewTypes))] - [SetupFakeApplication] // SetupFakeDriver resets app state; helps to avoid test pollution public void AllViews_Visible_False_No_HasFocus_Events (Type viewType) { - View view = CreateInstanceIfNotGeneric (viewType); + View? view = CreateInstanceIfNotGeneric (viewType); if (view == null) { @@ -271,16 +267,15 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews return; } - if (view is Toplevel && ((Toplevel)view).Modal) + if (view is IRunnable) { - output.WriteLine ($"Ignoring {viewType} - It's a Modal Toplevel"); + output.WriteLine ($"Ignoring {viewType} - It's an IRunnable"); return; } - Toplevel top = new (); - - Application.TopRunnable = top; + IApplication? app = Application.Create (); + app.Begin (new Runnable () { CanFocus = true }); View otherView = new () { @@ -295,7 +290,7 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews view.HasFocusChanging += (s, e) => hasFocusChangingCount++; view.HasFocusChanged += (s, e) => hasFocusChangedCount++; - top.Add (view, otherView); + app.TopRunnableView!.Add (view, otherView); // Start with the focus on our test view view.SetFocus (); @@ -303,21 +298,18 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews Assert.Equal (0, hasFocusChangingCount); Assert.Equal (0, hasFocusChangedCount); - Application.RaiseKeyDownEvent (Key.Tab); + app.Keyboard.RaiseKeyDownEvent (Key.Tab); Assert.Equal (0, hasFocusChangingCount); Assert.Equal (0, hasFocusChangedCount); - Application.RaiseKeyDownEvent (Key.F6); + app.Keyboard.RaiseKeyDownEvent (Key.F6); Assert.Equal (0, hasFocusChangingCount); Assert.Equal (0, hasFocusChangedCount); - - top.Dispose (); } [Fact] - [AutoInitShutdown] public void Application_Begin_FocusesDeepest () { var win1 = new Window { Id = "win1", Width = 10, Height = 1 }; @@ -327,12 +319,70 @@ public class NavigationTests (ITestOutputHelper output) : TestsAllViews win2.Add (view2); win1.Add (view1, win2); - Application.Begin (win1); + IApplication app = Application.Create (); + app.Begin (win1); Assert.True (win1.HasFocus); Assert.True (view1.HasFocus); Assert.False (win2.HasFocus); Assert.False (view2.HasFocus); - win1.Dispose (); + } + + // View.Focused & View.MostFocused tests + + // View.Focused - No subviews + [Fact] + public void Focused_NoSubViews () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + view.SetFocus (); + } + + [Fact] + public void GetMostFocused_NoSubViews_Returns_Null () + { + var view = new View (); + Assert.Null (view.Focused); + + view.CanFocus = true; + Assert.False (view.HasFocus); + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.Null (view.MostFocused); + } + + [Fact] + public void GetMostFocused_Returns_Most () + { + var view = new View + { + Id = "view", + CanFocus = true + }; + + var subview = new View + { + Id = "subview", + CanFocus = true + }; + + view.Add (subview); + + view.SetFocus (); + Assert.True (view.HasFocus); + Assert.True (subview.HasFocus); + Assert.Equal (subview, view.MostFocused); + + var subview2 = new View + { + Id = "subview2", + CanFocus = true + }; + + view.Add (subview2); + Assert.Equal (subview2, view.MostFocused); } } diff --git a/Tests/UnitTestsParallelizable/View/Navigation/CanFocusTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/CanFocusTests.cs similarity index 78% rename from Tests/UnitTestsParallelizable/View/Navigation/CanFocusTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/CanFocusTests.cs index 01b86092e..076908c11 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/CanFocusTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/CanFocusTests.cs @@ -1,7 +1,6 @@ using UnitTests; -using Xunit.Abstractions; +namespace ViewBaseTests.Navigation; -namespace UnitTests_Parallelizable.ViewTests; [Collection ("Global Test Setup")] public class CanFocusTests () : TestsAllViews @@ -181,7 +180,7 @@ public class CanFocusTests () : TestsAllViews [Fact] public void CanFocus_Faced_With_Container () { - var t = new Toplevel (); + var t = new Runnable (); var w = new Window (); var f = new FrameView (); var v = new View { CanFocus = true }; @@ -238,4 +237,44 @@ public class CanFocusTests () : TestsAllViews Assert.False (view.HasFocus); } + [Fact] + public void CanFocus_Set_True_Get_AdvanceFocus_Works () + { + IApplication app = Application.Create (); + app.Begin (new Runnable () { CanFocus = true }); + + Label label = new () { Text = "label" }; + View view = new () { Text = "view", CanFocus = true }; + app.TopRunnableView!.Add (label, view); + + app.TopRunnableView.SetFocus (); + Assert.Equal (view, app.Navigation!.GetFocused ()); + Assert.False (label.CanFocus); + Assert.False (label.HasFocus); + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + + Assert.False (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // Set label CanFocus to true + label.CanFocus = true; + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + // label can now be focused, so AdvanceFocus should move to it. + Assert.True (app.Navigation.AdvanceFocus (NavigationDirection.Forward, null)); + Assert.True (label.HasFocus); + Assert.False (view.HasFocus); + + // Move back to view + view.SetFocus (); + Assert.False (label.HasFocus); + Assert.True (view.HasFocus); + + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); + Assert.True (label.HasFocus); + Assert.False (view.HasFocus); + } } diff --git a/Tests/UnitTestsParallelizable/View/Navigation/EnabledTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/EnabledTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/EnabledTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/EnabledTests.cs index 6a31d7f90..b240a3b17 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/EnabledTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/EnabledTests.cs @@ -1,6 +1,6 @@ using UnitTests; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; [Collection ("Global Test Setup")] public class EnabledTests : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/HasFocusChangeEventTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusChangeEventTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/HasFocusChangeEventTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusChangeEventTests.cs index ea4fa6064..633dd3ff9 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/HasFocusChangeEventTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusChangeEventTests.cs @@ -1,7 +1,6 @@ using UnitTests; -using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; [Collection ("Global Test Setup")] public class HasFocusChangeEventTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/HasFocusTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/HasFocusTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusTests.cs index 787bbfe4d..da651aa00 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/HasFocusTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/HasFocusTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; [Collection ("Global Test Setup")] public class HasFocusTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/RestoreFocusTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/RestoreFocusTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/RestoreFocusTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/RestoreFocusTests.cs index 3fbd165e2..692657890 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/RestoreFocusTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/RestoreFocusTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; [Collection ("Global Test Setup")] public class RestoreFocusTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/SetFocusTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/SetFocusTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/SetFocusTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/SetFocusTests.cs index 1eb8c6383..f9161c37d 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/SetFocusTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/SetFocusTests.cs @@ -1,7 +1,6 @@ using UnitTests; -using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Navigation; [Collection ("Global Test Setup")] public class SetFocusTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Navigation/VisibleTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Navigation/VisibleTests.cs similarity index 99% rename from Tests/UnitTestsParallelizable/View/Navigation/VisibleTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Navigation/VisibleTests.cs index 295cef50e..0c23de343 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/VisibleTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Navigation/VisibleTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; [Collection ("Global Test Setup")] public class VisibleTests () : TestsAllViews diff --git a/Tests/UnitTestsParallelizable/View/Orientation/OrientationHelperTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationHelperTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/Orientation/OrientationHelperTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationHelperTests.cs index dcb32bc12..14a256d9f 100644 --- a/Tests/UnitTestsParallelizable/View/Orientation/OrientationHelperTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationHelperTests.cs @@ -1,6 +1,6 @@ using Moq; -namespace UnitTests_Parallelizable.ViewTests.OrientationTests; +namespace ViewBaseTests.OrientationTests; public class OrientationHelperTests { diff --git a/Tests/UnitTestsParallelizable/View/Orientation/OrientationTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationTests.cs similarity index 95% rename from Tests/UnitTestsParallelizable/View/Orientation/OrientationTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationTests.cs index ec9dbfb9a..c914c6159 100644 --- a/Tests/UnitTestsParallelizable/View/Orientation/OrientationTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Orientation/OrientationTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ViewTests.OrientationTests; +namespace ViewBaseTests.OrientationTests; public class OrientationTests { @@ -20,8 +20,8 @@ public class OrientationTests set => _orientationHelper.Orientation = value; } - public event EventHandler> OrientationChanging; - public event EventHandler> OrientationChanged; + public event EventHandler>? OrientationChanging; + public event EventHandler>? OrientationChanged; public bool CancelOnOrientationChanging { get; set; } diff --git a/Tests/UnitTestsParallelizable/View/SubviewTests.cs b/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs similarity index 98% rename from Tests/UnitTestsParallelizable/View/SubviewTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs index a447acd6e..3835dfa88 100644 --- a/Tests/UnitTestsParallelizable/View/SubviewTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/SubviewTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests.Hierarchy; [Collection ("Global Test Setup")] public class SubViewTests @@ -423,7 +423,7 @@ public class SubViewTests var tf1 = new TextField (); var w1 = new Window (); w1.Add (fv1, tf1); - var top1 = new Toplevel (); + var top1 = new Runnable (); top1.Add (w1); var v2 = new View (); @@ -432,7 +432,7 @@ public class SubViewTests var tf2 = new TextField (); var w2 = new Window (); w2.Add (fv2, tf2); - var top2 = new Toplevel (); + var top2 = new Runnable (); top2.Add (w2); Assert.Equal (top1, v1.GetTopSuperView ()); @@ -454,7 +454,7 @@ public class SubViewTests [Fact] public void Initialized_Event_Comparing_With_Added_Event () { - var top = new Toplevel { Id = "0" }; // Frame: 0, 0, 80, 25; Viewport: 0, 0, 80, 25 + var top = new Runnable { Id = "0" }; // Frame: 0, 0, 80, 25; Viewport: 0, 0, 80, 25 var winAddedToTop = new Window { @@ -480,25 +480,25 @@ public class SubViewTests winAddedToTop.SubViewAdded += (s, e) => { - Assert.Equal (e.SuperView.Frame.Width, winAddedToTop.Frame.Width); + Assert.Equal (e.SuperView!.Frame.Width, winAddedToTop.Frame.Width); Assert.Equal (e.SuperView.Frame.Height, winAddedToTop.Frame.Height); }; v1AddedToWin.SubViewAdded += (s, e) => { - Assert.Equal (e.SuperView.Frame.Width, v1AddedToWin.Frame.Width); + Assert.Equal (e.SuperView!.Frame.Width, v1AddedToWin.Frame.Width); Assert.Equal (e.SuperView.Frame.Height, v1AddedToWin.Frame.Height); }; v2AddedToWin.SubViewAdded += (s, e) => { - Assert.Equal (e.SuperView.Frame.Width, v2AddedToWin.Frame.Width); + Assert.Equal (e.SuperView!.Frame.Width, v2AddedToWin.Frame.Width); Assert.Equal (e.SuperView.Frame.Height, v2AddedToWin.Frame.Height); }; svAddedTov1.SubViewAdded += (s, e) => { - Assert.Equal (e.SuperView.Frame.Width, svAddedTov1.Frame.Width); + Assert.Equal (e.SuperView!.Frame.Width, svAddedTov1.Frame.Width); Assert.Equal (e.SuperView.Frame.Height, svAddedTov1.Frame.Height); }; diff --git a/Tests/UnitTestsParallelizable/View/TextTests.cs b/Tests/UnitTestsParallelizable/ViewBase/TextTests.cs similarity index 97% rename from Tests/UnitTestsParallelizable/View/TextTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/TextTests.cs index 110af6302..90c01d7a5 100644 --- a/Tests/UnitTestsParallelizable/View/TextTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/TextTests.cs @@ -2,7 +2,7 @@ using System.Text; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewBaseTests; /// /// Tests of the and properties. @@ -107,7 +107,7 @@ public class TextTests () Assert.Contains ( typeof (IsExternalInit), typeof (View).GetMethod ("set_TextFormatter") - .ReturnParameter.GetRequiredCustomModifiers ()); + ?.ReturnParameter.GetRequiredCustomModifiers ()!); } // Test that the Text property is set correctly. diff --git a/Tests/UnitTestsParallelizable/View/TitleTests.cs b/Tests/UnitTestsParallelizable/ViewBase/TitleTests.cs similarity index 95% rename from Tests/UnitTestsParallelizable/View/TitleTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/TitleTests.cs index 06dc3a631..b0d368b49 100644 --- a/Tests/UnitTestsParallelizable/View/TitleTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/TitleTests.cs @@ -1,7 +1,8 @@ -using System.Text; -using Xunit.Abstractions; +#nullable disable -namespace UnitTests_Parallelizable.ViewTests; +using System.Text; + +namespace ViewBaseTests; public class TitleTests { diff --git a/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs b/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs similarity index 96% rename from Tests/UnitTestsParallelizable/View/ViewCommandTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs index a571699c4..f358f58ed 100644 --- a/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/ViewCommandTests.cs @@ -1,4 +1,5 @@ -namespace UnitTests_Parallelizable.ViewTests; + +namespace ViewBaseTests.Commands; public class ViewCommandTests { #region OnAccept/Accept tests @@ -46,7 +47,7 @@ public class ViewCommandTests return; - void ViewOnAccept (object sender, CommandEventArgs e) + void ViewOnAccept (object? sender, CommandEventArgs e) { acceptInvoked = true; e.Handled = true; @@ -66,7 +67,7 @@ public class ViewCommandTests return; - void ViewOnAccept (object sender, CommandEventArgs e) { accepted = true; } + void ViewOnAccept (object? sender, CommandEventArgs e) { accepted = true; } } // Accept on subview should bubble up to parent @@ -177,7 +178,7 @@ public class ViewCommandTests return; - void ViewOnSelect (object sender, CommandEventArgs e) + void ViewOnSelect (object? sender, CommandEventArgs e) { selectingInvoked = true; e.Handled = true; @@ -197,7 +198,7 @@ public class ViewCommandTests return; - void ViewOnSelecting (object sender, CommandEventArgs e) { selecting = true; } + void ViewOnSelecting (object? sender, CommandEventArgs e) { selecting = true; } } [Fact] diff --git a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Viewport/ViewportSettings.TransparentMouseTests.cs similarity index 79% rename from Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs rename to Tests/UnitTestsParallelizable/ViewBase/Viewport/ViewportSettings.TransparentMouseTests.cs index 94082c3db..895b91050 100644 --- a/Tests/UnitTests/View/Viewport/ViewportSettings.TransparentMouseTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Viewport/ViewportSettings.TransparentMouseTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests.ViewTests; +namespace ViewBaseTests.Mouse; public class TransparentMouseTests { @@ -19,11 +19,12 @@ public class TransparentMouseTests public void TransparentMouse_Passes_Mouse_Events_To_Underlying_View () { // Arrange - var top = new Toplevel () + IApplication? app = Application.Create (); + var top = new Runnable () { Id = "top", }; - Application.TopRunnable = top; + app.Begin (top); var underlying = new MouseTrackingView { Id = "underlying", X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { Id = "overlay", X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; @@ -31,8 +32,6 @@ public class TransparentMouseTests top.Add (underlying); top.Add (overlay); - top.BeginInit (); - top.EndInit (); top.Layout (); var mouseEvent = new MouseEventArgs @@ -42,21 +41,19 @@ public class TransparentMouseTests }; // Act - Application.RaiseMouseEvent (mouseEvent); + app.Mouse.RaiseMouseEvent (mouseEvent); // Assert Assert.True (underlying.MouseEventReceived); - - top.Dispose (); - Application.ResetState (true); } [Fact] public void NonTransparentMouse_Consumes_Mouse_Events () { // Arrange - var top = new Toplevel (); - Application.TopRunnable = top; + IApplication? app = Application.Create (); + var top = new Runnable (); + app.Begin (top); var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10 }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.None }; @@ -64,8 +61,6 @@ public class TransparentMouseTests top.Add (underlying); top.Add (overlay); - top.BeginInit (); - top.EndInit (); top.Layout (); var mouseEvent = new MouseEventArgs @@ -75,22 +70,23 @@ public class TransparentMouseTests }; // Act - Application.RaiseMouseEvent (mouseEvent); + app.Mouse.RaiseMouseEvent (mouseEvent); // Assert Assert.True (overlay.MouseEventReceived); Assert.False (underlying.MouseEventReceived); - - top.Dispose (); - Application.ResetState (true); - } + } [Fact] public void TransparentMouse_Stacked_TransparentMouse_Views () { // Arrange - var top = new Toplevel (); - Application.TopRunnable = top; + IApplication? app = Application.Create (); + var top = new Runnable () + { + Id = "top", + }; + app.Begin (top); var underlying = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; var overlay = new MouseTrackingView { X = 0, Y = 0, Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.TransparentMouse }; @@ -98,8 +94,6 @@ public class TransparentMouseTests top.Add (underlying); top.Add (overlay); - top.BeginInit (); - top.EndInit (); top.Layout (); var mouseEvent = new MouseEventArgs @@ -116,14 +110,11 @@ public class TransparentMouseTests }; // Act - Application.RaiseMouseEvent (mouseEvent); + app.Mouse.RaiseMouseEvent (mouseEvent); // Assert Assert.False (overlay.MouseEventReceived); Assert.False (underlying.MouseEventReceived); Assert.True (topHandled); - - top.Dispose (); - Application.ResetState (true); } } diff --git a/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs b/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs index 1926538c8..6e59b09df 100644 --- a/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/Views/AllViewsDrawTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class AllViewsDrawTests (ITestOutputHelper output) : TestsAllViews { diff --git a/Tests/UnitTestsParallelizable/Views/AllViewsTests.cs b/Tests/UnitTestsParallelizable/Views/AllViewsTests.cs index d9b99f3a7..2aa9a09c0 100644 --- a/Tests/UnitTestsParallelizable/Views/AllViewsTests.cs +++ b/Tests/UnitTestsParallelizable/Views/AllViewsTests.cs @@ -3,7 +3,7 @@ using System.Reflection; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class AllViewsTests (ITestOutputHelper output) : TestsAllViews { diff --git a/Tests/UnitTestsParallelizable/Views/BarTests.cs b/Tests/UnitTestsParallelizable/Views/BarTests.cs index 4454c08b0..4c5d2748d 100644 --- a/Tests/UnitTestsParallelizable/Views/BarTests.cs +++ b/Tests/UnitTestsParallelizable/Views/BarTests.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; [TestSubject (typeof (Bar))] public class BarTests diff --git a/Tests/UnitTestsParallelizable/Views/ButtonTests.cs b/Tests/UnitTestsParallelizable/Views/ButtonTests.cs index b2b45dcbe..03b7abc80 100644 --- a/Tests/UnitTestsParallelizable/Views/ButtonTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ButtonTests.cs @@ -1,6 +1,7 @@ +#nullable disable using UnitTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// /// Pure unit tests for that don't require Application static dependencies. diff --git a/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs b/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs index e6721a906..047f2d8db 100644 --- a/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs +++ b/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs @@ -1,10 +1,65 @@ -using UnitTests; -using Xunit.Abstractions; +#nullable enable -namespace UnitTests_Parallelizable.ViewsTests; +using System.Collections.ObjectModel; + +namespace ViewsTests; public class CheckBoxTests () { + [Fact] + public void Commands_Select () + { + IApplication app = Application.Create (); + Runnable runnable = new (); + View otherView = new () { CanFocus = true }; + var ckb = new CheckBox (); + runnable.Add (ckb, otherView); + app.Begin (runnable); + ckb.SetFocus (); + Assert.True (ckb.HasFocus); + + var checkedStateChangingCount = 0; + ckb.CheckedStateChanging += (s, e) => checkedStateChangingCount++; + + var selectCount = 0; + ckb.Selecting += (s, e) => selectCount++; + + var acceptCount = 0; + ckb.Accepting += (s, e) => acceptCount++; + + Assert.Equal (CheckState.UnChecked, ckb.CheckedState); + Assert.Equal (0, checkedStateChangingCount); + Assert.Equal (0, selectCount); + Assert.Equal (0, acceptCount); + Assert.Equal (Key.Empty, ckb.HotKey); + + // Test while focused + ckb.Text = "_Test"; + Assert.Equal (Key.T, ckb.HotKey); + ckb.NewKeyDownEvent (Key.T); + Assert.Equal (CheckState.Checked, ckb.CheckedState); + Assert.Equal (1, checkedStateChangingCount); + Assert.Equal (1, selectCount); + Assert.Equal (0, acceptCount); + + ckb.Text = "T_est"; + Assert.Equal (Key.E, ckb.HotKey); + ckb.NewKeyDownEvent (Key.E.WithAlt); + Assert.Equal (2, checkedStateChangingCount); + Assert.Equal (2, selectCount); + Assert.Equal (0, acceptCount); + + ckb.NewKeyDownEvent (Key.Space); + Assert.Equal (3, checkedStateChangingCount); + Assert.Equal (3, selectCount); + Assert.Equal (0, acceptCount); + + ckb.NewKeyDownEvent (Key.Enter); + Assert.Equal (3, checkedStateChangingCount); + Assert.Equal (3, selectCount); + Assert.Equal (1, acceptCount); + } + [Theory] [InlineData ("01234", 0, 0, 0, 0)] [InlineData ("01234", 1, 0, 1, 0)] @@ -153,7 +208,7 @@ public class CheckBoxTests () return; - void ViewOnAccept (object sender, CommandEventArgs e) + void ViewOnAccept (object? sender, CommandEventArgs e) { acceptInvoked = true; e.Handled = true; diff --git a/Tests/UnitTestsParallelizable/Views/ColorPickerTests.cs b/Tests/UnitTestsParallelizable/Views/ColorPickerTests.cs index ce1543202..e90361b94 100644 --- a/Tests/UnitTestsParallelizable/Views/ColorPickerTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ColorPickerTests.cs @@ -1,15 +1,15 @@ -using UnitTests; +#nullable enable -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// -/// Pure unit tests for that don't require Application.Driver or View context. -/// These tests can run in parallel without interference. +/// Pure unit tests for that don't require Application.Driver or View context. +/// These tests can run in parallel without interference. /// -public class ColorPickerTests : FakeDriverBase +public class ColorPickerTests { [Fact] - public void ColorPicker_ChangedEvent_Fires () + public void ChangedEvent_Fires () { Color newColor = default; var count = 0; @@ -39,4 +39,840 @@ public class ColorPickerTests : FakeDriverBase // Should have no effect Assert.Equal (2, count); } + + [Fact] + public void ChangeValueOnUI_UpdatesAllUIElements () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true); + + var otherView = new View { CanFocus = true }; + + cp.App!.TopRunnableView?.Add (otherView); // thi sets focus to otherView + Assert.True (otherView.HasFocus); + + cp.SetFocus (); + Assert.False (otherView.HasFocus); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); + TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); + TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.Equal ("0", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#000000", hex.Text); + + // Change value using text field + TextField rBarTextField = cp.SubViews.OfType ().First (tf => tf.Text == "0"); + + rBarTextField.SetFocus (); + rBarTextField.Text = "128"; + + otherView.SetFocus (); + Assert.True (otherView.HasFocus); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal ("R:", r.Text); + Assert.Equal (9, r.TrianglePosition); + Assert.Equal ("128", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#800000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void ClickingAtEndOfBar_SetsMaxValue () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // Click at the end of the Red bar + cp.Focused!.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (19, 0) // Assuming 0-based indexing + }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void ClickingBeyondBar_ChangesToMaxValue () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // Click beyond the bar + cp.Focused!.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (21, 0) // Beyond the bar + }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void ClickingDifferentBars_ChangesFocus () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // Click on Green bar + cp.App!.Mouse.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + ScreenPosition = new (0, 1) + }); + + //cp.SubViews.OfType () + // .Single () + // .OnMouseEvent ( + // new () + // { + // Flags = MouseFlags.Button1Pressed, + // Position = new (0, 1) + // }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.IsAssignableFrom (cp.Focused); + + // Click on Blue bar + cp.App!.Mouse.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + ScreenPosition = new (0, 2) + }); + + //cp.SubViews.OfType () + // .Single () + // .OnMouseEvent ( + // new () + // { + // Flags = MouseFlags.Button1Pressed, + // Position = new (0, 2) + // }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.IsAssignableFrom (cp.Focused); + + cp.App?.Dispose (); + } + + [Fact] + public void Construct_DefaultValue () + { + ColorPicker cp = GetColorPicker (ColorModel.HSV, false); + + // Should be only a single text field (Hex) because ShowTextFields is false + Assert.Single (cp.SubViews.OfType ()); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // All bars should be at 0 with the triangle at 0 (+2 because of "H:", "S:" etc) + ColorBar h = GetColorBar (cp, ColorPickerPart.Bar1); + Assert.Equal ("H:", h.Text); + Assert.Equal (2, h.TrianglePosition); + Assert.IsType (h); + + ColorBar s = GetColorBar (cp, ColorPickerPart.Bar2); + Assert.Equal ("S:", s.Text); + Assert.Equal (2, s.TrianglePosition); + Assert.IsType (s); + + ColorBar v = GetColorBar (cp, ColorPickerPart.Bar3); + Assert.Equal ("V:", v.Text); + Assert.Equal (2, v.TrianglePosition); + Assert.IsType (v); + + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + Assert.Equal ("#000000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void DisposesOldViews_OnModelChange () + { + ColorPicker cp = GetColorPicker (ColorModel.HSL, true); + + ColorBar b1 = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar b2 = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b3 = GetColorBar (cp, ColorPickerPart.Bar3); + + TextField tf1 = GetTextField (cp, ColorPickerPart.Bar1); + TextField tf2 = GetTextField (cp, ColorPickerPart.Bar2); + TextField tf3 = GetTextField (cp, ColorPickerPart.Bar3); + + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + +#if DEBUG_IDISPOSABLE + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.False (b.WasDisposed)); +#endif + cp.Style.ColorModel = ColorModel.RGB; + cp.ApplyStyleChanges (); + + ColorBar b1After = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar b2After = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b3After = GetColorBar (cp, ColorPickerPart.Bar3); + + TextField tf1After = GetTextField (cp, ColorPickerPart.Bar1); + TextField tf2After = GetTextField (cp, ColorPickerPart.Bar2); + TextField tf3After = GetTextField (cp, ColorPickerPart.Bar3); + + TextField hexAfter = GetTextField (cp, ColorPickerPart.Hex); + + // Old bars should be disposed +#if DEBUG_IDISPOSABLE + Assert.All (new View [] { b1, b2, b3, tf1, tf2, tf3, hex }, b => Assert.True (b.WasDisposed)); +#endif + Assert.NotSame (hex, hexAfter); + + Assert.NotSame (b1, b1After); + Assert.NotSame (b2, b2After); + Assert.NotSame (b3, b3After); + + Assert.NotSame (tf1, tf1After); + Assert.NotSame (tf2, tf2After); + Assert.NotSame (tf3, tf3After); + + cp.App?.Dispose (); + } + + [Fact] + public void EnterHexFor_ColorName () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); + + cp.Draw (); // Draw is needed to update TrianglePosition + + TextField name = GetTextField (cp, ColorPickerPart.ColorName); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + hex.SetFocus (); + + Assert.True (hex.HasFocus); + Assert.Same (hex, cp.Focused); + + hex.Text = ""; + name.Text = ""; + + Assert.Empty (hex.Text); + Assert.Empty (name.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent ('#'); + Assert.Empty (name.Text); + + //7FFFD4 + + Assert.Equal ("#", hex.Text); + cp.App!.Keyboard.RaiseKeyDownEvent ('7'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('D'); + Assert.Empty (name.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent ('4'); + + Assert.True (hex.HasFocus); + + // Tab out of the hex field - should wrap to first focusable subview + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Tab); + Assert.False (hex.HasFocus); + Assert.NotSame (hex, cp.Focused); + + // Color name should be recognised as a known string and populated + Assert.Equal ("#7FFFD4", hex.Text); + Assert.Equal ("Aquamarine", name.Text); + + cp.App?.Dispose (); + } + + /// + /// In this version we use the Enter button to accept the typed text instead + /// of tabbing to the next view. + /// + [Fact] + public void EnterHexFor_ColorName_AcceptVariation () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); + + cp.Draw (); // Draw is needed to update TrianglePosition + + TextField name = GetTextField (cp, ColorPickerPart.ColorName); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + hex.SetFocus (); + + Assert.True (hex.HasFocus); + Assert.Same (hex, cp.Focused); + + hex.Text = ""; + name.Text = ""; + + Assert.Empty (hex.Text); + Assert.Empty (name.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent ('#'); + Assert.Empty (name.Text); + + //7FFFD4 + + Assert.Equal ("#", hex.Text); + cp.App!.Keyboard.RaiseKeyDownEvent ('7'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('F'); + cp.App!.Keyboard.RaiseKeyDownEvent ('D'); + Assert.Empty (name.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent ('4'); + + Assert.True (hex.HasFocus); + + // Should stay in the hex field (because accept not tab) + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Enter); + Assert.True (hex.HasFocus); + Assert.Same (hex, cp.Focused); + + // But still, Color name should be recognised as a known string and populated + Assert.Equal ("#7FFFD4", hex.Text); + Assert.Equal ("Aquamarine", name.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void InvalidHexInput_DoesNotChangeColor () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // Enter invalid hex value + TextField hexField = cp.SubViews.OfType ().First (tf => tf.Text == "#000000"); + hexField.SetFocus (); + hexField.Text = "#ZZZZZZ"; + Assert.True (hexField.HasFocus); + Assert.Equal ("#ZZZZZZ", hexField.Text); + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("#ZZZZZZ", hex.Text); + + // Advance away from hexField to cause validation + cp.AdvanceFocus (NavigationDirection.Forward, null); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#000000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void RGB_KeyboardNavigation () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.IsType (r); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.IsType (g); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.IsType (b); + Assert.Equal ("#000000", hex.Text); + + Assert.IsAssignableFrom (cp.Focused); + + cp.Draw (); // Draw is needed to update TrianglePosition + + cp.App!.Keyboard.RaiseKeyDownEvent (Key.CursorRight); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal (3, r.TrianglePosition); + Assert.Equal ("#0F0000", hex.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent (Key.CursorRight); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal (4, r.TrianglePosition); + Assert.Equal ("#1E0000", hex.Text); + + // Use cursor to move the triangle all the way to the right + for (var i = 0; i < 1000; i++) + { + cp.App!.Keyboard.RaiseKeyDownEvent (Key.CursorRight); + } + + cp.Draw (); // Draw is needed to update TrianglePosition + + // 20 width and TrianglePosition is 0 indexed + // Meaning we are asserting that triangle is at end + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void RGB_MouseNavigation () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (2, r.TrianglePosition); + Assert.IsType (r); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.IsType (g); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.IsType (b); + Assert.Equal ("#000000", hex.Text); + + Assert.IsAssignableFrom (cp.Focused); + + cp.Focused!.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (3, 0) + }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal (3, r.TrianglePosition); + Assert.Equal ("#0F0000", hex.Text); + + cp.Focused.RaiseMouseEvent ( + new () + { + Flags = MouseFlags.Button1Pressed, + Position = new (4, 0) + }); + + cp.Draw (); // Draw is needed to update TrianglePosition + + Assert.Equal (4, r.TrianglePosition); + Assert.Equal ("#1E0000", hex.Text); + + cp.App?.Dispose (); + } + + [Theory] + [MemberData (nameof (ColorPickerTestData))] + public void RGB_NoText ( + Color c, + string expectedR, + int expectedRTriangle, + string expectedG, + int expectedGTriangle, + string expectedB, + int expectedBTriangle, + string expectedHex + ) + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + cp.SelectedColor = c; + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal (expectedR, r.Text); + Assert.Equal (expectedRTriangle, r.TrianglePosition); + Assert.Equal (expectedG, g.Text); + Assert.Equal (expectedGTriangle, g.TrianglePosition); + Assert.Equal (expectedB, b.Text); + Assert.Equal (expectedBTriangle, b.TrianglePosition); + Assert.Equal (expectedHex, hex.Text); + + cp.App?.Dispose (); + } + + [Theory] + [MemberData (nameof (ColorPickerTestData_WithTextFields))] + public void RGB_NoText_WithTextFields ( + Color c, + string expectedR, + int expectedRTriangle, + int expectedRValue, + string expectedG, + int expectedGTriangle, + int expectedGValue, + string expectedB, + int expectedBTriangle, + int expectedBValue, + string expectedHex + ) + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true); + cp.SelectedColor = c; + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); + TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); + TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal (expectedR, r.Text); + Assert.Equal (expectedRTriangle, r.TrianglePosition); + Assert.Equal (expectedRValue.ToString (), rTextField.Text); + Assert.Equal (expectedG, g.Text); + Assert.Equal (expectedGTriangle, g.TrianglePosition); + Assert.Equal (expectedGValue.ToString (), gTextField.Text); + Assert.Equal (expectedB, b.Text); + Assert.Equal (expectedBTriangle, b.TrianglePosition); + Assert.Equal (expectedBValue.ToString (), bTextField.Text); + Assert.Equal (expectedHex, hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void SwitchingColorModels_ResetsBars () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, false); + cp.SelectedColor = new (255, 0); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + Assert.Equal ("R:", r.Text); + Assert.Equal (19, r.TrianglePosition); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + // Switch to HSV + cp.Style.ColorModel = ColorModel.HSV; + cp.ApplyStyleChanges (); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar h = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar s = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar v = GetColorBar (cp, ColorPickerPart.Bar3); + + Assert.Equal ("H:", h.Text); + Assert.Equal (2, h.TrianglePosition); + Assert.Equal ("S:", s.Text); + Assert.Equal (19, s.TrianglePosition); + Assert.Equal ("V:", v.Text); + Assert.Equal (19, v.TrianglePosition); + Assert.Equal ("#FF0000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void SyncBetweenTextFieldAndBars () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true); + + cp.Draw (); // Draw is needed to update TrianglePosition + + // Change value using the bar + RBar rBar = cp.SubViews.OfType ().First (); + rBar.Value = 128; + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + TextField rTextField = GetTextField (cp, ColorPickerPart.Bar1); + TextField gTextField = GetTextField (cp, ColorPickerPart.Bar2); + TextField bTextField = GetTextField (cp, ColorPickerPart.Bar3); + + Assert.Equal ("R:", r.Text); + Assert.Equal (9, r.TrianglePosition); + Assert.Equal ("128", rTextField.Text); + Assert.Equal ("G:", g.Text); + Assert.Equal (2, g.TrianglePosition); + Assert.Equal ("0", gTextField.Text); + Assert.Equal ("B:", b.Text); + Assert.Equal (2, b.TrianglePosition); + Assert.Equal ("0", bTextField.Text); + Assert.Equal ("#800000", hex.Text); + + cp.App?.Dispose (); + } + + [Fact] + public void TabCompleteColorName () + { + ColorPicker cp = GetColorPicker (ColorModel.RGB, true, true); + + cp.Draw (); // Draw is needed to update TrianglePosition + + ColorBar r = GetColorBar (cp, ColorPickerPart.Bar1); + ColorBar g = GetColorBar (cp, ColorPickerPart.Bar2); + ColorBar b = GetColorBar (cp, ColorPickerPart.Bar3); + TextField name = GetTextField (cp, ColorPickerPart.ColorName); + TextField hex = GetTextField (cp, ColorPickerPart.Hex); + + name.SetFocus (); + + Assert.True (name.HasFocus); + Assert.Same (name, cp.Focused); + + name.Text = ""; + Assert.Empty (name.Text); + + cp.App!.Keyboard.RaiseKeyDownEvent (Key.A); + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Q); + + Assert.Equal ("aq", name.Text); + + // Auto complete the color name + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Tab); + + // Match cyan alternative name + Assert.Equal ("Aqua", name.Text); + + Assert.True (name.HasFocus); + + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Tab); + + // Resolves to cyan color + Assert.Equal ("Aqua", name.Text); + + // Tab out of the text field + cp.App!.Keyboard.RaiseKeyDownEvent (Key.Tab); + + Assert.False (name.HasFocus); + Assert.NotSame (name, cp.Focused); + + Assert.Equal ("#00FFFF", hex.Text); + + cp.App?.Dispose (); + } + + public static IEnumerable ColorPickerTestData () + { + yield return + [ + new Color (255, 0), + "R:", 19, "G:", 2, "B:", 2, "#FF0000" + ]; + + yield return + [ + new Color (0, 255), + "R:", 2, "G:", 19, "B:", 2, "#00FF00" + ]; + + yield return + [ + new Color (0, 0, 255), + "R:", 2, "G:", 2, "B:", 19, "#0000FF" + ]; + + yield return + [ + new Color (125, 125, 125), + "R:", 11, "G:", 11, "B:", 11, "#7D7D7D" + ]; + } + + public static IEnumerable ColorPickerTestData_WithTextFields () + { + yield return + [ + new Color (255, 0), + "R:", 15, 255, "G:", 2, 0, "B:", 2, 0, "#FF0000" + ]; + + yield return + [ + new Color (0, 255), + "R:", 2, 0, "G:", 15, 255, "B:", 2, 0, "#00FF00" + ]; + + yield return + [ + new Color (0, 0, 255), + "R:", 2, 0, "G:", 2, 0, "B:", 15, 255, "#0000FF" + ]; + + yield return + [ + new Color (125, 125, 125), + "R:", 9, 125, "G:", 9, 125, "B:", 9, 125, "#7D7D7D" + ]; + } + + private ColorBar GetColorBar (ColorPicker cp, ColorPickerPart toGet) + { + if (toGet <= ColorPickerPart.Bar3) + { + return cp.SubViews.OfType ().ElementAt ((int)toGet); + } + + throw new NotSupportedException ("ColorPickerPart must be a bar"); + } + + private static ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, bool showName = false) + { + IApplication? app = Application.Create (); + app.Init ("Fake"); + + var cp = new ColorPicker { Width = 20, SelectedColor = new (0, 0) }; + cp.Style.ColorModel = colorModel; + cp.Style.ShowTextFields = showTextFields; + cp.Style.ShowColorName = showName; + cp.ApplyStyleChanges (); + Runnable? runnable = new () { Width = 20, Height = 5 }; + app.Begin (runnable); + runnable.Add (cp); + + app.LayoutAndDraw (); + + return cp; + } + + private TextField GetTextField (ColorPicker cp, ColorPickerPart toGet) + { + bool hasBarValueTextFields = cp.Style.ShowTextFields; + bool hasColorNameTextField = cp.Style.ShowColorName; + + switch (toGet) + { + case ColorPickerPart.Bar1: + case ColorPickerPart.Bar2: + case ColorPickerPart.Bar3: + if (!hasBarValueTextFields) + { + throw new NotSupportedException ("Corresponding Style option is not enabled"); + } + + return cp.SubViews.OfType ().ElementAt ((int)toGet); + case ColorPickerPart.ColorName: + if (!hasColorNameTextField) + { + throw new NotSupportedException ("Corresponding Style option is not enabled"); + } + + return cp.SubViews.OfType ().ElementAt (hasBarValueTextFields ? (int)toGet : (int)toGet - 3); + case ColorPickerPart.Hex: + + int offset = hasBarValueTextFields ? 0 : 3; + offset += hasColorNameTextField ? 0 : 1; + + return cp.SubViews.OfType ().ElementAt ((int)toGet - offset); + default: + throw new ArgumentOutOfRangeException (nameof (toGet), toGet, null); + } + } + + private enum ColorPickerPart + { + Bar1 = 0, + Bar2 = 1, + Bar3 = 2, + ColorName = 3, + Hex = 4 + } } diff --git a/Tests/UnitTestsParallelizable/Views/DateFieldTests.cs b/Tests/UnitTestsParallelizable/Views/DateFieldTests.cs index 78a8c899e..2c6fa4c4f 100644 --- a/Tests/UnitTestsParallelizable/Views/DateFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DateFieldTests.cs @@ -1,8 +1,9 @@ #nullable enable using System.Globalization; using System.Runtime.InteropServices; +using UnitTests_Parallelizable; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class DateFieldTests { @@ -61,7 +62,7 @@ public class DateFieldTests } finally { - app.Shutdown(); + app.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Views/DatePickerTests.cs b/Tests/UnitTestsParallelizable/Views/DatePickerTests.cs index 4ffc18b56..5b33256c4 100644 --- a/Tests/UnitTestsParallelizable/Views/DatePickerTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DatePickerTests.cs @@ -1,7 +1,7 @@ using System.Globalization; using UnitTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// /// Pure unit tests for that don't require Application.Driver or View context. diff --git a/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs index 5e9de8fcc..80e83b66a 100644 --- a/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class FlagSelectorTests { diff --git a/Tests/UnitTests/Views/HexViewTests.cs b/Tests/UnitTestsParallelizable/Views/HexViewTests.cs similarity index 70% rename from Tests/UnitTests/Views/HexViewTests.cs rename to Tests/UnitTestsParallelizable/Views/HexViewTests.cs index e2fe7e365..87c401d69 100644 --- a/Tests/UnitTests/Views/HexViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/HexViewTests.cs @@ -1,9 +1,10 @@ #nullable enable using System.Text; +using UnitTests; -namespace UnitTests.ViewsTests; +namespace ViewsTests; -public class HexViewTests +public class HexViewTests : FakeDriverBase { [Theory] [InlineData (0, 4)] @@ -32,34 +33,35 @@ public class HexViewTests { var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (hv); - Application.TopRunnable.SetFocus (); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (hv); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to left side + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); // Move to left side Assert.Empty (hv.Edits); hv.ReadOnly = true; - Assert.True (Application.RaiseKeyDownEvent (Key.Home)); - Assert.False (Application.RaiseKeyDownEvent (Key.A)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Home)); + Assert.False (app.Keyboard.RaiseKeyDownEvent (Key.A)); Assert.Empty (hv.Edits); Assert.Equal (126, hv.Source!.Length); hv.ReadOnly = false; - Assert.True (Application.RaiseKeyDownEvent (Key.D4)); - Assert.True (Application.RaiseKeyDownEvent (Key.D1)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D4)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D1)); Assert.Single (hv.Edits); Assert.Equal (65, hv.Edits.ToList () [0].Value); Assert.Equal ('A', (char)hv.Edits.ToList () [0].Value); Assert.Equal (126, hv.Source.Length); // Appends byte - Assert.True (Application.RaiseKeyDownEvent (Key.End)); - Assert.True (Application.RaiseKeyDownEvent (Key.D4)); - Assert.True (Application.RaiseKeyDownEvent (Key.D2)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.End)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D4)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D2)); Assert.Equal (2, hv.Edits.Count); Assert.Equal (66, hv.Edits.ToList () [1].Value); Assert.Equal ('B', (char)hv.Edits.ToList () [1].Value); @@ -68,15 +70,14 @@ public class HexViewTests hv.ApplyEdits (); Assert.Empty (hv.Edits); Assert.Equal (127, hv.Source.Length); - - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] public void ApplyEdits_With_Argument () { - Application.TopRunnable = new (); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); byte [] buffer = Encoding.Default.GetBytes ("Fest"); var original = new MemoryStream (); @@ -87,8 +88,7 @@ public class HexViewTests original.CopyTo (copy); copy.Flush (); var hv = new HexView (copy) { Width = Dim.Fill (), Height = Dim.Fill () }; - Application.TopRunnable.Add (hv); - Application.TopRunnable.SetFocus (); + runnable.Add (hv); // Needed because HexView relies on LayoutComplete to calc sizes hv.LayoutSubViews (); @@ -98,15 +98,15 @@ public class HexViewTests hv.Source.Read (readBuffer); Assert.Equal ("Fest", Encoding.Default.GetString (readBuffer)); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to left side - Assert.True (Application.RaiseKeyDownEvent (Key.D5)); - Assert.True (Application.RaiseKeyDownEvent (Key.D4)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); // Move to left side + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D5)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.D4)); readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); // Move to right side - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); - Assert.True (Application.RaiseKeyDownEvent (Key.Z.WithShift)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); // Move to right side + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Z.WithShift)); readBuffer [hv.Edits.ToList () [0].Key] = hv.Edits.ToList () [0].Value; Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); @@ -119,8 +119,6 @@ public class HexViewTests Assert.Equal ("Zest", Encoding.Default.GetString (readBuffer)); Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] @@ -143,71 +141,72 @@ public class HexViewTests public void Position_Encoding_Default () { var hv = new HexView (LoadStream (null, out _)) { Width = 100, Height = 100 }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (hv); - Application.TopRunnable.LayoutSubViews (); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (hv); Assert.Equal (63, hv.Source!.Length); Assert.Equal (20, hv.BytesPerLine); Assert.Equal (new (0, 0), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); Assert.Equal (new (0, 0), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); Assert.Equal (hv.BytesPerLine - 1, hv.GetPosition (hv.Address).X); - Assert.True (Application.RaiseKeyDownEvent (Key.Home)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Home)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); Assert.Equal (new (1, 0), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (new (1, 1), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.End)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.End)); Assert.Equal (new (3, 3), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] public void Position_Encoding_Unicode () { var hv = new HexView (LoadStream (null, out _, true)) { Width = 100, Height = 100 }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (hv); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (hv); - hv.LayoutSubViews (); + app.LayoutAndDraw (); Assert.Equal (126, hv.Source!.Length); Assert.Equal (20, hv.BytesPerLine); Assert.Equal (new (0, 0), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.Tab)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); Assert.Equal (hv.BytesPerLine - 1, hv.GetPosition (hv.Address).X); - Assert.True (Application.RaiseKeyDownEvent (Key.Home)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Home)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); Assert.Equal (new (1, 0), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (new (1, 1), hv.GetPosition (hv.Address)); - Assert.True (Application.RaiseKeyDownEvent (Key.End)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.End)); Assert.Equal (new (6, 6), hv.GetPosition (hv.Address)); Assert.Equal (hv.Source!.Length, hv.Address); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] @@ -258,67 +257,67 @@ public class HexViewTests [Fact] public void KeyBindings_Test_Movement_LeftSide () { - Application.TopRunnable = new (); var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.TopRunnable.Add (hv); - hv.LayoutSubViews (); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (hv); + app.LayoutAndDraw (); Assert.Equal (MEM_STRING_LENGTH, hv.Source!.Length); Assert.Equal (0, hv.Address); Assert.Equal (4, hv.BytesPerLine); // Default internal focus is on right side. Move back to left. - Assert.True (Application.RaiseKeyDownEvent (Key.Tab.WithShift)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab.WithShift)); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); + Assert.Equal (0, hv.Address); + + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight)); Assert.Equal (1, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft)); Assert.Equal (0, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorDown)); Assert.Equal (4, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorUp)); Assert.Equal (0, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.PageDown)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.PageDown)); Assert.Equal (40, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.PageUp)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.PageUp)); Assert.Equal (0, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.End)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.Home)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Home)); Assert.Equal (0, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorRight.WithCtrl)); Assert.Equal (3, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft.WithCtrl)); Assert.Equal (0, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorDown.WithCtrl)); Assert.Equal (36, hv.Address); - Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp.WithCtrl)); + Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorUp.WithCtrl)); Assert.Equal (0, hv.Address); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] public void PositionChanged_Event () { var hv = new HexView (LoadStream (null, out _)) { Width = 20, Height = 10 }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (hv); - - Application.TopRunnable.LayoutSubViews (); + hv.Layout (); HexViewEventArgs hexViewEventArgs = null!; hv.PositionChanged += (s, e) => hexViewEventArgs = e; @@ -331,41 +330,38 @@ public class HexViewTests Assert.Equal (4, hexViewEventArgs.BytesPerLine); Assert.Equal (new (1, 1), hexViewEventArgs.Position); Assert.Equal (5, hexViewEventArgs.Address); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } [Fact] public void Source_Sets_Address_To_Zero_If_Greater_Than_Source_Length () { var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 }; - Application.TopRunnable = new (); - Application.TopRunnable.Add (hv); - Application.TopRunnable.Layout (); + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (hv); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.TopRunnable.Layout (); + runnable.Layout (); Assert.Equal (0, hv.Address); hv.Source = LoadStream (null, out _); hv.Width = Dim.Fill (); hv.Height = Dim.Fill (); - Application.TopRunnable.Layout (); + runnable.Layout (); Assert.Equal (0, hv.Address); Assert.True (hv.NewKeyDownEvent (Key.End)); Assert.Equal (MEM_STRING_LENGTH, hv.Address); hv.Source = new MemoryStream (); - Application.TopRunnable.Layout (); + runnable.Layout (); Assert.Equal (0, hv.Address); - Application.TopRunnable.Dispose (); - Application.ResetState (true); } private const string MEM_STRING = "Hello world.\nThis is a test of the Emergency Broadcast System.\n"; diff --git a/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs b/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs index 29faa1aab..0fd14354e 100644 --- a/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs +++ b/Tests/UnitTestsParallelizable/Views/IListDataSourceTests.cs @@ -7,7 +7,7 @@ using Xunit.Abstractions; // ReSharper disable InconsistentNaming -namespace UnitTests_Parallelizable.ViewTests; +namespace ViewsTests; public class IListDataSourceTests (ITestOutputHelper output) { @@ -74,7 +74,7 @@ public class IListDataSourceTests (ITestOutputHelper output) } } - public void Render (ListView listView, bool selected, int item, int col, int line, int width, int viewportX = 0) + public void Render (Terminal.Gui.Views.ListView listView, bool selected, int item, int col, int line, int width, int viewportX = 0) { if (item < 0 || item >= _items.Count) { diff --git a/Tests/UnitTestsParallelizable/Views/LabelTests.cs b/Tests/UnitTestsParallelizable/Views/LabelTests.cs index a3730a1a1..eb1493632 100644 --- a/Tests/UnitTestsParallelizable/Views/LabelTests.cs +++ b/Tests/UnitTestsParallelizable/Views/LabelTests.cs @@ -1,12 +1,14 @@ +#nullable enable using UnitTests; +using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// /// Pure unit tests for that don't require Application.Driver or Application context. /// These tests can run in parallel without interference. /// -public class LabelTests : FakeDriverBase +public class LabelTests (ITestOutputHelper output) : FakeDriverBase { [Fact] public void Text_Mirrors_Title () @@ -88,7 +90,7 @@ public class LabelTests : FakeDriverBase return; - void LabelOnAccept (object sender, CommandEventArgs e) { accepted = true; } + void LabelOnAccept (object? sender, CommandEventArgs e) { accepted = true; } } [Fact] @@ -154,4 +156,206 @@ public class LabelTests : FakeDriverBase Assert.Equal ("Test", label.Text); } + + [Fact] + public void CanFocus_False_HotKey_SetsFocus_Next () + { + View otherView = new () + { + Text = "otherView", + CanFocus = true + }; + + Label label = new () + { + Text = "_label" + }; + + View nextView = new () + { + Text = "nextView", + CanFocus = true + }; + + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (otherView, label, nextView); + otherView.SetFocus (); + + // runnable.SetFocus (); + Assert.True (otherView.HasFocus); + + Assert.True (app.Keyboard.RaiseKeyDownEvent (label.HotKey)); + Assert.False (otherView.HasFocus); + Assert.False (label.HasFocus); + Assert.True (nextView.HasFocus); + } + + [Fact] + public void CanFocus_False_MouseClick_SetsFocus_Next () + { + View otherView = new () { X = 0, Y = 0, Width = 1, Height = 1, Id = "otherView", CanFocus = true }; + Label label = new () { X = 0, Y = 1, Text = "_label" }; + View nextView = new () + { + X = Pos.Right (label), Y = Pos.Top (label), Width = 1, Height = 1, Id = "nextView", CanFocus = true + }; + + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (otherView, label, nextView); + otherView.SetFocus (); + + // click on label + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = label.Frame.Location, Flags = MouseFlags.Button1Clicked }); + Assert.False (label.HasFocus); + Assert.True (nextView.HasFocus); + } + + + [Fact] + public void CanFocus_True_HotKey_SetsFocus () + { + Label label = new () + { + Text = "_label", + CanFocus = true + }; + + View view = new () + { + Text = "view", + CanFocus = true + }; + + IApplication app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + runnable.Add (label, view); + + view.SetFocus (); + Assert.True (label.CanFocus); + Assert.False (label.HasFocus); + Assert.True (view.CanFocus); + Assert.True (view.HasFocus); + + // No focused view accepts Tab, and there's no other view to focus, so OnKeyDown returns false + Assert.True (app.Keyboard.RaiseKeyDownEvent (label.HotKey)); + Assert.True (label.HasFocus); + Assert.False (view.HasFocus); + } + + + + [Fact] + public void CanFocus_True_MouseClick_Focuses () + { + Label label = new () + { + Text = "label", + X = 0, + Y = 0, + CanFocus = true + }; + + View otherView = new () + { + Text = "view", + X = 0, + Y = 1, + Width = 4, + Height = 1, + CanFocus = true + }; + + IApplication app = Application.Create (); + Runnable runnable = new () + { + Width = 10, + Height = 10 + }; ; + app.Begin (runnable); + runnable.Add (label, otherView); + label.SetFocus (); + + Assert.True (label.CanFocus); + Assert.True (label.HasFocus); + Assert.True (otherView.CanFocus); + Assert.False (otherView.HasFocus); + + otherView.SetFocus (); + Assert.True (otherView.HasFocus); + + // label can focus, so clicking on it set focus + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Clicked }); + Assert.True (label.HasFocus); + Assert.False (otherView.HasFocus); + + // click on view + app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (0, 1), Flags = MouseFlags.Button1Clicked }); + Assert.False (label.HasFocus); + Assert.True (otherView.HasFocus); + } + + + [Fact] + public void With_Top_Margin_Without_Top_Border () + { + IApplication app = Application.Create (); + app.Init ("Fake"); + Runnable runnable = new () + { + Width = 10, + Height = 10 + }; ; + app.Begin (runnable); + + var label = new Label { Text = "Test", /*Width = 6, Height = 3,*/ BorderStyle = LineStyle.Single }; + label.Margin!.Thickness = new (0, 1, 0, 0); + label.Border!.Thickness = new (1, 0, 1, 1); + runnable.Add (label); + app.LayoutAndDraw (); + + Assert.Equal (new (0, 0, 6, 3), label.Frame); + Assert.Equal (new (0, 0, 4, 1), label.Viewport); + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +│Test│ +└────┘", + output, + app.Driver + ); + } + + [Fact] + public void Without_Top_Border () + { + IApplication app = Application.Create (); + app.Init ("Fake"); + Runnable runnable = new () + { + Width = 10, + Height = 10 + }; ; + app.Begin (runnable); + + var label = new Label { Text = "Test", /* Width = 6, Height = 3, */BorderStyle = LineStyle.Single }; + label.Border!.Thickness = new (1, 0, 1, 1); + runnable.Add (label); + app.LayoutAndDraw (); + + Assert.Equal (new (0, 0, 6, 2), label.Frame); + Assert.Equal (new (0, 0, 4, 1), label.Viewport); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +│Test│ +└────┘", + output, + app.Driver + ); + } + } diff --git a/Tests/UnitTestsParallelizable/Views/LineTests.cs b/Tests/UnitTestsParallelizable/Views/LineTests.cs index a0aa17ca9..c3f58138a 100644 --- a/Tests/UnitTestsParallelizable/Views/LineTests.cs +++ b/Tests/UnitTestsParallelizable/Views/LineTests.cs @@ -1,6 +1,6 @@ using UnitTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class LineTests : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 47f6a0234..90ab19d41 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -11,7 +11,7 @@ using Xunit.Abstractions; // ReSharper disable AccessToModifiedClosure -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class ListViewTests (ITestOutputHelper output) { @@ -895,15 +895,16 @@ public class ListViewTests (ITestOutputHelper output) }; lv.SetSource (["One", "Two", "Three", "Four"]); lv.SelectedItemChanged += (s, e) => selected = e.Value!.ToString (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); - //AutoInitShutdownAttribute.RunIteration (); + //AutoInitDisposeAttribute.RunIteration (); Assert.Equal (new (1), lv.Border!.Thickness); Assert.Null (lv.SelectedItem); Assert.Equal ("", lv.Text); + app.LayoutAndDraw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -951,7 +952,7 @@ public class ListViewTests (ITestOutputHelper output) Assert.Equal (2, lv.SelectedItem); top.Dispose (); - app?.Shutdown (); + app?.Dispose (); } [Fact] @@ -971,11 +972,12 @@ public class ListViewTests (ITestOutputHelper output) var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper (source) }; var win = new Window (); win.Add (lv); - var top = new Toplevel (); + var top = new Runnable (); top.Add (win); app.Begin (top); Assert.Null (lv.SelectedItem); + app.LayoutAndDraw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1204,7 +1206,7 @@ public class ListViewTests (ITestOutputHelper output) _output, app.Driver ); top.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -1222,9 +1224,10 @@ public class ListViewTests (ITestOutputHelper output) } var lv = new ListView { Width = 10, Height = 5, Source = new ListWrapper (source) }; - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); + app.LayoutAndDraw (); DriverAssert.AssertDriverContentsWithFrameAre ( @" @@ -1250,7 +1253,7 @@ Item 6", _output, app.Driver ); top.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -1264,9 +1267,10 @@ Item 6", ObservableCollection source = ["First", "Second"]; var lv = new ListView { Width = Dim.Fill (), Height = 1, Source = new ListWrapper (source) }; lv.SelectedItem = 1; - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); + app.LayoutAndDraw (); Assert.Equal ("Second ", GetContents (0)); Assert.Equal (new (' ', 7), GetContents (1)); @@ -1290,7 +1294,7 @@ Item 6", } top.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -1314,7 +1318,7 @@ Item 6", }; lv.Height = lv.Source.Count; lv.Width = lv.MaxLength; - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); @@ -1339,7 +1343,7 @@ Item 6", tem 4", _output, app.Driver); top.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -1352,7 +1356,7 @@ Item 6", ObservableCollection source = ["one", "two", "three"]; var lv = new ListView { Width = Dim.Fill (), Height = Dim.Fill () }; lv.RowRender += (s, _) => rendered = true; - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); Assert.False (rendered); @@ -1361,7 +1365,7 @@ Item 6", lv.Draw (); Assert.True (rendered); top.Dispose (); - app.Shutdown (); + app.Dispose (); } [Fact] @@ -1377,7 +1381,7 @@ Item 6", }; lv.VerticalScrollBar.AutoShow = true; lv.SetSource (["One", "Two", "Three", "Four", "Five"]); - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); @@ -1402,7 +1406,7 @@ Four Five ", _output, app?.Driver); top.Dispose (); - app?.Shutdown (); + app?.Dispose (); } [Fact] @@ -1417,7 +1421,7 @@ Five ", Height = 3, }; lv.SetSource (["One", "Two", "Three", "Four", "Five"]); - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); @@ -1453,7 +1457,7 @@ Three", _output, app?.Driver); top.Dispose (); - app?.Shutdown (); + app?.Dispose (); } [Fact] @@ -1480,9 +1484,10 @@ Three", Height = 3, }; lv.SetSource (["One", "Two", "Three - long", "Four", "Five"]); - var top = new Toplevel (); + var top = new Runnable (); top.Add (lv); app.Begin (top); + app.LayoutAndDraw (); Assert.Equal (0, lv.LeftItem); DriverAssert.AssertDriverContentsWithFrameAre ( @@ -1525,7 +1530,7 @@ hree - lon", _output, app?.Driver); top.Dispose (); - app?.Shutdown (); + app?.Dispose (); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs index f4405628f..90320c399 100644 --- a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs @@ -1,8 +1,8 @@ using Xunit.Abstractions; -//using static Terminal.Gui.ViewTests.MenuTests; +//using static Terminal.Gui.ViewBaseTests.MenuTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class MenuBarItemTests () { diff --git a/Tests/UnitTestsParallelizable/Views/MenuTests.cs b/Tests/UnitTestsParallelizable/Views/MenuTests.cs index a20f7d53f..0bb2c5e46 100644 --- a/Tests/UnitTestsParallelizable/Views/MenuTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MenuTests.cs @@ -1,8 +1,8 @@ using Xunit.Abstractions; -//using static Terminal.Gui.ViewTests.MenuTests; +//using static Terminal.Gui.ViewBaseTests.MenuTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class MenuTests () { diff --git a/Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs b/Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs index d7c262789..47bdcf8be 100644 --- a/Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs +++ b/Tests/UnitTestsParallelizable/Views/MessageBoxTests.cs @@ -4,7 +4,7 @@ using UICatalog; using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class MessageBoxTests (ITestOutputHelper output) { @@ -21,7 +21,7 @@ public class MessageBoxTests (ITestOutputHelper output) var btnAcceptCount = 0; app.Iteration += OnApplicationOnIteration; - app.Run ().Dispose (); + app.Run> (); app.Iteration -= OnApplicationOnIteration; Assert.Equal (1, result); @@ -60,7 +60,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -76,7 +76,7 @@ public class MessageBoxTests (ITestOutputHelper output) var iteration = 0; app.Iteration += OnApplicationOnIteration; - app.Run ().Dispose (); + app.Run> (); app.Iteration -= OnApplicationOnIteration; Assert.Null (result); @@ -107,7 +107,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -124,7 +124,7 @@ public class MessageBoxTests (ITestOutputHelper output) var btnAcceptCount = 0; app.Iteration += OnApplicationOnIteration; - app.Run ().Dispose (); + app.Run> (); app.Iteration -= OnApplicationOnIteration; Assert.Equal (1, result); @@ -162,7 +162,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -193,7 +193,7 @@ public class MessageBoxTests (ITestOutputHelper output) var mbFrame = Rectangle.Empty; app.Iteration += OnApplicationOnIteration; - app.Run ().Dispose (); + app.Run> (); app.Iteration -= OnApplicationOnIteration; Assert.Equal (new (expectedX, expectedY, expectedW, expectedH), mbFrame); @@ -209,14 +209,14 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - mbFrame = app.TopRunnable!.Frame; + mbFrame = app.TopRunnableView!.Frame; app.RequestStop (); } } } finally { - app.Shutdown (); + app.Dispose (); } } @@ -229,7 +229,7 @@ public class MessageBoxTests (ITestOutputHelper output) try { int iterations = -1; - var top = new Toplevel (); + var top = new Runnable (); top.BorderStyle = LineStyle.None; app.Driver!.SetScreenSize (20, 10); @@ -300,7 +300,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -313,7 +313,7 @@ public class MessageBoxTests (ITestOutputHelper output) try { int iterations = -1; - var top = new Toplevel (); + var top = new Runnable (); top.BorderStyle = LineStyle.None; app.Driver!.SetScreenSize (20, 10); @@ -392,7 +392,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -426,15 +426,15 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - Assert.IsType (app.TopRunnable); - Assert.Equal (new (height, width), app.TopRunnable.Frame.Size); + Assert.IsType (app.TopRunnableView); + Assert.Equal (new (height, width), app.TopRunnableView.Frame.Size); app.RequestStop (); } }; } finally { - app.Shutdown (); + app.Dispose (); } } @@ -467,15 +467,15 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - Assert.IsType (app.TopRunnable); - Assert.Equal (new (height, width), app.TopRunnable.Frame.Size); + Assert.IsType (app.TopRunnableView); + Assert.Equal (new (height, width), app.TopRunnableView.Frame.Size); app.RequestStop (); } }; } finally { - app.Shutdown (); + app.Dispose (); } } @@ -504,15 +504,15 @@ public class MessageBoxTests (ITestOutputHelper output) } else if (iterations == 1) { - Assert.IsType (app.TopRunnable); - Assert.Equal (new (height, width), app.TopRunnable.Frame.Size); + Assert.IsType (app.TopRunnableView); + Assert.Equal (new (height, width), app.TopRunnableView.Frame.Size); app.RequestStop (); } }; } finally { - app.Shutdown (); + app.Dispose (); } } @@ -535,7 +535,7 @@ public class MessageBoxTests (ITestOutputHelper output) app.Iteration += OnApplicationOnIteration; - var top = new Toplevel (); + var top = new Runnable (); top.BorderStyle = LineStyle.Single; try { @@ -556,7 +556,7 @@ public class MessageBoxTests (ITestOutputHelper output) MessageBox.Query ( app, "", - UICatalogTop.GetAboutBoxMessage (), + UICatalogRunnable.GetAboutBoxMessage (), wrapMessage: false, buttons: "_Ok"); @@ -590,7 +590,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } @@ -613,7 +613,7 @@ public class MessageBoxTests (ITestOutputHelper output) } finally { - app.Shutdown (); + app.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs index 7e72ebe5f..309b28a06 100644 --- a/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs +++ b/Tests/UnitTestsParallelizable/Views/NumericUpDownTests.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class NumericUpDownTests { diff --git a/Tests/UnitTestsParallelizable/Views/OptionSelectorTests.cs b/Tests/UnitTestsParallelizable/Views/OptionSelectorTests.cs index 79e8f8420..15532bc8f 100644 --- a/Tests/UnitTestsParallelizable/Views/OptionSelectorTests.cs +++ b/Tests/UnitTestsParallelizable/Views/OptionSelectorTests.cs @@ -1,4 +1,5 @@ -namespace UnitTests_Parallelizable.ViewsTests; +#nullable disable +namespace ViewsTests; public class OptionSelectorTests { diff --git a/Tests/UnitTestsParallelizable/Views/ScrollBarTests.cs b/Tests/UnitTestsParallelizable/Views/ScrollBarTests.cs index fb1011ca0..4f7e428a7 100644 --- a/Tests/UnitTestsParallelizable/Views/ScrollBarTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ScrollBarTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class ScrollBarTests { @@ -22,7 +22,7 @@ public class ScrollBarTests [Fact] public void AutoHide_False_Is_Default_CorrectlyHidesAndShows () { - var super = new Toplevel () + var super = new Runnable () { Id = "super", Width = 1, @@ -55,7 +55,7 @@ public class ScrollBarTests [Fact] public void AutoHide_False_CorrectlyHidesAndShows () { - var super = new Toplevel () + var super = new Runnable () { Id = "super", Width = 1, @@ -81,7 +81,7 @@ public class ScrollBarTests [Fact] public void AutoHide_True_Changing_ScrollableContentSize_CorrectlyHidesAndShows () { - var super = new Toplevel () + var super = new Runnable () { Id = "super", Width = 1, @@ -125,7 +125,7 @@ public class ScrollBarTests [Fact] public void AutoHide_Change_VisibleContentSize_CorrectlyHidesAndShows () { - var super = new Toplevel () + var super = new Runnable () { Id = "super", Width = 1, diff --git a/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs b/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs index c28d5e7c1..0cb1785f2 100644 --- a/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ScrollSliderTests.cs @@ -1,7 +1,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class ScrollSliderTests (ITestOutputHelper output) : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs b/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs index 8077aa277..0f105b356 100644 --- a/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// /// Tests for functionality that applies to all selector implementations. diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs index 7ca25dbad..2e6bfe7fa 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs @@ -1,9 +1,7 @@ -using JetBrains.Annotations; -using UnitTests.Parallelizable; +#nullable enable +using JetBrains.Annotations; -namespace UnitTests_Parallelizable.ViewsTests; - -[Collection ("Global Test Setup")] +namespace ViewsTests; [TestSubject (typeof (Shortcut))] public class ShortcutTests @@ -300,7 +298,8 @@ public class ShortcutTests [Fact] public void BindKeyToApplication_Can_Be_Set () { - var shortcut = new Shortcut (); + IApplication? app = Application.Create (); + var shortcut = new Shortcut () { App = app }; shortcut.BindKeyToApplication = true; @@ -443,4 +442,54 @@ public class ShortcutTests Assert.False (shortcut.CanFocus); Assert.True (shortcut.CommandView.CanFocus); } + + [Theory (Skip = "Broke somehow!")] + [InlineData (true, KeyCode.A, 1, 1)] + [InlineData (true, KeyCode.C, 1, 1)] + [InlineData (true, KeyCode.C | KeyCode.AltMask, 1, 1)] + [InlineData (true, KeyCode.Enter, 1, 1)] + [InlineData (true, KeyCode.Space, 1, 1)] + [InlineData (true, KeyCode.F1, 0, 0)] + [InlineData (false, KeyCode.A, 1, 1)] + [InlineData (false, KeyCode.C, 1, 1)] + [InlineData (false, KeyCode.C | KeyCode.AltMask, 1, 1)] + [InlineData (false, KeyCode.Enter, 0, 0)] + [InlineData (false, KeyCode.Space, 0, 0)] + [InlineData (false, KeyCode.F1, 0, 0)] + public void KeyDown_CheckBox_Raises_Accepted_Selected (bool canFocus, KeyCode key, int expectedAccept, int expectedSelect) + { + IApplication? app = Application.Create (); + Runnable runnable = new (); + app.Begin (runnable); + + var shortcut = new Shortcut + { + Key = Key.A, + Text = "0", + CommandView = new CheckBox () + { + Title = "_C" + }, + CanFocus = canFocus + }; + runnable.Add (shortcut); + + Assert.Equal (canFocus, shortcut.HasFocus); + + var accepted = 0; + shortcut.Accepting += (s, e) => + { + accepted++; + e.Handled = true; + }; + + var selected = 0; + shortcut.Selecting += (s, e) => selected++; + + app.Keyboard.RaiseKeyDownEvent (key); + + Assert.Equal (expectedAccept, accepted); + Assert.Equal (expectedSelect, selected); + } + } diff --git a/Tests/UnitTestsParallelizable/Views/SliderTests.cs b/Tests/UnitTestsParallelizable/Views/SliderTests.cs index 9e3b8f2f4..9aa71097d 100644 --- a/Tests/UnitTestsParallelizable/Views/SliderTests.cs +++ b/Tests/UnitTestsParallelizable/Views/SliderTests.cs @@ -1,7 +1,7 @@ using System.Text; using UnitTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class SliderOptionTests : FakeDriverBase { diff --git a/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs b/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs index d230ff962..def4b6399 100644 --- a/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs +++ b/Tests/UnitTestsParallelizable/Views/SpinnerStyleTests.cs @@ -1,5 +1,5 @@ #nullable enable -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; /// /// Parallelizable tests for and its concrete implementations. diff --git a/Tests/UnitTestsParallelizable/Views/TableViewTests.cs b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs index 8fefa1230..a4edfc86f 100644 --- a/Tests/UnitTestsParallelizable/Views/TableViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TableViewTests.cs @@ -1,16 +1,200 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Data; +#nullable enable using JetBrains.Annotations; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; [TestSubject (typeof (TableView))] public class TableViewTests { + [Fact] + public void CanTabOutOfTableViewUsingCursor_Left () + { + GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); + + // Make the selected cell one in + tableView.SelectedColumn = 1; + + // Pressing left should move us to the first column without changing focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorLeft); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the leftmost cell a further left press should move focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorLeft); + + Assert.NotSame (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, tableView.App!.TopRunnableView.MostFocused); + Assert.True (tf1.HasFocus); + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Up () + { + GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); + + // Make the selected cell one in + tableView.SelectedRow = 1; + + // First press should move us up + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorUp); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the top row a further press should move focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorUp); + + Assert.NotSame (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tf1.HasFocus); + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Right () + { + GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); + + // Make the selected cell one in from the rightmost column + tableView.SelectedColumn = tableView.Table.Columns - 2; + + // First press should move us to the rightmost column without changing focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorRight); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the rightmost cell, a further right press should move focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorRight); + + Assert.NotSame (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf2, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tf2.HasFocus); + + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Down () + { + GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); + + // Make the selected cell one in from the bottommost row + tableView.SelectedRow = tableView.Table.Rows - 2; + + // First press should move us to the bottommost row without changing focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorDown); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the bottommost cell, a further down press should move focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorDown); + + Assert.NotSame (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf2, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tf2.HasFocus); + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Left_ClearsSelectionFirst () + { + GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2); + + // Make the selected cell one in + tableView.SelectedColumn = 1; + + // Pressing shift-left should give us a multi selection + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorLeft.WithShift); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); + + // Because we are now on the leftmost cell a further left press would normally move focus + // However there is an ongoing selection so instead the operation clears the selection and + // gets swallowed (not resulting in a focus change) + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorLeft); + + // Selection 'clears' just to the single cell and we remain focused + Assert.Single (tableView.GetAllSelectedCells ()); + Assert.Same (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tableView.HasFocus); + + // A further left will switch focus + tableView.App!.Keyboard.RaiseKeyDownEvent (Key.CursorLeft); + + Assert.NotSame (tableView, tableView.App!.TopRunnableView!.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, tableView.App!.TopRunnableView!.MostFocused); + Assert.True (tf1.HasFocus); + } + + /// + /// Creates 3 views on with the focus in the + /// . This is a helper method to setup tests that want to + /// explore moving input focus out of a tableview. + /// + /// + /// + /// + private void GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2) + { + IApplication? app = Application.Create (); + Runnable? runnable = new (); + app.Begin (runnable); + + tableView = new (); + + tf1 = new (); + tf2 = new (); + runnable.Add (tf1); + runnable.Add (tableView); + runnable.Add (tf2); + + tableView.SetFocus (); + + Assert.Same (tableView, runnable.MostFocused); + Assert.True (tableView.HasFocus); + + // Set big table + tableView.Table = BuildTable (25, 50); + } + + public static DataTableSource BuildTable (int cols, int rows) => BuildTable (cols, rows, out _); + + /// Builds a simple table of string columns with the requested number of columns and rows + /// + /// + /// + public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) + { + dt = new (); + + for (var c = 0; c < cols; c++) + { + dt.Columns.Add ("Col" + c); + } + + for (var r = 0; r < rows; r++) + { + DataRow newRow = dt.NewRow (); + + for (var c = 0; c < cols; c++) + { + newRow [c] = $"R{r}C{c}"; + } + + dt.Rows.Add (newRow); + } + + return new (dt); + } + [Fact] public void TableView_CollectionNavigatorMatcher_KeybindingsOverrideNavigator () { diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs index eff2c41b2..70d3121ab 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -2,7 +2,7 @@ using UnitTests; using Xunit.Abstractions; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase { @@ -51,7 +51,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied."); - Toplevel top = new (); + Runnable top = new (); top.Add (tf); tf.SetFocus (); top.NewKeyDownEvent (Key.Space); @@ -67,7 +67,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase var selectingCount = 0; tf.Selecting += (sender, args) => selectingCount++; - Toplevel top = new (); + Runnable top = new (); top.Add (tf); tf.SetFocus (); top.NewKeyDownEvent (Key.Enter); @@ -85,7 +85,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase var acceptedCount = 0; tf.Accepting += (sender, args) => acceptedCount++; - Toplevel top = new (); + Runnable top = new (); top.Add (tf); tf.SetFocus (); top.NewKeyDownEvent (Key.Enter); @@ -118,7 +118,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase return; - void OnAccept (object sender, CommandEventArgs e) { accepted = true; } + void OnAccept (object? sender, CommandEventArgs e) { accepted = true; } } [Fact] @@ -133,7 +133,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase return; - void Accept (object sender, CommandEventArgs e) { accepted = true; } + void Accept (object? sender, CommandEventArgs e) { accepted = true; } } [Fact] @@ -172,7 +172,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase return; - void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; } + void ButtonAccept (object? sender, CommandEventArgs e) { buttonAccept++; } } [Fact] @@ -199,7 +199,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase return; - void TextViewAccept (object sender, CommandEventArgs e) + void TextViewAccept (object? sender, CommandEventArgs e) { tfAcceptedInvoked = true; e.Handled = handle; @@ -209,7 +209,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility () { - var top = new Toplevel (); + var top = new Runnable (); var tf = new TextField { Width = 10 }; top.Add (tf); @@ -291,7 +291,7 @@ public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase return; - void HandleJKey (object s, Key arg) + void HandleJKey (object? s, Key arg) { if (arg.AsRune == new Rune ('j')) { diff --git a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs index d79b6a0c1..d8904f668 100644 --- a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; using UnitTests; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class TextValidateField_NET_Provider_Tests : FakeDriverBase { @@ -57,7 +57,7 @@ public class TextValidateField_NET_Provider_Tests : FakeDriverBase Assert.True (field.IsValid); var provider = field.Provider as NetMaskedTextProvider; - provider.Mask = "--------(00000000)--------"; + provider!.Mask = "--------(00000000)--------"; Assert.Equal ("--------(1234____)--------", field.Provider.DisplayText); Assert.False (field.IsValid); } diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs index a9b940bea..4618865cd 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs @@ -1,6 +1,7 @@ -using System.Text; +#nullable disable +using System.Text; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class TextViewTests { @@ -2069,7 +2070,7 @@ public class TextViewTests new () { Grapheme = new ("t") } }; TextView tv = CreateTextView (); - var top = new Toplevel (); + var top = new Runnable (); top.Add (tv); tv.Load (cells); diff --git a/Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs index f01b7cc38..2596e307d 100644 --- a/Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TimeFieldTests.cs @@ -1,4 +1,4 @@ -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; public class TimeFieldTests { @@ -67,7 +67,7 @@ public class TimeFieldTests } finally { - app.Shutdown(); + app.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs index 11e16bdf6..e269fc967 100644 --- a/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace UnitTests_Parallelizable.ViewsTests; +namespace ViewsTests; [TestSubject (typeof (TreeView))] public class TreeViewTests diff --git a/Tests/UnitTestsParallelizable/xunit.runner.json b/Tests/UnitTestsParallelizable/xunit.runner.json index 8a107f065..ba11279f6 100644 --- a/Tests/UnitTestsParallelizable/xunit.runner.json +++ b/Tests/UnitTestsParallelizable/xunit.runner.json @@ -3,5 +3,5 @@ "parallelizeTestCollections": true, "parallelizeAssembly": true, "stopOnFail": false, - "maxParallelThreads": "2x" + "maxParallelThreads": "default" } \ No newline at end of file diff --git a/docfx/docs/View.md b/docfx/docs/View.md index 635ceb824..c22585d1e 100644 --- a/docfx/docs/View.md +++ b/docfx/docs/View.md @@ -271,7 +271,7 @@ View view = new () ### 2. Initialization -When a View is added to a SuperView or when [Application.Run](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_Run_Terminal_Gui_Views_Toplevel_System_Func_System_Exception_System_Boolean__) is called: +When a View is added to a SuperView or when [Application.Run](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_Run_Terminal_Gui_Views_Runnable_System_Func_System_Exception_System_Boolean__) is called: 1. [BeginInit](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_BeginInit) is called 2. [EndInit](~/api/Terminal.Gui.ViewBase.View.yml#Terminal_Gui_ViewBase_View_EndInit) is called @@ -566,7 +566,7 @@ Views can implement [IRunnable](~/api/Terminal.Gui.App.IRunnable.yml) to run as The **IRunnable** pattern provides: -- **Interface-Based**: Implement `IRunnable` instead of inheriting from `Toplevel` +- **Interface-Based**: Implement `IRunnable` instead of inheriting from `Runnable` - **Type-Safe Results**: Generic `TResult` parameter for compile-time type safety - **Fluent API**: Chain `Init()`, `Run()`, and `Shutdown()` for concise code - **Automatic Disposal**: Framework manages lifecycle of created runnables @@ -674,14 +674,13 @@ protected override bool OnIsRunningChanging(bool oldIsRunning, bool newIsRunning - **`IsRunningChanging`** - Cancellable event before added/removed from stack - **`IsRunningChanged`** - Non-cancellable event after stack change -- **`IsModalChanging`** - Cancellable event before becoming/leaving top of stack - **`IsModalChanged`** - Non-cancellable event after modal state change --- ## Modal Views (Legacy) -Views can run modally (exclusively capturing all input until closed). See [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) for the legacy pattern. +Views can run modally (exclusively capturing all input until closed). See [Runnable](~/api/Terminal.Gui.Views.Runnable.yml) for the legacy pattern. **Note:** New code should use `IRunnable` pattern (see above) for better type safety and lifecycle management. @@ -708,7 +707,7 @@ dialog.Dispose(); ### Modal View Types (Legacy) -- **[Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml)** - Base class for modal views, can fill entire screen +- **[Runnable](~/api/Terminal.Gui.Views.Runnable.yml)** - Base class for modal views, can fill entire screen - **[Window](~/api/Terminal.Gui.Views.Window.yml)** - Overlapped container with border and title - **[Dialog](~/api/Terminal.Gui.Views.Dialog.yml)** - Modal Window, centered with button support - **[Wizard](~/api/Terminal.Gui.Views.Wizard.yml)** - Multi-step modal dialog diff --git a/docfx/docs/application.md b/docfx/docs/application.md index d624441b3..369488621 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -5,8 +5,9 @@ Terminal.Gui v2 uses an instance-based application architecture with the **IRunn ## Key Features - **Instance-Based**: Use `Application.Create()` to get an `IApplication` instance instead of static methods -- **IRunnable Interface**: Views implement `IRunnable` to participate in session management without inheriting from `Toplevel` -- **Fluent API**: Chain `Init()`, `Run()`, and `Shutdown()` for elegant, concise code +- **IRunnable Interface**: Views implement `IRunnable` to participate in session management without inheriting from `Runnable` +- **Fluent API**: Chain `Init()` and `Run()` for elegant, concise code +- **IDisposable Pattern**: Proper resource cleanup with `Dispose()` or `using` statements - **Automatic Disposal**: Framework-created runnables are automatically disposed - **Type-Safe Results**: Generic `TResult` parameter provides compile-time type safety - **CWP Compliance**: All lifecycle events follow the Cancellable Work Pattern @@ -34,8 +35,8 @@ graph TB subgraph Stack["app.SessionStack"] direction TB S1[Window
Currently Active] - S2[Previous Toplevel
Waiting] - S3[Base Toplevel
Waiting] + S2[Previous Runnable
Waiting] + S3[Base Runnable
Waiting] S1 -.-> S2 -.-> S3 end @@ -82,26 +83,33 @@ sequenceDiagram ```csharp // OLD (v1 / early v2 - still works but obsolete): Application.Init(); -var top = new Toplevel(); +var top = new Window(); top.Add(myView); Application.Run(top); top.Dispose(); -Application.Shutdown(); +Application.Shutdown(); // Obsolete - use Dispose() instead -// NEW (v2 recommended - instance-based): -var app = Application.Create(); -app.Init(); -var top = new Toplevel(); -top.Add(myView); -app.Run(top); -top.Dispose(); -app.Shutdown(); +// RECOMMENDED (v2 - instance-based with using statement): +using (var app = Application.Create().Init()) +{ + var top = new Window(); + top.Add(myView); + app.Run(top); + top.Dispose(); +} // app.Dispose() called automatically -// NEWEST (v2 with IRunnable and Fluent API): -Color? result = Application.Create() - .Init() - .Run() - .Shutdown() as Color?; +// WITH IRunnable (fluent API with automatic disposal): +using (var app = Application.Create().Init()) +{ + app.Run(); + Color? result = app.GetResult(); +} + +// SIMPLEST (manual disposal): +var app = Application.Create().Init(); +app.Run(); +Color? result = app.GetResult(); +app.Dispose(); ``` **Note:** The static `Application` class delegates to `ApplicationImpl.Instance` (a singleton). `Application.Create()` creates a **new** `ApplicationImpl` instance, enabling multiple application contexts and better testability. @@ -175,11 +183,11 @@ public class MyView : View ## IRunnable Architecture -Terminal.Gui v2 introduces the **IRunnable** interface pattern that decouples runnable behavior from the `Toplevel` class hierarchy. Views can implement `IRunnable` to participate in session management without inheritance constraints. +Terminal.Gui v2 introduces the **IRunnable** interface pattern that decouples runnable behavior from the `Runnable` class hierarchy. Views can implement `IRunnable` to participate in session management without inheritance constraints. ### Key Benefits -- **Interface-Based**: No forced inheritance from `Toplevel` +- **Interface-Based**: No forced inheritance from `Runnable` - **Type-Safe Results**: Generic `TResult` parameter provides compile-time type safety - **Fluent API**: Method chaining for elegant, concise code - **Automatic Disposal**: Framework manages lifecycle of created runnables @@ -190,11 +198,23 @@ Terminal.Gui v2 introduces the **IRunnable** interface pattern that decouples ru The fluent API enables elegant method chaining with automatic resource management: ```csharp -// All-in-one: Create, initialize, run, shutdown, and extract result -Color? result = Application.Create() - .Init() - .Run() - .Shutdown() as Color?; +// Recommended: using statement with GetResult +using (var app = Application.Create().Init()) +{ + app.Run(); + Color? result = app.GetResult(); + + if (result is { }) + { + ApplyColor(result); + } +} + +// Alternative: Manual disposal +var app = Application.Create().Init(); +app.Run(); +Color? result = app.GetResult(); +app.Dispose(); if (result is { }) { @@ -206,7 +226,8 @@ if (result is { }) - `Init()` - Returns `IApplication` for chaining - `Run()` - Creates and runs runnable, returns `IApplication` -- `Shutdown()` - Disposes framework-owned runnables, returns `object?` result +- `GetResult()` / `GetResult()` - Extract typed result after run +- `Dispose()` - Release all resources (called automatically with `using`) ### Disposal Semantics @@ -214,18 +235,25 @@ if (result is { }) | Method | Creator | Owner | Disposal | |--------|---------|-------|----------| -| `Run()` | Framework | Framework | Automatic in `Shutdown()` | +| `Run()` | Framework | Framework | Automatic when `Run()` returns | | `Run(IRunnable)` | Caller | Caller | Manual by caller | ```csharp // Framework ownership - automatic disposal -var result = app.Run().Shutdown(); +using (var app = Application.Create().Init()) +{ + app.Run(); // Dialog disposed automatically when Run returns + var result = app.GetResult(); +} // Caller ownership - manual disposal -var dialog = new MyDialog(); -app.Run(dialog); -var result = dialog.Result; -dialog.Dispose(); // Caller must dispose +using (var app = Application.Create().Init()) +{ + var dialog = new MyDialog(); + app.Run(dialog); + var result = dialog.Result; + dialog.Dispose(); // Caller must dispose +} ``` ### Creating Runnable Views @@ -277,7 +305,6 @@ All events follow Terminal.Gui's Cancellable Work Pattern: |-------|-------------|------|----------| | `IsRunningChanging` | ✓ | Before add/remove from stack | Extract result, prevent close | | `IsRunningChanged` | ✗ | After stack change | Post-start/stop cleanup | -| `IsModalChanging` | ✓ | Before becoming/leaving top | Prevent activation | | `IsModalChanged` | ✗ | After modal state change | Update UI after focus change | **Example - Result Extraction:** @@ -332,40 +359,35 @@ public interface IApplication ## IApplication Interface -The `IApplication` interface defines the application contract with support for both legacy `Toplevel` and modern `IRunnable` patterns: +The `IApplication` interface defines the application contract with support for both legacy `Runnable` and modern `IRunnable` patterns: ```csharp public interface IApplication { - // Legacy Toplevel support - Toplevel? Current { get; } - ConcurrentStack SessionStack { get; } - - // IRunnable support + // IRunnable support (primary) IRunnable? TopRunnable { get; } - ConcurrentStack? RunnableSessionStack { get; } - IRunnable? FrameworkOwnedRunnable { get; set; } + View? TopRunnableView { get; } + ConcurrentStack? SessionStack { get; } // Driver and lifecycle IDriver? Driver { get; } - IMainLoopCoordinator? MainLoop { get; } + IMainLoopCoordinator? Coordinator { get; } - // Fluent API methods + // Fluent API methods IApplication Init(string? driverName = null); - object? Shutdown(); + void Dispose(); // IDisposable // Runnable methods - RunnableSessionToken Begin(IRunnable runnable); - void Run(IRunnable runnable, Func? errorHandler = null); + SessionToken? Begin(IRunnable runnable); + object? Run(IRunnable runnable, Func? errorHandler = null); IApplication Run(Func? errorHandler = null) where TRunnable : IRunnable, new(); void RequestStop(IRunnable? runnable); - void End(RunnableSessionToken sessionToken); - - // Legacy Toplevel methods - SessionToken? Begin(Toplevel toplevel); - void Run(Toplevel view, Func? errorHandler = null); void End(SessionToken sessionToken); + // Result extraction + object? GetResult(); + T? GetResult() where T : class; + // ... other members } ``` @@ -376,28 +398,32 @@ Terminal.Gui v2 modernized its terminology for clarity: ### Application.TopRunnable (formerly "Current", and before that "Top") -The `TopRunnable` property represents the Toplevel on the top of the session stack (the active runnable session): +The `TopRunnable` property represents the `IRunnable` on the top of the session stack (the active runnable session): ```csharp // Access the top runnable session -Toplevel? topRunnable = app.TopRunnable; +IRunnable? topRunnable = app.TopRunnable; -// From within a view -Toplevel? topRunnable = App?.TopRunnable; +// From within a view +IRunnable? topRunnable = App?.TopRunnable; + +// Cast to View if needed +View? topView = app.TopRunnableView; ``` **Why "TopRunnable"?** - Clearly indicates it's the top of the runnable session stack -- Aligns with the IRunnable architecture proposal +- Aligns with the IRunnable architecture - Distinguishes from other concepts like "Current" which could be ambiguous +- Works with any view that implements `IRunnable`, not just `Runnable` -### Application.SessionStack (formerly "TopLevels") +### Application.SessionStack (formerly "Runnables") The `SessionStack` property is the stack of running sessions: ```csharp // Access all running sessions -foreach (var toplevel in app.SessionStack) +foreach (var runnable in app.SessionStack) { // Process each session } @@ -406,7 +432,7 @@ foreach (var toplevel in app.SessionStack) int sessionCount = App?.SessionStack.Count ?? 0; ``` -**Why "SessionStack" instead of "TopLevels"?** +**Why "SessionStack" instead of "Runnables"?** - Describes both content (sessions) and structure (stack) - Aligns with `SessionToken` terminology - Follows .NET naming patterns (descriptive + collection type) @@ -419,10 +445,13 @@ The static `Application` class delegates to `ApplicationImpl.Instance` (a single public static partial class Application { [Obsolete("The legacy static Application object is going away.")] - public static Toplevel? Current => ApplicationImpl.Instance.Current; + public static View? TopRunnableView => ApplicationImpl.Instance.TopRunnableView; [Obsolete("The legacy static Application object is going away.")] - public static ConcurrentStack SessionStack => ApplicationImpl.Instance.SessionStack; + public static IRunnable? TopRunnable => ApplicationImpl.Instance.TopRunnable; + + [Obsolete("The legacy static Application object is going away.")] + public static ConcurrentStack? SessionStack => ApplicationImpl.Instance.SessionStack; // ... other obsolete static members } @@ -444,7 +473,7 @@ void MyMethod() // NEW: void MyMethod(View view) { - view.App?.Current?.SetNeedsDraw(); + view.App?.TopRunnableView?.SetNeedsDraw(); } ``` @@ -454,7 +483,7 @@ void MyMethod(View view) // OLD: void ProcessSessions() { - foreach (var toplevel in Application.SessionStack) + foreach (var runnable in Application.SessionStack) { // Process } @@ -463,7 +492,7 @@ void ProcessSessions() // NEW: void ProcessSessions(IApplication app) { - foreach (var toplevel in app.SessionStack) + foreach (var runnable in app.SessionStack) { // Process } @@ -489,6 +518,118 @@ public class MyService } ``` +## Resource Management and Disposal + +Terminal.Gui v2 implements the `IDisposable` pattern for proper resource cleanup. Applications must be disposed after use to: +- Stop the input thread cleanly +- Release driver resources +- Prevent thread leaks in tests +- Free unmanaged resources + +### Using the `using` Statement (Recommended) + +```csharp +// Automatic disposal with using statement +using (var app = Application.Create().Init()) +{ + app.Run(); + // app.Dispose() automatically called when scope exits +} +``` + +### Manual Disposal + +```csharp +// Manual disposal +var app = Application.Create(); +try +{ + app.Init(); + app.Run(); +} +finally +{ + app.Dispose(); // Ensure cleanup even if exception occurs +} +``` + +### Dispose() and Result Retrieval + +- **`Dispose()`** - Standard IDisposable pattern for resource cleanup (required) +- **`GetResult()`** / **`GetResult()`** - Retrieve results after run completes +- **`Shutdown()`** - Obsolete (use `Dispose()` instead) + +```csharp +// RECOMMENDED (using statement): +using (var app = Application.Create().Init()) +{ + app.Run(); + var result = app.GetResult(); + // app.Dispose() called automatically here +} + +// ALTERNATIVE (manual disposal): +var app = Application.Create().Init(); +app.Run(); +var result = app.GetResult(); +app.Dispose(); // Must call explicitly + +// OLD (obsolete - do not use): +var result = app.Run().Shutdown() as MyResult; +``` + +### Input Thread Lifecycle + +When you call `Init()`, Terminal.Gui starts a dedicated input thread that continuously polls for console input. This thread must be stopped properly: + +```csharp +var app = Application.Create(); +app.Init("fake"); // Input thread starts here + +// Input thread runs in background at ~50 polls/second (20ms throttle) + +app.Dispose(); // Cancels input thread and waits for it to exit +``` + +**Important for Tests**: Always dispose applications in tests to prevent thread leaks: + +```csharp +[Fact] +public void My_Test() +{ + using var app = Application.Create(); + app.Init("fake"); + + // Test code here + + // app.Dispose() called automatically +} +``` + +### Singleton Re-initialization + +The legacy static `Application` singleton can be re-initialized after disposal (for backward compatibility with old tests): + +```csharp +// Test 1 +Application.Init(); +Application.Shutdown(); // Obsolete but still works for legacy singleton + +// Test 2 - singleton resets and can be re-initialized +Application.Init(); // ✅ Works! +Application.Shutdown(); // Obsolete but still works for legacy singleton +``` + +However, instance-based applications follow standard `IDisposable` semantics and cannot be reused after disposal: + +```csharp +var app = Application.Create(); +app.Init(); +app.Dispose(); + +app.Init(); // ❌ Throws ObjectDisposedException +``` + ## Session Management ### Begin and End @@ -496,16 +637,16 @@ public class MyService Applications manage sessions through `Begin()` and `End()`: ```csharp -var app = Application.Create (); +using var app = Application.Create (); app.Init(); -var toplevel = new Toplevel(); +var window = new Window(); // Begin a new session - pushes to SessionStack -SessionToken? token = app.Begin(toplevel); +SessionToken? token = app.Begin(window); -// Current now points to this toplevel -Debug.Assert(app.Current == toplevel); +// TopRunnable now points to this window +Debug.Assert(app.TopRunnable == window); // End the session - pops from SessionStack if (token != null) @@ -513,7 +654,7 @@ if (token != null) app.End(token); } -// Current restored to previous toplevel (if any) +// TopRunnable restored to previous runnable (if any) ``` ### Nested Sessions @@ -521,26 +662,26 @@ if (token != null) Multiple sessions can run nested: ```csharp -var app = Application.Create (); +using var app = Application.Create (); app.Init(); // Session 1 -var main = new Toplevel { Title = "Main" }; +var main = new Window { Title = "Main" }; var token1 = app.Begin(main); -// app.Current == main, SessionStack.Count == 1 +// app.TopRunnable == main, SessionStack.Count == 1 // Session 2 (nested) var dialog = new Dialog { Title = "Dialog" }; var token2 = app.Begin(dialog); -// app.Current == dialog, SessionStack.Count == 2 +// app.TopRunnable == dialog, SessionStack.Count == 2 // End dialog app.End(token2); -// app.Current == main, SessionStack.Count == 1 +// app.TopRunnable == main, SessionStack.Count == 1 // End main app.End(token1); -// app.Current == null, SessionStack.Count == 0 +// app.TopRunnable == null, SessionStack.Count == 0 ``` ## View.Driver Property @@ -586,7 +727,7 @@ public void MyView_DisplaysCorrectly() { // Create mock application var mockApp = new Mock(); - mockApp.Setup(a => a.Current).Returns(new Toplevel()); + mockApp.Setup(a => a.Current).Returns(new Runnable()); // Create view with mock app var view = new MyView { App = mockApp.Object }; @@ -605,29 +746,21 @@ public void MyView_DisplaysCorrectly() [Fact] public void MyView_WorksWithRealApplication() { - var app = Application.Create (); - try - { - app.Init(new FakeDriver()); - - var view = new MyView(); - var top = new Toplevel(); - top.Add(view); - - app.Begin(top); - - // View.App automatically set - Assert.NotNull(view.App); - Assert.Same(app, view.App); - - // Test view behavior - view.DoSomething(); - - } - finally - { - app.Shutdown(); - } + using var app = Application.Create (); + app.Init("fake"); + + var view = new MyView(); + var top = new Window(); + top.Add(view); + + app.Begin(top); + + // View.App automatically set + Assert.NotNull(view.App); + Assert.Same(app, view.App); + + // Test view behavior + view.DoSomething(); } ``` @@ -639,7 +772,7 @@ public void MyView_WorksWithRealApplication() ✅ GOOD: public void Refresh() { - App?.Current?.SetNeedsDraw(); + App?.TopRunnableView?.SetNeedsDraw(); } ``` @@ -649,7 +782,7 @@ public void Refresh() ❌ AVOID: public void Refresh() { - Application.TopRunnable?.SetNeedsDraw(); // Obsolete! + Application.TopRunnableView?.SetNeedsDraw(); // Obsolete! } ``` @@ -669,13 +802,13 @@ public class Service ❌ AVOID (obsolete pattern): public void Refresh() { - Application.TopRunnable?.SetNeedsDraw(); // Obsolete static access + Application.TopRunnableView?.SetNeedsDraw(); // Obsolete static access } ✅ PREFERRED: public void Refresh() { - App?.Current?.SetNeedsDraw(); // Use View.App property + App?.TopRunnableView?.SetNeedsDraw(); // Use View.App property } ``` @@ -702,15 +835,15 @@ The instance-based architecture enables multiple applications: ```csharp // Application 1 -var app1 = Application.Create (); -app1.Init(new WindowsDriver()); -var top1 = new Toplevel { Title = "App 1" }; +using var app1 = Application.Create (); +app1.Init("windows"); +var top1 = new Window { Title = "App 1" }; // ... configure top1 // Application 2 (different driver!) -var app2 = Application.Create (); -app2.Init(new CursesDriver()); -var top2 = new Toplevel { Title = "App 2" }; +using var app2 = Application.Create (); +app2.Init("unix"); +var top2 = new Window { Title = "App 2" }; // ... configure top2 // Views in top1 use app1 diff --git a/docfx/docs/arrangement.md b/docfx/docs/arrangement.md index 06c4dc7d8..c1775e194 100644 --- a/docfx/docs/arrangement.md +++ b/docfx/docs/arrangement.md @@ -376,14 +376,14 @@ See the [Multitasking Deep Dive](multitasking.md) for complete details on modal ### What Makes a View Modal A view is modal when: -- Run via [Application.Run](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_Run_Terminal_Gui_Views_Toplevel_System_Func_System_Exception_System_Boolean__) -- [Toplevel.Modal](~/api/Terminal.Gui.Views.Toplevel.yml#Terminal_Gui_Views_Toplevel_Modal) = `true` +- Run via [Application.Run](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_Run_Terminal_Gui_Views_Runnable_System_Func_System_Exception_System_Boolean__) +- [Runnable.Modal](~/api/Terminal.Gui.Views.Runnable.yml#Terminal_Gui_Views_Runnable_Modal) = `true` ### Modal Characteristics - **Exclusive Input** - All keyboard and mouse input goes to the modal view - **Constrained Z-Order** - Modal view has Z-order of 1, everything else at 0 -- **Blocks Execution** - `Application.Run` blocks until [Application.RequestStop](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_RequestStop_Terminal_Gui_Views_Toplevel_) is called +- **Blocks Execution** - `Application.Run` blocks until [Application.RequestStop](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_RequestStop_Terminal_Gui_Views_Runnable_) is called - **Own RunState** - Each modal view has its own [RunState](~/api/Terminal.Gui.App.RunState.yml) ### Modal View Types @@ -431,13 +431,13 @@ See the [Multitasking Deep Dive](multitasking.md) for complete details. ### Non-Modal Runnable Views ```csharp -var toplevel = new Toplevel +var runnable = new Runnable { Modal = false // Non-modal }; // Runs as independent application -Application.Run(toplevel); +Application.Run(runnable); ``` **Characteristics:** @@ -572,7 +572,7 @@ Application.Shutdown(); ```csharp Application.Init(); -var top = new Toplevel(); +var top = new Runnable(); var leftPane = new FrameView { @@ -606,7 +606,7 @@ Application.Shutdown(); ```csharp Application.Init(); -var desktop = new Toplevel +var desktop = new Runnable { Arrangement = ViewArrangement.Overlapped }; @@ -724,7 +724,7 @@ view.LayoutComplete += (s, e) => - [ViewArrangement](~/api/Terminal.Gui.ViewBase.ViewArrangement.yml) - [Border](~/api/Terminal.Gui.ViewBase.Border.yml) - [Application.ArrangeKey](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_ArrangeKey) -- [Toplevel.Modal](~/api/Terminal.Gui.Views.Toplevel.yml#Terminal_Gui_Views_Toplevel_Modal) +- [Runnable.Modal](~/api/Terminal.Gui.Views.Runnable.yml#Terminal_Gui_Views_Runnable_Modal) ### UICatalog Examples diff --git a/docfx/docs/command.md b/docfx/docs/command.md index 82f30e4c9..41d6ca1cd 100644 --- a/docfx/docs/command.md +++ b/docfx/docs/command.md @@ -427,11 +427,11 @@ The need for `Selected` and `Accepted` events is under consideration, with `Acce } ``` In contrast, `CheckBox` and `FlagSelector` do not use `Accepted`, relying on `Accepting`’s completion or view-specific events like `CheckedStateChanged` or `ValueChanged`. This suggests that `Accepted` is particularly valuable in composite views with hierarchical interactions but not universally needed across all views. The absence of `Accepted` in `CheckBox` and `FlagSelector` indicates that `Accepting` is often sufficient for simple confirmation scenarios, but the hierarchical use in menus and potential dialog applications highlight its potential for broader adoption in specific contexts. - - **Verdict**: The `Accepted` event is highly valuable in composite and hierarchical views like `Menu`, `MenuBar`, and potentially `Dialog`, where it supports coordinated action completion (e.g., closing menus or dialogs). However, adding it to the base `View` class is premature without broader validation across more view types, as many views (e.g., `CheckBox`, `FlagSelector`) function effectively without it, using `Accepting` or custom events. Implementing `Accepted` in specific views or base classes like `Bar` or `Toplevel` (e.g., for menus and dialogs) and reassessing its necessity for the base `View` class later is a prudent approach. This balances the demonstrated utility in hierarchical scenarios with the need to avoid unnecessary complexity in simpler views. + - **Verdict**: The `Accepted` event is highly valuable in composite and hierarchical views like `Menu`, `MenuBar`, and potentially `Dialog`, where it supports coordinated action completion (e.g., closing menus or dialogs). However, adding it to the base `View` class is premature without broader validation across more view types, as many views (e.g., `CheckBox`, `FlagSelector`) function effectively without it, using `Accepting` or custom events. Implementing `Accepted` in specific views or base classes like `Bar` or `Runnable` (e.g., for menus and dialogs) and reassessing its necessity for the base `View` class later is a prudent approach. This balances the demonstrated utility in hierarchical scenarios with the need to avoid unnecessary complexity in simpler views. **Recommendation**: Avoid adding `Selected` or `Accepted` events to the base `View` class for now. Instead: - Continue using view-specific events (e.g., `Menu.SelectedMenuItemChanged`, `CheckBox.CheckedStateChanged`, `FlagSelector.ValueChanged`, `ListView.SelectedItemChanged`, `Button.Clicked`) for their contextual specificity and clarity. -- Maintain and potentially formalize the use of `Accepted` in views like `Menu`, `MenuBar`, and `Dialog`, tracking its utility to determine if broader adoption in a base class like `Bar` or `Toplevel` is warranted. +- Maintain and potentially formalize the use of `Accepted` in views like `Menu`, `MenuBar`, and `Dialog`, tracking its utility to determine if broader adoption in a base class like `Bar` or `Runnable` is warranted. - If `Selected` or `Accepted` events are added in the future, ensure they fire only when their respective events (`Selecting`, `Accepting`) are not canceled (i.e., `args.Cancel` is `false`), maintaining consistency with the *Cancellable Work Pattern*’s post-event phase. ## Propagation of Selecting @@ -652,7 +652,7 @@ Based on the analysis of the current `Command` and `View.Command` system, as imp The `Command` and `View.Command` system in Terminal.Gui provides a robust framework for handling view actions, with `Selecting` and `Accepting` serving as opinionated mechanisms for state changes/preparation and action confirmations. The system is effectively implemented across `Menu`, `MenuBar`, `CheckBox`, and `FlagSelector`, supporting a range of stateful and stateless interactions. However, limitations in terminology (`Select`’s ambiguity), cancellation semantics (`Cancel`’s misleading implication), and propagation (local `Selecting` handling) highlight areas for improvement. -The `Selecting`/`Accepting` distinction is clear in principle but requires careful documentation to avoid confusion, particularly for stateless views where `Selecting` is focus-driven and for views like `FlagSelector` where implementation flaws conflate the two concepts. View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` are sufficient for post-selection notifications, negating the need for a generic `Selected` event. The `Accepted` event is valuable in hierarchical views like `Menu` and `MenuBar` but not universally required, suggesting inclusion in `Bar` or `Toplevel` rather than `View`. +The `Selecting`/`Accepting` distinction is clear in principle but requires careful documentation to avoid confusion, particularly for stateless views where `Selecting` is focus-driven and for views like `FlagSelector` where implementation flaws conflate the two concepts. View-specific events like `SelectedMenuItemChanged`, `CheckedStateChanged`, and `ValueChanged` are sufficient for post-selection notifications, negating the need for a generic `Selected` event. The `Accepted` event is valuable in hierarchical views like `Menu` and `MenuBar` but not universally required, suggesting inclusion in `Bar` or `Runnable` rather than `View`. By clarifying terminology, fixing implementation flaws (e.g., `FlagSelector`), enhancing `ICommandContext`, and developing a decoupled propagation model, Terminal.Gui can enhance the `Command` system’s clarity and flexibility, particularly for hierarchical components like `MenuBar`. The appendix summarizes proposed changes to address these limitations, aligning with a filed issue to guide future improvements. diff --git a/docfx/docs/config.md b/docfx/docs/config.md index e260cad34..a6c7a4cc7 100644 --- a/docfx/docs/config.md +++ b/docfx/docs/config.md @@ -231,7 +231,7 @@ ThemeManager.ThemeChanged += (sender, e) => ### Scheme System -A **Scheme** defines the colors and text styles for a specific UI context (e.g., Dialog, Menu, TopLevel). +A **Scheme** defines the colors and text styles for a specific UI context (e.g., Dialog, Menu, Runnable). See the [Scheme Deep Dive](scheme.md) for complete details on the scheme system. @@ -239,7 +239,7 @@ See the [Scheme Deep Dive](scheme.md) for complete details on the scheme system. [Schemes](~/api/Terminal.Gui.Drawing.Schemes.yml) enum defines the standard schemes: -- **TopLevel** - Top-level application windows +- **Runnable** - Top-level application windows - **Base** - Default for most views - **Dialog** - Dialogs and message boxes - **Menu** - Menus and status bars @@ -277,7 +277,7 @@ Each [Scheme](~/api/Terminal.Gui.Drawing.Scheme.yml) maps [VisualRole](~/api/Ter ```json { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "BrightGreen", "Background": "Black", @@ -576,7 +576,7 @@ A theme is a named collection bundling visual settings and schemes: "Button.DefaultShadow": "Opaque", "Schemes": [ { - "TopLevel": { + "Runnable": { "Normal": { "Foreground": "BrightGreen", "Background": "Black" }, "Focus": { "Foreground": "White", "Background": "Cyan" } }, diff --git a/docfx/docs/cursor.md b/docfx/docs/cursor.md index 3c60b55a1..5b7439910 100644 --- a/docfx/docs/cursor.md +++ b/docfx/docs/cursor.md @@ -51,7 +51,7 @@ It doesn't make sense the every View instance has it's own notion of `MostFocuse * Find all instances of `view._hasFocus = ` and change them to use `SetHasFocus` (today, anyplace that sets `_hasFocus` is a BUG!!). * Change `SetFocus`/`SetHasFocus` etc... such that if the focus is changed to a different view heirarchy, `Application.MostFocusedView` gets set appropriately. -**MORE THOUGHT REQUIRED HERE** - There be dragons given how `Toplevel` has `OnEnter/OnLeave` overrrides. The above needs more study, but is directioally correct. +**MORE THOUGHT REQUIRED HERE** - There be dragons given how `Runnable` has `OnEnter/OnLeave` overrrides. The above needs more study, but is directioally correct. ### `View` Cursor Changes * Add `public Point? CursorPosition` diff --git a/docfx/docs/getting-started.md b/docfx/docs/getting-started.md index caaa45c9e..e764ff129 100644 --- a/docfx/docs/getting-started.md +++ b/docfx/docs/getting-started.md @@ -25,10 +25,19 @@ Use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.template ## Sample Usage in C# -The following example shows a basic Terminal.Gui application in C# (this is `./Example/Example.cs`): +The following example shows a basic Terminal.Gui application using the modern instance-based model (this is `./Example/Example.cs`): [!code-csharp[Program.cs](../../Examples/Example/Example.cs)] +### Key aspects of the modern model: + +- Use `Application.Create()` to create an `IApplication` instance +- The application initializes automatically when you call `Run()` +- Use `app.Run()` to run a window that implements `IRunnable` +- Call `app.Dispose()` to clean up resources and restore the terminal +- Event handling uses `Accepting` event instead of legacy `Accept` event +- Set `e.Handled = true` in event handlers to prevent further processing + When run the application looks as follows: ![Simple Usage app](../images/Example.png) diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index 6a08190fa..74f36ff8c 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -61,7 +61,7 @@ Key Bindings can be added at the `Application` or `View` level. For **Application-scoped Key Bindings** there are two categories of Application-scoped Key Bindings: -1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.App.Application. For example, @Terminal.Gui.App.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.App.Application.RequestStop(Terminal.Gui.Views.Toplevel) being called. +1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.App.Application. For example, @Terminal.Gui.App.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.App.Application.RequestStop(Terminal.Gui.Views.Runnable) being called. 2) **Application Key Bindings** - Bindings for `Command`s supported on arbitrary `Views` that are meant to be invoked regardless of which part of the application is visible/active. Use @Terminal.Gui.App.Application.Keyboard.KeyBindings to add or modify Application-scoped Key Bindings. For backward compatibility, @Terminal.Gui.App.Application.KeyBindings also provides access to the same key bindings. @@ -97,7 +97,7 @@ Keyboard events are retrieved from [Drivers](drivers.md) each iteration of the [ > Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising KeyUp after KeyDown. -@Terminal.Gui.App.Application.RaiseKeyDownEvent* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked. Application-scoped key bindings are managed through @Terminal.Gui.App.Application.Keyboard.KeyBindings. +@Terminal.Gui.App.Application.RaiseKeyDownEvent* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all runnable Views. If no View handles the key event, any Application-scoped key bindings will be invoked. Application-scoped key bindings are managed through @Terminal.Gui.App.Application.Keyboard.KeyBindings. If a view is enabled, the @Terminal.Gui.ViewBase.View.NewKeyDownEvent* method will do the following: @@ -118,7 +118,7 @@ To define application key handling logic for an entire application in cases wher # General input model - Key Down and Up events are generated by the driver. -- `IApplication` implementations subscribe to driver KeyDown/Up events and forwards them to the most-focused `TopLevel` view using `View.NewKeyDownEvent` and `View.NewKeyUpEvent`. +- `IApplication` implementations subscribe to driver KeyDown/Up events and forwards them to the most-focused `Runnable` view using `View.NewKeyDownEvent` and `View.NewKeyUpEvent`. - The base (`View`) implementation of `NewKeyDownEvent` follows a pattern of "Before", "During", and "After" processing: - **Before** - If `Enabled == false` that view should *never* see keyboard (or mouse input). diff --git a/docfx/docs/multitasking.md b/docfx/docs/multitasking.md index 165913925..a632c697c 100644 --- a/docfx/docs/multitasking.md +++ b/docfx/docs/multitasking.md @@ -9,7 +9,7 @@ Terminal.Gui applications run on a single main thread with an event loop that pr Terminal.Gui follows the standard UI toolkit pattern where **all UI operations must happen on the main thread**. Attempting to modify views or their properties from background threads will result in undefined behavior and potential crashes. ### The Golden Rule -> Always use `Application.Invoke()` (static, obsolete) or `app.Invoke()` (instance-based, recommended) to update the UI from background threads. From within a View, use `App?.Invoke()`. +> Always use `App?.Invoke()` (from within a View) or `app.Invoke()` (with an IApplication instance) to update the UI from background threads. ## Background Operations @@ -74,11 +74,8 @@ private void StartBackgroundWork() } ``` -**Using IApplication instance (recommended):** +**Using IApplication instance:** ```csharp -var app = Application.Create(); -app.Init(); - private void StartBackgroundWork(IApplication app) { Task.Run(() => @@ -104,11 +101,6 @@ private void StartBackgroundWork(IApplication app) } ``` -**Using static Application (obsolete but still works):** -```csharp -Application.Invoke(() => { /* ... */ }); -``` - ## Timers Use timers for periodic updates like clocks, status refreshes, or animations: @@ -124,12 +116,11 @@ public class ClockView : View timeLabel = new Label { Text = DateTime.Now.ToString("HH:mm:ss") }; Add(timeLabel); - // Update every second - // Use App?.AddTimeout() when available, or Application.AddTimeout() (obsolete) + // Update every second using the View's App property timerToken = App?.AddTimeout( TimeSpan.FromSeconds(1), UpdateTime - ) ?? Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateTime); + ); } private bool UpdateTime() @@ -142,7 +133,7 @@ public class ClockView : View { if (disposing && timerToken != null) { - App?.RemoveTimeout(timerToken) ?? Application.RemoveTimeout(timerToken); + App?.RemoveTimeout(timerToken); } base.Dispose(disposing); } @@ -243,14 +234,18 @@ Task.Run(() => }); ``` -### ✅ Do: Use Application.Invoke() +### ✅ Do: Use App.Invoke() or app.Invoke() ```csharp Task.Run(() => { - Application.Invoke(() => + // From within a View: + App?.Invoke(() => { label.Text = "This is safe!"; // Correct! }); + + // Or with IApplication instance: + // app.Invoke(() => { label.Text = "This is safe!"; }); }); ``` @@ -262,9 +257,6 @@ App?.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); // Or with IApplication instance: app.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); - -// Or static (obsolete but works): -Application.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus); ``` ### ✅ Do: Remove timers in Dispose @@ -273,7 +265,11 @@ protected override void Dispose(bool disposing) { if (disposing && timerToken != null) { - Application.RemoveTimeout(timerToken); + // From within a View, use App property + App?.RemoveTimeout(timerToken); + + // Or with IApplication instance: + // app.RemoveTimeout(timerToken); } base.Dispose(disposing); } diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index f217ace2c..6ebb64911 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -183,7 +183,7 @@ This method is called from the `Command` handlers bound to the application-scope **Note:** When accessing from within a View, use `App?.Current` instead of `Application.TopRunnable` (which is obsolete). -This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`). +This method replaces about a dozen functions in v1 (scattered across `Application` and `Runnable`). ### Application Navigation Examples diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index 570507950..39e8bb6d5 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -34,7 +34,7 @@ Terminal.Gui v2 introduces an instance-based application architecture that decou // Recommended v2 pattern (instance-based) var app = Application.Create(); app.Init(); -var top = new Toplevel { Title = "My App" }; +var top = new Runnable { Title = "My App" }; top.Add(myView); app.Run(top); top.Dispose(); @@ -42,7 +42,7 @@ app.Shutdown(); // Static pattern (obsolete but still works) Application.Init(); -var top = new Toplevel { Title = "My App" }; +var top = new Runnable { Title = "My App" }; top.Add(myView); Application.Run(top); top.Dispose(); @@ -55,6 +55,52 @@ Application.Shutdown(); - **Multiple Contexts**: Multiple `IApplication` instances can coexist (useful for testing or complex scenarios) - **Clear Ownership**: Views explicitly know their application context via the `App` property - **Reduced Global State**: Less reliance on static singletons improves code maintainability +- **Proper Resource Management**: IDisposable pattern ensures clean shutdown of input threads and driver resources + +### Resource Management + +Terminal.Gui v2 implements the `IDisposable` pattern for proper resource cleanup: + +```csharp +// Recommended pattern with using statement +using (var app = Application.Create().Init()) +{ + app.Run(); + var result = app.GetResult(); +} + +// Or with try/finally +var app = Application.Create(); +try +{ + app.Init(); + app.Run(); +} +finally +{ + app.Dispose(); // Stops input thread, releases resources +} +``` + +**Key Changes from v1:** +- **Input Thread Management**: v2 starts a dedicated input thread that polls console input at ~50 polls/second (20ms throttle) to prevent CPU spinning +- **Clean Shutdown**: `Dispose()` cancels the input thread and waits for it to exit, preventing thread leaks +- **Test-Friendly**: Always dispose applications in tests to prevent thread pool exhaustion from leaked input threads + +**Obsolete `Shutdown()` Method:** +The `Shutdown()` method is marked obsolete. Use `Dispose()` and `GetResult()` instead: + +```csharp +// OLD (v1/early v2): +var result = app.Run().Shutdown() as MyResult; + +// NEW (v2 recommended): +using (var app = Application.Create().Init()) +{ + app.Run(); + var result = app.GetResult(); +} +``` ## Modern Look & Feel - Technical Details @@ -112,7 +158,7 @@ See the [Drawing Deep Dive](drawing.md) for complete details on LineCanvas and t ### Deterministic View Lifetime Management - **v1 Issue**: Lifetime rules for `View` objects were unclear, leading to memory leaks or premature disposal, especially with `Application.Run`. -- **v2 Solution**: v2 defines explicit rules for view disposal and ownership, enforced by unit tests. `Application.Run` now clearly manages the lifecycle of `Toplevel` views, ensuring deterministic cleanup. +- **v2 Solution**: v2 defines explicit rules for view disposal and ownership, enforced by unit tests. `Application.Run` now clearly manages the lifecycle of `Runnable` views, ensuring deterministic cleanup. - **Impact**: Developers can predict when resources are released, reducing bugs related to dangling references or uninitialized states. ### Adornments Framework @@ -247,7 +293,7 @@ See the [Keyboard Deep Dive](keyboard.md) and [Command Deep Dive](command.md) fo - **Example**: [TextField](~/api/Terminal.Gui.Views.TextField.yml) in v2 binds `Key.Tab` to text insertion rather than focus change, customizable by developers. ### Default Close Key -- **Change**: Changed from `Ctrl+Q` in v1 to `Esc` in v2 for closing apps or [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) views, accessible via [Application.QuitKey](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_QuitKey). +- **Change**: Changed from `Ctrl+Q` in v1 to `Esc` in v2 for closing apps or [Runnable](~/api/Terminal.Gui.Views.Runnable.yml) views, accessible via [Application.QuitKey](~/api/Terminal.Gui.App.Application.yml#Terminal_Gui_App_Application_QuitKey). - **Impact**: Aligns with common user expectations, improving UX consistency across terminal applications. ## Updated Mouse API - Enhanced Interaction diff --git a/docfx/docs/views.md b/docfx/docs/views.md index eb36ba2e4..80c96aba8 100644 --- a/docfx/docs/views.md +++ b/docfx/docs/views.md @@ -119,7 +119,7 @@ Lets the user pick a date from a visual calendar. ## [Dialog](~/api/Terminal.Gui.Views.Dialog.yml) -A [Toplevel.Modal](~/api/Terminal.Gui.Views.Toplevel.Modal.yml) [Window](~/api/Terminal.Gui.Views.Window.yml). Supports a simple API for adding [Button](~/api/Terminal.Gui.Views.Button.yml)s across the bottom. By default, the [Dialog](~/api/Terminal.Gui.Views.Dialog.yml) is centered and used the [Schemes.Dialog](~/api/Terminal.Gui.Drawing.Schemes.Dialog.yml) scheme. +A [Runnable.Modal](~/api/Terminal.Gui.Views.Runnable.Modal.yml) [Window](~/api/Terminal.Gui.Views.Window.yml). Supports a simple API for adding [Button](~/api/Terminal.Gui.Views.Button.yml)s across the bottom. By default, the [Dialog](~/api/Terminal.Gui.Views.Dialog.yml) is centered and used the [Schemes.Dialog](~/api/Terminal.Gui.Drawing.Schemes.Dialog.yml) scheme. ```text ┏┥Demo Title┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ @@ -327,7 +327,7 @@ Last List Item ## [MenuBar](~/api/Terminal.Gui.Views.MenuBar.yml) -Provides a menu bar that spans the top of a [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) View with drop-down and cascading menus. By default, any sub-sub-menus (sub-menus of the [MenuItem](~/api/Terminal.Gui.Views.MenuItem.yml)s added to [MenuBarItem](~/api/Terminal.Gui.Views.MenuBarItem.yml)s) are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting [MenuBar.UseSubMenusSingleFrame](~/api/Terminal.Gui.Views.MenuBar.UseSubMenusSingleFrame.yml) to true, this behavior can be changed such that all sub-sub-menus are drawn within a single frame below the MenuBar. +Provides a menu bar that spans the top of a [Runnable](~/api/Terminal.Gui.Views.Runnable.yml) View with drop-down and cascading menus. By default, any sub-sub-menus (sub-menus of the [MenuItem](~/api/Terminal.Gui.Views.MenuItem.yml)s added to [MenuBarItem](~/api/Terminal.Gui.Views.MenuBarItem.yml)s) are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting [MenuBar.UseSubMenusSingleFrame](~/api/Terminal.Gui.Views.MenuBar.UseSubMenusSingleFrame.yml) to true, this behavior can be changed such that all sub-sub-menus are drawn within a single frame below the MenuBar. ```text File Edit About (Top-Level) @@ -532,7 +532,7 @@ Displays a spinning glyph or combinations of glyphs to indicate progress or acti ## [StatusBar](~/api/Terminal.Gui.Views.StatusBar.yml) -A status bar is a [View](~/api/Terminal.Gui.ViewBase.View.yml) that snaps to the bottom of a [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) displaying set of [Shortcut](~/api/Terminal.Gui.Views.Shortcut.yml)s. The [StatusBar](~/api/Terminal.Gui.Views.StatusBar.yml) should be context sensitive. This means, if the main menu and an open text editor are visible, the items probably shown will be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a new instance of a status bar. +A status bar is a [View](~/api/Terminal.Gui.ViewBase.View.yml) that snaps to the bottom of a [Runnable](~/api/Terminal.Gui.Views.Runnable.yml) displaying set of [Shortcut](~/api/Terminal.Gui.Views.Shortcut.yml)s. The [StatusBar](~/api/Terminal.Gui.Views.StatusBar.yml) should be context sensitive. This means, if the main menu and an open text editor are visible, the items probably shown will be ~F1~ Help ~F2~ Save ~F3~ Load. While a dialog to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a new instance of a status bar. ```text Ctrl+Z Quit Quit │ F1 Help Text Help │ F10 ☐ @@ -645,9 +645,9 @@ Provides time editing functionality with mouse support 02:48:05 ``` -## [Toplevel](~/api/Terminal.Gui.Views.Toplevel.yml) +## [Runnable](~/api/Terminal.Gui.Views.Runnable.yml) -Toplevel views are used for both an application's main view (filling the entire screen and for modal (pop-up) views such as [Dialog](~/api/Terminal.Gui.Views.Dialog.yml), [MessageBox](~/api/Terminal.Gui.Views.MessageBox.yml), and [Wizard](~/api/Terminal.Gui.Views.Wizard.yml)). +Runnable views are used for both an application's main view (filling the entire screen and for modal (pop-up) views such as [Dialog](~/api/Terminal.Gui.Views.Dialog.yml), [MessageBox](~/api/Terminal.Gui.Views.MessageBox.yml), and [Wizard](~/api/Terminal.Gui.Views.Wizard.yml)). ```text Demo Text diff --git a/docfx/schemas/tui-config-schema.json b/docfx/schemas/tui-config-schema.json index 8eb937141..a86bc7559 100644 --- a/docfx/schemas/tui-config-schema.json +++ b/docfx/schemas/tui-config-schema.json @@ -606,7 +606,7 @@ }, "Schemes": { "type": "array", - "description": "A list of scheme definitions for this theme. Each item in the array is an object containing one or more named schemes (e.g., 'TopLevel', 'Base', 'Menu').", + "description": "A list of scheme definitions for this theme. Each item in the array is an object containing one or more named schemes (e.g., 'Runnable', 'Base', 'Menu').", "items": { "type": "object", "description": "An object where each key is a scheme name (e.g., 'Base', 'Error') and its value is the scheme definition.",