diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e675f975df..4982b92701 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -15,7 +15,7 @@ The repository uses multiple GitHub Actions workflows. What runs and when: - `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` +- Pack Release packages: `dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612` and `dotnet pack Terminal.Gui.Interop.Spectre/Terminal.Gui.Interop.Spectre.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612` - Publish `Tests/NativeAotSmoke` with AOT and run `--smoke-test` - Build Release solution @@ -66,14 +66,11 @@ The repository uses multiple GitHub Actions workflows. What runs and when: ### 5) Publish to NuGet (`.github/workflows/publish.yml`) -- **Triggers**: push to `main`, `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` +- **Triggers**: push to `develop` and tags `v*` (ignores `**.md`) +- Uses GitVersion to compute SemVer, builds Release, packs Terminal.Gui and Terminal.Gui.Interop.Spectre with symbols, and pushes both to NuGet.org using `NUGET_API_KEY` - **Automatically triggered** by the Create Release workflow when a new tag is pushed -- **Additional actions on main branch**: - - Delists old NuGet packages to keep package list clean: - - Keeps only the most recent `*-develop.*` package - - Keeps only the just-published `*-beta.*` or `*-rc.*` package - - Triggers Terminal.Gui.templates repository update via repository_dispatch (requires `PAT_FOR_TEMPLATES` secret) +- **Additional actions on tag push** (`v*`): + - Triggers Terminal.Gui.templates repository update via repository_dispatch (requires `TEMPLATE_REPO_TOKEN` secret) ### 6) Build and publish API docs (`.github/workflows/api-docs.yml`) diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml index a872cd9477..dc92240988 100644 --- a/.github/workflows/build-validation.yml +++ b/.github/workflows/build-validation.yml @@ -50,6 +50,9 @@ jobs: - name: Pack Release Terminal.Gui run: dotnet pack Terminal.Gui/Terminal.Gui.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612 + - name: Pack Release Terminal.Gui.Interop.Spectre + run: dotnet pack Terminal.Gui.Interop.Spectre/Terminal.Gui.Interop.Spectre.csproj --configuration Release --output ./local_packages -property:NoWarn=0618%3B0612 + - name: AOT Publish Test App (catches trimming and reflection errors) run: dotnet publish ./Tests/NativeAotSmoke/NativeAotSmoke.csproj --configuration Release --output ./aot-publish -property:NoWarn=0618%3B0612 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 51ff221a77..4edca3343e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,9 @@ on: paths-ignore: - '**.md' +permissions: + contents: read + jobs: publish: name: Build and Publish to Nuget.org @@ -41,12 +44,17 @@ jobs: dotnet-version: 10.x dotnet-quality: 'ga' - - name: Build Release Terminal.Gui - run: dotnet build Terminal.Gui/Terminal.Gui.csproj --no-incremental --nologo --force --configuration Release + - name: Build Release Terminal.Gui packages + run: | + dotnet build Terminal.Gui/Terminal.Gui.csproj --no-incremental --nologo --force --configuration Release + dotnet build Terminal.Gui.Interop.Spectre/Terminal.Gui.Interop.Spectre.csproj --no-incremental --nologo --force --configuration Release - name: Pack Release Terminal.Gui ${{ steps.gitversion.outputs.SemVer }} for Nuget run: dotnet pack Terminal.Gui/Terminal.Gui.csproj -c Release --include-symbols --no-build + - name: Pack Release Terminal.Gui.Interop.Spectre ${{ steps.gitversion.outputs.SemVer }} for Nuget + run: dotnet pack Terminal.Gui.Interop.Spectre/Terminal.Gui.Interop.Spectre.csproj -c Release --include-symbols --no-build + # - name: Test to generate Code Coverage Report # run: | # sed -i 's/"stopOnFail": false/"stopOnFail": true/g' UnitTests/xunit.runner.json @@ -72,6 +80,9 @@ jobs: - name: Publish Terminal.Gui.${{ steps.gitversion.outputs.SemVer }} to NuGet.org run: dotnet nuget push Terminal.Gui/bin/Release/Terminal.Gui.${{ steps.gitversion.outputs.SemVer }}.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_API_KEY }} + - name: Publish Terminal.Gui.Interop.Spectre.${{ steps.gitversion.outputs.SemVer }} to NuGet.org + run: dotnet nuget push Terminal.Gui.Interop.Spectre/bin/Release/Terminal.Gui.Interop.Spectre.${{ steps.gitversion.outputs.SemVer }}.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_API_KEY }} + # Deterministic version handoff to notify-clet.yml. Avoids the NuGet # search-API indexing lag that caused gui-cs/clet to skip TG develop.37 # (see clet workflow run 25406348354). notify-clet.yml downloads this diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/TreeViewEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/TreeViewEditor.cs index 9b6838a79c..2b119ec094 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/TreeViewEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/TreeViewEditor.cs @@ -9,6 +9,7 @@ namespace UICatalog.Scenarios; public sealed class TreeViewEditor : EditorBase { private CheckBox? _cbMultiSelect; + private CheckBox? _cbCheckboxMode; private CheckBox? _cbShowBranchLines; private CheckBox? _cbColorExpandSymbol; private CheckBox? _cbInvertExpandSymbolColors; @@ -66,6 +67,7 @@ private void SyncFromView () } _cbMultiSelect!.Value = treeView.MultiSelect ? CheckState.Checked : CheckState.UnChecked; + _cbCheckboxMode!.Value = treeView.CheckboxMode ? CheckState.Checked : CheckState.UnChecked; _cbShowBranchLines!.Value = treeView.Style.ShowBranchLines ? CheckState.Checked : CheckState.UnChecked; _cbColorExpandSymbol!.Value = treeView.Style.ColorExpandSymbol ? CheckState.Checked : CheckState.UnChecked; _cbInvertExpandSymbolColors!.Value = treeView.Style.InvertExpandSymbolColors ? CheckState.Checked : CheckState.UnChecked; @@ -81,10 +83,13 @@ private void TreeViewEditor_Initialized (object? s, EventArgs e) _cbMultiSelect = new CheckBox { Title = "MultiSelect", CanFocus = true }; _cbMultiSelect.ValueChanging += (_, args) => SetBool (args, (tv, v) => tv.MultiSelect = v); + _cbCheckboxMode = new CheckBox { X = Pos.Right (_cbMultiSelect) + 1, Title = "CheckboxMode", CanFocus = true }; + _cbCheckboxMode.ValueChanging += (_, args) => SetBool (args, (tv, v) => tv.CheckboxMode = v); + _cbShowBranchLines = new CheckBox { Y = Pos.Bottom (_cbMultiSelect), Title = "ShowBranchLines", CanFocus = true }; _cbShowBranchLines.ValueChanging += (_, args) => SetStyleBool (args, (style, v) => style.ShowBranchLines = v); - _cbColorExpandSymbol = new CheckBox { X = Pos.Right (_cbMultiSelect) + 1, Title = "ColorExpandSymbol", CanFocus = true }; + _cbColorExpandSymbol = new CheckBox { X = Pos.Right (_cbCheckboxMode) + 1, Title = "ColorExpandSymbol", CanFocus = true }; _cbColorExpandSymbol.ValueChanging += (_, args) => SetStyleBool (args, (style, v) => style.ColorExpandSymbol = v); _cbInvertExpandSymbolColors = new CheckBox @@ -189,6 +194,7 @@ private void TreeViewEditor_Initialized (object? s, EventArgs e) }; Add (_cbMultiSelect, + _cbCheckboxMode, _cbColorExpandSymbol, _cbShowBranchLines, _cbInvertExpandSymbolColors, diff --git a/Examples/UICatalog/Scenarios/Wizards.cs b/Examples/UICatalog/Scenarios/Wizards.cs index 524ad32d89..f2d07edd1a 100644 --- a/Examples/UICatalog/Scenarios/Wizards.cs +++ b/Examples/UICatalog/Scenarios/Wizards.cs @@ -1,8 +1,6 @@ #nullable enable // ReSharper disable AccessToDisposedClosure -using Terminal.Gui.Views; - namespace UICatalog.Scenarios; [ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")] @@ -35,15 +33,10 @@ public override void Main () }; win.Add (settingsFrame); - Label label = new () - { - X = 0, - TextAlignment = Alignment.End, - Text = "_Title:" - }; + Label label = new () { X = 0, TextAlignment = Alignment.End, Text = "_Title:" }; settingsFrame.Add (label); - _titleEdit = new () + _titleEdit = new TextField { X = Pos.Right (label) + 1, Y = Pos.Top (label), @@ -53,32 +46,17 @@ public override void Main () }; settingsFrame.Add (_titleEdit); - CheckBox cbRun = new () - { - Title = "_Run Wizard as a modal", - X = 0, - Y = Pos.Bottom (label), - Value = CheckState.Checked - }; + CheckBox cbRun = new () { Title = "_Run Wizard as a modal", X = 0, Y = Pos.Bottom (label), Value = CheckState.Checked }; settingsFrame.Add (cbRun); - Button showWizardButton = new () - { - X = Pos.Center (), - Y = Pos.Bottom (cbRun), - IsDefault = true, - Text = "_Show Wizard" - }; + Button showWizardButton = new () { X = Pos.Center (), Y = Pos.Bottom (cbRun), IsDefault = true, Text = "_Show Wizard" }; settingsFrame.Add (showWizardButton); - label = new () - { - X = Pos.Center (), Y = Pos.AnchorEnd (1), TextAlignment = Alignment.End, Text = "Action:" - }; + label = new Label { X = Pos.Center (), Y = Pos.AnchorEnd (1), TextAlignment = Alignment.End, Text = "Action:" }; win.Add (label); - _actionLabel = new () + _actionLabel = new View { X = Pos.Right (label), Y = Pos.AnchorEnd (1), @@ -96,32 +74,32 @@ public override void Main () } cbRun.ValueChanged += (_, a) => - { - if (a.NewValue == CheckState.Checked) - { - showWizardButton.Enabled = true; - _wizard!.X = Pos.Center (); - _wizard.Y = Pos.Center (); - - win.Remove (_wizard); - _wizard.Dispose (); - _wizard = null; - } - else - { - showWizardButton.Enabled = false; - _wizard = CreateWizard (); - _wizard.Y = Pos.Bottom (settingsFrame) + 1; - win.Add (_wizard); - } - }; + { + if (a.NewValue == CheckState.Checked) + { + showWizardButton.Enabled = true; + _wizard!.X = Pos.Center (); + _wizard.Y = Pos.Center (); + + win.Remove (_wizard); + _wizard.Dispose (); + _wizard = null; + } + else + { + showWizardButton.Enabled = false; + _wizard = CreateWizard (); + _wizard.Y = Pos.Bottom (settingsFrame) + 1; + win.Add (_wizard); + } + }; showWizardButton.Accepting += (_, _) => - { - _wizard = CreateWizard (); - app.Run (_wizard); - _wizard.Dispose (); - }; + { + _wizard = CreateWizard (); + app.Run (_wizard); + _wizard.Dispose (); + }; app.Run (win); } @@ -130,7 +108,7 @@ private Wizard CreateWizard () { Wizard wizard = new (); - if (_titleEdit is not null) + if (_titleEdit is { }) { wizard.Title = _titleEdit.Text; } @@ -161,7 +139,7 @@ private Wizard CreateWizard () } else { - wizard.App!.RequestStop(); + wizard.App!.RequestStop (); args.Handled = true; } }; diff --git a/Terminal.Gui.Analyzers.Internal/EnumExtensionMethodsGenerator.cs b/Terminal.Gui.Analyzers.Internal/EnumExtensionMethodsGenerator.cs index 87935bd763..dfc604c608 100644 --- a/Terminal.Gui.Analyzers.Internal/EnumExtensionMethodsGenerator.cs +++ b/Terminal.Gui.Analyzers.Internal/EnumExtensionMethodsGenerator.cs @@ -64,11 +64,11 @@ public void Initialize (IncrementalGeneratorInitializationContext context) // Only generate for int and uint backed enums. // Unsafe.As requires the enum to be exactly 4 bytes. string? underlyingTypeName = symbol.EnumUnderlyingType?.SpecialType switch - { - SpecialType.System_Int32 => "int", - SpecialType.System_UInt32 => "uint", - _ => null - }; + { + SpecialType.System_Int32 => "int", + SpecialType.System_UInt32 => "uint", + _ => null + }; if (underlyingTypeName is null) { @@ -91,15 +91,15 @@ public void Initialize (IncrementalGeneratorInitializationContext context) } int value = field.ConstantValue switch - { - int i => i, - uint u => unchecked ((int)u), - _ => 0 - }; + { + int i => i, + uint u => unchecked((int)u), + _ => 0 + }; memberValues.Add (value); } - return new EnumInfo (enumNamespace, enumName, underlyingTypeName, hasFlags, memberValues.OrderBy (static v => unchecked ((uint)v)).ToArray ()); + return new EnumInfo (enumNamespace, enumName, underlyingTypeName, hasFlags, memberValues.OrderBy (static v => unchecked((uint)v)).ToArray ()); } private static void Execute (SourceProductionContext context, EnumInfo info) diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index ed82a4180d..ea6ed4936e 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -2,21 +2,21 @@ // Put them here so they are available throughout the application. // Do not put them in AssemblyInfo.cs as it will break GitVersion's /updateassemblyinfo -global using Attribute = Terminal.Gui.Drawing.Attribute; -global using Color = Terminal.Gui.Drawing.Color; -global using CM = Terminal.Gui.Configuration.ConfigurationManager; global using Terminal.Gui.App; -global using Terminal.Gui.Testing; -global using Terminal.Gui.Time; +global using Terminal.Gui.Configuration; +global using Terminal.Gui.Drawing; global using Terminal.Gui.Drivers; +global using Terminal.Gui.FileServices; global using Terminal.Gui.Input; -global using Terminal.Gui.Configuration; +global using Terminal.Gui.Resources; +global using Terminal.Gui.Testing; +global using Terminal.Gui.Text; +global using Terminal.Gui.Time; global using Terminal.Gui.ViewBase; global using Terminal.Gui.Views; -global using Terminal.Gui.Drawing; -global using Terminal.Gui.Text; -global using Terminal.Gui.Resources; -global using Terminal.Gui.FileServices; +global using Attribute = Terminal.Gui.Drawing.Attribute; +global using CM = Terminal.Gui.Configuration.ConfigurationManager; +global using Color = Terminal.Gui.Drawing.Color; using System.Globalization; using System.Reflection; using System.Resources; diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs index b2c143292b..3b4cbfbd1f 100644 --- a/Terminal.Gui/App/ApplicationImpl.Run.cs +++ b/Terminal.Gui/App/ApplicationImpl.Run.cs @@ -199,7 +199,7 @@ public void Invoke (Action action) #region Session Lifecycle - Run /// - public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new () + public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new() { if (!Initialized) { @@ -314,7 +314,7 @@ public void Invoke (Action action) } /// - public Task RunAsync (CancellationToken cancellationToken, Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new () + public Task RunAsync (CancellationToken cancellationToken, Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new() { if (cancellationToken.IsCancellationRequested) { diff --git a/Terminal.Gui/App/ApplicationImpl.Screen.cs b/Terminal.Gui/App/ApplicationImpl.Screen.cs index 98f9c40ea8..bec9509ef4 100644 --- a/Terminal.Gui/App/ApplicationImpl.Screen.cs +++ b/Terminal.Gui/App/ApplicationImpl.Screen.cs @@ -280,6 +280,15 @@ public void LayoutAndDraw (bool forceRedraw = false) // Only force a complete redraw if needed (needsLayout or forceRedraw). // Otherwise, just redraw views that need it. + // + // NOTE (#5358): passing force=true here calls SetNeedsDraw on the top runnable, which + // cascades to all overlapping subviews via the existing SetNeedsDraw recursion. This + // is the remaining draw-fan-out source documented by TabsFanOutIntegrationTests. The + // proper fix requires tracking adornment thickness changes (in addition to Frame + // changes) so the SuperView can be invalidated precisely instead of force-redrawing + // the whole tree. Dropping force-on-neededLayout without that tracking exposes + // stale-content bugs in the shrink/move and adornment-rebalance paths (covered by + // ShadowTests / BorderViewTests). View.Draw (views.ToArray ().Cast (), neededLayout || forceRedraw); Driver.Clip = new Region (clipRect); diff --git a/Terminal.Gui/App/Clipboard/ClipboardBase.cs b/Terminal.Gui/App/Clipboard/ClipboardBase.cs index 46a1f26bf3..7addcf566f 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardBase.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Diagnostics; +using System.Diagnostics; namespace Terminal.Gui.App; diff --git a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs index 07f431dfd1..d3c34e168b 100644 --- a/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs +++ b/Terminal.Gui/App/Clipboard/ClipboardProcessRunner.cs @@ -38,7 +38,7 @@ public static (int exitCode, string result) Process ( using var process = new Process (); - process.StartInfo = new() + process.StartInfo = new () { FileName = cmd, Arguments = arguments, diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 5c55d3ab84..92dce0bc4e 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -294,7 +294,7 @@ public interface IApplication : IDisposable /// session. /// /// - Task RunAsync (CancellationToken cancellationToken, Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new (); + Task RunAsync (CancellationToken cancellationToken, Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new(); /// /// Runs a new Session creating a -derived object of type @@ -340,7 +340,7 @@ public interface IApplication : IDisposable /// The caller is responsible for disposing the object returned by this method. /// /// - public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new (); + public IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new(); #region Iteration & Invoke diff --git a/Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs b/Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs index be29bac3f4..37f1650c6c 100644 --- a/Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs +++ b/Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs @@ -255,10 +255,10 @@ internal void AddKeyBindings () while (viewToArrange is { Arrangement: ViewArrangement.Fixed }) { viewToArrange = viewToArrange switch - { - AdornmentView adornmentView => adornmentView.Adornment?.Parent, - _ => viewToArrange.SuperView - }; + { + AdornmentView adornmentView => adornmentView.Adornment?.Parent, + _ => viewToArrange.SuperView + }; } return viewToArrange is { } ? (viewToArrange.Border.View as BorderView)?.Arranger.EnterArrangeMode (ViewArrangement.Fixed) : false; diff --git a/Terminal.Gui/App/Legacy/Application.Run.cs b/Terminal.Gui/App/Legacy/Application.Run.cs index 6a80443dc1..e563aa1630 100644 --- a/Terminal.Gui/App/Legacy/Application.Run.cs +++ b/Terminal.Gui/App/Legacy/Application.Run.cs @@ -8,7 +8,7 @@ public static partial class Application // Run (Begin -> Run -> Layout/Draw -> E /// [Obsolete ("The legacy static Application object is going away.")] - public static IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new () => + public static IApplication Run (Func? errorHandler = null, string? driverName = null) where TRunnable : IRunnable, new() => ApplicationImpl.Instance.Run (errorHandler, driverName); /// diff --git a/Terminal.Gui/App/Popovers/Popover.cs b/Terminal.Gui/App/Popovers/Popover.cs index eec4d7ff4f..bc5b13cf25 100644 --- a/Terminal.Gui/App/Popovers/Popover.cs +++ b/Terminal.Gui/App/Popovers/Popover.cs @@ -49,7 +49,7 @@ namespace Terminal.Gui.App; /// popover.MakeVisible (); /// /// -public class Popover : PopoverImpl, IDesignable where TView : View, new () +public class Popover : PopoverImpl, IDesignable where TView : View, new() { private CommandBridge? _contentCommandBridge; diff --git a/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs index 25690eb244..d1ae49842a 100644 --- a/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs +++ b/Terminal.Gui/App/Timeout/LogarithmicTimeout.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.App; +namespace Terminal.Gui.App; /// Implements a logarithmic increasing timeout. public class LogarithmicTimeout : Timeout diff --git a/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs index 7a11dcddcd..e354b98a49 100644 --- a/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs +++ b/Terminal.Gui/App/Timeout/SmoothAcceleratingTimeout.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.App; +namespace Terminal.Gui.App; /// /// Timeout which accelerates slowly at first then fast up to a maximum speed. diff --git a/Terminal.Gui/App/Tracing/LoggingBackend.cs b/Terminal.Gui/App/Tracing/LoggingBackend.cs index 169f70061b..3a6ec80a32 100644 --- a/Terminal.Gui/App/Tracing/LoggingBackend.cs +++ b/Terminal.Gui/App/Tracing/LoggingBackend.cs @@ -9,15 +9,15 @@ public sealed class LoggingBackend : ITraceBackend public void Log (TraceEntry entry) { string prefix = entry.Category switch - { - TraceCategory.Lifecycle => FormatLifecycle (entry), - TraceCategory.Command => FormatCommand (entry), - TraceCategory.Mouse => FormatMouse (entry), - TraceCategory.Keyboard => FormatKeyboard (entry), - TraceCategory.Navigation => FormatNavigation (entry), - TraceCategory.Configuration => string.Empty, - _ => $"[{entry.Category}]" - }; + { + TraceCategory.Lifecycle => FormatLifecycle (entry), + TraceCategory.Command => FormatCommand (entry), + TraceCategory.Mouse => FormatMouse (entry), + TraceCategory.Keyboard => FormatKeyboard (entry), + TraceCategory.Navigation => FormatNavigation (entry), + TraceCategory.Configuration => string.Empty, + _ => $"[{entry.Category}]" + }; var message = $"{prefix}@\"{entry.Id}\""; @@ -47,12 +47,12 @@ private static string FormatCommand (TraceEntry entry) } string arrow = routing switch - { - CommandRouting.BubblingUp => "↑", - CommandRouting.DispatchingDown => "↓", - CommandRouting.Bridged => "↔", - _ => "•" - }; + { + CommandRouting.BubblingUp => "↑", + CommandRouting.DispatchingDown => "↓", + CommandRouting.Bridged => "↔", + _ => "•" + }; return $"{arrow} {cmd}"; } @@ -64,11 +64,11 @@ private static string FormatMouse (TraceEntry entry) case (MouseFlags flags, Point pos): return $"{flags} @({pos.X},{pos.Y})"; case Mouse mouse: - { - Point mousePos = mouse.Position ?? Point.Empty; + { + Point mousePos = mouse.Position ?? Point.Empty; - return $"{mouse.Flags} @({mousePos.X},{mousePos.Y})"; - } + return $"{mouse.Flags} @({mousePos.X},{mousePos.Y})"; + } default: return string.Empty; } diff --git a/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs b/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs index f584e9e9c6..d527f5058d 100644 --- a/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs +++ b/Terminal.Gui/Configuration/ConcurrentDictionaryJsonConverter.cs @@ -51,19 +51,19 @@ JsonSerializerOptions options return dictionary; } - public override void Write(Utf8JsonWriter writer, ConcurrentDictionary value, JsonSerializerOptions options) + public override void Write (Utf8JsonWriter writer, ConcurrentDictionary value, JsonSerializerOptions options) { - writer.WriteStartArray(); + writer.WriteStartArray (); foreach (KeyValuePair item in value) { - writer.WriteStartObject(); + writer.WriteStartObject (); - writer.WritePropertyName(item.Key); - JsonSerializer.Serialize(writer, item.Value, typeof(T), ConfigurationManager.SerializerContext); - writer.WriteEndObject(); + writer.WritePropertyName (item.Key); + JsonSerializer.Serialize (writer, item.Value, typeof (T), ConfigurationManager.SerializerContext); + writer.WriteEndObject (); } - writer.WriteEndArray(); + writer.WriteEndArray (); } } diff --git a/Terminal.Gui/Configuration/DeepCloner.cs b/Terminal.Gui/Configuration/DeepCloner.cs index 1d81f37691..c51b2f5916 100644 --- a/Terminal.Gui/Configuration/DeepCloner.cs +++ b/Terminal.Gui/Configuration/DeepCloner.cs @@ -456,7 +456,7 @@ private static void CheckForUnsupportedDictionaryTypes (Type type) #region AOT Support private static TScopeT CloneScope (TScopeT scope, ConcurrentDictionary visited) - where TScopeT : Scope, new () + where TScopeT : Scope, new() { TScopeT clonedScope = new (); visited.TryAdd (scope, clonedScope); diff --git a/Terminal.Gui/Configuration/SchemeJsonConverter.cs b/Terminal.Gui/Configuration/SchemeJsonConverter.cs index c84b475347..102f7bf04b 100644 --- a/Terminal.Gui/Configuration/SchemeJsonConverter.cs +++ b/Terminal.Gui/Configuration/SchemeJsonConverter.cs @@ -43,32 +43,32 @@ public override Scheme Read (ref Utf8JsonReader reader, Type typeToConvert, Json if (propertyName is { }) { scheme = propertyName.ToLowerInvariant () switch - { - "normal" => scheme with { Normal = attribute }, - "hotnormal" => scheme with { HotNormal = attribute }, - "focus" => scheme with { Focus = attribute }, - "hotfocus" => scheme with { HotFocus = attribute }, - "active" => scheme with { Active = attribute }, - "hotactive" => scheme with { HotActive = attribute }, - "highlight" => scheme with { Highlight = attribute }, - "editable" => scheme with { Editable = attribute }, - "readonly" => scheme with { ReadOnly = attribute }, - "disabled" => scheme with { Disabled = attribute }, - "code" => scheme with { Code = attribute }, - "codecomment" => scheme with { CodeComment = attribute }, - "codekeyword" => scheme with { CodeKeyword = attribute }, - "codestring" => scheme with { CodeString = attribute }, - "codenumber" => scheme with { CodeNumber = attribute }, - "codeoperator" => scheme with { CodeOperator = attribute }, - "codetype" => scheme with { CodeType = attribute }, - "codepreprocessor" => scheme with { CodePreprocessor = attribute }, - "codeidentifier" => scheme with { CodeIdentifier = attribute }, - "codeconstant" => scheme with { CodeConstant = attribute }, - "codepunctuation" => scheme with { CodePunctuation = attribute }, - "codefunctionname" => scheme with { CodeFunctionName = attribute }, - "codeattribute" => scheme with { CodeAttribute = attribute }, - _ => throw new JsonException ($"{propertyName}: Unrecognized Scheme Attribute name.") - }; + { + "normal" => scheme with { Normal = attribute }, + "hotnormal" => scheme with { HotNormal = attribute }, + "focus" => scheme with { Focus = attribute }, + "hotfocus" => scheme with { HotFocus = attribute }, + "active" => scheme with { Active = attribute }, + "hotactive" => scheme with { HotActive = attribute }, + "highlight" => scheme with { Highlight = attribute }, + "editable" => scheme with { Editable = attribute }, + "readonly" => scheme with { ReadOnly = attribute }, + "disabled" => scheme with { Disabled = attribute }, + "code" => scheme with { Code = attribute }, + "codecomment" => scheme with { CodeComment = attribute }, + "codekeyword" => scheme with { CodeKeyword = attribute }, + "codestring" => scheme with { CodeString = attribute }, + "codenumber" => scheme with { CodeNumber = attribute }, + "codeoperator" => scheme with { CodeOperator = attribute }, + "codetype" => scheme with { CodeType = attribute }, + "codepreprocessor" => scheme with { CodePreprocessor = attribute }, + "codeidentifier" => scheme with { CodeIdentifier = attribute }, + "codeconstant" => scheme with { CodeConstant = attribute }, + "codepunctuation" => scheme with { CodePunctuation = attribute }, + "codefunctionname" => scheme with { CodeFunctionName = attribute }, + "codeattribute" => scheme with { CodeAttribute = attribute }, + _ => throw new JsonException ($"{propertyName}: Unrecognized Scheme Attribute name.") + }; } else { @@ -84,7 +84,7 @@ public override void Write (Utf8JsonWriter writer, Scheme value, JsonSerializerO { writer.WriteStartObject (); - foreach (VisualRole role in Enum.GetValues()) + foreach (VisualRole role in Enum.GetValues ()) { // Get the attribute for the role diff --git a/Terminal.Gui/Configuration/ScopeJsonConverter.cs b/Terminal.Gui/Configuration/ScopeJsonConverter.cs index e3cdc34e3c..65fd079500 100644 --- a/Terminal.Gui/Configuration/ScopeJsonConverter.cs +++ b/Terminal.Gui/Configuration/ScopeJsonConverter.cs @@ -16,7 +16,7 @@ namespace Terminal.Gui.Configuration; internal class ScopeJsonConverter< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicProperties)] - TScopeT> : JsonConverter where TScopeT : Scope +TScopeT> : JsonConverter where TScopeT : Scope { [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", @@ -89,11 +89,7 @@ public override TScopeT Read (ref Utf8JsonReader reader, Type typeToConvert, Jso continue; } - throw new JsonException ($"{ - propertyName - }: Unsupported configuration converter type \"{ - converterType.FullName - }\" when dynamic code is unavailable."); + throw new JsonException ($"{propertyName}: Unsupported configuration converter type \"{converterType.FullName}\" when dynamic code is unavailable."); } scope! [propertyName].PropertyValue = DeserializePropertyValue (ref reader, propertyType!, options); diff --git a/Terminal.Gui/Drawing/Color/ColorStrings.cs b/Terminal.Gui/Drawing/Color/ColorStrings.cs index 88e3ab1bb2..037bc102e2 100644 --- a/Terminal.Gui/Drawing/Color/ColorStrings.cs +++ b/Terminal.Gui/Drawing/Color/ColorStrings.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Drawing; /// public static class ColorStrings { - private static readonly StandardColorsNameResolver _standard = new(); + private static readonly StandardColorsNameResolver _standard = new (); /// /// Gets the color name for . diff --git a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs index 14e44718fd..d5030c1478 100644 --- a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs @@ -22,7 +22,7 @@ public interface IColorNameResolver /// /// /// - bool TryNameColor (Color color, [NotNullWhen(true)]out string? name); + bool TryNameColor (Color color, [NotNullWhen (true)] out string? name); /// /// Returns if is a recognized diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index d3b89f8d2d..23a6b55976 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -55,7 +55,7 @@ public class Glyphs /// Checked indicator (e.g. for and ). [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateChecked { get; set; } = (Rune)'☒'; // '☑' is colored + public static Rune CheckStateChecked { get; set; } = (Rune)'☑'; /// Not Checked indicator (e.g. for and ). [ConfigurationProperty (Scope = typeof (ThemeScope))] @@ -63,7 +63,7 @@ public class Glyphs /// Null Checked indicator (e.g. for and ). [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Rune CheckStateNone { get; set; } = (Rune)'□'; // TODO: Verify this works as broadly as possible + public static Rune CheckStateNone { get; set; } = (Rune)'⬛'; /// Selected indicator (e.g. for and ). [ConfigurationProperty (Scope = typeof (ThemeScope))] diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index 0929ab9564..580b624b93 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -1227,30 +1227,30 @@ public static LineDirections GetLineDirections (string? grapheme) char ch = grapheme [0]; return ch switch - { - // Horizontal lines - '─' or '━' or '═' => LineDirections.Left | LineDirections.Right, - - // Vertical lines - '│' or '┃' or '║' => LineDirections.Up | LineDirections.Down, - - // Corners (single, rounded, double, heavy) - '┌' or '╭' or '╔' or '┏' => LineDirections.Right | LineDirections.Down, - '┐' or '╮' or '╗' or '┓' => LineDirections.Left | LineDirections.Down, - '└' or '╰' or '╚' or '┗' => LineDirections.Right | LineDirections.Up, - '┘' or '╯' or '╝' or '┛' => LineDirections.Left | LineDirections.Up, - - // T-junctions (single, double, heavy) - '├' or '╠' or '┣' => LineDirections.Up | LineDirections.Down | LineDirections.Right, - '┤' or '╣' or '┫' => LineDirections.Up | LineDirections.Down | LineDirections.Left, - '┬' or '╦' or '┳' => LineDirections.Left | LineDirections.Right | LineDirections.Down, - '┴' or '╩' or '┻' => LineDirections.Left | LineDirections.Right | LineDirections.Up, - - // Cross (single, double, heavy) - '┼' or '╬' or '╋' => LineDirections.Up | LineDirections.Down | LineDirections.Left | LineDirections.Right, - - _ => LineDirections.None - }; + { + // Horizontal lines + '─' or '━' or '═' => LineDirections.Left | LineDirections.Right, + + // Vertical lines + '│' or '┃' or '║' => LineDirections.Up | LineDirections.Down, + + // Corners (single, rounded, double, heavy) + '┌' or '╭' or '╔' or '┏' => LineDirections.Right | LineDirections.Down, + '┐' or '╮' or '╗' or '┓' => LineDirections.Left | LineDirections.Down, + '└' or '╰' or '╚' or '┗' => LineDirections.Right | LineDirections.Up, + '┘' or '╯' or '╝' or '┛' => LineDirections.Left | LineDirections.Up, + + // T-junctions (single, double, heavy) + '├' or '╠' or '┣' => LineDirections.Up | LineDirections.Down | LineDirections.Right, + '┤' or '╣' or '┫' => LineDirections.Up | LineDirections.Down | LineDirections.Left, + '┬' or '╦' or '┳' => LineDirections.Left | LineDirections.Right | LineDirections.Down, + '┴' or '╩' or '┻' => LineDirections.Left | LineDirections.Right | LineDirections.Up, + + // Cross (single, double, heavy) + '┼' or '╬' or '╋' => LineDirections.Up | LineDirections.Down | LineDirections.Left | LineDirections.Right, + + _ => LineDirections.None + }; } /// diff --git a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs index 1286ca2ce4..f3c6715dc6 100644 --- a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs +++ b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs @@ -56,25 +56,25 @@ public static Attribute GetAttributeForSegment (View view, StyledSegment segment Attribute baseAttr = viewNormal with { Background = bg }; return segment.StyleRole switch - { - MarkdownStyleRole.Heading => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.HeadingMarker => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.Emphasis => baseAttr with { Style = baseAttr.Style | TextStyle.Italic }, - MarkdownStyleRole.Strong => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.InlineCode or MarkdownStyleRole.CodeBlock => themeBackground is { } codeBg - ? view.GetAttributeForRole (VisualRole.Code) with { Background = codeBg } - : view.GetAttributeForRole (VisualRole.Code), - MarkdownStyleRole.Link => MakeLinkAttribute (baseAttr, segment), - MarkdownStyleRole.Quote => baseAttr with { Style = baseAttr.Style | TextStyle.Faint }, - MarkdownStyleRole.Table => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.ThematicBreak => baseAttr with { Style = baseAttr.Style | TextStyle.Faint }, - MarkdownStyleRole.ImageAlt => baseAttr with { Style = baseAttr.Style | TextStyle.Italic }, - MarkdownStyleRole.TaskDone => baseAttr with { Style = baseAttr.Style | TextStyle.Strikethrough }, - MarkdownStyleRole.TaskTodo => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.ListMarker => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, - MarkdownStyleRole.Strikethrough => baseAttr with { Style = baseAttr.Style | TextStyle.Strikethrough }, - _ => baseAttr - }; + { + MarkdownStyleRole.Heading => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.HeadingMarker => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.Emphasis => baseAttr with { Style = baseAttr.Style | TextStyle.Italic }, + MarkdownStyleRole.Strong => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.InlineCode or MarkdownStyleRole.CodeBlock => themeBackground is { } codeBg + ? view.GetAttributeForRole (VisualRole.Code) with { Background = codeBg } + : view.GetAttributeForRole (VisualRole.Code), + MarkdownStyleRole.Link => MakeLinkAttribute (baseAttr, segment), + MarkdownStyleRole.Quote => baseAttr with { Style = baseAttr.Style | TextStyle.Faint }, + MarkdownStyleRole.Table => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.ThematicBreak => baseAttr with { Style = baseAttr.Style | TextStyle.Faint }, + MarkdownStyleRole.ImageAlt => baseAttr with { Style = baseAttr.Style | TextStyle.Italic }, + MarkdownStyleRole.TaskDone => baseAttr with { Style = baseAttr.Style | TextStyle.Strikethrough }, + MarkdownStyleRole.TaskTodo => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.ListMarker => baseAttr with { Style = baseAttr.Style | TextStyle.Bold }, + MarkdownStyleRole.Strikethrough => baseAttr with { Style = baseAttr.Style | TextStyle.Strikethrough }, + _ => baseAttr + }; } private static Attribute ResolveRoleAttribute (View view, VisualRole role, Attribute roleAttr, Color? themeBackground) diff --git a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs index e2a2a65226..856f4add7c 100644 --- a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs +++ b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs @@ -42,18 +42,18 @@ public class TextMateSyntaxHighlighter : ISyntaxHighlighter /// private static readonly Dictionary _scopeMap = new () { - [MarkdownStyleRole.Heading] = [["entity.name.section"], ["markup.heading"]], - [MarkdownStyleRole.HeadingMarker] = [["entity.name.section"], ["markup.heading"]], - [MarkdownStyleRole.Emphasis] = [["markup.italic"]], - [MarkdownStyleRole.Strong] = [["markup.bold"]], - [MarkdownStyleRole.InlineCode] = [["markup.inline.raw"], ["markup.raw"]], - [MarkdownStyleRole.CodeBlock] = [["markup.fenced_code.block.markdown"], ["markup.raw"]], - [MarkdownStyleRole.Link] = [["markup.underline.link"], ["string.other.link"], ["markup.underline"]], - [MarkdownStyleRole.Quote] = [["markup.quote"], ["markup.changed"]], - [MarkdownStyleRole.ListMarker] = [["punctuation.definition.list.begin.markdown"], ["keyword.control"]], - [MarkdownStyleRole.ImageAlt] = [["markup.italic"]], - [MarkdownStyleRole.TaskDone] = [["markup.strikethrough"], ["markup.deleted"]], - [MarkdownStyleRole.ThematicBreak] = [["meta.separator.markdown"], ["comment"]] + [MarkdownStyleRole.Heading] = [ ["entity.name.section"], ["markup.heading"]], + [MarkdownStyleRole.HeadingMarker] = [ ["entity.name.section"], ["markup.heading"]], + [MarkdownStyleRole.Emphasis] = [ ["markup.italic"]], + [MarkdownStyleRole.Strong] = [ ["markup.bold"]], + [MarkdownStyleRole.InlineCode] = [ ["markup.inline.raw"], ["markup.raw"]], + [MarkdownStyleRole.CodeBlock] = [ ["markup.fenced_code.block.markdown"], ["markup.raw"]], + [MarkdownStyleRole.Link] = [ ["markup.underline.link"], ["string.other.link"], ["markup.underline"]], + [MarkdownStyleRole.Quote] = [ ["markup.quote"], ["markup.changed"]], + [MarkdownStyleRole.ListMarker] = [ ["punctuation.definition.list.begin.markdown"], ["keyword.control"]], + [MarkdownStyleRole.ImageAlt] = [ ["markup.italic"]], + [MarkdownStyleRole.TaskDone] = [ ["markup.strikethrough"], ["markup.deleted"]], + [MarkdownStyleRole.ThematicBreak] = [ ["meta.separator.markdown"], ["comment"]] }; internal static readonly (string ScopePrefix, VisualRole Role) [] TmScopeRoleMap = diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index e1eed15999..8eaf73794b 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -330,32 +330,32 @@ public bool TryGetExplicitlySetAttributeForRole (VisualRole role, out Attribute? { // Use a HashSet to guard against recursion cycles attribute = role switch - { - VisualRole.Normal => _normal, - VisualRole.HotNormal => _hotNormal, - VisualRole.Focus => _focus, - VisualRole.HotFocus => _hotFocus, - VisualRole.Active => _active, - VisualRole.HotActive => _hotActive, - VisualRole.Highlight => _highlight, - VisualRole.Editable => _editable, - VisualRole.ReadOnly => _readOnly, - VisualRole.Disabled => _disabled, - VisualRole.Code => _code, - VisualRole.CodeComment => _codeComment, - VisualRole.CodeKeyword => _codeKeyword, - VisualRole.CodeString => _codeString, - VisualRole.CodeNumber => _codeNumber, - VisualRole.CodeOperator => _codeOperator, - VisualRole.CodeType => _codeType, - VisualRole.CodePreprocessor => _codePreprocessor, - VisualRole.CodeIdentifier => _codeIdentifier, - VisualRole.CodeConstant => _codeConstant, - VisualRole.CodePunctuation => _codePunctuation, - VisualRole.CodeFunctionName => _codeFunctionName, - VisualRole.CodeAttribute => _codeAttribute, - _ => null - }; + { + VisualRole.Normal => _normal, + VisualRole.HotNormal => _hotNormal, + VisualRole.Focus => _focus, + VisualRole.HotFocus => _hotFocus, + VisualRole.Active => _active, + VisualRole.HotActive => _hotActive, + VisualRole.Highlight => _highlight, + VisualRole.Editable => _editable, + VisualRole.ReadOnly => _readOnly, + VisualRole.Disabled => _disabled, + VisualRole.Code => _code, + VisualRole.CodeComment => _codeComment, + VisualRole.CodeKeyword => _codeKeyword, + VisualRole.CodeString => _codeString, + VisualRole.CodeNumber => _codeNumber, + VisualRole.CodeOperator => _codeOperator, + VisualRole.CodeType => _codeType, + VisualRole.CodePreprocessor => _codePreprocessor, + VisualRole.CodeIdentifier => _codeIdentifier, + VisualRole.CodeConstant => _codeConstant, + VisualRole.CodePunctuation => _codePunctuation, + VisualRole.CodeFunctionName => _codeFunctionName, + VisualRole.CodeAttribute => _codeAttribute, + _ => null + }; return attribute is { }; } @@ -389,81 +389,81 @@ private Attribute GetAttributeForRoleCore (VisualRole role, HashSet switch (role) { case VisualRole.Focus: - { - Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - - result = normal with { - Foreground = ResolveNone (normal.Background, defaultTerminalColors), - Background = ResolveNone (normal.Foreground, defaultTerminalColors, true) - }; + Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - break; - } + result = normal with + { + Foreground = ResolveNone (normal.Background, defaultTerminalColors), + Background = ResolveNone (normal.Foreground, defaultTerminalColors, true) + }; - case VisualRole.Active: - { - Attribute focus = GetAttributeForRoleCore (VisualRole.Focus, stack, defaultTerminalColors); - Color focusBg = ResolveNone (focus.Background, defaultTerminalColors); - bool isDark = focusBg.IsDarkColor (); + break; + } - result = focus with + case VisualRole.Active: { - Foreground = ResolveNone (focus.Foreground, defaultTerminalColors, true).GetBrighterColor (0.2, isDark), - Background = focusBg.GetDimmerColor (0.2, !isDark), - Style = focus.Style | TextStyle.Bold - }; + Attribute focus = GetAttributeForRoleCore (VisualRole.Focus, stack, defaultTerminalColors); + Color focusBg = ResolveNone (focus.Background, defaultTerminalColors); + bool isDark = focusBg.IsDarkColor (); - break; - } + result = focus with + { + Foreground = ResolveNone (focus.Foreground, defaultTerminalColors, true).GetBrighterColor (0.2, isDark), + Background = focusBg.GetDimmerColor (0.2, !isDark), + Style = focus.Style | TextStyle.Bold + }; - case VisualRole.Highlight: - { - Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); - bool isDark = normalBg.IsDarkColor (); + break; + } - result = normal with + case VisualRole.Highlight: { - Foreground = normalBg.GetBrighterColor (0.2, isDark), - Background = normal.Background, - Style = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors).Style | TextStyle.Italic - }; + Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); + Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); + bool isDark = normalBg.IsDarkColor (); - break; - } + result = normal with + { + Foreground = normalBg.GetBrighterColor (0.2, isDark), + Background = normal.Background, + Style = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors).Style | TextStyle.Italic + }; + + break; + } case VisualRole.Editable: - { - Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); - bool isDark = normalBg.IsDarkColor (); - Color resolvedFg = ResolveNone (normal.Foreground, defaultTerminalColors, true); + { + Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); + Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); + bool isDark = normalBg.IsDarkColor (); + Color resolvedFg = ResolveNone (normal.Foreground, defaultTerminalColors, true); - result = normal with { Foreground = resolvedFg, Background = resolvedFg.GetDimmerColor (0.5, isDark) }; + result = normal with { Foreground = resolvedFg, Background = resolvedFg.GetDimmerColor (0.5, isDark) }; - break; - } + break; + } case VisualRole.ReadOnly: - { - Attribute editable = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors); - bool isDark = ResolveNone (editable.Background, defaultTerminalColors).IsDarkColor (); + { + Attribute editable = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors); + bool isDark = ResolveNone (editable.Background, defaultTerminalColors).IsDarkColor (); - result = editable with { Foreground = editable.Foreground.GetDimmerColor (0.05, isDark) }; + result = editable with { Foreground = editable.Foreground.GetDimmerColor (0.05, isDark) }; - break; - } + break; + } case VisualRole.Code: - { - Attribute editable = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors); - bool isDark = ResolveNone (editable.Background, defaultTerminalColors).IsDarkColor (); + { + Attribute editable = GetAttributeForRoleCore (VisualRole.Editable, stack, defaultTerminalColors); + bool isDark = ResolveNone (editable.Background, defaultTerminalColors).IsDarkColor (); - result = editable with { Background = editable.Background.GetDimmerColor (0.2, isDark), Style = editable.Style | TextStyle.Bold }; + result = editable with { Background = editable.Background.GetDimmerColor (0.2, isDark), Style = editable.Style | TextStyle.Bold }; - break; - } + break; + } case VisualRole.CodeComment: case VisualRole.CodeKeyword: @@ -477,49 +477,49 @@ private Attribute GetAttributeForRoleCore (VisualRole role, HashSet case VisualRole.CodePunctuation: case VisualRole.CodeFunctionName: case VisualRole.CodeAttribute: - { - result = GetAttributeForRoleCore (VisualRole.Code, stack, defaultTerminalColors); + { + result = GetAttributeForRoleCore (VisualRole.Code, stack, defaultTerminalColors); - break; - } + break; + } case VisualRole.Disabled: - { - Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); - bool isDark = normalBg.IsDarkColor (); + { + Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); + Color normalBg = ResolveNone (normal.Background, defaultTerminalColors); + bool isDark = normalBg.IsDarkColor (); - result = normal with { Foreground = ResolveNone (normal.Foreground, defaultTerminalColors, true).GetDimmerColor (0.05, isDark) }; + result = normal with { Foreground = ResolveNone (normal.Foreground, defaultTerminalColors, true).GetDimmerColor (0.05, isDark) }; - break; - } + break; + } case VisualRole.HotNormal: - { - Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); + { + Attribute normal = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); - result = normal with { Style = normal.Style | TextStyle.Underline }; + result = normal with { Style = normal.Style | TextStyle.Underline }; - break; - } + break; + } case VisualRole.HotFocus: - { - Attribute focus = GetAttributeForRoleCore (VisualRole.Focus, stack, defaultTerminalColors); + { + Attribute focus = GetAttributeForRoleCore (VisualRole.Focus, stack, defaultTerminalColors); - result = focus with { Style = focus.Style | TextStyle.Underline }; + result = focus with { Style = focus.Style | TextStyle.Underline }; - break; - } + break; + } case VisualRole.HotActive: - { - Attribute active = GetAttributeForRoleCore (VisualRole.Active, stack, defaultTerminalColors); + { + Attribute active = GetAttributeForRoleCore (VisualRole.Active, stack, defaultTerminalColors); - result = active with { Style = active.Style | TextStyle.Underline }; + result = active with { Style = active.Style | TextStyle.Underline }; - break; - } + break; + } default: result = GetAttributeForRoleCore (VisualRole.Normal, stack, defaultTerminalColors); diff --git a/Terminal.Gui/Drawing/Sixel/SixelEncoder.cs b/Terminal.Gui/Drawing/Sixel/SixelEncoder.cs index 4302d62394..efb8501d68 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelEncoder.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelEncoder.cs @@ -113,8 +113,8 @@ private string ProcessBand (Color [,] pixels, int startY, int bandHeight, int wi Array.Fill (accu, (ushort)1); Array.Fill (slots, (short)-1); - List usedColorIdx = new List (); - List> targets = new List> (); + List usedColorIdx = new(); + List> targets = new(); // Process columns within the band for (var x = 0; x < width; ++x) diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs index fa33514675..b40923314c 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs @@ -244,30 +244,30 @@ private IEnumerable ReadUnixInput (byte [] buffer) switch (readResult) { case > 0: - { - // Convert UTF-8 bytes to characters - string text = Encoding.UTF8.GetString (buffer, 0, readResult); - - foreach (char ch in text) { - yield return ch; - } + // Convert UTF-8 bytes to characters + string text = Encoding.UTF8.GetString (buffer, 0, readResult); - break; - } + foreach (char ch in text) + { + yield return ch; + } + + break; + } case 0: // EOF yield break; default: - { - // Error - int errno = Marshal.GetLastWin32Error (); - Logging.Warning ($"{nameof (AnsiInput)}: read() returned {readResult}, errno={errno}"); + { + // Error + int errno = Marshal.GetLastWin32Error (); + Logging.Warning ($"{nameof (AnsiInput)}: read() returned {readResult}, errno={errno}"); - yield break; - } + yield break; + } } } else diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs index 0f0b0253fb..d548c2c04c 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs @@ -415,6 +415,11 @@ public void Dispose () { Trace.Lifecycle (nameof (AnsiOutput), "Dispose", "Flushing output and releasing resources."); + if (_platform == AnsiPlatform.WindowsVT) + { + WindowsVTInputHelper.WakePendingRead (); + } + _windowsVTOutput?.Dispose (); } } diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs index eed7574439..0f7bd6d948 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs @@ -166,31 +166,31 @@ private static MouseFlags GetModifierFlags (int buttonCode) private static MouseFlags GetButtonFlags (int button, char terminator) { return button switch - { - 0 => terminator == 'M' ? MouseFlags.LeftButtonPressed : MouseFlags.LeftButtonReleased, - 1 => terminator == 'M' ? MouseFlags.MiddleButtonPressed : MouseFlags.MiddleButtonReleased, - 2 => terminator == 'M' ? MouseFlags.RightButtonPressed : MouseFlags.RightButtonReleased, - _ => MouseFlags.None - }; + { + 0 => terminator == 'M' ? MouseFlags.LeftButtonPressed : MouseFlags.LeftButtonReleased, + 1 => terminator == 'M' ? MouseFlags.MiddleButtonPressed : MouseFlags.MiddleButtonReleased, + 2 => terminator == 'M' ? MouseFlags.RightButtonPressed : MouseFlags.RightButtonReleased, + _ => MouseFlags.None + }; } private static MouseFlags GetWheelFlags (int buttonCode) { return buttonCode switch - { - 66 or 68 or 72 or 80 => MouseFlags.WheeledLeft, - 67 or 69 or 73 or 81 => MouseFlags.WheeledRight, - _ => GetVerticalWheelFlags (buttonCode) - }; + { + 66 or 68 or 72 or 80 => MouseFlags.WheeledLeft, + 67 or 69 or 73 or 81 => MouseFlags.WheeledRight, + _ => GetVerticalWheelFlags (buttonCode) + }; } private static MouseFlags GetVerticalWheelFlags (int buttonCode) { return (buttonCode & ButtonMask) switch - { - 0 => MouseFlags.WheeledUp, - 1 => MouseFlags.WheeledDown, - _ => MouseFlags.None - }; + { + 0 => MouseFlags.WheeledUp, + 1 => MouseFlags.WheeledDown, + _ => MouseFlags.None + }; } } diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs index 362d261fa7..0a4875e030 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs @@ -59,7 +59,7 @@ public bool Matches (string? cur) } int startIndex = 1; - int endIndex = input.IndexOfAny ([ ';', terminator [0] ], startIndex); + int endIndex = input.IndexOfAny ([';', terminator [0]], startIndex); if (endIndex < 0) { diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs index b562629e04..abaf373596 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs @@ -76,7 +76,7 @@ internal abstract class AnsiResponseParserBase (IHeld heldContent, ITimeProvider // Valid ANSI response terminators per CSI specification // See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s // Note: N and O are intentionally excluded as they have special handling - protected readonly HashSet _knownTerminators = [..EscSeqUtils.KnownTerminators]; + protected readonly HashSet _knownTerminators = [.. EscSeqUtils.KnownTerminators]; /// public AnsiResponseParserState State diff --git a/Terminal.Gui/Drivers/AnsiHandling/CsiCursorPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/CsiCursorPattern.cs index d7b029a6c4..bada45bf73 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/CsiCursorPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/CsiCursorPattern.cs @@ -1,5 +1,5 @@ -using System.Text.RegularExpressions; using System.Diagnostics; +using System.Text.RegularExpressions; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscAsAltPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/EscAsAltPattern.cs index 63cc473bc4..a50f53bd2c 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscAsAltPattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscAsAltPattern.cs @@ -24,11 +24,11 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern char ch = match.Groups [1].Value [0]; Key key = ch switch - { - >= '\u0001' and <= '\u001a' => ((Key)(ch + 96)).WithCtrl, - '\u001f' => Key.D7.WithCtrl.WithShift, - _ => ch - }; + { + >= '\u0001' and <= '\u001a' => ((Key)(ch + 96)).WithCtrl, + '\u001f' => Key.D7.WithCtrl.WithShift, + _ => ch + }; return new Key (key).WithAlt; } diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs index ca46525599..3014733ba3 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs @@ -448,9 +448,9 @@ public static ConsoleKeyInfo MapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) //uint ck = ConsoleKeyMapping.MapKeyCodeToConsoleKey ((KeyCode)consoleKeyInfo.KeyChar, out bool isConsoleKey); //if (isConsoleKey) - { - key = consoleKeyInfo.Key; // (ConsoleKey)ck; - } + { + key = consoleKeyInfo.Key; // (ConsoleKey)ck; + } newConsoleKeyInfo = new ConsoleKeyInfo (keyChar, key, @@ -672,15 +672,7 @@ public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col) { var tooLongCursorPositionSequence = $"{CSI}{row};{col}H"; - throw new InvalidOperationException ($"{ - nameof (CSI_WriteCursorPosition) - } buffer (len: { - buffer.Length - }) is too short for cursor position sequence '{ - tooLongCursorPositionSequence - }' (len: { - tooLongCursorPositionSequence.Length - })."); + throw new InvalidOperationException ($"{nameof (CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length})."); } ReadOnlySpan cursorPositionSequence = buffer [..charsWritten]; diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index 876e1d5e64..5bf5929f52 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -55,11 +55,7 @@ internal void Enable (KittyKeyboardFlags flags) { Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", - $"Post-enable detect did not update flags. IsSupported={ - result.IsSupported - }, HasCapabilities={ - _driver?.KittyKeyboardCapabilities is { } - }"); + $"Post-enable detect did not update flags. IsSupported={result.IsSupported}, HasCapabilities={_driver?.KittyKeyboardCapabilities is { }}"); return; } diff --git a/Terminal.Gui/Drivers/AnsiHandling/Ss3Pattern.cs b/Terminal.Gui/Drivers/AnsiHandling/Ss3Pattern.cs index 0e81762631..aa6d1ec6c8 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/Ss3Pattern.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/Ss3Pattern.cs @@ -9,7 +9,7 @@ namespace Terminal.Gui.Drivers; public class Ss3Pattern : AnsiKeyboardParserPattern { #pragma warning disable IDE1006 // Naming Styles - private static readonly Regex _pattern = new Regex (@"^\u001bO([PQRStDCABOHFwqysu])$"); + private static readonly Regex _pattern = new(@"^\u001bO([PQRStDCABOHFwqysu])$"); #pragma warning restore IDE1006 // Naming Styles /// @@ -30,24 +30,24 @@ public class Ss3Pattern : AnsiKeyboardParserPattern } return match.Groups [1].Value.Single () switch - { - 'P' => Key.F1, - 'Q' => Key.F2, - 'R' => Key.F3, - 'S' => Key.F4, - 't' => Key.F5, - 'D' => Key.CursorLeft, - 'C' => Key.CursorRight, - 'A' => Key.CursorUp, - 'B' => Key.CursorDown, - 'H' => Key.Home, - 'F' => Key.End, - 'w' => Key.Home, - 'q' => Key.End, - 'y' => Key.PageUp, - 's' => Key.PageDown, - 'u' => Key.Clear, - _ => null - }; + { + 'P' => Key.F1, + 'Q' => Key.F2, + 'R' => Key.F3, + 'S' => Key.F4, + 't' => Key.F5, + 'D' => Key.CursorLeft, + 'C' => Key.CursorRight, + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + 'H' => Key.Home, + 'F' => Key.End, + 'w' => Key.Home, + 'q' => Key.End, + 'y' => Key.PageUp, + 's' => Key.PageDown, + 'u' => Key.Clear, + _ => null + }; } } diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index df888831f2..dbfe0e055e 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -196,11 +196,7 @@ public void Suspend () // Check if we have a real console first if (Console.IsInputRedirected || Console.IsOutputRedirected) { - Logging.Information ($"Console redirected (Output: { - Console.IsOutputRedirected - }, Input: { - Console.IsInputRedirected - }). Running in degraded mode."); + Logging.Information ($"Console redirected (Output: {Console.IsOutputRedirected}, Input: {Console.IsInputRedirected}). Running in degraded mode."); return; } diff --git a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs index 678f20e27f..18341b2975 100644 --- a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs @@ -99,11 +99,7 @@ protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyCon // Check if this is an incomplete mouse sequence (timing issue when Run() blocks) - Logging.Warning ($"{ - nameof (InputProcessorImpl) - } ignored unrecognized response '{ - cur - }'. See https://github.com/gui-cs/Terminal.Gui/issues/4587"); + Logging.Warning ($"{nameof (InputProcessorImpl)} ignored unrecognized response '{cur}'. See https://github.com/gui-cs/Terminal.Gui/issues/4587"); AnsiSequenceSwallowed?.Invoke (this, cur); @@ -151,11 +147,7 @@ private void FlushStaleBracketedPasteIfNeeded () } Logging.Warning ( - $"{ - nameof (InputProcessorImpl) - } abandoning stale bracketed-paste state after { - _bracketedPasteTimeout.TotalSeconds - }s without activity"); + $"{nameof (InputProcessorImpl)} abandoning stale bracketed-paste state after {_bracketedPasteTimeout.TotalSeconds}s without activity"); Parser.FlushStaleBracketedPaste (); } diff --git a/Terminal.Gui/Drivers/Keyboard/ConsoleKeyMapping.cs b/Terminal.Gui/Drivers/Keyboard/ConsoleKeyMapping.cs index 08fce78c7e..e4aedebd0d 100644 --- a/Terminal.Gui/Drivers/Keyboard/ConsoleKeyMapping.cs +++ b/Terminal.Gui/Drivers/Keyboard/ConsoleKeyMapping.cs @@ -309,14 +309,14 @@ private static (ConsoleKey consoleKey, char keyChar) MapToConsoleKeyAndChar (Key // Special keys don't have printable characters char specialChar = specialKey switch - { - ConsoleKey.Enter => '\r', - ConsoleKey.Tab => '\t', - ConsoleKey.Escape => '\u001B', - ConsoleKey.Backspace => '\b', - ConsoleKey.Spacebar => ' ', - _ => '\0' // Function keys, arrows, etc. have no character - }; + { + ConsoleKey.Enter => '\r', + ConsoleKey.Tab => '\t', + ConsoleKey.Escape => '\u001B', + ConsoleKey.Backspace => '\b', + ConsoleKey.Spacebar => ' ', + _ => '\0' // Function keys, arrows, etc. have no character + }; return (specialKey, specialChar); } @@ -360,16 +360,16 @@ private static (ConsoleKey consoleKey, char keyChar) MapToConsoleKeyAndChar (Key if (Enum.IsDefined (typeof (ConsoleKey), (int)keyValue)) { char standardChar = standardKey switch - { - ConsoleKey.Enter => '\r', - ConsoleKey.Tab => '\t', - ConsoleKey.Escape => '\u001B', - ConsoleKey.Backspace => '\b', - ConsoleKey.Spacebar => ' ', - ConsoleKey.Clear => '\0', - _ when keyValue <= 0x1F => '\0', // Control characters - _ => (char)keyValue - }; + { + ConsoleKey.Enter => '\r', + ConsoleKey.Tab => '\t', + ConsoleKey.Escape => '\u001B', + ConsoleKey.Backspace => '\b', + ConsoleKey.Spacebar => ' ', + ConsoleKey.Clear => '\0', + _ when keyValue <= 0x1F => '\0', // Control characters + _ => (char)keyValue + }; return (standardKey, standardChar); } diff --git a/Terminal.Gui/Drivers/Mouse/MouseInterpreter.cs b/Terminal.Gui/Drivers/Mouse/MouseInterpreter.cs index c300c93236..ea52506641 100644 --- a/Terminal.Gui/Drivers/Mouse/MouseInterpreter.cs +++ b/Terminal.Gui/Drivers/Mouse/MouseInterpreter.cs @@ -288,32 +288,32 @@ private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) } return buttonIdx switch - { - 0 => numberOfClicks switch - { - 1 => MouseFlags.LeftButtonClicked, - 2 => MouseFlags.LeftButtonDoubleClicked, - _ => MouseFlags.LeftButtonTripleClicked - }, - 1 => numberOfClicks switch - { - 1 => MouseFlags.MiddleButtonClicked, - 2 => MouseFlags.MiddleButtonDoubleClicked, - _ => MouseFlags.MiddleButtonTripleClicked - }, - 2 => numberOfClicks switch - { - 1 => MouseFlags.RightButtonClicked, - 2 => MouseFlags.RightButtonDoubleClicked, - _ => MouseFlags.RightButtonTripleClicked - }, - 3 => numberOfClicks switch - { - 1 => MouseFlags.Button4Clicked, - 2 => MouseFlags.Button4DoubleClicked, - _ => MouseFlags.Button4TripleClicked - }, - _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), @"Unsupported button index") - }; + { + 0 => numberOfClicks switch + { + 1 => MouseFlags.LeftButtonClicked, + 2 => MouseFlags.LeftButtonDoubleClicked, + _ => MouseFlags.LeftButtonTripleClicked + }, + 1 => numberOfClicks switch + { + 1 => MouseFlags.MiddleButtonClicked, + 2 => MouseFlags.MiddleButtonDoubleClicked, + _ => MouseFlags.MiddleButtonTripleClicked + }, + 2 => numberOfClicks switch + { + 1 => MouseFlags.RightButtonClicked, + 2 => MouseFlags.RightButtonDoubleClicked, + _ => MouseFlags.RightButtonTripleClicked + }, + 3 => numberOfClicks switch + { + 1 => MouseFlags.Button4Clicked, + 2 => MouseFlags.Button4DoubleClicked, + _ => MouseFlags.Button4TripleClicked + }, + _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), @"Unsupported button index") + }; } } diff --git a/Terminal.Gui/Drivers/UnixHelpers/UnixClipboard.cs b/Terminal.Gui/Drivers/UnixHelpers/UnixClipboard.cs index 61df0cf797..c45991f359 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/UnixClipboard.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/UnixClipboard.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; diff --git a/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs b/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs index ecaa7600e7..313f3f19f1 100644 --- a/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs +++ b/Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs @@ -78,7 +78,7 @@ public enum Condition : short /// Timeout in milliseconds (0 = non-blocking, -1 = infinite) /// Number of file descriptors with events, or -1 on error [DllImport ("libc", SetLastError = true)] - public static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout); + public static extern int poll ([In][Out] Pollfd [] ufds, uint nfds, int timeout); /// /// Read bytes from a file descriptor. diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs index fa191262e7..7db8262f4c 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -257,14 +257,14 @@ public struct InputRecord public readonly override string ToString () { return (EventType switch - { - EventType.Focus => FocusEvent.ToString (), - EventType.Key => KeyEvent.ToString (), - EventType.Menu => MenuEvent.ToString (), - EventType.Mouse => MouseEvent.ToString (), - EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), - _ => "Unknown event type: " + EventType - })!; + { + EventType.Focus => FocusEvent.ToString (), + EventType.Key => KeyEvent.ToString (), + EventType.Menu => MenuEvent.ToString (), + EventType.Mouse => MouseEvent.ToString (), + EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), + _ => "Unknown event type: " + EventType + })!; } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs index 0a8f2427f7..7aefe06524 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyConverter.cs @@ -72,7 +72,7 @@ public WindowsConsole.InputRecord ToKeyInfo (Key key) }; // Create and return an InputRecord with the keyboard event - return new() + return new () { EventType = WindowsConsole.EventType.Key, KeyEvent = keyEvent @@ -113,70 +113,70 @@ private static ushort GetScanCodeForKey (ConsoleKey key) // Fallback: Use a simple heuristic for common keys // For most test scenarios, these values work fine return key switch - { - ConsoleKey.Escape => 1, - ConsoleKey.D1 => 2, - ConsoleKey.D2 => 3, - ConsoleKey.D3 => 4, - ConsoleKey.D4 => 5, - ConsoleKey.D5 => 6, - ConsoleKey.D6 => 7, - ConsoleKey.D7 => 8, - ConsoleKey.D8 => 9, - ConsoleKey.D9 => 10, - ConsoleKey.D0 => 11, - ConsoleKey.Tab => 15, - ConsoleKey.Q => 16, - ConsoleKey.W => 17, - ConsoleKey.E => 18, - ConsoleKey.R => 19, - ConsoleKey.T => 20, - ConsoleKey.Y => 21, - ConsoleKey.U => 22, - ConsoleKey.I => 23, - ConsoleKey.O => 24, - ConsoleKey.P => 25, - ConsoleKey.Enter => 28, - ConsoleKey.A => 30, - ConsoleKey.S => 31, - ConsoleKey.D => 32, - ConsoleKey.F => 33, - ConsoleKey.G => 34, - ConsoleKey.H => 35, - ConsoleKey.J => 36, - ConsoleKey.K => 37, - ConsoleKey.L => 38, - ConsoleKey.Z => 44, - ConsoleKey.X => 45, - ConsoleKey.C => 46, - ConsoleKey.V => 47, - ConsoleKey.B => 48, - ConsoleKey.N => 49, - ConsoleKey.M => 50, - ConsoleKey.Spacebar => 57, - ConsoleKey.F1 => 59, - ConsoleKey.F2 => 60, - ConsoleKey.F3 => 61, - ConsoleKey.F4 => 62, - ConsoleKey.F5 => 63, - ConsoleKey.F6 => 64, - ConsoleKey.F7 => 65, - ConsoleKey.F8 => 66, - ConsoleKey.F9 => 67, - ConsoleKey.F10 => 68, - ConsoleKey.Home => 71, - ConsoleKey.UpArrow => 72, - ConsoleKey.PageUp => 73, - ConsoleKey.LeftArrow => 75, - ConsoleKey.RightArrow => 77, - ConsoleKey.End => 79, - ConsoleKey.DownArrow => 80, - ConsoleKey.PageDown => 81, - ConsoleKey.Insert => 82, - ConsoleKey.Delete => 83, - ConsoleKey.F11 => 87, - ConsoleKey.F12 => 88, - _ => 0 // Unknown or not needed for test simulation - }; + { + ConsoleKey.Escape => 1, + ConsoleKey.D1 => 2, + ConsoleKey.D2 => 3, + ConsoleKey.D3 => 4, + ConsoleKey.D4 => 5, + ConsoleKey.D5 => 6, + ConsoleKey.D6 => 7, + ConsoleKey.D7 => 8, + ConsoleKey.D8 => 9, + ConsoleKey.D9 => 10, + ConsoleKey.D0 => 11, + ConsoleKey.Tab => 15, + ConsoleKey.Q => 16, + ConsoleKey.W => 17, + ConsoleKey.E => 18, + ConsoleKey.R => 19, + ConsoleKey.T => 20, + ConsoleKey.Y => 21, + ConsoleKey.U => 22, + ConsoleKey.I => 23, + ConsoleKey.O => 24, + ConsoleKey.P => 25, + ConsoleKey.Enter => 28, + ConsoleKey.A => 30, + ConsoleKey.S => 31, + ConsoleKey.D => 32, + ConsoleKey.F => 33, + ConsoleKey.G => 34, + ConsoleKey.H => 35, + ConsoleKey.J => 36, + ConsoleKey.K => 37, + ConsoleKey.L => 38, + ConsoleKey.Z => 44, + ConsoleKey.X => 45, + ConsoleKey.C => 46, + ConsoleKey.V => 47, + ConsoleKey.B => 48, + ConsoleKey.N => 49, + ConsoleKey.M => 50, + ConsoleKey.Spacebar => 57, + ConsoleKey.F1 => 59, + ConsoleKey.F2 => 60, + ConsoleKey.F3 => 61, + ConsoleKey.F4 => 62, + ConsoleKey.F5 => 63, + ConsoleKey.F6 => 64, + ConsoleKey.F7 => 65, + ConsoleKey.F8 => 66, + ConsoleKey.F9 => 67, + ConsoleKey.F10 => 68, + ConsoleKey.Home => 71, + ConsoleKey.UpArrow => 72, + ConsoleKey.PageUp => 73, + ConsoleKey.LeftArrow => 75, + ConsoleKey.RightArrow => 77, + ConsoleKey.End => 79, + ConsoleKey.DownArrow => 80, + ConsoleKey.PageDown => 81, + ConsoleKey.Insert => 82, + ConsoleKey.Delete => 83, + ConsoleKey.F11 => 87, + ConsoleKey.F12 => 88, + _ => 0 // Unknown or not needed for test simulation + }; } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs index cd424136e4..09e7f45825 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsKeyHelper.cs @@ -149,13 +149,13 @@ public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) // returned (e.g. on ENG OemPlus un-shifted is =, not +). This is important // for key persistence ("Ctrl++" vs. "Ctrl+="). mappedChar = keyInfo.Key switch - { - ConsoleKey.OemPeriod => '.', - ConsoleKey.OemComma => ',', - ConsoleKey.OemPlus => '+', - ConsoleKey.OemMinus => '-', - _ => mappedChar - }; + { + ConsoleKey.OemPeriod => '.', + ConsoleKey.OemComma => ',', + ConsoleKey.OemPlus => '+', + ConsoleKey.OemMinus => '-', + _ => mappedChar + }; } // Return the mappedChar with modifiers. Because mappedChar is un-shifted, if Shift was down diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 72c253c1c7..87ff0f0b69 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -172,15 +172,15 @@ public void SetCursor (Cursor cursor) cursorInfo.bVisible = true; cursorInfo.dwSize = cursor.Style switch - { - CursorStyle.BlinkingBlock => 100, - CursorStyle.SteadyBlock => 100, - CursorStyle.BlinkingUnderline => 15, - CursorStyle.SteadyUnderline => 15, - CursorStyle.BlinkingBar => 15, - CursorStyle.SteadyBar => 15, - _ => 100 - }; + { + CursorStyle.BlinkingBlock => 100, + CursorStyle.SteadyBlock => 100, + CursorStyle.BlinkingUnderline => 15, + CursorStyle.SteadyUnderline => 15, + CursorStyle.BlinkingBar => 15, + CursorStyle.SteadyBar => 15, + _ => 100 + }; } SetConsoleCursorInfo (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref cursorInfo); diff --git a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs index a122dfeac3..931b78918f 100644 --- a/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs +++ b/Terminal.Gui/Drivers/WindowsHelpers/WindowsVTInputHelper.cs @@ -45,11 +45,17 @@ internal sealed class WindowsVTInputHelper : IDisposable [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool ReadFile (nint hFile, byte [] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, nint lpOverlapped); + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool CancelIoEx (nint hFile, nint lpOverlapped); + [DllImport ("kernel32.dll")] private static extern uint GetConsoleCP (); #endregion + private const int ERROR_NOT_FOUND = 1168; + private const int ERROR_OPERATION_ABORTED = 995; + // Console mode flags private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; private const uint ENABLE_PROCESSED_INPUT = 0x0001; @@ -183,6 +189,12 @@ public bool TryRead (byte [] buffer, out int bytesRead) if (!success) { int error = Marshal.GetLastWin32Error (); + + if (error == ERROR_OPERATION_ABORTED) + { + return false; + } + Logging.Warning ($"{nameof (WindowsVTInputHelper)}: ReadFile failed with error code: {error}"); return false; @@ -253,6 +265,36 @@ public void Dispose () /// true if there is at least one input event available. public bool Peek () => GetNumberOfConsoleInputEvents (InputHandle, out uint count) && count > 0; + /// + /// Cancels any pending synchronous on the Windows console input handle. + /// + public static void WakePendingRead () + { + if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + return; + } + + nint inputHandle = TerminalDevice.InputHandle; + + if (inputHandle == nint.Zero || inputHandle == new nint (-1)) + { + return; + } + + if (CancelIoEx (inputHandle, nint.Zero)) + { + return; + } + + int error = Marshal.GetLastWin32Error (); + + if (error != ERROR_NOT_FOUND) + { + Logging.Warning ($"{nameof (WindowsVTInputHelper)}: CancelIoEx failed with error code: {error}"); + } + } + [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool FlushConsoleInputBuffer (nint hConsoleInput); diff --git a/Terminal.Gui/Input/CommandBindingsBase.cs b/Terminal.Gui/Input/CommandBindingsBase.cs index ad9fb608d0..62dc2804c1 100644 --- a/Terminal.Gui/Input/CommandBindingsBase.cs +++ b/Terminal.Gui/Input/CommandBindingsBase.cs @@ -8,7 +8,7 @@ namespace Terminal.Gui.Input; /// /// The type of the event (e.g. or ). /// The binding type (e.g. ). -public abstract class CommandBindingsBase where TBinding : ICommandBinding, new () where TEvent : notnull +public abstract class CommandBindingsBase where TBinding : ICommandBinding, new() where TEvent : notnull { /// /// Initializes a new instance. diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 0008a888c7..5d487a2b66 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -84,7 +84,7 @@ public IReadOnlyList Values /// /// The value to append. /// A new with the value appended to . - public CommandContext WithValue (object? value) => this with { Values = [..Values, value] }; + public CommandContext WithValue (object? value) => this with { Values = [.. Values, value] }; /// public override string ToString () diff --git a/Terminal.Gui/Input/CommandContextExtensions.cs b/Terminal.Gui/Input/CommandContextExtensions.cs index 72896e5486..b364437218 100644 --- a/Terminal.Gui/Input/CommandContextExtensions.cs +++ b/Terminal.Gui/Input/CommandContextExtensions.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Input; public static class CommandContextExtensions { /// The command context. - extension (ICommandContext? context) + extension(ICommandContext? context) { /// /// Tries to get the source from a command context. diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index 58321a99c6..e48e571b62 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -385,7 +385,7 @@ public Rune AsRune /// /// /// - /// When the terminal reports associated text, this contains the exact text produced by the key event. + /// When the terminal reports associated text, this contains the exact text produced by the key event. /// This is preferred for text insertion because it preserves layout-specific printable characters. /// For example, in kitty input like ESC[50:64;2;64u, the primary key code is '2', /// the shifted alternate key code is '@', and the associated text is also '@'. diff --git a/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs b/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs index 1d975c9c79..0e764db7d8 100644 --- a/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/PlatformKeyBinding.cs @@ -67,12 +67,12 @@ public IEnumerable GetCurrentPlatformKeys () } Key []? platKeys = PlatformDetection.GetCurrentPlatform () switch - { - TuiPlatform.Windows => Windows, - TuiPlatform.Linux => Linux, - TuiPlatform.Macos => Macos, - _ => null - }; + { + TuiPlatform.Windows => Windows, + TuiPlatform.Linux => Linux, + TuiPlatform.Macos => Macos, + _ => null + }; if (platKeys is null) { diff --git a/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs b/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs index 2278081eb1..5513e3b331 100644 --- a/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/PlatformMouseBinding.cs @@ -67,12 +67,12 @@ public IEnumerable GetCurrentPlatformMouseFlags () } MouseFlags []? platformFlags = PlatformDetection.GetCurrentPlatform () switch - { - TuiPlatform.Windows => Windows, - TuiPlatform.Linux => Linux, - TuiPlatform.Macos => Macos, - _ => null - }; + { + TuiPlatform.Windows => Windows, + TuiPlatform.Linux => Linux, + TuiPlatform.Macos => Macos, + _ => null + }; if (platformFlags is null) { diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index ff07593cd2..15666b3424 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -119,8 +119,8 @@ } } ], - "Glyphs.CheckStateChecked": "☒", - "Glyphs.CheckStateNone": "□", + "Glyphs.CheckStateChecked": "☑", + "Glyphs.CheckStateNone": "⬛", "Glyphs.CheckStateUnChecked": "☐", "Glyphs.LeftBracket": "[", "Glyphs.RightBracket": "]" @@ -185,8 +185,8 @@ } } ], - "Glyphs.CheckStateChecked": "☒", - "Glyphs.CheckStateNone": "□", + "Glyphs.CheckStateChecked": "☑", + "Glyphs.CheckStateNone": "⬛", "Glyphs.CheckStateUnChecked": "☐", "Glyphs.LeftBracket": "[", "Glyphs.RightBracket": "]" diff --git a/Terminal.Gui/Testing/InputInjectionExtensions.cs b/Terminal.Gui/Testing/InputInjectionExtensions.cs index f569f4efcc..096120b163 100644 --- a/Terminal.Gui/Testing/InputInjectionExtensions.cs +++ b/Terminal.Gui/Testing/InputInjectionExtensions.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Testing; public static class InputInjectionExtensions { /// The application instance. - extension (IApplication app) + extension(IApplication app) { /// /// Injects a key event (convenience method). diff --git a/Terminal.Gui/Text/RuneExtensions.cs b/Terminal.Gui/Text/RuneExtensions.cs index 075a525248..78e6f7a03b 100644 --- a/Terminal.Gui/Text/RuneExtensions.cs +++ b/Terminal.Gui/Text/RuneExtensions.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Text; /// Extends to support TUI text manipulation. public static class RuneExtensions { - private static readonly Lock _wcwidthLock = new Lock (); + private static readonly Lock _wcwidthLock = new(); /// Maximum Unicode code point. public static readonly int MaxUnicodeCodePoint = 0x10FFFF; diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 35c52555a7..615d606b3c 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -149,315 +149,315 @@ public void Draw ( int x = 0, y = 0; - // Horizontal Alignment - if (Alignment is Alignment.End) - { - if (isVertical) - { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth); - x = screen.Right - runesWidth; - CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); - } - else - { - int runesWidth = strings.GetColumns (); - x = screen.Right - runesWidth; - CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); - } - } - else if (Alignment is Alignment.Start) - { - if (isVertical) + // Horizontal Alignment + if (Alignment is Alignment.End) { - int runesWidth = line > 0 - ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) - : 0; - x = screen.Left + runesWidth; + if (isVertical) + { + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth); + x = screen.Right - runesWidth; + CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); + } + else + { + int runesWidth = strings.GetColumns (); + x = screen.Right - runesWidth; + CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); + } } - else + else if (Alignment is Alignment.Start) { - x = screen.Left; - } + if (isVertical) + { + int runesWidth = line > 0 + ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) + : 0; + x = screen.Left + runesWidth; + } + else + { + x = screen.Left; + } - CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; - } - else if (Alignment is Alignment.Fill) - { - if (isVertical) - { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); - int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0; - int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth); - int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth); - var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count); - - x = line == 0 - ? screen.Left - : line < linesFormatted.Count - 1 - ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval - : screen.Right - lastLineWidth; + CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else + else if (Alignment is Alignment.Fill) { - x = screen.Left; - } + if (isVertical) + { + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0; + int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth); + int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth); + var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count); - CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; - } - else if (Alignment is Alignment.Center) - { - if (isVertical) - { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); - int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); - x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2; + x = line == 0 + ? screen.Left + : line < linesFormatted.Count - 1 + ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval + : screen.Right - lastLineWidth; + } + else + { + x = screen.Left; + } - CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); + CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else + else if (Alignment is Alignment.Center) { - int runesWidth = strings.GetColumns (); - x = screen.Left + (screen.Width - runesWidth) / 2; - - CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); - } - } - else - { - Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + if (isVertical) + { + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); + x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2; - return; - } + CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); + } + else + { + int runesWidth = strings.GetColumns (); + x = screen.Left + (screen.Width - runesWidth) / 2; - // Vertical Alignment - if (VerticalAlignment is Alignment.End) - { - if (isVertical) - { - y = screen.Bottom - graphemeCount; + CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); + } } else { - y = screen.Bottom - linesFormatted.Count + line; - } - } - else if (VerticalAlignment is Alignment.Start) - { - if (isVertical) - { - y = screen.Top; + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return; } - else + + // Vertical Alignment + if (VerticalAlignment is Alignment.End) { - y = screen.Top + line; + if (isVertical) + { + y = screen.Bottom - graphemeCount; + } + else + { + y = screen.Bottom - linesFormatted.Count + line; + } } - } - else if (VerticalAlignment is Alignment.Fill) - { - if (isVertical) + else if (VerticalAlignment is Alignment.Start) { - y = screen.Top; + if (isVertical) + { + y = screen.Top; + } + else + { + y = screen.Top + line; + } } - else + else if (VerticalAlignment is Alignment.Fill) { - var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count); + if (isVertical) + { + y = screen.Top; + } + else + { + var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count); - y = line == 0 ? screen.Top : - line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; + y = line == 0 ? screen.Top : + line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; + } } - } - else if (VerticalAlignment is Alignment.Center) - { - if (isVertical) + else if (VerticalAlignment is Alignment.Center) { - int s = (screen.Height - graphemeCount) / 2; - y = screen.Top + s; + if (isVertical) + { + int s = (screen.Height - graphemeCount) / 2; + y = screen.Top + s; + } + else + { + int s = (screen.Height - linesFormatted.Count) / 2; + y = screen.Top + line + s; + } } else { - int s = (screen.Height - linesFormatted.Count) / 2; - y = screen.Top + line + s; - } - } - else - { - Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); - return; - } + return; + } - int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; - int start = isVertical ? screen.Top : screen.Left; - int size = isVertical ? screen.Height : screen.Width; - int current = start + colOffset; - List lastZeroWidthPos = null!; - string text = string.Empty; - int zeroLengthCount = isVertical ? GraphemeHelper.GetGraphemes (strings).Sum (s => s.GetColumns (false) == 0 ? 1 : 0) : 0; - - for (int idx = (isVertical ? start - y : start - x) + colOffset; - current < start + size + zeroLengthCount; - idx++) - { - string lastTextUsed = text; + int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; + int start = isVertical ? screen.Top : screen.Left; + int size = isVertical ? screen.Height : screen.Width; + int current = start + colOffset; + List lastZeroWidthPos = null!; + string text = string.Empty; + int zeroLengthCount = isVertical ? GraphemeHelper.GetGraphemes (strings).Sum (s => s.GetColumns (false) == 0 ? 1 : 0) : 0; - if (lastZeroWidthPos is null) + for (int idx = (isVertical ? start - y : start - x) + colOffset; + current < start + size + zeroLengthCount; + idx++) { - if (idx < 0 - || (isVertical - ? VerticalAlignment != Alignment.End && current < 0 - : Alignment != Alignment.End && x + current + colOffset < 0)) + string lastTextUsed = text; + + if (lastZeroWidthPos is null) { - current++; + if (idx < 0 + || (isVertical + ? VerticalAlignment != Alignment.End && current < 0 + : Alignment != Alignment.End && x + current + colOffset < 0)) + { + current++; - continue; - } + continue; + } - if (!FillRemaining && idx > graphemeCount - 1) - { - break; - } + if (!FillRemaining && idx > graphemeCount - 1) + { + break; + } - if ((!isVertical - && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset - || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))) - || (isVertical - && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) - || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))) - { - break; + if ((!isVertical + && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset + || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))) + || (isVertical + && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) + || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))) + { + break; + } } - } - //if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - viewport.X + colOffset) - // || (isVertical && idx > maxBounds.Top + maxBounds.Height - viewport.Y)) + //if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - viewport.X + colOffset) + // || (isVertical && idx > maxBounds.Top + maxBounds.Height - viewport.Y)) - // break; + // break; - text = " "; + text = " "; - if (isVertical) - { - if (idx >= 0 && idx < graphemeCount) - { - text = graphemes [idx]; - } - - if (lastZeroWidthPos is null) - { - driver?.Move (x, current); - } - else + if (isVertical) { - int foundIdx = lastZeroWidthPos.IndexOf ( - p => - p is { } && p.Value.Y == current - ); + if (idx >= 0 && idx < graphemeCount) + { + text = graphemes [idx]; + } - if (foundIdx > -1) + if (lastZeroWidthPos is null) { - if (Rune.GetRuneAt (text, 0).IsCombiningMark ()) - { - lastZeroWidthPos [foundIdx] = - new Point ( - lastZeroWidthPos [foundIdx]!.Value.X + 1, - current - ); + driver?.Move (x, current); + } + else + { + int foundIdx = lastZeroWidthPos.IndexOf ( + p => + p is { } && p.Value.Y == current + ); - driver?.Move ( - lastZeroWidthPos [foundIdx]!.Value.X, - current - ); - } - else if (!Rune.GetRuneAt (text, 0).IsCombiningMark () && Rune.GetRuneAt (lastTextUsed, 0).IsCombiningMark ()) + if (foundIdx > -1) { - current++; - driver?.Move (x, current); + if (Rune.GetRuneAt (text, 0).IsCombiningMark ()) + { + lastZeroWidthPos [foundIdx] = + new Point ( + lastZeroWidthPos [foundIdx]!.Value.X + 1, + current + ); + + driver?.Move ( + lastZeroWidthPos [foundIdx]!.Value.X, + current + ); + } + else if (!Rune.GetRuneAt (text, 0).IsCombiningMark () && Rune.GetRuneAt (lastTextUsed, 0).IsCombiningMark ()) + { + current++; + driver?.Move (x, current); + } + else + { + driver?.Move (x, current); + } } else { driver?.Move (x, current); } } - else - { - driver?.Move (x, current); - } } - } - else - { - driver?.Move (current, y); - - if (idx >= 0 && idx < graphemeCount) + else { - text = graphemes [idx]; + driver?.Move (current, y); + + if (idx >= 0 && idx < graphemeCount) + { + text = graphemes [idx]; + } } - } - int textWidth = GetTextWidth (text, TabWidth); + int textWidth = GetTextWidth (text, TabWidth); - if (HotKeyPos > -1 && idx == HotKeyPos) - { - if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill)) + if (HotKeyPos > -1 && idx == HotKeyPos) { - CursorPosition = idx - start; - } + if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill)) + { + CursorPosition = idx - start; + } - driver?.SetAttribute (hotColor); - driver?.AddStr (text); - driver?.SetAttribute (normalColor); - } - else - { - if (isVertical) + driver?.SetAttribute (hotColor); + driver?.AddStr (text); + driver?.SetAttribute (normalColor); + } + else { - if (textWidth == 0) + if (isVertical) { - if (lastZeroWidthPos is null) + if (textWidth == 0) { - lastZeroWidthPos = new (); + if (lastZeroWidthPos is null) + { + lastZeroWidthPos = new (); + } + + int foundIdx = lastZeroWidthPos.IndexOf ( + p => + p is { } && p.Value.Y == current + ); + + if (foundIdx == -1) + { + current--; + lastZeroWidthPos.Add (new Point (x + 1, current)); + } + + driver?.Move (x + 1, current); } + } - int foundIdx = lastZeroWidthPos.IndexOf ( - p => - p is { } && p.Value.Y == current - ); - - if (foundIdx == -1) - { - current--; - lastZeroWidthPos.Add (new Point (x + 1, current)); - } + driver?.AddStr (text); + } - driver?.Move (x + 1, current); + if (isVertical) + { + if (textWidth > 0) + { + current++; } } - - driver?.AddStr (text); - } - - if (isVertical) - { - if (textWidth > 0) + else { - current++; + current += textWidth; } - } - else - { - current += textWidth; - } - int nextTextWidth = idx + 1 > -1 && idx + 1 < graphemeCount - ? graphemes [idx + 1].GetColumns () - : 0; + int nextTextWidth = idx + 1 > -1 && idx + 1 < graphemeCount + ? graphemes [idx + 1].GetColumns () + : 0; - if (!isVertical && idx + 1 < graphemeCount && current + nextTextWidth > start + size) - { - break; + if (!isVertical && idx + 1 < graphemeCount && current + nextTextWidth > start + size) + { + break; + } } } - } finally { ArrayPool.Shared.Return (graphemes, clearArray: true); @@ -912,7 +912,7 @@ private T EnableNeedsFormat (T value) /// A representing the areas where text would be drawn. public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default) { - Region drawnRegion = new Region (); + Region drawnRegion = new(); // With this check, we protect against subclasses with overrides of Text (like Button) if (string.IsNullOrEmpty (Text)) @@ -965,7 +965,7 @@ public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default) } string strings = linesFormatted [line]; - + // Use ArrayPool to avoid per-line allocations int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters string [] graphemes = ArrayPool.Shared.Rent (estimatedCount); @@ -990,195 +990,195 @@ public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default) // When text is justified, we lost left or right, so we use the direction to align. int x = 0, y = 0; - switch (Alignment) - { - // Horizontal Alignment - case Alignment.End when isVertical: - { - int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth); - x = screen.Right - runesWidth; + switch (Alignment) + { + // Horizontal Alignment + case Alignment.End when isVertical: + { + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth); + x = screen.Right - runesWidth; - break; - } - case Alignment.End: - { - int stringsWidth = strings.GetColumns (); - x = screen.Right - stringsWidth; + break; + } + case Alignment.End: + { + int stringsWidth = strings.GetColumns (); + x = screen.Right - stringsWidth; + + break; + } + case Alignment.Start when isVertical: + { + int stringsWidth = line > 0 + ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) + : 0; + x = screen.Left + stringsWidth; + + break; + } + case Alignment.Start: + x = screen.Left; break; - } - case Alignment.Start when isVertical: - { - int stringsWidth = line > 0 - ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) - : 0; - x = screen.Left + stringsWidth; + case Alignment.Fill when isVertical: + { + int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0; + int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth); + int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth); + var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count); + + x = line == 0 + ? screen.Left + : line < linesFormatted.Count - 1 + ? screen.Width - stringsWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval + : screen.Right - lastLineWidth; + + break; + } + case Alignment.Fill: + x = screen.Left; break; - } - case Alignment.Start: - x = screen.Left; + case Alignment.Center when isVertical: + { + int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); + x = screen.Left + linesWidth + (screen.Width - stringsWidth) / 2; - break; - case Alignment.Fill when isVertical: - { - int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); - int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0; - int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth); - int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth); - var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count); + break; + } + case Alignment.Center: + { + int stringsWidth = strings.GetColumns (); + x = screen.Left + (screen.Width - stringsWidth) / 2; - x = line == 0 - ? screen.Left - : line < linesFormatted.Count - 1 - ? screen.Width - stringsWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval - : screen.Right - lastLineWidth; + break; + } + default: + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return drawnRegion; + } + + switch (VerticalAlignment) + { + // Vertical Alignment + case Alignment.End when isVertical: + y = screen.Bottom - graphemeCount; break; - } - case Alignment.Fill: - x = screen.Left; + case Alignment.End: + y = screen.Bottom - linesFormatted.Count + line; - break; - case Alignment.Center when isVertical: - { - int stringsWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); - int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); - x = screen.Left + linesWidth + (screen.Width - stringsWidth) / 2; + break; + case Alignment.Start when isVertical: + y = screen.Top; break; - } - case Alignment.Center: - { - int stringsWidth = strings.GetColumns (); - x = screen.Left + (screen.Width - stringsWidth) / 2; + case Alignment.Start: + y = screen.Top + line; break; - } - default: - Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + case Alignment.Fill when isVertical: + y = screen.Top; - return drawnRegion; - } + break; + case Alignment.Fill: + { + var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count); - switch (VerticalAlignment) - { - // Vertical Alignment - case Alignment.End when isVertical: - y = screen.Bottom - graphemeCount; + y = line == 0 ? screen.Top : + line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; - break; - case Alignment.End: - y = screen.Bottom - linesFormatted.Count + line; + break; + } + case Alignment.Center when isVertical: + { + int s = (screen.Height - graphemeCount) / 2; + y = screen.Top + s; - break; - case Alignment.Start when isVertical: - y = screen.Top; + break; + } + case Alignment.Center: + { + int s = (screen.Height - linesFormatted.Count) / 2; + y = screen.Top + line + s; - break; - case Alignment.Start: - y = screen.Top + line; + break; + } + default: + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); - break; - case Alignment.Fill when isVertical: - y = screen.Top; + return drawnRegion; + } - break; - case Alignment.Fill: - { - var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count); + int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; + int start = isVertical ? screen.Top : screen.Left; + int size = isVertical ? screen.Height : screen.Width; + int current = start + colOffset; + int zeroLengthCount = isVertical ? GraphemeHelper.GetGraphemes (strings).Sum (s => s.GetColumns (false) == 0 ? 1 : 0) : 0; - y = line == 0 ? screen.Top : - line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; + int lineX = x, lineY = y, lineWidth = 0, lineHeight = 1; - break; - } - case Alignment.Center when isVertical: + for (int idx = (isVertical ? start - y : start - x) + colOffset; + current < start + size + zeroLengthCount; + idx++) + { + if (idx < 0 + || (isVertical + ? VerticalAlignment != Alignment.End && current < 0 + : Alignment != Alignment.End && x + current + colOffset < 0)) { - int s = (screen.Height - graphemeCount) / 2; - y = screen.Top + s; + current++; - break; + continue; } - case Alignment.Center: - { - int s = (screen.Height - linesFormatted.Count) / 2; - y = screen.Top + line + s; + if (!FillRemaining && idx > graphemeCount - 1) + { break; } - default: - Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); - - return drawnRegion; - } - - int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; - int start = isVertical ? screen.Top : screen.Left; - int size = isVertical ? screen.Height : screen.Width; - int current = start + colOffset; - int zeroLengthCount = isVertical ? GraphemeHelper.GetGraphemes (strings).Sum (s => s.GetColumns (false) == 0 ? 1 : 0) : 0; - int lineX = x, lineY = y, lineWidth = 0, lineHeight = 1; - - for (int idx = (isVertical ? start - y : start - x) + colOffset; - current < start + size + zeroLengthCount; - idx++) - { - if (idx < 0 - || (isVertical - ? VerticalAlignment != Alignment.End && current < 0 - : Alignment != Alignment.End && x + current + colOffset < 0)) - { - current++; - - continue; - } - - if (!FillRemaining && idx > graphemeCount - 1) - { - break; - } - - if ((!isVertical - && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset - || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))) - || (isVertical - && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) - || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))) - { - break; - } + if ((!isVertical + && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset + || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))) + || (isVertical + && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y) + || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))) + { + break; + } - string text = idx >= 0 && idx < graphemeCount ? graphemes [idx] : " "; - int textWidth = GetStringWidth (text, TabWidth); + string text = idx >= 0 && idx < graphemeCount ? graphemes [idx] : " "; + int textWidth = GetStringWidth (text, TabWidth); - if (isVertical) - { - if (textWidth > 0) + if (isVertical) { - // Update line height for vertical text (each rune is a column) - lineHeight = Math.Max (lineHeight, current - y + 1); - lineWidth = Math.Max (lineWidth, 1); // Width is 1 per rune in vertical + if (textWidth > 0) + { + // Update line height for vertical text (each rune is a column) + lineHeight = Math.Max (lineHeight, current - y + 1); + lineWidth = Math.Max (lineWidth, 1); // Width is 1 per rune in vertical + } + } + else + { + // Update line width and position for horizontal text + lineWidth += textWidth; } - } - else - { - // Update line width and position for horizontal text - lineWidth += textWidth; - } - current += isVertical && textWidth > 0 ? 1 : textWidth; + current += isVertical && textWidth > 0 ? 1 : textWidth; - int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemeCount - ? graphemes [idx + 1].GetColumns () - : 0; + int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemeCount + ? graphemes [idx + 1].GetColumns () + : 0; - if (!isVertical && idx + 1 < graphemeCount && current + nextStringWidth > start + size) - { - break; + if (!isVertical && idx + 1 < graphemeCount && current + nextStringWidth > start + size) + { + break; + } } - } // Add the line's drawn region to the overall region if (lineWidth > 0 && lineHeight > 0) diff --git a/Terminal.Gui/ViewBase/Adornment/AdornmentView.cs b/Terminal.Gui/ViewBase/Adornment/AdornmentView.cs index 61f1fdebc5..b4bef14208 100644 --- a/Terminal.Gui/ViewBase/Adornment/AdornmentView.cs +++ b/Terminal.Gui/ViewBase/Adornment/AdornmentView.cs @@ -15,9 +15,10 @@ namespace Terminal.Gui.ViewBase; /// back-reference, making the single authoritative owner of Thickness. /// /// -/// During the incremental migration, existing and continue -/// to extend . Only newly migrated adornments (starting with MarginView) -/// extend . +/// , , and all extend +/// . is the authoritative owner of +/// ; the corresponding hosts the +/// render-layer behavior. /// /// public class AdornmentView : View, IAdornmentView, IDesignable @@ -151,8 +152,9 @@ protected override bool OnClearingViewport () Adornment.Thickness.Draw (Driver, ViewportToScreen (Viewport), Diagnostics, ToString ()); } - SetNeedsDraw (); - + // Do NOT call SetNeedsDraw () here. The thickness has been drawn; calling + // SetNeedsDraw cascades to the parent's SubViewNeedsDraw mid-pass and adds + // churn that competes with the needsDrawSelf gate added for issue #5358. return true; } diff --git a/Terminal.Gui/ViewBase/Adornment/BorderView.cs b/Terminal.Gui/ViewBase/Adornment/BorderView.cs index 75ee090c11..1108d6a8f3 100644 --- a/Terminal.Gui/ViewBase/Adornment/BorderView.cs +++ b/Terminal.Gui/ViewBase/Adornment/BorderView.cs @@ -768,13 +768,13 @@ private int GetTabDepth () } return TabSide switch - { - Side.Top => Adornment.Thickness.Top, - Side.Bottom => Adornment.Thickness.Bottom, - Side.Left => Adornment.Thickness.Left, - Side.Right => Adornment.Thickness.Right, - _ => 3 - }; + { + Side.Top => Adornment.Thickness.Top, + Side.Bottom => Adornment.Thickness.Bottom, + Side.Left => Adornment.Thickness.Left, + Side.Right => Adornment.Thickness.Right, + _ => 3 + }; } /// @@ -956,179 +956,179 @@ private static void AddTabSideContentBorder (LineCanvas lc, switch (side) { case Side.Top: - { - int borderY = contentBorderRect.Y; - - if (!openGap) { - lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute); - } - else - { - // Reserve the gap cells so overlapped compositing suppresses - // lower-Z views' content border lines at these positions. - int gapStart = clipped.X + 1; - int gapEnd = clipped.Right - 1; + int borderY = contentBorderRect.Y; - if (gapEnd > gapStart) + if (!openGap) { - lc.Reserve (new Rectangle (gapStart, borderY, gapEnd - gapStart, 1)); + lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute); } - - if (clipped.X > contentBorderRect.X) + else { - lc.AddLine (new Point (contentBorderRect.X, borderY), - clipped.X - contentBorderRect.X + 1, - Orientation.Horizontal, - lineStyle, - attribute); + // Reserve the gap cells so overlapped compositing suppresses + // lower-Z views' content border lines at these positions. + int gapStart = clipped.X + 1; + int gapEnd = clipped.Right - 1; + + if (gapEnd > gapStart) + { + lc.Reserve (new Rectangle (gapStart, borderY, gapEnd - gapStart, 1)); + } + + if (clipped.X > contentBorderRect.X) + { + lc.AddLine (new Point (contentBorderRect.X, borderY), + clipped.X - contentBorderRect.X + 1, + Orientation.Horizontal, + lineStyle, + attribute); + } + + if (clipped.Right - 1 < contentBorderRect.Right - 1) + { + lc.AddLine (new Point (clipped.Right - 1, borderY), + contentBorderRect.Right - (clipped.Right - 1), + Orientation.Horizontal, + lineStyle, + attribute); + } } - if (clipped.Right - 1 < contentBorderRect.Right - 1) - { - lc.AddLine (new Point (clipped.Right - 1, borderY), - contentBorderRect.Right - (clipped.Right - 1), - Orientation.Horizontal, - lineStyle, - attribute); - } + break; } - break; - } - case Side.Bottom: - { - int borderY = contentBorderRect.Bottom - 1; - - if (!openGap) - { - lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute); - } - else { - int gapStart = clipped.X + 1; - int gapEnd = clipped.Right - 1; + int borderY = contentBorderRect.Bottom - 1; - if (gapEnd > gapStart) + if (!openGap) { - lc.Reserve (new Rectangle (gapStart, borderY, gapEnd - gapStart, 1)); + lc.AddLine (new Point (contentBorderRect.X, borderY), contentBorderRect.Width, Orientation.Horizontal, lineStyle, attribute); } - - if (clipped.X > contentBorderRect.X) + else { - lc.AddLine (new Point (contentBorderRect.X, borderY), - clipped.X - contentBorderRect.X + 1, - Orientation.Horizontal, - lineStyle, - attribute); + int gapStart = clipped.X + 1; + int gapEnd = clipped.Right - 1; + + if (gapEnd > gapStart) + { + lc.Reserve (new Rectangle (gapStart, borderY, gapEnd - gapStart, 1)); + } + + if (clipped.X > contentBorderRect.X) + { + lc.AddLine (new Point (contentBorderRect.X, borderY), + clipped.X - contentBorderRect.X + 1, + Orientation.Horizontal, + lineStyle, + attribute); + } + + if (clipped.Right - 1 < contentBorderRect.Right - 1) + { + lc.AddLine (new Point (clipped.Right - 1, borderY), + contentBorderRect.Right - (clipped.Right - 1), + Orientation.Horizontal, + lineStyle, + attribute); + } } - if (clipped.Right - 1 < contentBorderRect.Right - 1) - { - lc.AddLine (new Point (clipped.Right - 1, borderY), - contentBorderRect.Right - (clipped.Right - 1), - Orientation.Horizontal, - lineStyle, - attribute); - } + break; } - break; - } - case Side.Left: - { - int borderX = contentBorderRect.X; - - if (!openGap) - { - lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute); - } - else { - int gapStart = clipped.Y + 1; - int gapEnd = clipped.Bottom - 1; - - if (gapEnd > gapStart) - { - lc.Reserve (new Rectangle (borderX, gapStart, 1, gapEnd - gapStart)); - } + int borderX = contentBorderRect.X; - if (clipped.Y > contentBorderRect.Y) + if (!openGap) { - lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute); + lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute); } - else if (clipped.Y > headerRect.Y) + else { - // Header clipped at top (overflow) — suppress corner glyph - lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1))); + int gapStart = clipped.Y + 1; + int gapEnd = clipped.Bottom - 1; + + if (gapEnd > gapStart) + { + lc.Reserve (new Rectangle (borderX, gapStart, 1, gapEnd - gapStart)); + } + + if (clipped.Y > contentBorderRect.Y) + { + lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute); + } + else if (clipped.Y > headerRect.Y) + { + // Header clipped at top (overflow) — suppress corner glyph + lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1))); + } + + if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1) + { + lc.AddLine (new Point (borderX, clipped.Bottom - 1), + contentBorderRect.Bottom - (clipped.Bottom - 1), + Orientation.Vertical, + lineStyle, + attribute); + } + else if (clipped.Bottom < headerRect.Bottom) + { + // Header clipped at bottom (overflow) — suppress corner glyph + lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1))); + } } - if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1) - { - lc.AddLine (new Point (borderX, clipped.Bottom - 1), - contentBorderRect.Bottom - (clipped.Bottom - 1), - Orientation.Vertical, - lineStyle, - attribute); - } - else if (clipped.Bottom < headerRect.Bottom) - { - // Header clipped at bottom (overflow) — suppress corner glyph - lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1))); - } + break; } - break; - } - case Side.Right: - { - int borderX = contentBorderRect.Right - 1; - - if (!openGap) { - lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute); - } - else - { - int gapStart = clipped.Y + 1; - int gapEnd = clipped.Bottom - 1; + int borderX = contentBorderRect.Right - 1; - if (gapEnd > gapStart) + if (!openGap) { - lc.Reserve (new Rectangle (borderX, gapStart, 1, gapEnd - gapStart)); + lc.AddLine (new Point (borderX, contentBorderRect.Y), contentBorderRect.Height, Orientation.Vertical, lineStyle, attribute); } - - if (clipped.Y > contentBorderRect.Y) + else { - lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute); - } - else if (clipped.Y > headerRect.Y) - { - // Header clipped at top (overflow) — suppress corner glyph - lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1))); + int gapStart = clipped.Y + 1; + int gapEnd = clipped.Bottom - 1; + + if (gapEnd > gapStart) + { + lc.Reserve (new Rectangle (borderX, gapStart, 1, gapEnd - gapStart)); + } + + if (clipped.Y > contentBorderRect.Y) + { + lc.AddLine (new Point (borderX, contentBorderRect.Y), clipped.Y - contentBorderRect.Y + 1, Orientation.Vertical, lineStyle, attribute); + } + else if (clipped.Y > headerRect.Y) + { + // Header clipped at top (overflow) — suppress corner glyph + lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Y, 1, 1))); + } + + if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1) + { + lc.AddLine (new Point (borderX, clipped.Bottom - 1), + contentBorderRect.Bottom - (clipped.Bottom - 1), + Orientation.Vertical, + lineStyle, + attribute); + } + else if (clipped.Bottom < headerRect.Bottom) + { + // Header clipped at bottom (overflow) — suppress corner glyph + lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1))); + } } - if (clipped.Bottom - 1 < contentBorderRect.Bottom - 1) - { - lc.AddLine (new Point (borderX, clipped.Bottom - 1), - contentBorderRect.Bottom - (clipped.Bottom - 1), - Orientation.Vertical, - lineStyle, - attribute); - } - else if (clipped.Bottom < headerRect.Bottom) - { - // Header clipped at bottom (overflow) — suppress corner glyph - lc.Exclude (new Region (new Rectangle (borderX, contentBorderRect.Bottom - 1, 1, 1))); - } + break; } - break; - } - default: throw new ArgumentOutOfRangeException (nameof (side), side, null); } } diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 3e49b781bb..6d2e7c6665 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -86,19 +86,19 @@ public ShadowStyles? ShadowStyle { case ShadowStyles.Opaque: case ShadowStyles.Transparent when marginView.ShadowSize.Width == 0 || marginView.ShadowSize.Height == 0: - { - if (marginView.ShadowSize.Width != 1) { - marginView.ShadowSize = marginView.ShadowSize with { Width = 1 }; - } + if (marginView.ShadowSize.Width != 1) + { + marginView.ShadowSize = marginView.ShadowSize with { Width = 1 }; + } - if (marginView.ShadowSize.Height != 1) - { - marginView.ShadowSize = marginView.ShadowSize with { Height = 1 }; - } + if (marginView.ShadowSize.Height != 1) + { + marginView.ShadowSize = marginView.ShadowSize with { Height = 1 }; + } - break; - } + break; + } } // Always call SetShadow to update thickness and shadow views diff --git a/Terminal.Gui/ViewBase/Adornment/TitleView.cs b/Terminal.Gui/ViewBase/Adornment/TitleView.cs index bb3a78fa23..0274ce5bb2 100644 --- a/Terminal.Gui/ViewBase/Adornment/TitleView.cs +++ b/Terminal.Gui/ViewBase/Adornment/TitleView.cs @@ -227,13 +227,13 @@ public void UpdateLayout (in TabLayoutContext context) Thickness padding = hasFocus && tabDepth > 2 ? TabSide switch - { - Side.Top => new Thickness (0, 0, 0, 1), - Side.Bottom => new Thickness (0, 1, 0, 0), - Side.Right => new Thickness (1, 0, 0, 0), - Side.Left => new Thickness (0, 0, 1, 0), - _ => new Thickness (0) - } + { + Side.Top => new Thickness (0, 0, 0, 1), + Side.Bottom => new Thickness (0, 1, 0, 0), + Side.Right => new Thickness (1, 0, 0, 0), + Side.Left => new Thickness (0, 0, 1, 0), + _ => new Thickness (0) + } : new Thickness (0); Padding.Thickness = padding; @@ -312,13 +312,13 @@ internal static Thickness ComputeTitleViewThickness (Side tabSide, int depth, bo int contentSide = depth >= 3 && !hasFocus ? 1 : 0; return tabSide switch - { - Side.Top => new Thickness (1, cap, 1, contentSide), - Side.Bottom => new Thickness (1, contentSide, 1, cap), - Side.Left => new Thickness (cap, 1, contentSide, 1), - Side.Right => new Thickness (contentSide, 1, cap, 1), - _ => Thickness.Empty - }; + { + Side.Top => new Thickness (1, cap, 1, contentSide), + Side.Bottom => new Thickness (1, contentSide, 1, cap), + Side.Left => new Thickness (cap, 1, contentSide, 1), + Side.Right => new Thickness (contentSide, 1, cap, 1), + _ => Thickness.Empty + }; } #endregion diff --git a/Terminal.Gui/ViewBase/IValueParser.cs b/Terminal.Gui/ViewBase/IValueParser.cs index e6c4a53283..198dc7e043 100644 --- a/Terminal.Gui/ViewBase/IValueParser.cs +++ b/Terminal.Gui/ViewBase/IValueParser.cs @@ -83,7 +83,7 @@ public static bool TryParseValue (string input, out TValue? parsed) return false; } - object?[] args = [input, null, null]; + object? [] args = [input, null, null]; var success = (bool)tryParse.Invoke (null, args)!; if (!success) diff --git a/Terminal.Gui/ViewBase/Layout/Aligner.cs b/Terminal.Gui/ViewBase/Layout/Aligner.cs index 082581aba4..a3ce1f1eae 100644 --- a/Terminal.Gui/ViewBase/Layout/Aligner.cs +++ b/Terminal.Gui/ViewBase/Layout/Aligner.cs @@ -209,34 +209,34 @@ internal static int [] IgnoreFirst (ref readonly int [] sizes, int containerSize switch (sizes.Length) { case > 1: - { - var currentPosition = 0; - positions [0] = currentPosition; // first item is flush left - - for (int i = sizes.Length - 1; i >= 0; i--) { - CheckSizeCannotBeNegative (i, in sizes); - - if (i == sizes.Length - 1) - { - // start at right - currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i]; - positions [i] = currentPosition; - } + var currentPosition = 0; + positions [0] = currentPosition; // first item is flush left - if (i >= sizes.Length - 1 || i <= 0) + for (int i = sizes.Length - 1; i >= 0; i--) { - continue; + CheckSizeCannotBeNegative (i, in sizes); + + if (i == sizes.Length - 1) + { + // start at right + currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i]; + positions [i] = currentPosition; + } + + if (i >= sizes.Length - 1 || i <= 0) + { + continue; + } + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition - sizes [i] - spaceBefore; + currentPosition = positions [i]; } - int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; - positions [i] = currentPosition - sizes [i] - spaceBefore; - currentPosition = positions [i]; + break; } - break; - } - case 1: CheckSizeCannotBeNegative (0, in sizes); positions [0] = 0; // single item is flush left diff --git a/Terminal.Gui/ViewBase/Layout/DimAuto.cs b/Terminal.Gui/ViewBase/Layout/DimAuto.cs index 37298a2744..e6d3dd1be6 100644 --- a/Terminal.Gui/ViewBase/Layout/DimAuto.cs +++ b/Terminal.Gui/ViewBase/Layout/DimAuto.cs @@ -339,12 +339,12 @@ internal override int Calculate (int location, int superviewContentSize, View us Thickness thickness = us.GetAdornmentsThickness (); int adornmentThickness = dimension switch - { - Dimension.Width => thickness.Horizontal, - Dimension.Height => thickness.Vertical, - Dimension.None => 0, - _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null) - }; + { + Dimension.Width => thickness.Horizontal, + Dimension.Height => thickness.Vertical, + Dimension.None => 0, + _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null) + }; max += adornmentThickness; diff --git a/Terminal.Gui/ViewBase/Orientation/IOrientation.cs b/Terminal.Gui/ViewBase/Orientation/IOrientation.cs index c56a0cfe51..5ebb1d3c93 100644 --- a/Terminal.Gui/ViewBase/Orientation/IOrientation.cs +++ b/Terminal.Gui/ViewBase/Orientation/IOrientation.cs @@ -1,6 +1,7 @@ namespace Terminal.Gui.ViewBase; + using System; /// diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs index 3c1feb4f97..2c57dc7fbd 100644 --- a/Terminal.Gui/ViewBase/View.Command.cs +++ b/Terminal.Gui/ViewBase/View.Command.cs @@ -1005,7 +1005,7 @@ protected virtual void OnActivated (ICommandContext? ctx) { } { Trace.Command (this, ctx, "Entry"); - if (RaiseHandlingHotKey (ctx) is true) + if (!CanBeVisible (this) || RaiseHandlingHotKey (ctx) is true) { // The hotkey was cancelled by OnHandlingHotKey or HandlingHotKey event. // Return false so the key is not consumed and can be processed as normal input diff --git a/Terminal.Gui/ViewBase/View.Drawing.Adornments.cs b/Terminal.Gui/ViewBase/View.Drawing.Adornments.cs index 689c164f70..ebabfe0c2f 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Adornments.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Adornments.cs @@ -133,9 +133,10 @@ internal void DoDrawAdornments (Region? originalClip) Padding.View?.SetNeedsDraw (); Margin.View?.SetNeedsDraw (); - // Ensure NeedsDraw is true for the rest of the draw pipeline (DoClearViewport, DoDrawText, etc.) - // When adornment Views are null (lightweight), their NeedsDraw doesn't contribute to the parent's - // NeedsDraw property. But if we're here, the parent IS drawing, so we must set NeedsDrawRect. + // Keep NeedsDraw true for DoRenderLineCanvas and ClearNeedsDraw. The self-content + // methods (DoClearViewport, DoDrawText, DoDrawContent) are now gated on the + // needsDrawSelf snapshot captured in Draw() *before* this escalation, so this no + // longer forces a full parent redraw when only a child was dirty (issue #5358). if (NeedsDrawRect == Rectangle.Empty) { NeedsDrawRect = Viewport; diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index e17a052e73..3417168b93 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -91,8 +91,12 @@ public void Draw (DrawContext? context = null) Region? originalClip = GetClip (); - // TODO: This can be further optimized by checking NeedsDraw below and only - // TODO: clearing, drawing text, drawing content, etc. if it is true. + // Capture whether THIS view's own content needs redrawing BEFORE DoDrawAdornments + // escalates NeedsDrawRect (see View.Drawing.Adornments.cs DoDrawAdornments). + // When only SubViewNeedsDraw is true, needsDrawSelf is false and we skip + // ClearViewport/DrawText/DrawContent so child-only invalidations stay narrow. + bool needsDrawSelf = NeedsDraw; + if (NeedsDraw || SubViewNeedsDraw) { // ------------------------------------ @@ -120,10 +124,22 @@ public void Draw (DrawContext? context = null) // SuperView's ClearViewport or peer SubViews' content. // This follows the same pattern as DrawAdornments(), which creates // per-adornment DrawContexts for the same reason. - _localDrawContext = new DrawContext (); + // + // Issue #5358 (review feedback item 2): only recreate _localDrawContext when + // we actually intend to redraw self-content this pass. On child-only passes + // (needsDrawSelf=false), we must preserve the prior context so DoDrawComplete + // doesn't overwrite CachedDrawnRegion with an empty region and break + // TransparentMouse hit-testing until the next full self-redraw. + if (needsDrawSelf) + { + _localDrawContext = new DrawContext (); + } - SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); - DoClearViewport (context); + if (needsDrawSelf) + { + SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); + DoClearViewport (context); + } // ------------------------------------ // Draw the SubViews first (order matters: SubViews, Text, Content) @@ -142,20 +158,23 @@ public void Draw (DrawContext? context = null) _lastClearedViewport = null; } - // ------------------------------------ - // Draw the text — tracked in both shared (clip exclusion) and local (hit-testing) contexts - Trace.Draw (this.ToIdentifyingString (), "Text"); - SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); - DoDrawText (_localDrawContext); - - // ------------------------------------ - // Draw the content — tracked in both shared (clip exclusion) and local (hit-testing) contexts - Trace.Draw (this.ToIdentifyingString (), "Content"); - DoDrawContent (_localDrawContext); - - // Merge this view's own draws into the shared context so the SuperView - // can track the aggregate for clip exclusion. - context.AddDrawnRegion (_localDrawContext.GetDrawnRegion ()); + if (needsDrawSelf) + { + // ------------------------------------ + // Draw the text — tracked in both shared (clip exclusion) and local (hit-testing) contexts + Trace.Draw (this.ToIdentifyingString (), "Text"); + SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); + DoDrawText (_localDrawContext); + + // ------------------------------------ + // Draw the content — tracked in both shared (clip exclusion) and local (hit-testing) contexts + Trace.Draw (this.ToIdentifyingString (), "Content"); + DoDrawContent (_localDrawContext); + + // Merge this view's own draws into the shared context so the SuperView + // can track the aggregate for clip exclusion. + context.AddDrawnRegion (_localDrawContext.GetDrawnRegion ()); + } // ------------------------------------ // Draw adornment SubViews BEFORE rendering LineCanvas so their lines @@ -221,11 +240,85 @@ internal void DoClearViewport (DrawContext? context = null) return; } - ClearViewport (context); + // Issue #5358: narrow the framework's clear to NeedsDrawRect when it's a true + // partial region AND this view is not itself scrolled. Narrowing in the public + // ClearViewport API would silently change the contract for direct callers + // (Code.OnClearingViewport, MarkdownCodeBlock, direct test calls) that expect + // a full fill of the viewport background — see review feedback items 1, 3. + // + // The "this view is not itself scrolled" guard sidesteps a separate coordinate- + // space inconsistency: SetNeedsDraw(Rectangle) cascades to subviews using + // frame-local coordinates (subtracts subview.Frame.X/Y), while the no-arg + // SetNeedsDraw passes Viewport (content-coord). For an unscrolled view those + // coincide; for a scrolled view they don't, and the narrowing math would shift + // the clear off-screen. Until that convention is normalized (out of scope here), + // only narrow when Viewport.Location is the origin. + if (CanNarrowClearToNeedsDrawRect (out Rectangle narrowedScreen)) + { + Driver?.FillRect (narrowedScreen); + _lastClearedViewport = narrowedScreen; + SetNeedsDraw (NeedsDrawRect); + } + else + { + ClearViewport (context); + } + OnClearedViewport (); ClearedViewport?.Invoke (this, new DrawEventArgs (Viewport, Viewport, null)); } + /// + /// Determines whether the framework's can safely narrow + /// the clear to just . See for + /// the rationale. + /// + private bool CanNarrowClearToNeedsDrawRect (out Rectangle narrowedScreen) + { + narrowedScreen = Rectangle.Empty; + + if (Driver is null) + { + return false; + } + + if (NeedsDrawRect.IsEmpty) + { + return false; + } + + Rectangle viewport = Viewport; + + // Only narrow when this view is NOT itself scrolled — see DoClearViewport comment. + if (viewport.Location != Point.Empty) + { + return false; + } + + // Only narrow when NeedsDrawRect is strictly smaller than the viewport. SetNeedsDraw() + // (no-arg) sets NeedsDrawRect to the current Viewport, meaning "everything is dirty"; + // we don't want to narrow in that case. + if (NeedsDrawRect.Width >= viewport.Width && NeedsDrawRect.Height >= viewport.Height) + { + return false; + } + + // ClearContentOnly: skip narrowing; the existing visible-content intersection is + // already a content-area optimization and combining the two correctly is non-trivial. + if (ViewportSettings.FastHasFlags (ViewportSettingsFlags.ClearContentOnly)) + { + return false; + } + + // NeedsDrawRect is in this view's coords; for an unscrolled view those equal viewport- + // local coords (origin (0,0) = top-left of visible area). Convert to screen. + Rectangle dirtyScreen = ViewportToScreen (NeedsDrawRect); + Rectangle toClear = ViewportToScreen (viewport with { Location = Point.Empty }); + narrowedScreen = Rectangle.Intersect (toClear, dirtyScreen); + + return !narrowedScreen.IsEmpty; + } + /// /// Called when the is to be cleared. /// diff --git a/Terminal.Gui/ViewBase/View.Keyboard.cs b/Terminal.Gui/ViewBase/View.Keyboard.cs index 4381f7b8e2..bbe5ba49d8 100644 --- a/Terminal.Gui/ViewBase/View.Keyboard.cs +++ b/Terminal.Gui/ViewBase/View.Keyboard.cs @@ -820,6 +820,11 @@ private void ApplyLayer (Dictionary layer, HashSet< return false; } + if (!Visible) + { + return false; + } + bool? handled = null; // Process this View diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index e234499186..0db07b69a4 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/Terminal.Gui/ViewBase/View.Layout.cs @@ -95,6 +95,7 @@ private bool SetFrame (in Rectangle frame) return false; } + Rectangle? oldFrame = _frame; var oldViewport = Rectangle.Empty; if (IsInitialized) @@ -111,6 +112,16 @@ private bool SetFrame (in Rectangle frame) SetNeedsDraw (); SetNeedsLayout (); + // Issue #5358: when Frame shrinks or moves, the SuperView's old-frame area is now + // uncovered and must be cleared on the next draw. Invalidate the union of the old and + // new frames on the SuperView so its region-aware ClearViewport repaints just that area. + // SetFrame is the single source of truth for this invalidation for both direct Frame + // assignment and layout-driven frame updates. + if (oldFrame is { } prev && SuperView is { }) + { + SuperView.SetNeedsDraw (Rectangle.Union (prev, frame)); + } + // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK? OnFrameChanged (in frame); FrameChanged?.Invoke (this, new EventArgs (in frame)); @@ -578,6 +589,12 @@ public bool Layout (Size contentSize) // recomputed during dependency resolution. Draw after layout only when this view was // directly invalidated for layout or its resolved frame actually changed. SetNeedsDraw (); + + // NOTE (#5358, review feedback item 4): the SuperView invalidation for frame + // changes is handled in SetFrame, which is the single source of truth for Frame + // mutation. Both direct (view.Frame = ...) and layout-driven (SetRelativeLayout + // → SetFrame) paths go through it, so calling SuperView.SetNeedsDraw(union) here + // too would be redundant and would do the cascade work twice on the hot path. } return true; diff --git a/Terminal.Gui/ViewBase/View.NeedsDraw.cs b/Terminal.Gui/ViewBase/View.NeedsDraw.cs index fc2e123b5d..5303d20106 100644 --- a/Terminal.Gui/ViewBase/View.NeedsDraw.cs +++ b/Terminal.Gui/ViewBase/View.NeedsDraw.cs @@ -85,11 +85,12 @@ public void SetNeedsDraw (Rectangle viewPortRelativeRegion) } else { - int x = Math.Min (Viewport.X, viewPortRelativeRegion.X); - int y = Math.Min (Viewport.Y, viewPortRelativeRegion.Y); - int w = Math.Max (Viewport.Width, viewPortRelativeRegion.Width); - int h = Math.Max (Viewport.Height, viewPortRelativeRegion.Height); - NeedsDrawRect = new Rectangle (x, y, w, h); + // Union NeedsDrawRect with the incoming region. The previous formula unioned + // against Viewport (a bug — it widened to nearly viewport-size on every call), + // which made NeedsDrawRect useless for narrowing draw work. Issue #5358 + // requires an accurate dirty rect so the region-aware ClearViewport and + // SetNeedsDraw cascade can stay narrow. + NeedsDrawRect = Rectangle.Union (NeedsDrawRect, viewPortRelativeRegion); } // Do not set on Margin - it will be drawn in a separate pass. diff --git a/Terminal.Gui/ViewBase/ViewExtensions.cs b/Terminal.Gui/ViewBase/ViewExtensions.cs index 65b6196ed3..1a9725ed12 100644 --- a/Terminal.Gui/ViewBase/ViewExtensions.cs +++ b/Terminal.Gui/ViewBase/ViewExtensions.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.ViewBase; public static class ViewExtensions { /// The view to identify. - extension (View? view) + extension(View? view) { /// /// Returns a formatted string that identifies the View for debugging/logging purposes. diff --git a/Terminal.Gui/ViewBase/WeakReferenceExtensions.cs b/Terminal.Gui/ViewBase/WeakReferenceExtensions.cs index 8d587e91b5..da158031c7 100644 --- a/Terminal.Gui/ViewBase/WeakReferenceExtensions.cs +++ b/Terminal.Gui/ViewBase/WeakReferenceExtensions.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.ViewBase; public static class WeakReferenceExtensions { /// The weak reference to format. - extension (WeakReference? weakRef) + extension(WeakReference? weakRef) { /// /// Returns a formatted string representation of the to a View. diff --git a/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs index 92bff2ae3a..1336ad994e 100644 --- a/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/AppendAutocomplete.cs @@ -1,5 +1,5 @@ #nullable disable - + namespace Terminal.Gui.Views; /// @@ -111,7 +111,7 @@ public override void RenderOverlay (Point renderAt) _textField.SetAttribute ( new Attribute ( Scheme.Normal.Foreground, - _textField.GetAttributeForRole(VisualRole.Focus).Background, + _textField.GetAttributeForRole (VisualRole.Focus).Background, Scheme.Normal.Style ) ); diff --git a/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs b/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs index fd4640df38..053b19ea87 100644 --- a/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs +++ b/Terminal.Gui/Views/Autocomplete/AutocompleteBase.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs index ca8a443712..12f40904f8 100644 --- a/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/IAutocomplete.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs index 79f62045e1..04ef7e2f11 100644 --- a/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/ISuggestionGenerator.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// Generates autocomplete based on a given cursor location within a string public interface ISuggestionGenerator diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs index 3fe9161460..b28b105fda 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs @@ -10,7 +10,7 @@ public Popup (PopupAutocomplete autoComplete) _autoComplete = autoComplete; CanFocus = true; TabStop = TabBehavior.NoStop; - MousePositionTracking = true; + MousePositionTracking = true; } private readonly PopupAutocomplete _autoComplete; diff --git a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs index fc9504027f..8fb2d97583 100644 --- a/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs +++ b/Terminal.Gui/Views/Autocomplete/SingleWordSuggestionGenerator.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// /// which suggests from a collection of words those that match the diff --git a/Terminal.Gui/Views/Autocomplete/Suggestion.cs b/Terminal.Gui/Views/Autocomplete/Suggestion.cs index a1facdc38b..fdb6f9375d 100644 --- a/Terminal.Gui/Views/Autocomplete/Suggestion.cs +++ b/Terminal.Gui/Views/Autocomplete/Suggestion.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// A replacement suggestion made by public class Suggestion diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 844e178d5b..292e27276b 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -62,6 +62,10 @@ public class CharMap : View, IDesignable, IValue // ReSharper disable once InconsistentNaming private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End); + private static readonly int MAX_ROW = MAX_CODE_POINT / 16; + private const int SURROGATE_ROW_START = 0xD800 / 16; + private const int SURROGATE_ROW_END = 0xDFFF / 16; + private const int SURROGATE_ROW_COUNT = SURROGATE_ROW_END - SURROGATE_ROW_START + 1; /// /// Initializes a new instance. @@ -148,16 +152,29 @@ public CharMap () Cursor = new Cursor { Style = DefaultCursorStyle }; } - // Visible rows management: each entry is the starting code point of a 16-wide row - private readonly List _visibleRowStarts = []; - private readonly Dictionary _rowStartToVisibleIndex = []; + // Visible rows management when filtering by Unicode category. + private List? _visibleRowStarts; + private Dictionary? _rowStartToVisibleIndex; private void RebuildVisibleRows () { + if (!ShowUnicodeCategory.HasValue) + { + _visibleRowStarts = null; + _rowStartToVisibleIndex = null; + SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, VisibleRowCount * _rowHeight + HEADER_HEIGHT)); + VerticalScrollBar.ScrollableContentSize = GetContentHeight (); + + return; + } + + _visibleRowStarts ??= []; + _rowStartToVisibleIndex ??= []; + _visibleRowStarts.Clear (); _rowStartToVisibleIndex.Clear (); - int maxRow = MAX_CODE_POINT / 16; + int maxRow = MAX_ROW; for (var row = 0; row <= maxRow; row++) { @@ -209,17 +226,107 @@ private void RebuildVisibleRows () } // Update content size to match visible rows - SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, _visibleRowStarts.Count * _rowHeight + HEADER_HEIGHT)); + SetContentSize (new Size (COLUMN_WIDTH * 16 + RowLabelWidth, VisibleRowCount * _rowHeight + HEADER_HEIGHT)); // Keep vertical scrollbar aligned with new content size VerticalScrollBar.ScrollableContentSize = GetContentHeight (); } + private int VisibleRowCount + { + get + { + if (ShowUnicodeCategory.HasValue) + { + return _visibleRowStarts?.Count ?? 0; + } + + int rowCount = MAX_ROW + 1; + + if (MAX_ROW >= SURROGATE_ROW_START) + { + rowCount -= Math.Min (MAX_ROW, SURROGATE_ROW_END) - SURROGATE_ROW_START + 1; + } + + return rowCount; + } + } + private int VisibleRowIndexForCodePoint (int codePoint) { int start = codePoint / 16 * 16; - return _rowStartToVisibleIndex.GetValueOrDefault (start, -1); + if (ShowUnicodeCategory.HasValue) + { + return _rowStartToVisibleIndex?.GetValueOrDefault (start, -1) ?? -1; + } + + int row = start / 16; + + if (row > MAX_ROW || row is >= SURROGATE_ROW_START and <= SURROGATE_ROW_END) + { + return -1; + } + + return row > SURROGATE_ROW_END ? row - SURROGATE_ROW_COUNT : row; + } + + private bool TryGetVisibleRowStart (int visibleRow, out int rowStart) + { + if (visibleRow < 0 || visibleRow >= VisibleRowCount) + { + rowStart = 0; + + return false; + } + + if (ShowUnicodeCategory.HasValue) + { + rowStart = _visibleRowStarts! [visibleRow]; + + return true; + } + + int row = visibleRow >= SURROGATE_ROW_START ? visibleRow + SURROGATE_ROW_COUNT : visibleRow; + rowStart = row * 16; + + return rowStart <= MAX_CODE_POINT; + } + + private bool TryGetNearestVisibleRowStart (int desiredRowStart, out int rowStart) + { + if (ShowUnicodeCategory.HasValue) + { + List? visibleRowStarts = _visibleRowStarts; + int idx = visibleRowStarts?.FindIndex (s => s >= desiredRowStart) ?? -1; + + if (idx < 0 && visibleRowStarts?.Count > 0) + { + idx = visibleRowStarts.Count - 1; + } + + if (idx >= 0) + { + rowStart = visibleRowStarts! [idx]; + + return true; + } + + rowStart = 0; + + return false; + } + + int row = Math.Clamp (desiredRowStart / 16, 0, MAX_ROW); + + if (row is >= SURROGATE_ROW_START and <= SURROGATE_ROW_END) + { + row = SURROGATE_ROW_END + 1; + } + + rowStart = Math.Min (row * 16, MAX_CODE_POINT); + + return true; } private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested @@ -357,19 +464,11 @@ public UnicodeCategory? ShowUnicodeCategory // Ensure selection is on a visible row int desiredRowStart = SelectedCodePoint / 16 * 16; - if (!_rowStartToVisibleIndex.ContainsKey (desiredRowStart)) + if (VisibleRowIndexForCodePoint (desiredRowStart) < 0) { - // Find nearest visible row (prefer next; fallback to last) - int idx = _visibleRowStarts.FindIndex (s => s >= desiredRowStart); - - if (idx < 0 && _visibleRowStarts.Count > 0) + if (TryGetNearestVisibleRowStart (desiredRowStart, out int rowStart)) { - idx = _visibleRowStarts.Count - 1; - } - - if (idx >= 0) - { - SelectedCodePoint = _visibleRowStarts [idx]; + SelectedCodePoint = rowStart; } } @@ -692,7 +791,7 @@ protected override bool OnDrawingContent (DrawContext? context) // Which visible row is this? int visibleRow = (y + Viewport.Y - 1) / _rowHeight; - if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count) + if (!TryGetVisibleRowStart (visibleRow, out int rowStart)) { // No row at this y; clear label area and continue Move (0, y); @@ -701,8 +800,6 @@ protected override bool OnDrawingContent (DrawContext? context) continue; } - int rowStart = _visibleRowStarts [visibleRow]; - // Draw the row label (U+XXXX_) SetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active); Move (0, y); @@ -1042,7 +1139,7 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) int visibleRow = (position.Y - 1 - -Viewport.Y) / _rowHeight; - if (visibleRow < 0 || visibleRow >= _visibleRowStarts.Count) + if (!TryGetVisibleRowStart (visibleRow, out int rowStart)) { codePoint = 0; @@ -1056,7 +1153,7 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) col = 15; } - codePoint = _visibleRowStarts [visibleRow] + col; + codePoint = rowStart + col; if (codePoint > MAX_CODE_POINT) { diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 4f3f595f9d..5fc1bb4cc2 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -108,7 +108,7 @@ public bool AllowCheckStateNone /// /// If is and , the /// - /// will display the Glyphs.CheckStateNone character (☒). + /// will display the Glyphs.CheckStateNone character (⬛). /// /// /// If , the @@ -216,12 +216,12 @@ protected virtual void OnValueChanged (ValueChangedEventArgs args) { public bool? AdvanceCheckState () { CheckState nextValue = Value switch - { - CheckState.None => CheckState.Checked, - CheckState.Checked => CheckState.UnChecked, - CheckState.UnChecked => AllowCheckStateNone ? CheckState.None : CheckState.Checked, - _ => CheckState.UnChecked - }; + { + CheckState.None => CheckState.Checked, + CheckState.Checked => CheckState.UnChecked, + CheckState.UnChecked => AllowCheckStateNone ? CheckState.None : CheckState.Checked, + _ => CheckState.UnChecked + }; return ChangeValue (nextValue); } diff --git a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs index 420c496745..519f2311b0 100644 --- a/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs +++ b/Terminal.Gui/Views/CollectionNavigation/ICollectionNavigatorMatcher.cs @@ -1,5 +1,5 @@ #nullable disable - + namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs index b382bc6273..961e08d4ad 100644 --- a/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs +++ b/Terminal.Gui/Views/CollectionNavigation/IListCollectionNavigator.cs @@ -1,5 +1,5 @@ #nullable disable -using System.Collections; +using System.Collections; namespace Terminal.Gui.Views; diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index a4e18fd794..56732bb01c 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -13,7 +13,7 @@ namespace Terminal.Gui.Views; /// /// /// By default, is centered with sizing and uses the -/// color scheme when running. +/// color scheme (applied at construction time, regardless of running state). /// /// /// To run modally, pass the dialog to . diff --git a/Terminal.Gui/Views/DialogTResult.cs b/Terminal.Gui/Views/DialogTResult.cs index 8b9c306bff..297ead8b6e 100644 --- a/Terminal.Gui/Views/DialogTResult.cs +++ b/Terminal.Gui/Views/DialogTResult.cs @@ -12,7 +12,7 @@ namespace Terminal.Gui.Views; /// /// /// By default, is centered with sizing and uses the -/// color scheme when running. +/// color scheme (applied at construction time, regardless of running state). /// /// /// To run modally, pass the dialog to . @@ -99,6 +99,8 @@ public Dialog () SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog); + Arrangement |= ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped; + CommandsToBubbleUp = [Command.Accept]; _buttonContainer = new View @@ -117,7 +119,6 @@ public Dialog () Padding.GetOrCreateView (); Padding.View?.Add (_buttonContainer); UpdateSizes (); - SetStyle (); } /// @@ -334,40 +335,6 @@ public Button [] Buttons } } - /// - protected override void OnIsRunningChanged (bool newIsModal) - { - base.OnIsRunningChanged (newIsModal); - - if (newIsModal) - { - SetStyle (); - } - } - - private void SetStyle () - { - if (IsRunning) - { - // When running, restore to Dialog scheme only if it was set to Base by SetStyle - // (i.e., the scheme was not explicitly overridden before running, e.g. by - // MessageBox.ErrorQuery which sets SchemeName = "Error" before calling app.Run). - if (SchemeName == SchemeManager.SchemesToSchemeName (Schemes.Base)) - { - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog); - } - - Arrangement |= ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped; - } - else - { - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base); - - // strip out movable and resizable - Arrangement &= ~(ViewArrangement.Movable | ViewArrangement.Resizable); - } - } - // Dialogs are Modal and Focus is indicated by their Border. _drawingText ensures the // Text of the dialog (e.g. for a MessageBox) is always drawn using the Normal Attribute // instead of the Focus attribute. diff --git a/Terminal.Gui/Views/DropDownList.cs b/Terminal.Gui/Views/DropDownList.cs index 2dea042c7b..71b8738342 100644 --- a/Terminal.Gui/Views/DropDownList.cs +++ b/Terminal.Gui/Views/DropDownList.cs @@ -333,18 +333,18 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri case VisualRole.ReadOnly when ReadOnly: case VisualRole.Active when ReadOnly: - { - currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); + { + currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); - return true; - } + return true; + } case VisualRole.Editable when ReadOnly: - { - currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); + { + currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); - break; - } + break; + } } return false; diff --git a/Terminal.Gui/Views/FileDialogs/FileSystemCollectionNavigationMatcher.cs b/Terminal.Gui/Views/FileDialogs/FileSystemCollectionNavigationMatcher.cs index 2e086aacd0..2692ed409f 100644 --- a/Terminal.Gui/Views/FileDialogs/FileSystemCollectionNavigationMatcher.cs +++ b/Terminal.Gui/Views/FileDialogs/FileSystemCollectionNavigationMatcher.cs @@ -10,7 +10,7 @@ internal class FileSystemCollectionNavigationMatcher : DefaultCollectionNavigato public override bool IsMatch (string search, object? value) { - if(value is IFileSystemInfo fsi) + if (value is IFileSystemInfo fsi) { return fsi.Name.StartsWith (search, Comparer); } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index b439253a45..511b23f037 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -558,18 +558,18 @@ protected override bool OnDrawingContent (DrawContext? context) // break; case > 127: - { - byte [] utf8 = GetData (data, offset, 4, out bool _); + { + byte [] utf8 = GetData (data, offset, 4, out bool _); - OperationStatus status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); + OperationStatus status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); - while (status == OperationStatus.NeedMoreData) - { - status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); - } + while (status == OperationStatus.NeedMoreData) + { + status = Rune.DecodeFromUtf8 (utf8, out c, out utf8BytesConsumed); + } - break; - } + break; + } default: Rune.DecodeFromUtf8 (new ReadOnlySpan (ref b), out c, out _); diff --git a/Terminal.Gui/Views/HexViewEventArgs.cs b/Terminal.Gui/Views/HexViewEventArgs.cs index 2ee496b173..bfdf26c875 100644 --- a/Terminal.Gui/Views/HexViewEventArgs.cs +++ b/Terminal.Gui/Views/HexViewEventArgs.cs @@ -1,5 +1,5 @@ #nullable disable -// +// // HexView.cs: A hexadecimal viewer // // TODO: diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs b/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs index dc9cad34e9..a6f0eb232b 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeOption.cs @@ -1,8 +1,8 @@ namespace Terminal.Gui.Views; - /// Represents an option in a . - /// Data type of the option. - public class LinearRangeOption +/// Represents an option in a . +/// Data type of the option. +public class LinearRangeOption { /// Creates a new empty instance of the class. public LinearRangeOption () { } diff --git a/Terminal.Gui/Views/LinearRange/LinearRangeT.cs b/Terminal.Gui/Views/LinearRange/LinearRangeT.cs index 38effc20b9..b404cb81af 100644 --- a/Terminal.Gui/Views/LinearRange/LinearRangeT.cs +++ b/Terminal.Gui/Views/LinearRange/LinearRangeT.cs @@ -194,40 +194,40 @@ private List IndicesForValue (LinearRangeSpan span) case LinearRangeSpanKind.None: return []; case LinearRangeSpanKind.LeftBounded: - { - int end = span.EndIndex >= 0 ? span.EndIndex : IndexOfData (span.End); + { + int end = span.EndIndex >= 0 ? span.EndIndex : IndexOfData (span.End); - return end >= 0 ? [end] : []; - } + return end >= 0 ? [end] : []; + } case LinearRangeSpanKind.RightBounded: - { - int start = span.StartIndex >= 0 ? span.StartIndex : IndexOfData (span.Start); + { + int start = span.StartIndex >= 0 ? span.StartIndex : IndexOfData (span.Start); - return start >= 0 ? [start] : []; - } + return start >= 0 ? [start] : []; + } case LinearRangeSpanKind.Closed: default: - { - int start = span.StartIndex >= 0 ? span.StartIndex : IndexOfData (span.Start); - int end = span.EndIndex >= 0 ? span.EndIndex : IndexOfData (span.End); - - if (start < 0 && end < 0) { - return []; - } + int start = span.StartIndex >= 0 ? span.StartIndex : IndexOfData (span.Start); + int end = span.EndIndex >= 0 ? span.EndIndex : IndexOfData (span.End); - if (start < 0) - { - return [end]; - } + if (start < 0 && end < 0) + { + return []; + } - if (end < 0 || start == end) - { - return [start]; - } + if (start < 0) + { + return [end]; + } - return [start, end]; - } + if (end < 0 || start == end) + { + return [start]; + } + + return [start, end]; + } } } diff --git a/Terminal.Gui/Views/ListView/ListView.Drawing.cs b/Terminal.Gui/Views/ListView/ListView.Drawing.cs index c3cb2066da..b887566336 100644 --- a/Terminal.Gui/Views/ListView/ListView.Drawing.cs +++ b/Terminal.Gui/Views/ListView/ListView.Drawing.cs @@ -12,7 +12,8 @@ protected override bool OnDrawingContent (DrawContext? context) var current = Attribute.Default; int item = Viewport.Y; - int col = ShowMarks ? 2 : 0; + // Reserve mark glyph + separator column. If a configured mark glyph is wide, it can consume the separator. + int reservedMarkColumns = ShowMarks ? 2 : 0; Move (0, 0); for (var row = 0; row < Viewport.Height; row++, item++) @@ -35,30 +36,30 @@ protected override bool OnDrawingContent (DrawContext? context) break; case false when MarkMultiple: - { - switch (isSelected) { - // Combination 2: Hidden marks with visual role indicators - // Mark glyphs: None (MarkWidth = 0) - marks exist internally - // Visual roles use Highlight for marked items; compose TextStyle when marked+selected+focused - case true when isMarked: - role = HasFocus ? VisualRole.Focus : VisualRole.Highlight; - applyHighlightStyle = HasFocus; // Apply Highlight's TextStyle to Focus + switch (isSelected) + { + // Combination 2: Hidden marks with visual role indicators + // Mark glyphs: None (MarkWidth = 0) - marks exist internally + // Visual roles use Highlight for marked items; compose TextStyle when marked+selected+focused + case true when isMarked: + role = HasFocus ? VisualRole.Focus : VisualRole.Highlight; + applyHighlightStyle = HasFocus; // Apply Highlight's TextStyle to Focus - break; + break; - case true: role = HasFocus ? VisualRole.Focus : VisualRole.Normal; break; + case true: role = HasFocus ? VisualRole.Focus : VisualRole.Normal; break; - default: - { - role = isMarked ? VisualRole.Highlight : VisualRole.Normal; + default: + { + role = isMarked ? VisualRole.Highlight : VisualRole.Normal; - break; + break; + } } - } - break; - } + break; + } case true when !MarkMultiple: // Combination 3: Radio button style @@ -70,7 +71,7 @@ protected override bool OnDrawingContent (DrawContext? context) default: // Combination 4: Checkbox style - // Mark glyphs: Checkbox style (☒ marked, ☐ unmarked) + // Mark glyphs: Checkbox style (☑ marked, ☐ unmarked) // Visual roles: Standard selection (mark glyphs provide visual indication) role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; @@ -145,7 +146,7 @@ protected override bool OnDrawingContent (DrawContext? context) } } - int contentCol = col > 0 ? col : markWidth; + int contentCol = reservedMarkColumns > 0 ? reservedMarkColumns : markWidth; Source.Render (this, isSelected, item, contentCol, row, Viewport.Width - contentCol, Viewport.X); } } diff --git a/Terminal.Gui/Views/ListView/ListView.Selection.cs b/Terminal.Gui/Views/ListView/ListView.Selection.cs index b152c68829..eb8b8143e4 100644 --- a/Terminal.Gui/Views/ListView/ListView.Selection.cs +++ b/Terminal.Gui/Views/ListView/ListView.Selection.cs @@ -49,7 +49,7 @@ public bool MarkMultiple /// Set to to show mark glyphs; to hide them. /// /// - /// When , marks are rendered with glyphs: checkboxes (☒/☐) when + /// When , marks are rendered with glyphs: checkboxes (☑/☐) when /// is , or radio buttons (◉/○) when . /// /// diff --git a/Terminal.Gui/Views/ListView/ListView.cs b/Terminal.Gui/Views/ListView/ListView.cs index 1d177ed8e4..bef4d46f9d 100644 --- a/Terminal.Gui/Views/ListView/ListView.cs +++ b/Terminal.Gui/Views/ListView/ListView.cs @@ -252,7 +252,10 @@ protected virtual void OnSourceChanged () { } /// protected override void OnViewportChanged (DrawEventArgs e) => SetContentSize (new Size (EffectiveMaxItemLength, Source?.Count ?? Viewport.Height)); - /// INTERNAL: Gets the width reserved for mark rendering (checkbox and space). + /// + /// INTERNAL: Gets the columns reserved for mark rendering (mark glyph + separator column). Wide glyphs may + /// consume the separator column. + /// private int MarkWidth => ShowMarks ? 2 : 0; /// INTERNAL: Gets the effective content width including mark columns when is true. diff --git a/Terminal.Gui/Views/Markdown/MarkdownTable.cs b/Terminal.Gui/Views/Markdown/MarkdownTable.cs index 5158fccab3..c250ddf38c 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownTable.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownTable.cs @@ -104,11 +104,11 @@ public override string Text for (var i = 0; i < _data.ColumnCount; i++) { seps [i] = _data.ColumnAlignments [i] switch - { - Alignment.Center => ":---:", - Alignment.End => "---:", - _ => "---" - }; + { + Alignment.Center => ":---:", + Alignment.End => "---:", + _ => "---" + }; } lines.Add ($"| {string.Join (" | ", seps)} |"); @@ -696,7 +696,7 @@ internal static List> WrapSegments (List segm { if (maxWidth <= 0) { - return [[]]; + return [ []]; } List> lines = []; @@ -1076,11 +1076,11 @@ private static int CalculateLeftPadding (int cellWidth, int textWidth, Alignment int usableTextWidth = Math.Min (textWidth, innerWidth); return alignment switch - { - Alignment.Center => 1 + Math.Max ((innerWidth - usableTextWidth) / 2, 0), - Alignment.End => 1 + Math.Max (innerWidth - usableTextWidth, 0), - _ => 1 - }; + { + Alignment.Center => 1 + Math.Max ((innerWidth - usableTextWidth) / 2, 0), + Alignment.End => 1 + Math.Max (innerWidth - usableTextWidth, 0), + _ => 1 + }; } private static string TruncateToWidth (string text, int maxWidth) diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 1b7dc72a3c..d89170d6bf 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -153,14 +153,14 @@ protected override void OnSubViewAdded (View view) switch (view) { case MenuItem menuItem: - { - menuItem.CanFocus = true; + { + menuItem.CanFocus = true; - // Accept propagation is handled by CommandsToBubbleUp=[Accept] (line 36). - // An explicit Accepting subscription here caused double-fire of Accepted. + // Accept propagation is handled by CommandsToBubbleUp=[Accept] (line 36). + // An explicit Accepting subscription here caused double-fire of Accepted. - break; - } + break; + } case Line line: // Grow line so we get auto-join line diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index c4b9b7e4e0..8297d79130 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -591,7 +591,8 @@ public override bool EnableForDesign (ref TContext targetView) new MenuItem (targetView as View, Command.SelectAll), new Line (), new MenuItem (targetView as View, Command.Quit) - ]) { Id = "enableForDesignRoot" }; + ]) + { Id = "enableForDesignRoot" }; // NOTE: This is a workaround for the fact that the PopoverMenu is not visible in the designer // NOTE: without being activated via App?.Popover. But we want it to be visible. diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 579df84865..50b0aef8a6 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -10,16 +10,16 @@ namespace Terminal.Gui.Views; /// All methods return where the value is the 0-based index of the button pressed, /// or if the user pressed (typically Esc). /// - /// - /// uses the default Dialog color scheme. - /// uses the Error color scheme. - /// - /// - /// The last button provided is always the default button. - /// - /// - /// Important: All MessageBox methods require an instance to be passed. - /// This enables proper modal dialog management and respects the application's lifecycle. Pass your +/// +/// uses the default Dialog color scheme. +/// uses the Error color scheme. +/// +/// +/// The last button provided is always the default button. +/// +/// +/// Important: All MessageBox methods require an instance to be passed. +/// This enables proper modal dialog management and respects the application's lifecycle. Pass your /// application instance (from ) or use the legacy /// if using the static Application pattern. /// diff --git a/Terminal.Gui/Views/Prompt.cs b/Terminal.Gui/Views/Prompt.cs index a53aab28f5..61dead20c6 100644 --- a/Terminal.Gui/Views/Prompt.cs +++ b/Terminal.Gui/Views/Prompt.cs @@ -50,7 +50,7 @@ namespace Terminal.Gui.Views; /// } /// /// -public class Prompt : Dialog where TView : View, new () +public class Prompt : Dialog where TView : View, new() { private readonly TView? _wrappedView; diff --git a/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs b/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs index 0a615c92f2..7b2cea0351 100644 --- a/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs +++ b/Terminal.Gui/Views/ReadOnlyCollectionExtensions.cs @@ -1,5 +1,5 @@ #nullable disable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// /// Extends with methods to find the index of an element. diff --git a/Terminal.Gui/Views/Runnable/RunnableWrapper.cs b/Terminal.Gui/Views/Runnable/RunnableWrapper.cs index 6f32490529..009dd3d1ab 100644 --- a/Terminal.Gui/Views/Runnable/RunnableWrapper.cs +++ b/Terminal.Gui/Views/Runnable/RunnableWrapper.cs @@ -34,7 +34,7 @@ namespace Terminal.Gui.Views; /// if (wrapper.Result is { } date) Console.WriteLine (date); /// /// -public class RunnableWrapper : Runnable where TView : View, new () +public class RunnableWrapper : Runnable where TView : View, new() { private readonly TView _wrappedView; @@ -48,7 +48,7 @@ public RunnableWrapper () _wrappedView = new TView (); Width = Dim.Fill (); Height = Dim.Auto (); - Add (_wrappedView); + Add (_wrappedView); CommandsToBubbleUp = [Command.Accept]; } @@ -59,7 +59,7 @@ public RunnableWrapper () public RunnableWrapper (TView wrappedView) { KeyBindings.Clear (); - MouseBindings.Clear(); + MouseBindings.Clear (); _wrappedView = wrappedView; Width = Dim.Fill (); Height = Dim.Auto (); diff --git a/Terminal.Gui/Views/ScrollBar/ScrollButton.cs b/Terminal.Gui/Views/ScrollBar/ScrollButton.cs index 737fbd3358..82e0bc9518 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollButton.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollButton.cs @@ -102,20 +102,20 @@ private void SetGlyph () if (Orientation == Orientation.Horizontal) { Title = Direction switch - { - NavigationDirection.Backward => Glyphs.LeftArrow.ToString (), - NavigationDirection.Forward => Glyphs.RightArrow.ToString (), - _ => Title - }; + { + NavigationDirection.Backward => Glyphs.LeftArrow.ToString (), + NavigationDirection.Forward => Glyphs.RightArrow.ToString (), + _ => Title + }; } else { Title = Direction switch - { - NavigationDirection.Backward => Glyphs.UpArrow.ToString (), - NavigationDirection.Forward => Glyphs.DownArrow.ToString (), - _ => Title - }; + { + NavigationDirection.Backward => Glyphs.UpArrow.ToString (), + NavigationDirection.Forward => Glyphs.DownArrow.ToString (), + _ => Title + }; } } diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index 3bdefd2dca..23ec2180a9 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -123,17 +123,17 @@ private bool MovePrevious (Command command) break; default: - { - if (Styles.HasFlag (SelectorStyles.ShowValue)) { - _valueField?.SetFocus (); + if (Styles.HasFlag (SelectorStyles.ShowValue)) + { + _valueField?.SetFocus (); - return true; - } - active = SubViews.OfType ().Count () - 1; + return true; + } + active = SubViews.OfType ().Count () - 1; - break; - } + break; + } } SubViews.OfType ().ToArray ().ElementAt (active).SetFocus (); diff --git a/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs b/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs index 7f845cef2b..0e3abc3948 100644 --- a/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs +++ b/Terminal.Gui/Views/TableView/CellColorGetterArgs.cs @@ -1,5 +1,5 @@ #nullable enable - + namespace Terminal.Gui.Views; /// diff --git a/Terminal.Gui/Views/TableView/ITableSource.cs b/Terminal.Gui/Views/TableView/ITableSource.cs index f9fcd2ec6b..c97150de46 100644 --- a/Terminal.Gui/Views/TableView/ITableSource.cs +++ b/Terminal.Gui/Views/TableView/ITableSource.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// Tabular matrix of data to be displayed in a . public interface ITableSource diff --git a/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs b/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs index e287f5e84e..e4e3550384 100644 --- a/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs +++ b/Terminal.Gui/Views/TableView/RowColorGetterArgs.cs @@ -1,5 +1,5 @@ #nullable enable -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// /// Arguments for . Describes a row of data in a diff --git a/Terminal.Gui/Views/Tabs.cs b/Terminal.Gui/Views/Tabs.cs index 278cb29a6a..38e6e74bfc 100644 --- a/Terminal.Gui/Views/Tabs.cs +++ b/Terminal.Gui/Views/Tabs.cs @@ -572,13 +572,13 @@ private void UpdateTabBorderThickness () } tab.Border.Thickness = _tabSide switch - { - Side.Top => new Thickness (1, TabDepth, 1, 1), - Side.Bottom => new Thickness (1, 1, 1, TabDepth), - Side.Left => new Thickness (TabDepth, 1, 1, 1), - Side.Right => new Thickness (1, 1, TabDepth, 1), - _ => new Thickness (1, TabDepth, 1, 1) - }; + { + Side.Top => new Thickness (1, TabDepth, 1, 1), + Side.Bottom => new Thickness (1, 1, 1, TabDepth), + Side.Left => new Thickness (TabDepth, 1, 1, 1), + Side.Right => new Thickness (1, 1, TabDepth, 1), + _ => new Thickness (1, TabDepth, 1, 1) + }; } } @@ -594,26 +594,26 @@ private void UpdateTabBorderThickness () } return TabSide switch - { - Side.Top or Side.Bottom when ctx.Command == Command.Right => SelectNextTab (), - Side.Top or Side.Bottom when ctx.Command == Command.Left => SelectPreviousTab (), + { + Side.Top or Side.Bottom when ctx.Command == Command.Right => SelectNextTab (), + Side.Top or Side.Bottom when ctx.Command == Command.Left => SelectPreviousTab (), - Side.Top when ctx.Command == Command.Down => FocusContent (), - Side.Top when ctx.Command == Command.Up => SelectPreviousTab (), + Side.Top when ctx.Command == Command.Down => FocusContent (), + Side.Top when ctx.Command == Command.Up => SelectPreviousTab (), - Side.Bottom when ctx.Command == Command.Up => FocusContent (), - Side.Bottom when ctx.Command == Command.Down => SelectNextTab (), + Side.Bottom when ctx.Command == Command.Up => FocusContent (), + Side.Bottom when ctx.Command == Command.Down => SelectNextTab (), - Side.Left or Side.Right when ctx.Command == Command.Down => SelectNextTab (), - Side.Left or Side.Right when ctx.Command == Command.Up => SelectPreviousTab (), + Side.Left or Side.Right when ctx.Command == Command.Down => SelectNextTab (), + Side.Left or Side.Right when ctx.Command == Command.Up => SelectPreviousTab (), - Side.Left when ctx.Command == Command.Right => FocusContent (), - Side.Left when ctx.Command == Command.Left => SelectPreviousTab (), + Side.Left when ctx.Command == Command.Right => FocusContent (), + Side.Left when ctx.Command == Command.Left => SelectPreviousTab (), - Side.Right when ctx.Command == Command.Left => FocusContent (), - Side.Right when ctx.Command == Command.Right => SelectNextTab (), - _ => false - }; + Side.Right when ctx.Command == Command.Left => FocusContent (), + Side.Right when ctx.Command == Command.Right => SelectNextTab (), + _ => false + }; } private bool? SelectNextTab () diff --git a/Terminal.Gui/Views/TextInput/TextModel.cs b/Terminal.Gui/Views/TextInput/TextModel.cs index 6a9849354f..0c0a5b02c3 100644 --- a/Terminal.Gui/Views/TextInput/TextModel.cs +++ b/Terminal.Gui/Views/TextInput/TextModel.cs @@ -1225,11 +1225,11 @@ internal static bool IsSameRuneType (Rune newRune, RuneType runeType, bool useSa } return runeType switch - { - RuneType.IsSymbol or RuneType.IsPunctuation => rt is RuneType.IsSymbol or RuneType.IsPunctuation, - RuneType.IsWhiteSpace or RuneType.IsLetterOrDigit or RuneType.IsUnknown => rt == runeType, - _ => throw new ArgumentOutOfRangeException (nameof (runeType), runeType, null) - }; + { + RuneType.IsSymbol or RuneType.IsPunctuation => rt is RuneType.IsSymbol or RuneType.IsPunctuation, + RuneType.IsWhiteSpace or RuneType.IsLetterOrDigit or RuneType.IsUnknown => rt == runeType, + _ => throw new ArgumentOutOfRangeException (nameof (runeType), runeType, null) + }; } internal static bool MatchWholeWord (string source, string matchText, int index = 0) diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs index 097c65c933..4ea9f9f723 100644 --- a/Terminal.Gui/Views/TextInput/TextValidateField.cs +++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs @@ -543,12 +543,12 @@ private bool EndKeyHandler () int total = width - count; return TextAlignment switch - { - Alignment.Start => (0, total), - Alignment.Center => (total / 2, total / 2 + total % 2), - Alignment.End => (total, 0), - _ => (0, total) - }; + { + Alignment.Start => (0, total), + Alignment.Center => (total / 2, total / 2 + total % 2), + Alignment.End => (total, 0), + _ => (0, total) + }; } /// Moves the cursor to first char. diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Commands.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Commands.cs index 2e5fbb9d98..7dbaefdfbb 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Commands.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Commands.cs @@ -218,7 +218,7 @@ public bool Cut () { ClearRegion (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); } UpdateWrapModel (); @@ -345,13 +345,13 @@ public bool DeleteCharLeft () if (IsSelecting) { SetWrapModel (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint); ClearSelectedRegion (); List currentLine = GetCurrentLine (); - _historyText.Add ([[.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); @@ -378,13 +378,13 @@ public bool DeleteCharRight () if (IsSelecting) { SetWrapModel (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint); ClearSelectedRegion (); List currentLine = GetCurrentLine (); - _historyText.Add ([[.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); @@ -409,7 +409,7 @@ private bool DeleteTextLeft () // Delete backwards List currentLine = GetCurrentLine (); - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); currentLine.RemoveAt (CurrentColumn - 1); @@ -420,7 +420,7 @@ private bool DeleteTextLeft () CurrentColumn--; - _historyText.Add ([[.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); } else { @@ -437,9 +437,9 @@ private bool DeleteTextLeft () int prowIdx = CurrentRow - 1; List prevRow = _model.GetLine (prowIdx); - _historyText.Add ([[.. prevRow]], InsertionPoint); + _historyText.Add ([ [.. prevRow]], InsertionPoint); - List> removedLines = [[.. prevRow], [.. GetCurrentLine ()]]; + List> removedLines = [ [.. prevRow], [.. GetCurrentLine ()]]; _historyText.Add (removedLines, new Point (CurrentColumn, prowIdx), TextEditingLineStatus.Removed); @@ -491,9 +491,9 @@ private bool DeleteTextRight () if (CurrentColumn == currentLine.Count) { // We're at the end of the line; need to merge with the next line - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); - List> removedLines = [[.. currentLine]]; + List> removedLines = [ [.. currentLine]]; List nextLine = _model.GetLine (CurrentRow + 1); removedLines.Add ([.. nextLine]); _historyText.Add (removedLines, InsertionPoint, TextEditingLineStatus.Removed); @@ -506,7 +506,7 @@ private bool DeleteTextRight () // _model.RemoveLine already invalidates the max width cache for the removed line, but we also need to check if the merged line's width changed UpdateContentSize (); - _historyText.Add ([[.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); if (_wordWrap) { @@ -519,7 +519,7 @@ private bool DeleteTextRight () } // We're not at the end of the line; delete to end of line - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); currentLine.RemoveAt (CurrentColumn); @@ -532,7 +532,7 @@ private bool DeleteTextRight () UpdateContentSize (); } - _historyText.Add ([[.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. currentLine]], InsertionPoint, TextEditingLineStatus.Replaced); if (_wordWrap) { @@ -570,13 +570,13 @@ private bool CutToEndOfLine () return true; } - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); if (currentLine.Count == 0) { if (CurrentRow < _model.Count - 1) { - List> removedLines = [[.. currentLine]]; + List> removedLines = [ [.. currentLine]]; _model.RemoveLine (CurrentRow); SetNeedsDraw (); @@ -626,7 +626,7 @@ private bool CutToEndOfLine () SetNeedsDraw (); } - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -664,7 +664,7 @@ private bool CutToStartOfLine () return true; } - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); if (currentLine.Count == 0) { @@ -695,7 +695,7 @@ private bool CutToStartOfLine () CurrentRow--; currentLine = _model.GetLine (CurrentRow); - List> removedLine = [[.. currentLine], []]; + List> removedLine = [ [.. currentLine], []]; _historyText.Add ([.. removedLine], InsertionPoint, TextEditingLineStatus.Removed); @@ -722,7 +722,7 @@ private bool CutToStartOfLine () CurrentColumn = 0; } - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -745,14 +745,14 @@ private bool KillWordLeft () List currentLine = GetCurrentLine (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint); if (CurrentColumn == 0) { DeleteTextLeft (); OnContentsChanged (); - _historyText.ReplaceLast ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.ReplaceLast ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -806,7 +806,7 @@ private bool KillWordLeft () CurrentRow = newPos.Value.row; } - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -826,13 +826,13 @@ private bool KillWordRight () List currentLine = GetCurrentLine (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint); if (currentLine.Count == 0 || CurrentColumn == currentLine.Count) { DeleteTextRight (); - _historyText.ReplaceLast ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.ReplaceLast ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -860,7 +860,7 @@ private bool KillWordRight () _wrapNeeded = true; } - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); @@ -917,7 +917,7 @@ private bool ProcessEnterKey (ICommandContext? commandContext) SetWrapModel (); List currentLine = GetCurrentLine (); - _historyText.Add ([[.. currentLine]], InsertionPoint); + _historyText.Add ([ [.. currentLine]], InsertionPoint); if (IsSelecting) { @@ -927,7 +927,7 @@ private bool ProcessEnterKey (ICommandContext? commandContext) int restCount = currentLine.Count - CurrentColumn; List rest = currentLine.GetRange (CurrentColumn, restCount); currentLine.RemoveRange (CurrentColumn, restCount); - List> addedLines = [[.. currentLine]]; + List> addedLines = [ [.. currentLine]]; _model.AddLine (CurrentRow + 1, rest); addedLines.Add ([.. _model.GetLine (CurrentRow + 1)]); _historyText.Add (addedLines, InsertionPoint, TextEditingLineStatus.Added); @@ -940,7 +940,7 @@ private bool ProcessEnterKey (ICommandContext? commandContext) CurrentColumn = 0; - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); if (!_wordWrap && CurrentColumn < Viewport.X) { diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs index 299968cc09..a9fbc687b5 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Drawing.cs @@ -345,70 +345,70 @@ private void ProcessInheritsPreviousScheme (int row, int col) switch (cell.Attribute) { case { } when colWithColor == 0 && lineTo.Attribute is { }: - { - for (int r = row - 1; r > -1; r--) { - List l = GetLine (r); - - for (int c = l.Count - 1; c > -1; c--) + for (int r = row - 1; r > -1; r--) { - Cell cell1 = l [c]; + List l = GetLine (r); - if (cell1.Attribute is null) - { - cell1.Attribute = cell.Attribute; - l [c] = cell1; - } - else + for (int c = l.Count - 1; c > -1; c--) { - return; + Cell cell1 = l [c]; + + if (cell1.Attribute is null) + { + cell1.Attribute = cell.Attribute; + l [c] = cell1; + } + else + { + return; + } } } - } - return; - } + return; + } case null: - { - for (int r = row; r > -1; r--) { - List l = GetLine (r); + for (int r = row; r > -1; r--) + { + List l = GetLine (r); - colWithColor = l.FindLastIndex (colWithColor > -1 ? colWithColor : l.Count - 1, c => c.Attribute != null); + colWithColor = l.FindLastIndex (colWithColor > -1 ? colWithColor : l.Count - 1, c => c.Attribute != null); - if (colWithColor <= -1 || l [colWithColor].Attribute is null) - { - continue; + if (colWithColor <= -1 || l [colWithColor].Attribute is null) + { + continue; + } + cell = l [colWithColor]; + + break; } - cell = l [colWithColor]; break; } - break; - } - default: - { - int cRow = row; - - while (cell.Attribute is null) { - if ((colWithColor == 0 || cell.Attribute is null) && cRow > 0) - { - line = GetLine (--cRow); - colWithColor = line.Count - 1; - cell = line [colWithColor]; - } - else if (cRow == 0 && colWithColor < line.Count) + int cRow = row; + + while (cell.Attribute is null) { - cell = line [colWithColor + 1]; + if ((colWithColor == 0 || cell.Attribute is null) && cRow > 0) + { + line = GetLine (--cRow); + colWithColor = line.Count - 1; + cell = line [colWithColor]; + } + else if (cRow == 0 && colWithColor < line.Count) + { + cell = line [colWithColor + 1]; + } } - } - break; - } + break; + } } if (cell.Attribute is null || colWithColor <= -1 || colWithoutColor >= lineToSet.Count || lineTo.Attribute is { }) diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Movement.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Movement.cs index dbffdc6f9c..440e7f41fe 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Movement.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Movement.cs @@ -69,7 +69,8 @@ public void ScrollTo (Point position) Viewport = Viewport with { - X = newPositionX, Y = Math.Max (position.Y > _model.Count - 1 - Viewport.Height ? _model.Count - Viewport.Height : position.Y, 0) + X = newPositionX, + Y = Math.Max (position.Y > _model.Count - 1 - Viewport.Height ? _model.Count - Viewport.Height : position.Y, 0) }; PositionCursor (); SetNeedsDraw (); diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs index 48b2da8839..d27e6b4b0a 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Text.cs @@ -309,13 +309,13 @@ protected override bool OnPaste (string text) { List runeList = Cell.ToCellList (text); List currentLine = GetCurrentLine (); - _historyText.Add ([[.. currentLine]], InsertionPoint); - List> addedLine = [[.. currentLine], runeList]; + _historyText.Add ([ [.. currentLine]], InsertionPoint); + List> addedLine = [ [.. currentLine], runeList]; _historyText.Add ([.. addedLine], InsertionPoint, TextEditingLineStatus.Added); _model.AddLine (CurrentRow, runeList); SetNeedsDraw (); CurrentRow++; - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); OnContentsChanged (); } @@ -331,7 +331,7 @@ protected override bool OnPaste (string text) if (IsSelecting) { - _historyText.ReplaceLast ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Original); + _historyText.ReplaceLast ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Original); } } @@ -379,7 +379,7 @@ private void ClearRegion () var endCol = (int)(end & 0xffffffff); List line = _model.GetLine (startRow); - _historyText.Add ([[.. line]], new Point (startCol, startRow)); + _historyText.Add ([ [.. line]], new Point (startCol, startRow)); List> removedLines = []; @@ -549,7 +549,7 @@ private void InsertAllText (string text, bool fromClipboard = false) List line = GetCurrentLine (); - _historyText.Add ([[.. line]], InsertionPoint); + _historyText.Add ([ [.. line]], InsertionPoint); // Optimize single line if (lines.Count == 1) @@ -557,7 +557,7 @@ private void InsertAllText (string text, bool fromClipboard = false) line.InsertRange (CurrentColumn, lines [0]); CurrentColumn += lines [0].Count; - _historyText.Add ([[.. line]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. line]], InsertionPoint, TextEditingLineStatus.Replaced); if (!_wordWrap && CurrentColumn - Viewport.X > Viewport.Width) { @@ -584,7 +584,7 @@ private void InsertAllText (string text, bool fromClipboard = false) // First line is inserted at the current location, the rest is appended line.InsertRange (CurrentColumn, lines [0]); - List> addedLines = [[.. line]]; + List> addedLines = [ [.. line]]; for (var i = 1; i < lines.Count; i++) { @@ -609,7 +609,7 @@ private void InsertAllText (string text, bool fromClipboard = false) CurrentColumn = rest is { } ? lastPosition : lines [^1].Count; AdjustViewport (); - _historyText.Add ([[.. line]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. line]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); @@ -625,7 +625,7 @@ private void InsertText (Key a, Attribute? attribute = null) SetWrapModel (); - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint); if (IsSelecting) { @@ -667,7 +667,7 @@ private void InsertText (Key a, Attribute? attribute = null) } } - _historyText.Add ([[.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); + _historyText.Add ([ [.. GetCurrentLine ()]], InsertionPoint, TextEditingLineStatus.Replaced); UpdateWrapModel (); OnContentsChanged (); diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 95fc64d400..9d167e4a16 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -95,6 +95,11 @@ public virtual void Draw (int y, int availableWidth) string expansion = GetExpandableSymbol (); string lineBody = _tree.AspectGetter (Model); + if (_tree.CheckboxMode) + { + lineBody = $"{_tree.GetCheckGlyph (Model)} {lineBody}"; + } + _tree.Move (0, y); // if we have scrolled to the right then bits of the prefix will have disappeared off the screen @@ -160,8 +165,10 @@ public virtual void Draw (int y, int availableWidth) if (toSkip > 0) { // For the event record a negative location for where model text starts since it - // is pushed off to the left because of scrolling - indexOfModelText = -toSkip; + // is pushed off to the left because of scrolling. + // Account for checkbox cells (glyph + space) prepended to lineBody. + int checkboxOffset = _tree.CheckboxMode ? 2 : 0; + indexOfModelText = -(toSkip - checkboxOffset); if (toSkip > lineBody.Length) { @@ -174,7 +181,7 @@ public virtual void Draw (int y, int availableWidth) } else { - indexOfModelText = cells.Count; + indexOfModelText = cells.Count + (_tree.CheckboxMode ? 2 : 0); } // If body of line is too long @@ -296,7 +303,10 @@ public string GetExpandableSymbol () /// /// public virtual int GetWidth () => - GetLinePrefix ().Sum (r => r.GetColumns ()) + GetExpandableSymbol ().GetColumns () + _tree.AspectGetter (Model).GetColumns (); + GetLinePrefix ().Sum (r => r.GetColumns ()) + + GetExpandableSymbol ().GetColumns () + + (_tree.CheckboxMode ? 2 : 0) + + _tree.AspectGetter (Model).GetColumns (); /// Refreshes cached knowledge in this branch e.g. what children an object has. /// True to also refresh all branches (starting with the root). @@ -462,6 +472,11 @@ internal bool IsHitOnExpandableSymbol (int x) return false; } + /// Returns true if the given x offset on the branch line is the checkbox glyph. + /// The x offset on the branch line. + /// if checkbox mode is enabled and hits the checkbox glyph. + internal bool IsHitOnCheckbox (int x) => _tree.CheckboxMode && x == GetLinePrefix ().Count () + GetExpandableSymbol ().GetColumns (); + /// Calls on the current branch and all expanded children. internal void Rebuild () { diff --git a/Terminal.Gui/Views/TreeView/CheckedChangedEventArgs.cs b/Terminal.Gui/Views/TreeView/CheckedChangedEventArgs.cs new file mode 100644 index 0000000000..e64fca3a93 --- /dev/null +++ b/Terminal.Gui/Views/TreeView/CheckedChangedEventArgs.cs @@ -0,0 +1,18 @@ +namespace Terminal.Gui.Views; + +/// Describes a tree node check state change. +/// The type of object represented by nodes in the tree. +public class CheckedChangedEventArgs (TreeView tree, T @object, CheckState oldValue, CheckState newValue) : EventArgs where T : class +{ + /// The tree whose node check state changed. + public TreeView Tree { get; } = tree; + + /// The object whose check state changed. + public T Object { get; } = @object; + + /// The previous check state. + public CheckState OldValue { get; } = oldValue; + + /// The new check state. + public CheckState NewValue { get; } = newValue; +} diff --git a/Terminal.Gui/Views/TreeView/ITreeView.cs b/Terminal.Gui/Views/TreeView/ITreeView.cs index 9d4706f9e8..044bec4922 100644 --- a/Terminal.Gui/Views/TreeView/ITreeView.cs +++ b/Terminal.Gui/Views/TreeView/ITreeView.cs @@ -12,6 +12,9 @@ public interface ITreeView /// Gets or sets whether the user can navigate the tree using letter keys. bool AllowLetterBasedNavigation { get; set; } + /// Gets or sets whether built-in checkbox rendering and toggling is enabled for tree nodes. + bool CheckboxMode { get; set; } + /// Gets or sets the maximum depth to which the tree will expand. int MaxDepth { get; set; } diff --git a/Terminal.Gui/Views/TreeView/TreeView.Navigation.cs b/Terminal.Gui/Views/TreeView/TreeView.Navigation.cs index e68daf8ecd..62cd9b5d0d 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.Navigation.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.Navigation.cs @@ -350,6 +350,48 @@ public IEnumerable GetAllSelectedObjects () } } + /// Returns all objects whose effective check state is . + /// Checked objects in tree order, followed by any checked objects that are not currently in the tree. + public IEnumerable GetCheckedObjects () + { + HashSet yielded = []; + + foreach (T model in EnumerateKnownObjects ()) + { + if (GetCheckState (model) == CheckState.Checked && yielded.Add (model)) + { + yield return model; + } + } + + foreach (T model in _checkedStates.Keys) + { + if (GetCheckState (model) == CheckState.Checked && yielded.Add (model)) + { + yield return model; + } + } + } + + /// Gets the effective check state of , derived from children if applicable. + /// The node object. + /// The effective check state. + public CheckState GetCheckState (T? o) => o is null ? CheckState.UnChecked : GetEffectiveCheckState (o, []); + + /// Sets the explicit check state of and propagates to all descendants. + /// The node object. + /// The new check state. + public void SetChecked (T? o, CheckState state) + { + if (o is null) + { + return; + } + + SetCheckedRecursive (o, state); + SetNeedsDraw (); + } + /// /// Returns the currently expanded children of the passed object. Returns an empty collection if the branch is not /// exposed or not expanded. @@ -640,6 +682,14 @@ protected virtual void Space () { return; } + + if (CheckboxMode) + { + ToggleChecked (SelectedObject); + + return; + } + Toggle (SelectedObject); } @@ -648,4 +698,197 @@ protected virtual void Space () // TODO: Refactor to use CWP protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) => SelectionChanged?.Invoke (this, e); + + private IEnumerable EnumerateKnownObjects () + { + if (Roots is null) + { + yield break; + } + + HashSet seen = []; + + foreach (T root in Roots.Keys) + { + foreach (T model in EnumerateKnownObjects (root, seen, 0)) + { + yield return model; + } + } + } + + private IEnumerable EnumerateKnownObjects (T model, HashSet seen, int depth) + { + if (!seen.Add (model)) + { + yield break; + } + + yield return model; + + if (depth >= MaxDepth || TreeBuilder is null) + { + yield break; + } + + foreach (T child in TreeBuilder.GetChildren (model)) + { + foreach (T descendant in EnumerateKnownObjects (child, seen, depth + 1)) + { + yield return descendant; + } + } + } + + internal Rune GetCheckGlyph (T model) => + GetCheckState (model) switch + { + CheckState.Checked => Glyphs.CheckStateChecked, + CheckState.UnChecked => Glyphs.CheckStateUnChecked, + CheckState.None => Glyphs.CheckStateNone, + _ => throw new ArgumentOutOfRangeException () + }; + + private CheckState GetEffectiveCheckState (T model, HashSet visited) + { + if (!visited.Add (model)) + { + return _checkedStates.GetValueOrDefault (model, CheckState.UnChecked); + } + + CheckState explicitState = _checkedStates.GetValueOrDefault (model, CheckState.UnChecked); + + if (TreeBuilder is null) + { + return explicitState; + } + + // Derive state from known children (ChildBranches). These are preserved even after + // collapse, so we can derive correct tri-state without calling TreeBuilder.GetChildren. + Branch? branch = ObjectToBranch (model); + + if (branch is not { ChildBranches: { Count: > 0 } childBranches }) + { + return explicitState; + } + + CheckState [] childStates = childBranches + .Select (child => GetEffectiveCheckState (child.Model, visited)) + .ToArray (); + + if (childStates.All (state => state is CheckState.Checked)) + { + return CheckState.Checked; + } + + if (childStates.All (state => state is CheckState.UnChecked)) + { + return CheckState.UnChecked; + } + + // Mixed states among children - indeterminate + return CheckState.None; + } + + private void SetCheckedRecursive (T model, CheckState state) + { + SetCheckedRecursive (model, state, []); + } + + private void SetCheckedRecursive (T model, CheckState state, HashSet visited) + { + if (!visited.Add (model)) + { + return; + } + + SetCheckedCore (model, state); + + if (TreeBuilder is null) + { + return; + } + + foreach (T child in TreeBuilder.GetChildren (model)) + { + SetCheckedRecursive (child, state, visited); + } + } + + private void SetCheckedCore (T model, CheckState state) + { + CheckState oldEffective = GetCheckState (model); + + if (oldEffective == state) + { + return; + } + + _checkedStates [model] = state; + CheckedChanged?.Invoke (this, new CheckedChangedEventArgs (this, model, oldEffective, state)); + } + + private void ToggleChecked (T model) + { + CheckState current = GetCheckState (model); + + // If effective state is not UnChecked, toggle to UnChecked + if (current != CheckState.UnChecked) + { + SetChecked (model, CheckState.UnChecked); + + return; + } + + // Effective state is UnChecked - but for collapsed nodes, descendants might + // still be explicitly checked in the dictionary. Check for that case. + if (HasCheckedDescendantsInState (model)) + { + SetChecked (model, CheckState.UnChecked); + + return; + } + + SetChecked (model, CheckState.Checked); + } + + private bool HasCheckedDescendantsInState (T model) + { + if (TreeBuilder is null) + { + return false; + } + + HashSet visited = []; + + return HasCheckedDescendantsRecursive (model, visited); + } + + private bool HasCheckedDescendantsRecursive (T model, HashSet visited) + { + if (!visited.Add (model)) + { + return false; + } + + if (TreeBuilder is null) + { + return false; + } + + foreach (T child in TreeBuilder.GetChildren (model)) + { + if (_checkedStates.TryGetValue (child, out CheckState state) && state == CheckState.Checked) + { + return true; + } + + if (HasCheckedDescendantsRecursive (child, visited)) + { + return true; + } + } + + return false; + } } diff --git a/Terminal.Gui/Views/TreeView/TreeViewT.cs b/Terminal.Gui/Views/TreeView/TreeViewT.cs index 2d745289a8..2a6fafa741 100644 --- a/Terminal.Gui/Views/TreeView/TreeViewT.cs +++ b/Terminal.Gui/Views/TreeView/TreeViewT.cs @@ -296,6 +296,26 @@ public TreeView () /// public ITreeViewFilter? Filter { get; set; } = null; + /// Enables built-in checkbox rendering and toggling for tree nodes. + public bool CheckboxMode + { + get; + set + { + if (field == value) + { + return; + } + + field = value; + UpdateContentSize (); + SetNeedsDraw (); + } + } + + /// Raised when a node check state changes. + public event EventHandler>? CheckedChanged; + /// True makes a letter key press navigate to the next visible branch that begins with that letter/digit. public bool AllowLetterBasedNavigation { get; set; } = true; @@ -393,11 +413,15 @@ public T? SelectedObject /// Secondary selected regions of tree when is true. private readonly Stack> _multiSelectedRegions = new (); + /// Explicit check states for nodes in checkbox mode. + private readonly Dictionary _checkedStates = new (); + /// Removes all objects from the tree and clears . public void ClearObjects () { SelectedObject = null; _multiSelectedRegions.Clear (); + _checkedStates.Clear (); Roots = new Dictionary> (); InvalidateLineMap (); SetNeedsDraw (); @@ -408,18 +432,9 @@ protected override void OnActivated (ICommandContext? ctx) { base.OnActivated (ctx); - T? o = SelectedObject; - - if (o is null) - { - return; - } - - var isExpandToggleAttempt = true; - + // Mouse activation: use hit-test to find the clicked branch directly if (ctx?.Binding is MouseBinding { MouseEvent: { } } mouseBinding) { - // The line they clicked on a branch Branch? clickedBranch = HitTest (mouseBinding.MouseEvent.Position!.Value.Y); if (clickedBranch is null) @@ -429,13 +444,37 @@ protected override void OnActivated (ICommandContext? ctx) SelectedObject = clickedBranch.Model; - isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (mouseBinding.MouseEvent.Position!.Value.X); + if (clickedBranch.IsHitOnCheckbox (mouseBinding.MouseEvent.Position!.Value.X)) + { + ToggleChecked (clickedBranch.Model); + + return; + } + + if (clickedBranch.IsHitOnExpandableSymbol (mouseBinding.MouseEvent.Position!.Value.X)) + { + Toggle (clickedBranch.Model); + } + + return; } - if (isExpandToggleAttempt) + // Keyboard activation: operate on currently selected object + T? o = SelectedObject; + + if (o is null) { - Toggle (SelectedObject); + return; } + + if (CheckboxMode) + { + ToggleChecked (o); + + return; + } + + Toggle (o); } /// @@ -561,6 +600,11 @@ public void Remove (T o) return; } + foreach (T model in EnumerateKnownObjects (o, [], 0)) + { + _checkedStates.Remove (model); + } + Roots.Remove (o); InvalidateLineMap (); SetNeedsDraw (); diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 3620eceafe..a07554ddff 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views; /// A multi-step dialog for collecting related data across sequential steps. /// /// -/// Wizard demo +/// Wizard demo /// /// Each can host arbitrary s and display help text. /// Navigation buttons (Back/Next/Finish) are automatically managed. @@ -49,8 +49,6 @@ namespace Terminal.Gui.Views; /// public class Wizard : Dialog, IDesignable { - private string _wizardTitle = string.Empty; - /// /// Initializes a new instance of the class. /// @@ -66,46 +64,16 @@ public Wizard () ButtonAlignment = Alignment.Fill; - SetStyle (); - BackButton = new Button { Text = Strings.wzBack, X = 0, Y = Pos.AnchorEnd () }; - NextFinishButton = new Button { Text = Strings.wzFinish, IsDefault = true, X = Pos.AnchorEnd (), Y = Pos.AnchorEnd () }; + NextFinishButton = new Button { Text = Strings.wzFinish, X = Pos.AnchorEnd (), Y = Pos.AnchorEnd () }; - BackButton.Accepting += BackBtnOnAccepting; - NextFinishButton.Accepting += NextFinishBtnOnAccepting; + BackButton.Accepting += BackButtonOnAccepting; AddButton (BackButton); AddButton (NextFinishButton); } - private void SetStyle () - { - if (IsRunning) - { - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog); - Arrangement |= ViewArrangement.Movable | ViewArrangement.Resizable; - } - else - { - SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base); - BorderStyle = LineStyle.Dotted; - - // strip out movable and resizable - Arrangement &= ~(ViewArrangement.Movable | ViewArrangement.Resizable); - base.ShadowStyle = null; - } - } - - /// - protected override void OnTitleChanged () - { - if (string.IsNullOrEmpty (_wizardTitle)) - { - _wizardTitle = Title; - } - } - /// public override void EndInit () { @@ -114,11 +82,24 @@ public override void EndInit () } /// - protected override void OnIsModalChanged (bool newIsModal) + protected override bool OnAccepting (CommandEventArgs args) { - SetStyle (); + if (CurrentStep is null || CurrentStep == GetLastStep ()) + { + return base.OnAccepting (args); + } - base.OnIsModalChanged (newIsModal); + CancelEventArgs ce = new (); + MovingNext?.Invoke (this, ce); + + if (ce.Cancel) + { + return true; + } + + GoNext (); + + return true; } /// @@ -129,14 +110,21 @@ protected override void OnIsModalChanged (bool newIsModal) /// public Button BackButton { get; } - private readonly LinkedList _steps = []; - private WizardStep? _currentStep; + private void BackButtonOnAccepting (object? sender, CommandEventArgs e) + { + CancelEventArgs args = new (); + MovingBack?.Invoke (this, args); - /// Gets or sets the currently displayed step. - /// - /// Setting this property calls and may be canceled via . - /// - public WizardStep? CurrentStep { get => _currentStep; set => GoToStep (value); } + if (args.Cancel) + { + e.Handled = true; + + return; + } + + e.Handled = true; + GoBack (); + } /// /// The Next/Finish button. Advances to the next step or completes the wizard. @@ -151,6 +139,7 @@ protected override void OnIsModalChanged (bool newIsModal) /// public Button NextFinishButton { get; } + private readonly LinkedList _steps = []; private Size _maxStepSize = Size.Empty; /// @@ -168,7 +157,7 @@ public void AddStep (WizardStep newStep) _steps.AddLast (newStep); // Find the step's natural size - //newStep.SuperViewRendersLineCanvas = true; + // newStep.SuperViewRendersLineCanvas = true; newStep.Width = Dim.Auto (); newStep.Height = Dim.Auto (); newStep.SetRelativeLayout (App?.Screen.Size ?? new Size (2048, 2048)); @@ -185,14 +174,22 @@ public void AddStep (WizardStep newStep) Add (newStep); - //newStep.SetRelativeLayout (App?.Screen.Size ?? new Size (2048, 2048)); - //newStep.LayoutSubViews (); - //Width = Dim.Auto (minimumContentDim: _maxStepSize.Width + 2); - //Height = Dim.Auto (minimumContentDim: _maxStepSize.Height); + // newStep.SetRelativeLayout (App?.Screen.Size ?? new Size (2048, 2048)); + // newStep.LayoutSubViews (); + // Width = Dim.Auto (minimumContentDim: _maxStepSize.Width + 2); + // Height = Dim.Auto (minimumContentDim: _maxStepSize.Height); UpdateButtonsAndTitle (); } + private WizardStep? _currentStep; + + /// Gets or sets the currently displayed step. + /// + /// Setting this property calls and may be canceled via . + /// + public WizardStep? CurrentStep { get => _currentStep; set => GoToStep (value); } + /// /// Gets the first enabled step in the wizard. /// @@ -309,16 +306,6 @@ public bool GoNext () return nextStep is { } && GoToStep (nextStep); } - /// - /// Raised when the Back button is pressed. Set Cancel to prevent navigation. - /// - public event EventHandler? MovingBack; - - /// - /// Raised when the Next button is pressed on a non-final step. Set Cancel to prevent navigation. - /// - public event EventHandler? MovingNext; - /// /// Navigates to the specified step. /// @@ -361,11 +348,19 @@ public bool GoToStep (WizardStep? newStep) => out _); /// - /// Called before changes. Override to add custom validation. + /// Raised when the Back button is pressed. Set Cancel to prevent navigation. /// - /// Event arguments containing old and new step values. - /// to cancel the step change; otherwise . - protected virtual bool OnStepChanging (ValueChangingEventArgs args) => false; + public event EventHandler? MovingBack; + + /// + /// Raised when the Next button is pressed on a non-final step. Set Cancel to prevent navigation. + /// + public event EventHandler? MovingNext; + + /// + /// Raised after changes. + /// + public event EventHandler>? StepChanged; /// /// Raised before changes. Set Handled to cancel navigation. @@ -379,36 +374,20 @@ public bool GoToStep (WizardStep? newStep) => protected virtual void OnStepChanged (ValueChangedEventArgs args) { } /// - /// Raised after changes. + /// Called before changes. Override to add custom validation. /// - public event EventHandler>? StepChanged; - - private void BackBtnOnAccepting (object? sender, CommandEventArgs e) - { - CancelEventArgs args = new (); - MovingBack?.Invoke (this, args); - - if (args.Cancel) - { - return; - } + /// Event arguments containing old and new step values. + /// to cancel the step change; otherwise . + protected virtual bool OnStepChanging (ValueChangingEventArgs args) => false; - e.Handled = true; - GoBack (); - } + private string _wizardTitle = string.Empty; - private void NextFinishBtnOnAccepting (object? sender, CommandEventArgs e) + /// + protected override void OnTitleChanged () { - if (CurrentStep == GetLastStep ()) - { - return; - } - CancelEventArgs args = new (); - MovingNext?.Invoke (this, new CancelEventArgs ()); - - if (!args.Cancel) + if (string.IsNullOrEmpty (_wizardTitle)) { - e.Handled = GoNext (); + _wizardTitle = Title; } } @@ -434,6 +413,8 @@ private void UpdateButtonsAndTitle () { NextFinishButton.Text = CurrentStep.NextButtonText != string.Empty ? CurrentStep.NextButtonText : Strings.wzNext; // "_Next..."; } + + CurrentStep?.RestoreFocus (); } bool IDesignable.EnableForDesign () @@ -444,19 +425,20 @@ bool IDesignable.EnableForDesign () (firstStep as IDesignable).EnableForDesign (); AddStep (firstStep); + WizardStep secondStep = new () { Title = "Second Step", HelpText = "## Second Step\nThis is the help text for the Second Step." }; Label schemeLabel = new () { Title = "_Scheme:" }; - OptionSelector selector = new () { X = Pos.Right (schemeLabel) + 1, Title = "Select Scheme" }; + OptionSelector schemeSelector = new () { X = Pos.Right (schemeLabel) + 1, Title = "Select Scheme" }; - selector.ValueChanged += (_, _) => - { - if (selector.Value is { } scheme) - { - SchemeName = SchemeManager.SchemesToSchemeName (scheme); - } - }; + schemeSelector.ValueChanged += (_, _) => + { + if (schemeSelector.Value is { } scheme) + { + SchemeName = SchemeManager.SchemesToSchemeName (scheme); + } + }; - Label borderStyleLabel = new () { Title = "_Border Style:", X = Pos.Right (selector) + 2 }; + Label borderStyleLabel = new () { Title = "Border S_tyle:", X = Pos.Right (schemeSelector) + 2 }; OptionSelector borderStyleSelector = new () { X = Pos.Right (borderStyleLabel) + 1, Title = "Select Border Style" }; @@ -468,14 +450,36 @@ bool IDesignable.EnableForDesign () } }; - WizardStep secondStep = new () { Title = "Second Step", HelpText = "This is the help text for the Second Step." }; - secondStep.Add (schemeLabel, selector, borderStyleLabel, borderStyleSelector); + TextField test = new () { Y = Pos.Bottom (schemeSelector), Width = 10 }; + + secondStep.Add (schemeLabel, schemeSelector, borderStyleLabel, borderStyleSelector, test); AddStep (secondStep); + WizardStep lastStep = new () { Title = "Last Step", HelpText = "## Last Step\nThis is the help text for the Last Step." }; + Label schemeResultsLabel = new () { Text = "Scheme Results:" }; + Label schemeResultValueLabel = new () { X = Pos.Right (schemeResultsLabel) + 1, Text = SchemeName ?? string.Empty }; + Label borderStyleResultsLabel = new () { Text = "Border Style Results:", Y = Pos.Bottom (schemeResultsLabel) }; + + Label borderStyleResultValueLabel = + new () { X = Pos.Right (borderStyleResultsLabel) + 1, Y = Pos.Bottom (schemeResultsLabel), Text = $"{BorderStyle}" }; + + lastStep.Add (schemeResultsLabel, schemeResultValueLabel, borderStyleResultsLabel, borderStyleResultValueLabel); + + lastStep.VisibleChanged += (_, _) => + { + if (!lastStep.Visible) + { + return; + } + schemeResultValueLabel.Text = SchemeName ?? string.Empty; + borderStyleResultValueLabel.Text = $"{BorderStyle}"; + }; + AddStep (lastStep); + return true; } /// - string? IDesignable.GetDemoKeyStrokes () => "wait:500,Tab,wait:500,Tab,wait:500,Enter,wait:1000"; + string IDesignable.GetDemoKeyStrokes () => "wait:500,Tab,wait:500,Tab,wait:500,Enter,wait:1000"; } diff --git a/Terminal.Gui/Views/Wizard/WizardStep.cs b/Terminal.Gui/Views/Wizard/WizardStep.cs index 57e53864c8..7a3e32244f 100644 --- a/Terminal.Gui/Views/Wizard/WizardStep.cs +++ b/Terminal.Gui/Views/Wizard/WizardStep.cs @@ -11,9 +11,11 @@ namespace Terminal.Gui.Views; /// public class WizardStep : View, IDesignable { - private readonly Code _helpTextView = new () + private readonly Markdown _helpTextView = new () { - SyntaxHighlighter = null, + SyntaxHighlighter = new TextMateSyntaxHighlighter (), + ShowHeadingPrefix = false, + ViewportSettings = ViewportSettingsFlags.HasVerticalScrollBar, X = Pos.AnchorEnd () + 1, Height = Dim.Fill (), #if DEBUG @@ -30,6 +32,7 @@ public WizardStep () CanFocus = true; Width = Dim.Fill (); Height = Dim.Fill (); + CommandsToBubbleUp = [Command.Accept]; } /// @@ -69,7 +72,7 @@ protected override void OnFrameChanged (in Rectangle frame) /// The help text displayed in the right . /// If empty, the right padding is hidden and content fills the entire step. /// - /// The help text is displayed using a read-only view. + /// The help text is displayed using a read-only view that supports markdown formatting. public string HelpText { get => _helpTextView.Text; @@ -112,23 +115,12 @@ bool IDesignable.EnableForDesign () { Title = "Example Step"; - Label label = new () - { - Title = "_Enter Text:" - }; + Label label = new () { Title = "_Enter Text:" }; - TextField textField = new () - { - X = Pos.Right (label) + 1, - Width = 20 - }; + TextField textField = new () { X = Pos.Right (label) + 1, Width = 20 }; Add (label, textField); - label = new Label - { - Title = " _A List:", - Y = Pos.Bottom (label) + 1 - }; + label = new Label { Title = " _A List:", Y = Pos.Bottom (label) + 1 }; ListView listView = new () { @@ -143,8 +135,13 @@ bool IDesignable.EnableForDesign () Add (label, listView); HelpText = """ - This is some help text for the WizardStep. - You can provide instructions or information to guide the user through this step of the wizard. + ## WizardStep Help + + Provide **markdown-formatted** instructions or information to guide the user through each step of the wizard. + + Set `WizardStep.HelpText` to update this content. If `HelpText` is empty, the right padding will be hidden and the content will fill the entire step. + + [Learn more about markdown formatting](https://www.markdownguide.org/basic-syntax/) """; return true; diff --git a/Tests/IntegrationTests/TabsFanOutIntegrationTests.cs b/Tests/IntegrationTests/TabsFanOutIntegrationTests.cs index 849094e050..de1d9175a1 100644 --- a/Tests/IntegrationTests/TabsFanOutIntegrationTests.cs +++ b/Tests/IntegrationTests/TabsFanOutIntegrationTests.cs @@ -45,6 +45,7 @@ private sealed class Counters public int SubViewsLaidOut; public int DrawComplete; public int ClearedViewport; + public int DrawingText; } private static string MakeText (string prefix, int lines) @@ -104,11 +105,13 @@ public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTa codes [i].SubViewsLaidOut += (_, _) => perTab [captured].SubViewsLaidOut++; codes [i].DrawComplete += (_, _) => perTab [captured].DrawComplete++; codes [i].ClearedViewport += (_, _) => perTab [captured].ClearedViewport++; + codes [i].DrawingText += (_, _) => perTab [captured].DrawingText++; } tabs.SubViewsLaidOut += (_, _) => tabsContainer.SubViewsLaidOut++; tabs.DrawComplete += (_, _) => tabsContainer.DrawComplete++; tabs.ClearedViewport += (_, _) => tabsContainer.ClearedViewport++; + tabs.DrawingText += (_, _) => tabsContainer.DrawingText++; ScrollableCode active = codes [0]; @@ -123,11 +126,13 @@ public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTa perTab [i].SubViewsLaidOut = 0; perTab [i].DrawComplete = 0; perTab [i].ClearedViewport = 0; + perTab [i].DrawingText = 0; } tabsContainer.SubViewsLaidOut = 0; tabsContainer.DrawComplete = 0; tabsContainer.ClearedViewport = 0; + tabsContainer.DrawingText = 0; }) .KeyDown (Key.PageDown) .KeyDown (Key.PageDown) @@ -136,12 +141,12 @@ public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTa outputHelper.WriteLine ($"Driver: {driverName}"); outputHelper.WriteLine ($"Active tab viewport Y after 3 PageDowns: {active.Viewport.Y}"); outputHelper.WriteLine ("Per-tab counters (after 3 PageDowns on active tab):"); - outputHelper.WriteLine (" tab laidOut drawComplete clearedViewport"); - outputHelper.WriteLine ($" Tabs {tabsContainer.SubViewsLaidOut,7} {tabsContainer.DrawComplete,12} {tabsContainer.ClearedViewport,15}"); + outputHelper.WriteLine (" tab laidOut drawComplete clearedViewport drawingText"); + outputHelper.WriteLine ($" Tabs {tabsContainer.SubViewsLaidOut,7} {tabsContainer.DrawComplete,12} {tabsContainer.ClearedViewport,15} {tabsContainer.DrawingText,11}"); for (var i = 0; i < TabCount; i++) { - outputHelper.WriteLine ($" Code{i + 1,-6} {perTab [i].SubViewsLaidOut,7} {perTab [i].DrawComplete,12} {perTab [i].ClearedViewport,15}"); + outputHelper.WriteLine ($" Code{i + 1,-6} {perTab [i].SubViewsLaidOut,7} {perTab [i].DrawComplete,12} {perTab [i].ClearedViewport,15} {perTab [i].DrawingText,11}"); } Assert.True ( @@ -152,27 +157,34 @@ public void Integration_RealPageDown_OnActiveTab_DoesNotFanOutLayoutToInactiveTa perTab [0].DrawComplete > 0, $"Active tab must draw in response to real PageDown, got DrawComplete={perTab [0].DrawComplete}."); - int inactiveDraws = 0; + int inactiveTextDraws = 0; int inactiveLayouts = 0; for (var i = 1; i < TabCount; i++) { - inactiveDraws += perTab [i].DrawComplete; + inactiveTextDraws += perTab [i].DrawingText; inactiveLayouts += perTab [i].SubViewsLaidOut; } - outputHelper.WriteLine ($"Sum inactive DrawComplete = {inactiveDraws}"); + outputHelper.WriteLine ($"Sum inactive DrawingText = {inactiveTextDraws}"); outputHelper.WriteLine ($"Sum inactive SubViewsLaidOut = {inactiveLayouts}"); - // CURRENT BEHAVIOR: draw fan-out still exists through the real input → command → main-loop path, - // but layout fan-out should now be eliminated. + // Issue #5358 fix narrows draw fan-out at the View.Draw pipeline level (verified by + // TabsFanOutDiagnosticTests at synthetic level). At integration level a separate + // cascade source remains: ApplicationImpl.LayoutAndDraw passes force=true to + // View.Draw whenever any view needed layout, which calls SetNeedsDraw on the top + // runnable, which cascades to overlapping subviews via the existing SetNeedsDraw + // recursion. Removing that force=true uncovers stale-content bugs in the shrink/move + // path (covered by existing ShadowTests and BorderViewTests) and is out of scope + // for #5358. Until that broader fix lands, inactive tab pages still receive + // NeedsDraw via the LayoutAndDraw force path. Layout fan-out is already fully + // eliminated by PR #5373. Assert.True ( - inactiveDraws > 0, - $"Documents issue #4973 (integration-level): inactive_total DrawComplete={inactiveDraws}. " + - "Flip to Assert.Equal(0, inactiveDraws) after fix lands."); + inactiveTextDraws > 0, + $"Documents the remaining draw fan-out via ApplicationImpl.LayoutAndDraw's force=true path: " + + $"inactive_total DrawingText={inactiveTextDraws}. Flip to Assert.Equal(0, inactiveTextDraws) " + + "after the broader LayoutAndDraw cascade is addressed (out of scope for #5358)."); - Assert.Equal ( - 0, - inactiveLayouts); + Assert.Equal (0, inactiveLayouts); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs index 70c36f8ead..3039b9ec94 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs @@ -239,6 +239,10 @@ public void NeedsDraw_False_After_Draw () [Fact] public void NeedsDrawRect_Is_Viewport_Relative () { + // Issue #5358: NeedsDrawRect is now a true accumulating union of all dirty regions + // since the last Draw() / ClearNeedsDraw(). Previously the broken union math clamped + // each call's result to the current Viewport size, which incidentally produced + // "current Viewport" semantics. This test asserts the accumulating union semantics. View superView = new () { Id = "superView", Width = 10, Height = 10 }; Assert.Equal (new Rectangle (0, 0, 10, 10), superView.Frame); Assert.Equal (new Rectangle (0, 0, 10, 10), superView.Viewport); @@ -262,37 +266,46 @@ public void NeedsDrawRect_Is_Viewport_Relative () view.Frame = new Rectangle (3, 3, 5, 5); Assert.Equal (new Rectangle (3, 3, 5, 5), view.Frame); Assert.Equal (new Rectangle (0, 0, 5, 5), view.Viewport); + // Union((0,0,2,3),(0,0,5,5)) = (0,0,5,5) Assert.Equal (new Rectangle (0, 0, 5, 5), view.NeedsDrawRect); view.Frame = new Rectangle (3, 3, 6, 6); // Grow right/bottom 1 Assert.Equal (new Rectangle (3, 3, 6, 6), view.Frame); Assert.Equal (new Rectangle (0, 0, 6, 6), view.Viewport); + // Union((0,0,5,5),(0,0,6,6)) = (0,0,6,6) Assert.Equal (new Rectangle (0, 0, 6, 6), view.NeedsDrawRect); view.Frame = new Rectangle (3, 3, 5, 5); // Shrink right/bottom 1 Assert.Equal (new Rectangle (3, 3, 5, 5), view.Frame); Assert.Equal (new Rectangle (0, 0, 5, 5), view.Viewport); - Assert.Equal (new Rectangle (0, 0, 5, 5), view.NeedsDrawRect); + // Union((0,0,6,6),(0,0,5,5)) = (0,0,6,6) — accumulates the prior larger area. + Assert.Equal (new Rectangle (0, 0, 6, 6), view.NeedsDrawRect); view.SetContentSize (new Size (10, 10)); Assert.Equal (new Rectangle (3, 3, 5, 5), view.Frame); Assert.Equal (new Rectangle (0, 0, 5, 5), view.Viewport); - Assert.Equal (new Rectangle (0, 0, 5, 5), view.NeedsDrawRect); + // SetContentSize only calls SetNeedsLayout, not SetNeedsDraw. NeedsDrawRect unchanged. + Assert.Equal (new Rectangle (0, 0, 6, 6), view.NeedsDrawRect); view.Viewport = new Rectangle (1, 1, 5, 5); // Scroll up/left 1 Assert.Equal (new Rectangle (3, 3, 5, 5), view.Frame); Assert.Equal (new Rectangle (1, 1, 5, 5), view.Viewport); - Assert.Equal (new Rectangle (0, 0, 5, 5), view.NeedsDrawRect); + // Scroll calls SetNeedsLayout (not SetNeedsDraw). NeedsDrawRect unchanged. + Assert.Equal (new Rectangle (0, 0, 6, 6), view.NeedsDrawRect); view.Frame = new Rectangle (3, 3, 6, 6); // Grow right/bottom 1 Assert.Equal (new Rectangle (3, 3, 6, 6), view.Frame); Assert.Equal (new Rectangle (1, 1, 6, 6), view.Viewport); - Assert.Equal (new Rectangle (1, 1, 6, 6), view.NeedsDrawRect); + // Union((0,0,6,6),(1,1,6,6)) — Viewport now has scroll offset (1,1). + // SetFrame's SetNeedsDraw passes the current Viewport = (1,1,6,6). + // Rectangle.Union((0,0,6,6),(1,1,6,6)) = (0,0,7,7). + Assert.Equal (new Rectangle (0, 0, 7, 7), view.NeedsDrawRect); view.Frame = new Rectangle (3, 3, 5, 5); Assert.Equal (new Rectangle (3, 3, 5, 5), view.Frame); Assert.Equal (new Rectangle (1, 1, 5, 5), view.Viewport); - Assert.Equal (new Rectangle (1, 1, 5, 5), view.NeedsDrawRect); + // Union((0,0,7,7),(1,1,5,5)) = (0,0,7,7). + Assert.Equal (new Rectangle (0, 0, 7, 7), view.NeedsDrawRect); } [Fact] @@ -498,9 +511,11 @@ public void SetNeedsDraw_MultipleRectangles_Expands () view.SetNeedsDraw (new Rectangle (5, 5, 10, 10)); view.SetNeedsDraw (new Rectangle (15, 15, 10, 10)); - // Should expand to cover the entire viewport when we have overlapping regions - // The current implementation expands to viewport size - var expected = new Rectangle (0, 0, 30, 30); + // Issue #5358: NeedsDrawRect is now a true union of all SetNeedsDraw regions, + // not a max-of-Viewport-and-region clamp. Previously the broken union math + // expanded to viewport size on any second call; that masked partial-dirty regions + // and made region-aware ClearViewport impossible. + var expected = new Rectangle (5, 5, 20, 20); Assert.Equal (expected, view.NeedsDrawRect); } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/RegionAwareClearViewportTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/RegionAwareClearViewportTests.cs new file mode 100644 index 0000000000..78b68f4576 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/RegionAwareClearViewportTests.cs @@ -0,0 +1,259 @@ +#nullable disable +using System.Text; +using UnitTests; + +namespace ViewBaseTests.Draw; + +// Claude - Opus 4.7 +/// +/// Issue #5358: the framework's DoClearViewport narrows the clear to NeedsDrawRect when an +/// explicit partial region has been set AND the view is not itself scrolled. The public +/// ClearViewport API always does a full clear (preserves the contract used by +/// Code.OnClearingViewport, MarkdownCodeBlock, direct test callers). View.SetFrame +/// invalidates the SuperView for the union of the old and new Frame when Frame changes, +/// so stale uncovered cells get cleared on the next pass. +/// +public class RegionAwareClearViewportTests : TestDriverBase +{ + /// + /// Public ClearViewport always does a full clear regardless of NeedsDrawRect state — + /// this is the backward-compatible contract used by Code.OnClearingViewport, + /// MarkdownCodeBlock.OnClearingViewport, and direct test callers like + /// ClearViewport_FillsViewportArea. + /// + [Fact] + public void PublicClearViewport_AlwaysClearsFullViewport () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View view = new () { Driver = driver, X = 5, Y = 5, Width = 10, Height = 5 }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + + driver.FillRect (driver.Screen, new Rune ('X')); + + // Even with a partial NeedsDrawRect, public ClearViewport must clear the full viewport. + view.ClearNeedsDraw (); + view.SetNeedsDraw (new Rectangle (1, 1, 3, 2)); + Assert.Equal (new Rectangle (1, 1, 3, 2), view.NeedsDrawRect); + + view.ClearViewport (); + + Rectangle screen = view.ViewportToScreen (view.Viewport with { Location = new Point (0, 0) }); + + for (int y = screen.Y; y < screen.Y + screen.Height; y++) + { + for (int x = screen.X; x < screen.X + screen.Width; x++) + { + Assert.Equal (" ", driver.Contents [y, x].Grapheme); + } + } + + view.Dispose (); + driver.Dispose (); + } + + /// + /// The framework's draw pipeline (Draw → DoClearViewport) narrows the clear to the + /// dirty region when the view has a partial NeedsDrawRect and is not itself scrolled. + /// Verifies by invoking Draw() and observing that cells outside the dirty region keep + /// their pre-fill 'X' value. + /// + [Fact] + public void FrameworkDraw_PartialNeedsDrawRect_ClearsOnlyDirtyRegion () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View view = new () { Driver = driver, X = 5, Y = 5, Width = 10, Height = 5 }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + view.Draw (); + + Assert.True (view.NeedsDrawRect.IsEmpty); + + // Re-set clip after Draw (DoDrawComplete excludes the view's frame from the clip, + // which would prevent the next FillRect from reaching cells inside the view's area). + driver.Clip = new Region (driver.Screen); + driver.FillRect (driver.Screen, new Rune ('X')); + Assert.Equal ("X", driver.Contents [5, 5].Grapheme); + + // Invalidate just a 3×2 region — strictly smaller than the 10×5 viewport. + view.SetNeedsDraw (new Rectangle (1, 1, 3, 2)); + Assert.Equal (new Rectangle (1, 1, 3, 2), view.NeedsDrawRect); + + // Drive the framework's DoClearViewport via the normal draw path. + view.Draw (); + + // The dirty region (viewport-local (1,1,3,2) → screen (6,6,3,2)) is cleared. + for (var y = 6; y < 8; y++) + { + for (var x = 6; x < 9; x++) + { + Assert.Equal (" ", driver.Contents [y, x].Grapheme); + } + } + + // Cells outside the dirty region inside the viewport remain 'X'. + Assert.Equal ("X", driver.Contents [5, 5].Grapheme); + Assert.Equal ("X", driver.Contents [9, 14].Grapheme); + + view.Dispose (); + driver.Dispose (); + } + + /// + /// SetNeedsDraw() (no-arg) sets NeedsDrawRect = Viewport. The framework must still + /// clear the full viewport in this case — narrowing only fires when the rect is + /// strictly smaller than the viewport. + /// + [Fact] + public void FrameworkDraw_FullViewportNeedsDrawRect_ClearsFullViewport () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View view = new () { Driver = driver, X = 5, Y = 5, Width = 10, Height = 5 }; + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + view.Draw (); + + Assert.True (view.NeedsDrawRect.IsEmpty); + + driver.Clip = new Region (driver.Screen); + driver.FillRect (driver.Screen, new Rune ('X')); + + // SetNeedsDraw() no-arg sets NeedsDrawRect to Viewport (full). + view.SetNeedsDraw (); + Assert.Equal (view.Viewport, view.NeedsDrawRect); + + view.Draw (); + + Rectangle screen = view.ViewportToScreen (view.Viewport with { Location = new Point (0, 0) }); + + for (int y = screen.Y; y < screen.Y + screen.Height; y++) + { + for (int x = screen.X; x < screen.X + screen.Width; x++) + { + Assert.Equal (" ", driver.Contents [y, x].Grapheme); + } + } + + view.Dispose (); + driver.Dispose (); + } + + /// + /// Review feedback item 1: narrowing must NOT fire when the view is itself scrolled, + /// because SetNeedsDraw(Rectangle) cascades to subviews using frame-local coordinates + /// while the no-arg version passes content-coord Viewport. Until that convention is + /// normalized, the framework falls back to a full clear for scrolled views. + /// + [Fact] + public void FrameworkDraw_ScrolledView_FallsBackToFullClear () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View view = new () { Driver = driver, X = 5, Y = 5, Width = 10, Height = 5 }; + view.SetContentSize (new Size (100, 100)); + view.BeginInit (); + view.EndInit (); + view.LayoutSubViews (); + view.Draw (); + + // Scroll so Viewport.Location is non-empty. + view.Viewport = view.Viewport with { Y = 5 }; + Assert.Equal (new Point (0, 5), view.Viewport.Location); + + view.Draw (); + driver.Clip = new Region (driver.Screen); + driver.FillRect (driver.Screen, new Rune ('X')); + + // Set a partial dirty rect (which would narrow if the view weren't scrolled). + view.ClearNeedsDraw (); + view.SetNeedsDraw (new Rectangle (1, 6, 3, 2)); + + view.Draw (); + + // Full viewport should be cleared (narrowing must not fire on scrolled view). + Rectangle screen = view.ViewportToScreen (view.Viewport with { Location = new Point (0, 0) }); + for (int y = screen.Y; y < screen.Y + screen.Height; y++) + { + for (int x = screen.X; x < screen.X + screen.Width; x++) + { + Assert.Equal (" ", driver.Contents [y, x].Grapheme); + } + } + + view.Dispose (); + driver.Dispose (); + } + + /// + /// When a subview's Frame shrinks, View.Layout invalidates the SuperView for the union + /// of the old and new frames. This is the foundation for clearing stale uncovered cells + /// after geometry changes (without forcing the whole SuperView viewport to redraw). + /// + [Fact] + public void FrameShrink_InvalidatesSuperViewWithUnionOfOldAndNewFrames () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View superView = new () { Driver = driver, Width = 20, Height = 10 }; + View view = new () { X = 0, Y = 0, Width = 7, Height = 4 }; + superView.Add (view); + + superView.Layout (); + superView.Draw (); + + Assert.True (superView.NeedsDrawRect.IsEmpty); + Assert.True (view.NeedsDrawRect.IsEmpty); + + // Shrink the subview's frame. + view.Frame = new Rectangle (0, 0, 3, 2); + + // SuperView's NeedsDrawRect must include the OLD frame area so stale cells get cleared. + Assert.False (superView.NeedsDrawRect.IsEmpty); + Assert.True (superView.NeedsDrawRect.Contains (new Rectangle (0, 0, 7, 4))); + + superView.Dispose (); + driver.Dispose (); + } + + /// + /// Pure scroll (Viewport.X/Y change without Frame change) must NOT invalidate the + /// SuperView. Otherwise we re-introduce the per-scroll draw fan-out that issue #5358 + /// is fixing for overlapping tab content. + /// + [Fact] + public void Scroll_DoesNotInvalidateSuperView () + { + IDriver driver = CreateTestDriver (40, 20); + driver.Clip = new Region (driver.Screen); + + View superView = new () { Driver = driver, Width = 20, Height = 10 }; + View view = new () { X = 0, Y = 0, Width = 10, Height = 5 }; + view.SetContentSize (new Size (100, 100)); + superView.Add (view); + + superView.Layout (); + superView.Draw (); + + Assert.True (superView.NeedsDrawRect.IsEmpty); + + // Pure scroll — Viewport location changes, Frame does not. + view.Viewport = view.Viewport with { Y = 5 }; + + // SuperView must NOT have been invalidated by the scroll alone. + Assert.True (superView.NeedsDrawRect.IsEmpty); + + superView.Dispose (); + driver.Dispose (); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/SubViewOnlyRedrawTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/SubViewOnlyRedrawTests.cs new file mode 100644 index 0000000000..d8a95351c7 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/SubViewOnlyRedrawTests.cs @@ -0,0 +1,241 @@ +using UnitTests; + +namespace ViewBaseTests.Draw; + +// Claude - Opus 4.7 +/// +/// Issue #5358: when a parent enters Draw() only because a child is dirty, +/// the parent must NOT clear its viewport, re-draw its text, or re-draw its +/// own content. Only adornments and subviews are allowed to run. +/// +public class SubViewOnlyRedrawTests : TestDriverBase +{ + /// + /// GIVEN a parent where only the child is dirty + /// WHEN Draw runs + /// THEN the parent's ClearingViewport / DrewText / DrawingContent events MUST NOT fire. + /// + [Fact] + public void ChildOnlyDirty_ParentDoesNotClearOrRedrawSelf () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () { Driver = driver, Width = 20, Height = 10, Text = "parent-text" }; + View child = new () { Width = 10, Height = 5, X = 1, Y = 1 }; + parent.Add (child); + + parent.Layout (); + parent.Draw (); + + var cleared = 0; + var drewText = 0; + var drewContent = 0; + parent.ClearedViewport += (_, _) => cleared++; + parent.DrewText += (_, _) => drewText++; + parent.DrawingContent += (_, _) => drewContent++; + + child.SetNeedsDraw (); + + parent.Draw (); + + Assert.Equal (0, cleared); + Assert.Equal (0, drewText); + Assert.Equal (0, drewContent); + + parent.Dispose (); + driver.Dispose (); + } + + /// + /// GIVEN a parent dirtied directly via SetNeedsDraw() + /// WHEN Draw runs + /// THEN the parent MUST clear and redraw its text/content (regression guard: + /// the new gate must not break the normal full-redraw path). + /// + [Fact] + public void ParentDirectlyDirty_StillClearsAndRedrawsSelf () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () { Driver = driver, Width = 20, Height = 10, Text = "parent-text" }; + View child = new () { Width = 10, Height = 5, X = 1, Y = 1 }; + parent.Add (child); + + parent.Layout (); + parent.Draw (); + + var cleared = 0; + var drewText = 0; + parent.ClearedViewport += (_, _) => cleared++; + parent.DrewText += (_, _) => drewText++; + + parent.SetNeedsDraw (); + + parent.Draw (); + + Assert.True (cleared > 0, $"Parent should clear when it itself is dirty (got {cleared})."); + Assert.True (drewText > 0, $"Parent should redraw text when it itself is dirty (got {drewText})."); + + parent.Dispose (); + driver.Dispose (); + } + + /// + /// GIVEN a transparent parent where only the child is dirty + /// WHEN Draw runs + /// THEN the existing Transparent early-return in DoClearViewport still wins, + /// and the child-only path stays correct (no regression to transparency). + /// + [Fact] + public void TransparentParent_ChildOnlyDirty_DoesNotClear () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () + { + Driver = driver, + Width = 20, + Height = 10, + ViewportSettings = ViewportSettingsFlags.Transparent + }; + View child = new () { Width = 10, Height = 5, X = 1, Y = 1 }; + parent.Add (child); + + parent.Layout (); + parent.Draw (); + + var cleared = 0; + parent.ClearedViewport += (_, _) => cleared++; + + child.SetNeedsDraw (); + parent.Draw (); + + Assert.Equal (0, cleared); + + parent.Dispose (); + driver.Dispose (); + } + + /// + /// GIVEN a parent with a Border adornment where only the child is dirty + /// WHEN Draw runs + /// THEN the parent's viewport is not cleared and the parent's own text/content + /// is not redrawn. (Adornments still run independently — they always redraw + /// when the parent is entered; that is by design.) + /// + [Fact] + public void ParentWithBorder_ChildOnlyDirty_ParentDoesNotClear () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () { Driver = driver, Width = 20, Height = 10 }; + parent.Border.Thickness = new Thickness (1); + + View child = new () { Width = 5, Height = 3, X = 2, Y = 2 }; + parent.Add (child); + + parent.Layout (); + parent.Draw (); + + var parentCleared = 0; + var parentDrewText = 0; + parent.ClearedViewport += (_, _) => parentCleared++; + parent.DrewText += (_, _) => parentDrewText++; + + child.SetNeedsDraw (); + parent.Draw (); + + Assert.Equal (0, parentCleared); + Assert.Equal (0, parentDrewText); + + parent.Dispose (); + driver.Dispose (); + } + + /// + /// GIVEN a parent and child rendered to a real driver / IOutput + /// WHEN only the child is dirtied and Draw runs + /// THEN IOutput must still produce non-empty output (the fix does not + /// accidentally suppress the child's render at the IOutput layer). + /// + [Fact] + public void ChildOnlyDirty_StillProducesOutputAtIOutputLayer () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () { Driver = driver, Width = 20, Height = 10 }; + View child = new () { Width = 10, Height = 5, X = 1, Y = 1, Text = "CHILD" }; + parent.Add (child); + + parent.Layout (); + parent.Draw (); + + IOutput output = driver.GetOutput (); + IOutputBuffer buffer = driver.GetOutputBuffer (); + + child.Text = "CHILDX"; + child.SetNeedsDraw (); + parent.Draw (); + + output.Write (buffer); + string ansi = output.GetLastOutput (); + + Assert.False (string.IsNullOrEmpty (ansi)); + + parent.Dispose (); + driver.Dispose (); + } + + /// + /// Review feedback item 2: a transparent parent with TransparentMouse must keep its + /// CachedDrawnRegion populated across child-only redraws. Before the fix, the + /// unconditional `_localDrawContext = new DrawContext()` reset combined with skipping + /// DoDrawText / DoDrawContent on child-only passes meant DoDrawComplete wrote an + /// empty region into CachedDrawnRegion, breaking mouse hit-testing until the next + /// full self-redraw. + /// + [Fact] + public void TransparentMouseParent_ChildOnlyDirty_PreservesCachedDrawnRegion () + { + IDriver driver = CreateTestDriver (40, 20); + + View parent = new () + { + Driver = driver, + Width = 20, + Height = 10, + ViewportSettings = ViewportSettingsFlags.Transparent | ViewportSettingsFlags.TransparentMouse + }; + View child = new () { Width = 10, Height = 5, X = 1, Y = 1 }; + parent.Add (child); + + // Force the parent to actually draw something so CachedDrawnRegion is populated + // (transparent views only cache cells they actually touched). + parent.DrawingContent += (_, e) => + { + parent.FillRect (new Rectangle (0, 0, 5, 3), new System.Text.Rune ('P')); + e.DrawContext?.AddDrawnRectangle (parent.ViewportToScreen (new Rectangle (0, 0, 5, 3))); + }; + + parent.Layout (); + parent.Draw (); + + Region? cachedAfterFullDraw = parent.CachedDrawnRegion; + Assert.NotNull (cachedAfterFullDraw); + Assert.False (cachedAfterFullDraw.GetBounds ().IsEmpty); + + // Child-only invalidation: only the child becomes dirty, parent stays clean. + child.SetNeedsDraw (); + Assert.False (parent.NeedsDraw); + + parent.Draw (); + + // After the child-only redraw, parent's cache must still reflect the previously-drawn + // region. If we naively wiped it, transparent-parent mouse hit-testing would break. + Assert.NotNull (parent.CachedDrawnRegion); + Assert.False (parent.CachedDrawnRegion.GetBounds ().IsEmpty); + + parent.Dispose (); + driver.Dispose (); + } +} diff --git a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs index 3276587901..5a536fe6f2 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Keyboard/KeyBindingsTests.cs @@ -218,6 +218,56 @@ public void Unhandled_Default_KeyBinding_Does_Not_Block_HotKey (string keyName) Assert.True (hotKeyFired); } + // Copilot - Opus 4.6 + /// + /// A view with Visible = false should not have its HotKey invoked via InvokeCommandsBoundToHotKey. + /// + [Fact] + public void HotKey_Visible_False_Does_Not_Invoke () + { + IApplication app = Application.Create (); + app.Begin (new Runnable ()); + + ScopedKeyBindingView view = new (); + app!.TopRunnableView!.Add (view); + + // Sanity check: hotkey works when visible + app.Keyboard.RaiseKeyDownEvent (Key.H); + Assert.True (view.HotKeyCommandInvoked); + + // Now hide the view and try again + view.HotKeyCommandInvoked = false; + view.Visible = false; + app.Keyboard.RaiseKeyDownEvent (Key.H); + Assert.False (view.HotKeyCommandInvoked); + } + + // Copilot - Opus 4.6 + /// + /// A SubView of an invisible SuperView should not have its HotKey invoked. + /// + [Fact] + public void HotKey_Invisible_SuperView_SubView_Does_Not_Invoke () + { + IApplication app = Application.Create (); + app.Begin (new Runnable ()); + + View container = new () { Id = "container" }; + ScopedKeyBindingView subView = new (); + container.Add (subView); + app!.TopRunnableView!.Add (container); + + // Sanity check: hotkey works when container is visible + app.Keyboard.RaiseKeyDownEvent (Key.H); + Assert.True (subView.HotKeyCommandInvoked); + + // Hide the container and try again + subView.HotKeyCommandInvoked = false; + container.Visible = false; + app.Keyboard.RaiseKeyDownEvent (Key.H); + Assert.False (subView.HotKeyCommandInvoked); + } + // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) public class ScopedKeyBindingView : View diff --git a/Tests/UnitTestsParallelizable/Views/CharMapTests.cs b/Tests/UnitTestsParallelizable/Views/CharMapTests.cs index 223f176186..03f50ee264 100644 --- a/Tests/UnitTestsParallelizable/Views/CharMapTests.cs +++ b/Tests/UnitTestsParallelizable/Views/CharMapTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text; using UnitTests; @@ -9,6 +10,53 @@ namespace ViewsTests; /// public class CharMapTests : TestDriverBase { + // Copilot + [Fact] + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void Constructor_Default_Does_Not_Preallocate_Visible_Row_Index () + { + using (new CharMap ()) + { } + + GC.Collect (); + GC.WaitForPendingFinalizers (); + GC.Collect (); + + long before = GC.GetAllocatedBytesForCurrentThread (); + using CharMap charMap = new (); + long allocated = GC.GetAllocatedBytesForCurrentThread () - before; + + Assert.True (allocated < 1_000_000, $"Expected CharMap construction to allocate less than 1 MB, but allocated {allocated:N0} bytes."); + + int maxRow = UnicodeRange.Ranges.Max (r => r.End) / 16; + int surrogateRowStart = 0xD800 / 16; + int surrogateRowEnd = 0xDFFF / 16; + int surrogateRows = Math.Min (maxRow, surrogateRowEnd) - surrogateRowStart + 1; + int expectedContentHeight = maxRow + 1 - surrogateRows + 1; + + Assert.Equal (expectedContentHeight, charMap.GetContentHeight ()); + } + + // Copilot + [Fact] + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void ShowUnicodeCategory_Rebuilds_Filtered_Index_And_Can_Clear_Filter () + { + using CharMap charMap = new () { SelectedCodePoint = 'a' }; + int defaultContentHeight = charMap.GetContentHeight (); + + charMap.ShowUnicodeCategory = UnicodeCategory.UppercaseLetter; + + Assert.True (charMap.GetContentHeight () < defaultContentHeight); + Assert.Equal (UnicodeCategory.UppercaseLetter, CharUnicodeInfo.GetUnicodeCategory (charMap.SelectedCodePoint)); + + charMap.ShowUnicodeCategory = null; + + Assert.Equal (defaultContentHeight, charMap.GetContentHeight ()); + } + /// /// Verifies that is raised when changes. /// diff --git a/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs b/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs index 5c5ad9a31e..a9ab309436 100644 --- a/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs +++ b/Tests/UnitTestsParallelizable/Views/CheckBoxTests.cs @@ -263,6 +263,15 @@ public void Constructors_Defaults () Assert.Equal (new Rectangle (3, 4, 6, 1), ckb.Frame); } + // Copilot + [Fact] + public void Default_CheckState_Glyphs_Are_Distinct () + { + Assert.Equal ((System.Text.Rune)'☑', Glyphs.CheckStateChecked); + Assert.Equal ((System.Text.Rune)'☐', Glyphs.CheckStateUnChecked); + Assert.Equal ((System.Text.Rune)'⬛', Glyphs.CheckStateNone); + } + [Fact] public void LeftButtonReleased_Activates () { diff --git a/Tests/UnitTestsParallelizable/Views/DialogTests.Generic.cs b/Tests/UnitTestsParallelizable/Views/DialogTests.Generic.cs index 5aa590967e..6a608a2c93 100644 --- a/Tests/UnitTestsParallelizable/Views/DialogTests.Generic.cs +++ b/Tests/UnitTestsParallelizable/Views/DialogTests.Generic.cs @@ -700,7 +700,7 @@ public void GenericInherits_Properties () Assert.Equal (AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, dialog.ButtonAlignmentModes); Assert.Equal (LineStyle.Heavy, dialog.BorderStyle); Assert.Equal (ShadowStyles.Transparent, dialog.ShadowStyle); - Assert.Equal (ViewArrangement.Overlapped, dialog.Arrangement); + Assert.Equal (ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped, dialog.Arrangement); dialog.Dispose (); } diff --git a/Tests/UnitTestsParallelizable/Views/DialogTests.cs b/Tests/UnitTestsParallelizable/Views/DialogTests.cs index 8257e2f590..f5d5602556 100644 --- a/Tests/UnitTestsParallelizable/Views/DialogTests.cs +++ b/Tests/UnitTestsParallelizable/Views/DialogTests.cs @@ -163,7 +163,7 @@ public void Constructor_Initializes_DefaultValues () Assert.Empty (dialog.Buttons); Assert.Null (dialog.Result); Assert.True (dialog.Canceled); // Canceled is true when Result is null - Assert.Equal (ViewArrangement.Overlapped, dialog.Arrangement); + Assert.Equal (ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped, dialog.Arrangement); dialog.Dispose (); } @@ -195,7 +195,7 @@ public void Arrangement_Default () { Dialog dialog = new (); - Assert.Equal (ViewArrangement.Overlapped, dialog.Arrangement); + Assert.Equal (ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped, dialog.Arrangement); dialog.Dispose (); } @@ -415,12 +415,12 @@ public void ShadowStyle_Can_Be_Changed () // Copilot [Fact] - public void SchemeName_IsBase_WhenNotRunning () + public void SchemeName_IsDialog_WhenNotRunning () { - // When a Dialog is not running, it should use the Base scheme (not Dialog) + // A Dialog uses the Dialog scheme by default, set in the constructor Dialog dialog = new (); - Assert.Equal (SchemeManager.SchemesToSchemeName (Schemes.Base), dialog.SchemeName); + Assert.Equal (SchemeManager.SchemesToSchemeName (Schemes.Dialog), dialog.SchemeName); dialog.Dispose (); } @@ -452,6 +452,43 @@ void AppOnIteration (object? sender, EventArgs e) } } + // Copilot + [Fact] + public void Custom_SchemeName_And_Arrangement_Are_Not_Overwritten_By_Run () + { + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + using Dialog dialog = new (); + string customSchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base)!; + const ViewArrangement customArrangement = ViewArrangement.Overlapped; + + dialog.SchemeName = customSchemeName; + dialog.Arrangement = customArrangement; + + string? schemeNameWhileRunning = null; + ViewArrangement arrangementWhileRunning = default; + + app.Iteration += AppOnIteration; + app.Run (dialog); + app.Iteration -= AppOnIteration; + + Assert.Equal (customSchemeName, schemeNameWhileRunning); + Assert.Equal (customArrangement, arrangementWhileRunning); + Assert.Equal (customSchemeName, dialog.SchemeName); + Assert.Equal (customArrangement, dialog.Arrangement); + + return; + + void AppOnIteration (object? sender, EventArgs e) + { + schemeNameWhileRunning = dialog.SchemeName; + arrangementWhileRunning = dialog.Arrangement; + app.Iteration -= AppOnIteration; + app.RequestStop (); + } + } + [Fact] public void Text_Property () { diff --git a/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs b/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs index 5792502d1e..b368361511 100644 --- a/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TabView/TabsFanOutDiagnosticTests.cs @@ -57,6 +57,7 @@ public void Track (View view, string label) view.DrawComplete += (_, _) => counts.DrawComplete++; view.ClearedViewport += (_, _) => counts.ClearedViewport++; view.DrawingContent += (_, _) => counts.DrawingContent++; + view.DrawingText += (_, _) => counts.DrawingText++; view.FrameChanged += (_, _) => counts.FrameChanged++; } @@ -74,14 +75,14 @@ public string Report (string title) { StringBuilder sb = new (); sb.AppendLine (title); - sb.AppendLine (" view laidOut drawComplete clearedViewport drawingContent frameChanged"); + sb.AppendLine (" view laidOut drawComplete clearedViewport drawingContent drawingText frameChanged"); foreach ((View view, string label) in _order) { Counts c = _counts [view]; sb.AppendLine ( - $" {label,-26} {c.SubViewsLaidOut,7} {c.DrawComplete,12} {c.ClearedViewport,15} {c.DrawingContent,14} {c.FrameChanged,12}"); + $" {label,-26} {c.SubViewsLaidOut,7} {c.DrawComplete,12} {c.ClearedViewport,15} {c.DrawingContent,14} {c.DrawingText,11} {c.FrameChanged,12}"); } return sb.ToString (); @@ -93,6 +94,7 @@ public sealed class Counts public int DrawComplete; public int ClearedViewport; public int DrawingContent; + public int DrawingText; public int FrameChanged; public void Reset () @@ -101,6 +103,7 @@ public void Reset () DrawComplete = 0; ClearedViewport = 0; DrawingContent = 0; + DrawingText = 0; FrameChanged = 0; } } @@ -269,6 +272,7 @@ public void Diagnostic_ActiveTabScroll_DrawEvents_OnEachTab () int inactiveDraws = 0; int inactiveClears = 0; int inactiveContentDraws = 0; + int inactiveTextDraws = 0; for (var i = 1; i < TabCount; i++) { @@ -276,31 +280,27 @@ public void Diagnostic_ActiveTabScroll_DrawEvents_OnEachTab () inactiveDraws += c.DrawComplete; inactiveClears += c.ClearedViewport; inactiveContentDraws += c.DrawingContent; + inactiveTextDraws += c.DrawingText; } output.WriteLine ($"Active DrawComplete: {activeCounts.DrawComplete}"); output.WriteLine ($"Sum of inactive DrawComplete: {inactiveDraws}"); output.WriteLine ($"Sum of inactive ClearedViewport: {inactiveClears}"); output.WriteLine ($"Sum of inactive DrawingContent: {inactiveContentDraws}"); - - // CURRENT BEHAVIOR (issue #4973): inactive tabs receive full draw passes when active scrolls. - // DrawComplete is the widget-agnostic signal — it always fires once per call to Draw(), - // regardless of whether the view overrides OnClearingViewport / OnDrawingContent (as Code does). - // After #4973 is fixed, flip this to `Assert.Equal (0, inactiveDraws)`. - Assert.True ( - inactiveDraws > 0, - $"Documents issue #4973 draw fan-out: inactive_total DrawComplete={inactiveDraws}, " + - $"active={activeCounts.DrawComplete}. Flip to Assert.Equal(0, inactiveDraws) after #4973 fix."); - - // ClearedViewport / DrawingContent on Code instances will be 0 because Code overrides those - // handlers and returns true (suppressing the events). The Tabs container still fires them, - // so they remain useful as a secondary diagnostic — recorded in the report above but not - // asserted at the per-tab level. + output.WriteLine ($"Sum of inactive DrawingText: {inactiveTextDraws}"); + + // Issue #5358 fix: inactive tabs' NeedsDraw is no longer set as a side-effect + // of the parent entering Draw(). DrawingText is the right metric here because + // it's gated on NeedsDraw inside DoDrawText — and Code does NOT override + // OnDrawingText (unlike OnClearingViewport / OnDrawingContent which Code + // intercepts, suppressing those events). DrawComplete fires unconditionally + // for clip-exclusion bookkeeping, so it is informational only. + Assert.Equal (0, inactiveTextDraws); + + // Issue #5358 fix: the Tabs container itself no longer clears its viewport + // when only a child tab is dirty. Before the fix this was > 0; after, == 0. ViewActivityCounters.Counts tabsCounts = counters.Get (tabs); - Assert.True ( - tabsCounts.ClearedViewport > 0, - $"Tabs container must register clear activity (got {tabsCounts.ClearedViewport}); " + - "this confirms the lower-level draw pipeline is being exercised."); + Assert.Equal (0, tabsCounts.ClearedViewport); root.Dispose (); driver.Dispose (); @@ -349,33 +349,30 @@ public void Diagnostic_TabbedFanOut_ComparedTo_SingleViewBaseline () ViewActivityCounters.Counts tabActiveCounts = tabCounters.Get (active); output.WriteLine (tabCounters.Report ("Tabbed scenario (5 scrolls of active tab):")); - int totalTabDraws = tabActiveCounts.DrawComplete; + int totalTabTextDraws = tabActiveCounts.DrawingText; int totalTabLayouts = tabActiveCounts.SubViewsLaidOut; for (var i = 1; i < TabCount; i++) { - totalTabDraws += tabCounters.Get (codes [i]).DrawComplete; + totalTabTextDraws += tabCounters.Get (codes [i]).DrawingText; totalTabLayouts += tabCounters.Get (codes [i]).SubViewsLaidOut; } - double drawFanOut = singleCounts.DrawComplete == 0 ? double.NaN : (double)totalTabDraws / singleCounts.DrawComplete; + double textDrawFanOut = singleCounts.DrawingText == 0 ? double.NaN : (double)totalTabTextDraws / singleCounts.DrawingText; double layoutFanOut = singleCounts.SubViewsLaidOut == 0 ? double.NaN : (double)totalTabLayouts / singleCounts.SubViewsLaidOut; - output.WriteLine ($"Tabbed total draws / single draws = {totalTabDraws} / {singleCounts.DrawComplete} = {drawFanOut:F2}"); - output.WriteLine ($"Tabbed total layouts / single layouts = {totalTabLayouts} / {singleCounts.SubViewsLaidOut} = {layoutFanOut:F2}"); + output.WriteLine ($"Tabbed total text draws / single text draws = {totalTabTextDraws} / {singleCounts.DrawingText} = {textDrawFanOut:F2}"); + output.WriteLine ($"Tabbed total layouts / single layouts = {totalTabLayouts} / {singleCounts.SubViewsLaidOut} = {layoutFanOut:F2}"); - Assert.True (singleCounts.DrawComplete > 0, "Single-Code baseline must record draw activity."); - Assert.True (tabActiveCounts.DrawComplete > 0, "Active tab in tabbed scenario must record draw activity."); + Assert.True (singleCounts.DrawingText > 0, "Single-Code baseline must record text-draw activity (NeedsDraw-gated)."); + Assert.True (tabActiveCounts.DrawingText > 0, "Active tab in tabbed scenario must record text-draw activity."); - // CURRENT BEHAVIOR: draw fan-out still exists, but layout fan-out should now match baseline. - Assert.True ( - drawFanOut > 1.0, - $"Documents issue #4973: tabbed draw fan-out ({drawFanOut:F2}x) exceeds single-Code baseline. " + - "After fix, drawFanOut should approach 1.0."); + // Issue #5358 fix: tabbed text-draw fan-out is now at parity with the single-Code baseline. + // DrawingText is gated on NeedsDraw, so it only fires on tabs whose NeedsDraw was set — + // which is just the active tab after the fix. + Assert.Equal (1.0, textDrawFanOut); - Assert.Equal ( - 1.0, - layoutFanOut); + Assert.Equal (1.0, layoutFanOut); singleRoot.Dispose (); tabRoot.Dispose (); @@ -420,10 +417,9 @@ public void Diagnostic_TransparentInactiveTab_StillObservable () { ViewActivityCounters.Counts c = counters.Get (codes [i]); - // Copilot: Lock in current fan-out behavior. After #4973, this should likely become == 0. - Assert.True ( - c.DrawComplete > 0, - $"Tab {i + 1} should currently still record DrawComplete even with Transparent (got {c.DrawComplete}). Update this expectation after #4973."); + // Issue #5358 fix: transparent inactive peers also stay out of NeedsDraw-gated work. + // DrawingText is the right per-Code metric (Code intercepts ClearedViewport / DrawingContent). + Assert.Equal (0, c.DrawingText); } root.Dispose (); diff --git a/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs index 1e92fd9fb5..3c5056f0f7 100644 --- a/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TreeViewTests.cs @@ -217,6 +217,483 @@ public void Command_Toggle_ExpandCollapse () tree.Dispose (); } + // Copilot + [Fact] + public void CheckboxMode_Space_Toggles_Checked_State_Not_Expansion () + { + TreeView tree = CreateTree (out Factory f, out Car car1, out Car car2); + tree.CheckboxMode = true; + tree.SelectedObject = f; + + List checkedObjects = []; + tree.CheckedChanged += (_, e) => + { + checkedObjects.Add (e.Object!); + }; + + tree.NewKeyDownEvent (Key.Space); + + Assert.False (tree.IsExpanded (f)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (f)); + + // Propagation: f and all its children should be checked + Assert.Equal (CheckState.Checked, tree.GetCheckState (car1)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (car2)); + + // Events fire for each node that changed + Assert.Contains (f, checkedObjects); + Assert.Contains (car1, checkedObjects); + Assert.Contains (car2, checkedObjects); + } + + // Copilot + [Fact] + public void CheckboxMode_Draws_Checkbox_Glyphs_And_Indeterminate_Parent () + { + IDriver driver = CreateTestDriver (); + TreeView tree = CreateTree (out Factory f, out Car car1, out _); + tree.Driver = driver; + tree.CheckboxMode = true; + tree.Frame = new Rectangle (0, 0, 20, 3); + tree.Expand (f); + + tree.SetChecked (car1, CheckState.Checked); + tree.Draw (); + + DriverAssert.AssertDriverContentsAre ($""" + └-{Glyphs.CheckStateNone} Factory + ├─{Glyphs.CheckStateChecked} + └─{Glyphs.CheckStateUnChecked} + """, + output, + driver); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_MouseClick_OnCheckbox_Toggles_Check () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + root.Children.Add (new TreeNode { Text = "Child1" }); + tree.AddObject (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Layout (no border): └ + ☐ · R o o t + // Pos: 0 1 2 3 4 5 6 7 + // Checkbox is at screen position x=2 + + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (2, 0))); + + Assert.False (tree.IsExpanded (root)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void MouseClick_OnExpandSymbol_Expands_Node () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + + TreeNode root = new () { Text = "Root" }; + root.Children.Add (new TreeNode { Text = "Child1" }); + tree.AddObject (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Layout (no border): └ + R o o t + // Pos: 0 1 2 3 4 5 + // Expand symbol at position 1 + + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (1, 0))); + + Assert.True (tree.IsExpanded (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_MouseClick_OnExpandSymbol_Expands_Not_Toggles_Check () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + root.Children.Add (new TreeNode { Text = "Child1" }); + tree.AddObject (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Layout (no border): └ + ☐ · R o o t + // Pos: 0 1 2 3 4 5 6 7 + // Expand symbol at position 1, checkbox at position 2 + + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (1, 0))); + + Assert.True (tree.IsExpanded (root)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_TriState_Parent_Reflects_Children () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + + // Initially all unchecked + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + + // Check one child → parent becomes indeterminate + tree.SetChecked (child1, CheckState.Checked); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child2)); + + // Check all children → parent becomes checked + tree.SetChecked (child2, CheckState.Checked); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + // Uncheck one child → parent becomes indeterminate again + tree.SetChecked (child1, CheckState.UnChecked); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); + + // Uncheck all children → parent becomes unchecked + tree.SetChecked (child2, CheckState.UnChecked); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_Toggling_Parent_Propagates_To_Children () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + + // Toggle parent ON → all children become checked + tree.SetChecked (root, CheckState.Checked); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (child2)); + + // Toggle parent OFF → all children become unchecked + tree.SetChecked (root, CheckState.UnChecked); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child2)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_Toggle_Parent_When_All_Children_Checked_Unchecks_All () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Check all children so parent derives as Checked + tree.SetChecked (child1, CheckState.Checked); + tree.SetChecked (child2, CheckState.Checked); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + // Select root and press Space to toggle it OFF + tree.SelectedObject = root; + tree.NewKeyDownEvent (Key.Space); + + // Parent and all children should now be unchecked + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child2)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_MouseClick_Uncheck_Parent_When_Children_Checked () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 20, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Check all children so parent derives as Checked + tree.SetChecked (child1, CheckState.Checked); + tree.SetChecked (child2, CheckState.Checked); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + // Click on root's checkbox glyph (x=2 for expanded root with branch lines) + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (2, 0))); + + // Parent and all children should now be unchecked + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child2)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_MouseClick_Children_Then_Uncheck_Parent () + { + // Reproduce exact user scenario: click child1 cb, click child2 cb, then click root cb + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 30, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Layout with ShowBranchLines=true (default), CheckboxMode=true: + // GetLinePrefix yields: for each parent 2 elements (line + space), then 1 junction element. + // IsHitOnCheckbox = GetLinePrefix().Count() + GetExpandableSymbol().GetColumns() + // Root (depth=0): prefix count=1 (junction only), expand=1 col → checkbox at x=2 + // Children (depth=1): prefix count=3 (parent line+space + junction), expand=1 col → checkbox at x=4 + + // Click child1's checkbox at (4, 1) + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (4, 1))); + Assert.Equal (CheckState.Checked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); // indeterminate + + // Click child2's checkbox at (4, 2) + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (4, 2))); + Assert.Equal (CheckState.Checked, tree.GetCheckState (child2)); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); // all children checked + + // Click root's checkbox at (2, 0) to uncheck everything + app.InjectSequence (InputInjectionExtensions.LeftButtonClick (new Point (2, 0))); + + // Parent and all children should now be unchecked + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child1)); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (child2)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_Collapsed_Parent_Shows_Indeterminate_When_One_Child_Checked () + { + // When only one child is checked and parent is collapsed, parent should show + // indeterminate (None), not Checked or UnChecked. + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 30, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Check only child1 - parent should be indeterminate + tree.SetChecked (child1, CheckState.Checked); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); + + // Collapse root - should STILL show indeterminate + tree.Collapse (root); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); + + // Expand again - should still be indeterminate + tree.Expand (root); + Assert.Equal (CheckState.None, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_Collapsed_Parent_Shows_Checked_When_All_Children_Checked () + { + // When all children are checked and parent is collapsed, parent should show Checked. + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 30, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // Check both children + tree.SetChecked (child1, CheckState.Checked); + tree.SetChecked (child2, CheckState.Checked); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + // Collapse root - should STILL show Checked + tree.Collapse (root); + Assert.Equal (CheckState.Checked, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_Collapsed_Parent_Shows_UnChecked_When_No_Children_Checked () + { + // When no children are checked and parent is collapsed, parent should show UnChecked. + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + TreeView tree = new () { Width = 30, Height = 5 }; + tree.CheckboxMode = true; + + TreeNode root = new () { Text = "Root" }; + TreeNode child1 = new () { Text = "C1" }; + TreeNode child2 = new () { Text = "C2" }; + root.Children.Add (child1); + root.Children.Add (child2); + tree.AddObject (root); + tree.Expand (root); + + Runnable top = new (); + top.Add (tree); + app.Begin (top); + app.LayoutAndDraw (); + + // No children checked - parent is UnChecked + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + + // Collapse root - should STILL show UnChecked + tree.Collapse (root); + Assert.Equal (CheckState.UnChecked, tree.GetCheckState (root)); + + top.Dispose (); + app.Dispose (); + } + [Fact] public void ContentWidth_BiggerAfterExpand () { @@ -896,5 +1373,57 @@ private TreeView CreateTree (out Factory factory1, out Car car1, out Car return tree; } + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_SetChecked_Handles_Cyclic_TreeBuilder () + { + // A tree builder that creates a cycle: A -> B -> A + object a = "A"; + object b = "B"; + + Dictionary graph = new () + { + { a, [b] }, + { b, [a] } + }; + + TreeView tree = new (new DelegateTreeBuilder ( + o => graph.TryGetValue (o, out object []? children) ? children : [], + _ => true)); + tree.CheckboxMode = true; + tree.AddObject (a); + + // Should not stack overflow - cycle protection should prevent infinite recursion + Exception? ex = Record.Exception (() => tree.SetChecked (a, CheckState.Checked)); + Assert.Null (ex); + } + + // Copilot - Opus 4.6 + [Fact] + public void CheckboxMode_GetCheckState_Handles_Cyclic_TreeBuilder () + { + // A tree builder that creates a cycle: A -> B -> A + object a = "A"; + object b = "B"; + + Dictionary graph = new () + { + { a, [b] }, + { b, [a] } + }; + + TreeView tree = new (new DelegateTreeBuilder ( + o => graph.TryGetValue (o, out object []? children) ? children : [], + _ => true)); + tree.CheckboxMode = true; + tree.AddObject (a); + + tree.SetChecked (a, CheckState.Checked); + + // Should not stack overflow when deriving state + Exception? ex = Record.Exception (() => tree.GetCheckState (a)); + Assert.Null (ex); + } + #endregion } diff --git a/Tests/UnitTestsParallelizable/Views/WizardTests.cs b/Tests/UnitTestsParallelizable/Views/WizardTests.cs index dcd7039af5..f068400000 100644 --- a/Tests/UnitTestsParallelizable/Views/WizardTests.cs +++ b/Tests/UnitTestsParallelizable/Views/WizardTests.cs @@ -2,6 +2,141 @@ namespace ViewsTests; public class WizardTests { + // Copilot + [Fact] + public void Enter_In_ListView_Moves_To_Next_Step () + { + Wizard wizard = new (); + WizardStep step1 = new () { Title = "Step 1" }; + ListView listView = new () { Width = 10, Height = 1, Source = new ListWrapper (["One"]) }; + WizardStep step2 = new () { Title = "Step 2" }; + step1.Add (listView); + wizard.AddStep (step1); + wizard.AddStep (step2); + wizard.BeginInit (); + wizard.EndInit (); + + listView.SetFocus (); + Assert.True (listView.HasFocus); + + wizard.NewKeyDownEvent (Key.Enter); + + Assert.Equal (step2, wizard.CurrentStep); + Assert.False (wizard.StopRequested); + + wizard.Dispose (); + } + + // Copilot + [Fact] + public void Enter_In_TextField_Moves_To_Next_Step () + { + Wizard wizard = new (); + WizardStep step1 = new () { Title = "Step 1" }; + TextField textField = new () { Width = Dim.Fill () }; + WizardStep step2 = new () { Title = "Step 2" }; + step1.Add (textField); + wizard.AddStep (step1); + wizard.AddStep (step2); + wizard.BeginInit (); + wizard.EndInit (); + + textField.SetFocus (); + Assert.True (textField.HasFocus); + + wizard.NewKeyDownEvent (Key.Enter); + + Assert.Equal (step2, wizard.CurrentStep); + Assert.False (wizard.StopRequested); + + wizard.Dispose (); + } + + // Copilot + [Fact] + public void Enter_In_TextField_On_Last_Step_Accepts_Wizard () + { + Wizard wizard = new (); + WizardStep step1 = new () { Title = "Step 1" }; + TextField textField = new () { Width = Dim.Fill () }; + step1.Add (textField); + wizard.AddStep (step1); + wizard.BeginInit (); + wizard.EndInit (); + + var wizardAccepted = 0; + + wizard.Accepted += (_, e) => { wizardAccepted++; }; + + textField.SetFocus (); + Assert.True (textField.HasFocus); + + wizard.NewKeyDownEvent (Key.Enter); + + Assert.Equal (1, wizardAccepted); + Assert.Equal (step1, wizard.CurrentStep); + + wizard.Dispose (); + } + + // Copilot + [Fact] + public void MovingNext_Cancel_Prevents_Navigation_And_Does_Not_Bubble () + { + Wizard wizard = new (); + WizardStep step1 = new () { Title = "Step 1" }; + WizardStep step2 = new () { Title = "Step 2" }; + wizard.AddStep (step1); + wizard.AddStep (step2); + wizard.BeginInit (); + wizard.EndInit (); + + wizard.MovingNext += (_, args) => { args.Cancel = true; }; + + var wizardAccepting = 0; + wizard.Accepting += (_, _) => wizardAccepting++; + + // Act + wizard.NextFinishButton.InvokeCommand (Command.Accept); + + // Assert - step should not change + Assert.Equal (step1, wizard.CurrentStep); + + // Assert - accept should not bubble to wizard (dialog would close) + Assert.Equal (0, wizardAccepting); + + wizard.Dispose (); + } + + // Copilot + [Fact] + public void StepChanging_Cancel_Prevents_Navigation_And_Does_Not_Bubble () + { + Wizard wizard = new (); + WizardStep step1 = new () { Title = "Step 1" }; + WizardStep step2 = new () { Title = "Step 2" }; + wizard.AddStep (step1); + wizard.AddStep (step2); + wizard.BeginInit (); + wizard.EndInit (); + + wizard.StepChanging += (_, args) => { args.Handled = true; }; + + var wizardAccepting = 0; + wizard.Accepting += (_, _) => wizardAccepting++; + + // Act + wizard.NextFinishButton.InvokeCommand (Command.Accept); + + // Assert - step should not change + Assert.Equal (step1, wizard.CurrentStep); + + // Assert - accept should not bubble to wizard (dialog would close) + Assert.Equal (0, wizardAccepting); + + wizard.Dispose (); + } + #region Constructor Tests [Fact] @@ -15,9 +150,6 @@ public void Constructor_Initializes_Properties () Assert.NotNull (wizard.BackButton); Assert.NotNull (wizard.NextFinishButton); Assert.Null (wizard.CurrentStep); - Assert.Equal (LineStyle.Dotted, wizard.BorderStyle); - Assert.False (wizard.Arrangement.HasFlag (ViewArrangement.Movable)); - Assert.False (wizard.Arrangement.HasFlag (ViewArrangement.Resizable)); } [Fact] @@ -1014,43 +1146,81 @@ public void Enabling_Step_Updates_Navigation () #endregion Enabled State Tests - // Claude - Opus 4.5 - // Behavior documented in docfx/docs/command.md - View Command Behaviors table - // This test verifies current behavior which may change per issue #4473 + #region Regression Tests + + // Copilot [Fact] - public void Wizard_NextButton_Accept_AdvancesStep () + public void Enter_In_TextField_On_Last_Step_Does_Not_Navigate () { + // Regression: OnAccepting must call base on last step so Dialog.OnAccepting + // can call RequestStop() in modal mode. Without this, Enter from content views on the + // last step won't close the wizard. We verify OnAccepting doesn't intercept (navigate) + // on the last step, and that the accept propagates to Accepted (proving base chain ran). Wizard wizard = new (); WizardStep step1 = new () { Title = "Step 1" }; WizardStep step2 = new () { Title = "Step 2" }; + TextField textField = new () { Width = Dim.Fill () }; + step2.Add (textField); wizard.AddStep (step1); wizard.AddStep (step2); wizard.BeginInit (); wizard.EndInit (); + wizard.GoNext (); + Assert.Equal (step2, wizard.CurrentStep); - // Wizard uses buttons internally to navigate - // Verify the wizard is set up correctly - Assert.Equal (step1, wizard.CurrentStep); + var accepted = 0; + wizard.Accepted += (_, _) => accepted++; + + // MovingNext should NOT be raised on last step + var movingNextRaised = false; + wizard.MovingNext += (_, _) => movingNextRaised = true; + + textField.SetFocus (); + Assert.True (textField.HasFocus); + + wizard.NewKeyDownEvent (Key.Enter); + + // Should stay on last step (not navigate) + Assert.Equal (step2, wizard.CurrentStep); + // MovingNext should not fire on last step + Assert.False (movingNextRaised); + // Accepted should fire (base chain processed it) + Assert.Equal (1, accepted); wizard.Dispose (); } - // Claude - Opus 4.5 - // Behavior documented in docfx/docs/command.md - View Command Behaviors table - // This test verifies current behavior which may change per issue #4473 + // Copilot [Fact] - public void Wizard_FinishButton_Accept_Completes () + public void MovingBack_Cancel_Does_Not_Bubble_Accept () { + // Bug: BackBtnOnAccepting doesn't set e.Handled when MovingBack is cancelled, + // so accept bubbles up to the dialog and may close it. Wizard wizard = new (); WizardStep step1 = new () { Title = "Step 1" }; + WizardStep step2 = new () { Title = "Step 2" }; wizard.AddStep (step1); + wizard.AddStep (step2); wizard.BeginInit (); wizard.EndInit (); + wizard.GoNext (); + Assert.Equal (step2, wizard.CurrentStep); - // Wizard uses buttons internally to complete - // Verify the wizard is set up correctly - Assert.Equal (step1, wizard.CurrentStep); + wizard.MovingBack += (_, args) => { args.Cancel = true; }; + + var wizardAccepting = 0; + wizard.Accepting += (_, _) => wizardAccepting++; + + // Act + wizard.BackButton.InvokeCommand (Command.Accept); + + // Assert - step should not change + Assert.Equal (step2, wizard.CurrentStep); + // Assert - accept should not bubble to wizard + Assert.Equal (0, wizardAccepting); wizard.Dispose (); } + + #endregion Regression Tests } diff --git a/docfx/docs/command.md b/docfx/docs/command.md index 05deb01067..ff3b9c2787 100644 --- a/docfx/docs/command.md +++ b/docfx/docs/command.md @@ -627,7 +627,7 @@ When an inner activates (via click/space), th | **** | Not bound | Not bound | | Handled by SubViews | Handled by SubViews | Handled by SubViews | Not bound | | **** | Handled by SubViews | Handled by SubViews | | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | | **** | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | -| **** | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | +| **** | Handled by SubViews | (advances step or finishes) | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | | **** | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | | **** | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | Handled by SubViews | | **** | Handled by SubViews | Handled by SubViews | | OnMouseEvent (toggle) | Handled by SubViews | Handled by SubViews | Handled by SubViews | diff --git a/docfx/docs/tableview.md b/docfx/docs/tableview.md index b0fc21ce40..629b1d6b13 100644 --- a/docfx/docs/tableview.md +++ b/docfx/docs/tableview.md @@ -309,6 +309,8 @@ tv.Table = src; Arrow Left/Right collapse/expand nodes when the tree column has focus. +> **Note:** `TreeTableSource` renders tree structure (branch lines, expand/collapse symbols) in column 0 but does **not** render checkboxes from `TreeView.CheckboxMode`. To add checkboxes to a tree-table, wrap the `TreeTableSource` with a `CheckBoxTableSourceWrapperByIndex` or `CheckBoxTableSourceWrapperByObject` (see [Checkbox Columns](#checkbox-columns) above). + --- ## Events diff --git a/docfx/docs/treeview.md b/docfx/docs/treeview.md index 27e8b73d1b..439d4382ab 100644 --- a/docfx/docs/treeview.md +++ b/docfx/docs/treeview.md @@ -36,6 +36,7 @@ Both share the same rendering, navigation, selection, and command behavior. - [Multi-Select](#multi-select) - [Letter-Based Navigation](#letter-based-navigation) - [Filtering](#filtering) +- [Checkbox Mode](#checkbox-mode) - [Dynamic Updates](#dynamic-updates) - [See Also](#see-also) @@ -187,7 +188,7 @@ TreeView integrates with the Terminal.Gui [command system](command.md). Input fl | Key | Command | Behavior | |-----|---------|----------| | **Enter** | `Command.Accept` | Raises `Accepting`/`Accepted` (CWP) | -| **Space** | `Command.Activate` | Raises `Activating`/`Activated`; toggles expand/collapse | +| **Space** | `Command.Activate` | Raises `Activating`/`Activated`; toggles expand/collapse (or toggles checkbox when `CheckboxMode` is enabled) | | **→** | `Command.Expand` | Expand selected node | | **Ctrl+→** | `Command.ExpandAll` | Expand node and all descendants | | **←** | `Command.Collapse` | Collapse selected node, or navigate to parent node | @@ -211,7 +212,7 @@ TreeView integrates with the Terminal.Gui [command system](command.md). Input fl | Input | Behavior | |-------|----------| -| **Single click** | Select the clicked node. If the click lands on the expand/collapse symbol (`+`/`-`), toggle expansion. | +| **Single click** | Select the clicked node. If the click lands on the expand/collapse symbol (`+`/`-`), toggle expansion. If `CheckboxMode` is enabled and the click lands on the checkbox glyph, toggle check state. | | **Double click** | Raises `Command.Accept` → fires `Accepting`/`Accepted` (CWP). Also toggles expand/collapse. | | **Wheel up/down** | Scroll viewport vertically | | **Wheel left/right** | Scroll viewport horizontally | @@ -390,6 +391,74 @@ When a filter is active, parent nodes leading to matches remain visible even if Set `Filter` to `null` to remove filtering. +## Checkbox Mode + +To enable built-in checkboxes, set `CheckboxMode = true`. Each node displays a checkbox glyph between the expand symbol and the text: + +```csharp +tree.CheckboxMode = true; +``` + +This produces: + +``` +├-☐ Root1 +│ ├─☐ Child1.1 +│ └─☐ Child1.2 +└-☐ Root2 +``` + +### Tri-State Behavior + +TreeView implements standard tri-state checkbox semantics: + +| Parent State | Meaning | +|-------------|---------| +| **Unchecked** | No descendants are checked | +| **Checked** | All descendants are checked | +| **Indeterminate** | Some (but not all) descendants are checked | + +- **Toggling a parent** propagates the new state to all descendants. +- **Toggling a leaf** updates ancestor states automatically via derivation. +- The indeterminate state is always derived — it cannot be set directly by the user. + +### Interacting with Checkboxes + +| Input | Behavior | +|-------|----------| +| **Space** | Toggle the check state of the selected node | +| **Click on checkbox glyph** | Toggle the check state of the clicked node | + +### Programmatic Access + +```csharp +// Get the effective check state (derived for parents) +CheckState state = tree.GetCheckState (node); + +// Set check state (propagates to descendants) +tree.SetChecked (node, CheckState.Checked); + +// Get all checked objects (includes parents derived as checked from children) +IEnumerable checkedObjects = tree.GetCheckedObjects (); +``` + +### CheckedChanged Event + +Fires for each node whose state changes (including descendants during propagation): + +```csharp +tree.CheckedChanged += (sender, e) => + { + // e.Object — the node whose state changed + // e.OldValue — previous CheckState + // e.NewValue — new CheckState + }; +``` + +### CheckboxMode vs. CheckBoxTableSourceWrapper + +When using `TreeTableSource` to display a tree inside a `TableView`, the checkbox glyphs from `CheckboxMode` are **not** rendered in the table column. To add checkboxes to a tree-table, wrap the `TreeTableSource` with a `CheckBoxTableSourceWrapperByIndex` or `CheckBoxTableSourceWrapperByObject` (see [TableView — Checkbox Columns](tableview.md#checkbox-columns)). + ## Dynamic Updates TreeView caches the expanded tree structure. After modifying nodes at runtime: