From 7c3c2a07ff2185469681427e5c00f7d7ea451cd2 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 4 Mar 2026 15:18:53 -0700 Subject: [PATCH 1/4] Fixes #4791 - Refactor ListView drawing to use Viewport consistently Refactored OnDrawingContent to use Viewport properties (X, Y, Width, Height) throughout, replacing the local Rectangle variable. Improved safety by checking item bounds before calling IsMarked. Replaced local focus variable with direct HasFocus property usage. Updated drawing loops and Source.Render calls to use Viewport dimensions, and removed redundant variables for clarity. These changes ensure rendering is always consistent with the current viewport and improve code readability. --- .../Views/ListView/ListView.Drawing.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/Views/ListView/ListView.Drawing.cs b/Terminal.Gui/Views/ListView/ListView.Drawing.cs index 980619c9fc..1dab8d1a6a 100644 --- a/Terminal.Gui/Views/ListView/ListView.Drawing.cs +++ b/Terminal.Gui/Views/ListView/ListView.Drawing.cs @@ -11,18 +11,14 @@ protected override bool OnDrawingContent (DrawContext? context) } var current = Attribute.Default; - Move (0, 0); - Rectangle f = Viewport; int item = Viewport.Y; - bool focused = HasFocus; int col = ShowMarks ? 2 : 0; - int start = Viewport.X; + Move (0, 0); - for (var row = 0; row < f.Height; row++, item++) + for (var row = 0; row < Viewport.Height; row++, item++) { bool isSelected = item == SelectedItem; - bool isMarked = Source!.IsMarked (item); - bool hasFocus = focused; + bool isMarked = item < Source?.Count && Source?.IsMarked (item) is true; // Determine visual role based on the 4 combinations of ShowMarks and MarkMultiple VisualRole role; @@ -33,7 +29,7 @@ protected override bool OnDrawingContent (DrawContext? context) // Combination 1: Standard selection mode (no marking) // Mark glyphs: None (MarkWidth = 0) // Visual roles: SelectedItem uses Focus (focused) or Active (not focused) - role = isSelected ? hasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; } else if (!ShowMarks && MarkMultiple) { @@ -42,12 +38,12 @@ protected override bool OnDrawingContent (DrawContext? context) // Visual roles use Highlight for marked items; compose TextStyle when marked+selected+focused if (isSelected && isMarked) { - role = hasFocus ? VisualRole.Focus : VisualRole.Highlight; - applyHighlightStyle = hasFocus; // Apply Highlight's TextStyle to Focus + role = HasFocus ? VisualRole.Focus : VisualRole.Highlight; + applyHighlightStyle = HasFocus; // Apply Highlight's TextStyle to Focus } else if (isSelected) { - role = hasFocus ? VisualRole.Focus : VisualRole.Normal; + role = HasFocus ? VisualRole.Focus : VisualRole.Normal; } else if (isMarked) { @@ -63,14 +59,14 @@ protected override bool OnDrawingContent (DrawContext? context) // Combination 3: Radio button style // Mark glyphs: Radio-button style (◉ marked, ○ unmarked) // Visual roles: Standard selection (mark glyphs provide visual indication) - role = isSelected ? hasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; } - else // ShowMarks == true && MarkMultiple == true + else { // Combination 4: Checkbox style // Mark glyphs: Checkbox style (☒ marked, ☐ unmarked) // Visual roles: Standard selection (mark glyphs provide visual indication) - role = isSelected ? hasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; } Attribute newAttribute = GetAttributeForRole (role); @@ -92,7 +88,7 @@ protected override bool OnDrawingContent (DrawContext? context) if (Source is null || item >= Source.Count) { - for (var c = 0; c < f.Width; c++) + for (var c = 0; c < Viewport.Width; c++) { AddRune ((Rune)' '); } @@ -142,7 +138,7 @@ protected override bool OnDrawingContent (DrawContext? context) } int contentCol = col > 0 ? col : markWidth; - Source.Render (this, isSelected, item, contentCol, row, f.Width - contentCol, start); + Source.Render (this, isSelected, item, contentCol, row, Viewport.Width - contentCol, Viewport.X); } } From 22f0ce87d02ef668c5fe86d6d671df87fc1ac3ef Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 4 Mar 2026 15:20:47 -0700 Subject: [PATCH 2/4] =?UTF-8?q?=20=20-=20Draw=5FDoesNotCall=5FIsMarked=5FO?= =?UTF-8?q?utOfRange=20=E2=80=94=20Viewport=20height=20(10)=20>=20source?= =?UTF-8?q?=20count=20(2),=20asserts=20IsMarked=20is=20never=20called=20wi?= =?UTF-8?q?th=20index=20>=3D=202=20=20=20-=20Draw=5FDoesNotCall=5FRender?= =?UTF-8?q?=5FOutOfRange=20=E2=80=94=20Same=20setup,=20asserts=20Render=20?= =?UTF-8?q?is=20never=20called=20with=20out-of-range=20index=20=20=20-=20D?= =?UTF-8?q?raw=5FDoesNotCall=5FSetMark=5FOutOfRange=20=E2=80=94=20Same=20s?= =?UTF-8?q?etup=20with=20ShowMarks=20=3D=20true,=20asserts=20SetMark=20is?= =?UTF-8?q?=20never=20called=20with=20out-of-range=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/ListViewTests.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 8debc4a000..393d67d060 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -2670,4 +2670,116 @@ public void HorizontalScroll_With_Marks_Accounts_For_MarkWidth () } #endregion + + #region Out-of-range guard tests + + // Claude - Opus 4.6 + // Verifies that ListView.OnDrawingContent does not call IsMarked with out-of-range indices + // when the Viewport is taller than the number of items in the source. + [Fact] + public void Draw_DoesNotCall_IsMarked_OutOfRange () + { + ObservableCollection source = ["one", "two"]; + + // Track all indices passed to IsMarked + List isMarkedCalls = []; + Mock mockSource = new (MockBehavior.Strict); + mockSource.Setup (s => s.Count).Returns (source.Count); + + mockSource.Setup (s => s.IsMarked (It.IsAny ())) + .Returns ((int item) => + { + isMarkedCalls.Add (item); + Assert.True (item >= 0 && item < source.Count, $"IsMarked called with out-of-range index {item}"); + + return false; + }); + + mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())); + mockSource.Setup (s => s.RenderMark (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())).Returns (false); + mockSource.Setup (s => s.MaxItemLength).Returns (5); + mockSource.Setup (s => s.ToList ()).Returns (source); + mockSource.Setup (s => s.SuspendCollectionChangedEvent).Returns (false); + mockSource.As ().Setup (d => d.Dispose ()); + + // Viewport height (10) is much larger than item count (2) + ListView lv = new () { Width = 20, Height = 10, Source = mockSource.Object }; + lv.Layout (); + lv.Draw (); + + // Verify IsMarked was only called with valid indices + Assert.All (isMarkedCalls, i => Assert.InRange (i, 0, source.Count - 1)); + } + + // Claude - Opus 4.6 + // Verifies that ListView.OnDrawingContent does not call Render with out-of-range indices + // when the Viewport is taller than the number of items in the source. + [Fact] + public void Draw_DoesNotCall_Render_OutOfRange () + { + ObservableCollection source = ["one", "two"]; + + List renderCalls = []; + Mock mockSource = new (MockBehavior.Strict); + mockSource.Setup (s => s.Count).Returns (source.Count); + mockSource.Setup (s => s.IsMarked (It.IsAny ())).Returns (false); + + mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())) + .Callback ((ListView _, bool _, int item, int _, int _, int _, int _) => + { + renderCalls.Add (item); + Assert.True (item >= 0 && item < source.Count, $"Render called with out-of-range index {item}"); + }); + + mockSource.Setup (s => s.RenderMark (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())).Returns (false); + mockSource.Setup (s => s.MaxItemLength).Returns (5); + mockSource.Setup (s => s.ToList ()).Returns (source); + mockSource.Setup (s => s.SuspendCollectionChangedEvent).Returns (false); + mockSource.As ().Setup (d => d.Dispose ()); + + ListView lv = new () { Width = 20, Height = 10, Source = mockSource.Object }; + lv.Layout (); + lv.Draw (); + + // Verify Render was only called with valid indices + Assert.All (renderCalls, i => Assert.InRange (i, 0, source.Count - 1)); + } + + // Claude - Opus 4.6 + // Verifies that ListView.OnDrawingContent does not call SetMark with out-of-range indices + // when the Viewport is taller than the number of items in the source. + [Fact] + public void Draw_DoesNotCall_SetMark_OutOfRange () + { + ObservableCollection source = ["one", "two"]; + + List setMarkCalls = []; + Mock mockSource = new (MockBehavior.Strict); + mockSource.Setup (s => s.Count).Returns (source.Count); + mockSource.Setup (s => s.IsMarked (It.IsAny ())).Returns (false); + + mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())); + mockSource.Setup (s => s.RenderMark (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())).Returns (false); + + mockSource.Setup (s => s.SetMark (It.IsAny (), It.IsAny ())) + .Callback ((int item, bool _) => + { + setMarkCalls.Add (item); + Assert.True (item >= 0 && item < source.Count, $"SetMark called with out-of-range index {item}"); + }); + + mockSource.Setup (s => s.MaxItemLength).Returns (5); + mockSource.Setup (s => s.ToList ()).Returns (source); + mockSource.Setup (s => s.SuspendCollectionChangedEvent).Returns (false); + mockSource.As ().Setup (d => d.Dispose ()); + + ListView lv = new () { Width = 20, Height = 10, ShowMarks = true, Source = mockSource.Object }; + lv.Layout (); + lv.Draw (); + + // SetMark should not have been called at all during Draw, but if it was, only with valid indices + Assert.All (setMarkCalls, i => Assert.InRange (i, 0, source.Count - 1)); + } + + #endregion } From 063af22afb6dc3e50235dae2ad25c677aa039a2a Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 4 Mar 2026 15:26:04 -0700 Subject: [PATCH 3/4] Refactor ListView visual role logic with switch pattern Refactored the logic for determining the visual role in ListView from nested if/else statements to a switch statement using C# pattern matching on ShowMarks and MarkMultiple. This improves code clarity and maintainability while preserving existing behavior for all four selection/marking combinations. No changes to public APIs or event signatures. --- .../Views/ListView/ListView.Drawing.cs | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/Terminal.Gui/Views/ListView/ListView.Drawing.cs b/Terminal.Gui/Views/ListView/ListView.Drawing.cs index 1dab8d1a6a..c3cb2066da 100644 --- a/Terminal.Gui/Views/ListView/ListView.Drawing.cs +++ b/Terminal.Gui/Views/ListView/ListView.Drawing.cs @@ -24,49 +24,57 @@ protected override bool OnDrawingContent (DrawContext? context) VisualRole role; var applyHighlightStyle = false; - if (!ShowMarks && !MarkMultiple) + switch (ShowMarks) { - // Combination 1: Standard selection mode (no marking) - // Mark glyphs: None (MarkWidth = 0) - // Visual roles: SelectedItem uses Focus (focused) or Active (not focused) - role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; - } - else if (!ShowMarks && MarkMultiple) - { - // 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 - if (isSelected && isMarked) - { - role = HasFocus ? VisualRole.Focus : VisualRole.Highlight; - applyHighlightStyle = HasFocus; // Apply Highlight's TextStyle to Focus - } - else if (isSelected) - { - role = HasFocus ? VisualRole.Focus : VisualRole.Normal; - } - else if (isMarked) - { - role = VisualRole.Highlight; - } - else + case false when !MarkMultiple: + // Combination 1: Standard selection mode (no marking) + // Mark glyphs: None (MarkWidth = 0) + // Visual roles: SelectedItem uses Focus (focused) or Active (not focused) + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + + break; + + case false when MarkMultiple: { - role = VisualRole.Normal; + 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; + + case true: role = HasFocus ? VisualRole.Focus : VisualRole.Normal; break; + + default: + { + role = isMarked ? VisualRole.Highlight : VisualRole.Normal; + + break; + } + } + + break; } - } - else if (ShowMarks && !MarkMultiple) - { - // Combination 3: Radio button style - // Mark glyphs: Radio-button style (◉ marked, ○ unmarked) - // Visual roles: Standard selection (mark glyphs provide visual indication) - role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; - } - else - { - // Combination 4: Checkbox style - // Mark glyphs: Checkbox style (☒ marked, ☐ unmarked) - // Visual roles: Standard selection (mark glyphs provide visual indication) - role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + + case true when !MarkMultiple: + // Combination 3: Radio button style + // Mark glyphs: Radio-button style (◉ marked, ○ unmarked) + // Visual roles: Standard selection (mark glyphs provide visual indication) + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + + break; + + default: + // Combination 4: Checkbox style + // Mark glyphs: Checkbox style (☒ marked, ☐ unmarked) + // Visual roles: Standard selection (mark glyphs provide visual indication) + role = isSelected ? HasFocus ? VisualRole.Focus : VisualRole.Active : VisualRole.Normal; + + break; } Attribute newAttribute = GetAttributeForRole (role); @@ -155,5 +163,4 @@ protected override bool OnDrawingContent (DrawContext? context) /// This event is invoked when this is being drawn before rendering. public event EventHandler? RowRender; - } From c9984849b48343108362e33e27d53561615419dd Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 4 Mar 2026 15:52:10 -0700 Subject: [PATCH 4/4] Add advanced ListView unit tests for selection and events Expand ListViewTests with coverage for IValue events (Value, ValueChanging, ValueChanged), keyboard mark/unmark (Ctrl+A/U), and RowRender attribute overrides. Also improve mock setup formatting. These tests ensure correct behavior for selection, event firing, and row rendering customization. --- .../Views/ListViewTests.cs | 305 +++++++++++++++++- 1 file changed, 301 insertions(+), 4 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index 393d67d060..549d7188e0 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -873,7 +873,7 @@ public void KeyBindings_Command () Assert.Equal (2, lv.SelectedItem); Assert.True (lv.NewKeyDownEvent (Key.PageUp)); Assert.Equal (0, lv.SelectedItem); - + // In standard selection mode (ShowMarks=false), Space doesn't mark Assert.False (lv.Source.IsMarked (lv.SelectedItem!.Value)); lv.NewKeyDownEvent (Key.Space); @@ -2695,7 +2695,13 @@ public void Draw_DoesNotCall_IsMarked_OutOfRange () return false; }); - mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())); + mockSource.Setup (s => s.Render (It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny ())); mockSource.Setup (s => s.RenderMark (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())).Returns (false); mockSource.Setup (s => s.MaxItemLength).Returns (5); mockSource.Setup (s => s.ToList ()).Returns (source); @@ -2724,7 +2730,13 @@ public void Draw_DoesNotCall_Render_OutOfRange () mockSource.Setup (s => s.Count).Returns (source.Count); mockSource.Setup (s => s.IsMarked (It.IsAny ())).Returns (false); - mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())) + mockSource.Setup (s => s.Render (It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny ())) .Callback ((ListView _, bool _, int item, int _, int _, int _, int _) => { renderCalls.Add (item); @@ -2758,7 +2770,13 @@ public void Draw_DoesNotCall_SetMark_OutOfRange () mockSource.Setup (s => s.Count).Returns (source.Count); mockSource.Setup (s => s.IsMarked (It.IsAny ())).Returns (false); - mockSource.Setup (s => s.Render (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())); + mockSource.Setup (s => s.Render (It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny (), + It.IsAny ())); mockSource.Setup (s => s.RenderMark (It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny (), It.IsAny ())).Returns (false); mockSource.Setup (s => s.SetMark (It.IsAny (), It.IsAny ())) @@ -2782,4 +2800,283 @@ public void Draw_DoesNotCall_SetMark_OutOfRange () } #endregion + + #region IValue Implementation + + // Claude - Opus 4.6 + [Fact] + public void ValueChanging_Event_Can_Cancel_Selection_Change () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + lv.ValueChanging += (_, args) => args.Handled = true; + + lv.SelectedItem = 1; + + Assert.Equal (0, lv.SelectedItem); + } + + // Claude - Opus 4.6 + [Fact] + public void ValueChanging_Event_Provides_CurrentValue_And_NewValue () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + int? capturedCurrent = null; + int? capturedNew = null; + + lv.ValueChanging += (_, args) => + { + capturedCurrent = args.CurrentValue; + capturedNew = args.NewValue; + }; + + lv.SelectedItem = 2; + + Assert.Equal (0, capturedCurrent); + Assert.Equal (2, capturedNew); + Assert.Equal (2, lv.SelectedItem); + } + + // Claude - Opus 4.6 + [Fact] + public void ValueChanged_Event_Fires_After_Selection_Change () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + int? oldValue = null; + int? newValue = null; + + lv.ValueChanged += (_, args) => + { + oldValue = args.OldValue; + newValue = args.NewValue; + }; + + lv.SelectedItem = 2; + + Assert.Equal (0, oldValue); + Assert.Equal (2, newValue); + } + + // Claude - Opus 4.6 + [Fact] + public void ValueChanged_DoesNotFire_When_ValueChanging_Cancels () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + var changedFired = false; + lv.ValueChanging += (_, args) => args.Handled = true; + lv.ValueChanged += (_, _) => changedFired = true; + + lv.SelectedItem = 1; + + Assert.False (changedFired); + Assert.Equal (0, lv.SelectedItem); + } + + // Claude - Opus 4.6 + [Fact] + public void ValueChangedUntyped_Event_Fires_After_Selection_Change () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + object? oldValue = null; + object? newValue = null; + + lv.ValueChangedUntyped += (_, args) => + { + oldValue = args.OldValue; + newValue = args.NewValue; + }; + + lv.SelectedItem = 1; + + Assert.Equal (0, oldValue); + Assert.Equal (1, newValue); + } + + // Claude - Opus 4.6 + [Fact] + public void Value_Property_Is_Alias_For_SelectedItem () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + + lv.Value = 2; + Assert.Equal (2, lv.SelectedItem); + Assert.Equal (2, lv.Value); + + lv.SelectedItem = 0; + Assert.Equal (0, lv.Value); + } + + // Claude - Opus 4.6 + [Fact] + public void IValue_GetValue_Returns_SelectedItem () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 1; + + object? value = ((IValue)lv).GetValue (); + + Assert.Equal (1, value); + } + + // Claude - Opus 4.6 + [Fact] + public void IValue_GetValue_Returns_Null_When_No_Selection () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + + object? value = ((IValue)lv).GetValue (); + + Assert.Null (value); + } + + #endregion IValue Implementation + + #region Ctrl+A / Ctrl+U SelectAll + + // Claude - Opus 4.6 + [Fact] + public void CtrlA_Marks_All_Items_When_MarkMultiple_True () + { + ObservableCollection source = ["one", "two", "three", "four"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source), MarkMultiple = true }; + lv.SelectedItem = 0; + + lv.NewKeyDownEvent (Key.A.WithCtrl); + + List marked = lv.GetAllMarkedItems ().ToList (); + Assert.Equal (4, marked.Count); + Assert.Equal ([0, 1, 2, 3], marked); + } + + // Claude - Opus 4.6 + [Fact] + public void CtrlU_Unmarks_All_Items_When_MarkMultiple_True () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source), MarkMultiple = true }; + lv.SelectedItem = 0; + + // Mark all first + lv.NewKeyDownEvent (Key.A.WithCtrl); + Assert.Equal (3, lv.GetAllMarkedItems ().Count ()); + + // Unmark all + lv.NewKeyDownEvent (Key.U.WithCtrl); + Assert.Empty (lv.GetAllMarkedItems ()); + } + + // Claude - Opus 4.6 + [Fact] + public void CtrlA_Does_Nothing_When_MarkMultiple_False () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source), MarkMultiple = false }; + lv.SelectedItem = 0; + + lv.NewKeyDownEvent (Key.A.WithCtrl); + + Assert.Empty (lv.GetAllMarkedItems ()); + } + + #endregion Ctrl+A / Ctrl+U SelectAll + + #region RowRender RowAttribute Override + + // Claude - Opus 4.6 + [Fact] + public void RowRender_Event_RowAttribute_Applied_To_Specific_Row () + { + IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Width = 10, Height = 5, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + Attribute customAttr = new (Color.Red, Color.Blue); + List renderedRows = []; + + lv.RowRender += (_, args) => + { + renderedRows.Add (args.Row); + + if (args.Row == 1) + { + args.RowAttribute = customAttr; + } + }; + + Runnable top = new (); + top.Add (lv); + app.Begin (top); + app.LayoutAndDraw (); + + // Verify the event was called for each visible row + Assert.Contains (0, renderedRows); + Assert.Contains (1, renderedRows); + Assert.Contains (2, renderedRows); + + top.Dispose (); + app.Dispose (); + } + + // Claude - Opus 4.6 + [Fact] + public void RowRender_Event_Receives_Correct_Row_Indices () + { + ObservableCollection source = ["a", "b", "c", "d", "e"]; + ListView lv = new () { Width = 10, Height = 3, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + List receivedRows = []; + + lv.RowRender += (_, args) => receivedRows.Add (args.Row); + + lv.Layout (); + lv.Draw (); + + // With height=3, only 3 rows visible starting from viewport Y=0 + Assert.Equal ([0, 1, 2], receivedRows); + } + + // Claude - Opus 4.6 + [Fact] + public void RowRender_Event_Receives_Correct_Row_Indices_After_Scroll () + { + ObservableCollection source = ["a", "b", "c", "d", "e"]; + ListView lv = new () { Width = 10, Height = 3, Source = new ListWrapper (source) }; + lv.SelectedItem = 0; + + lv.Layout (); + + // Scroll down so viewport starts at row 2 + lv.Viewport = lv.Viewport with { Y = 2 }; + + List receivedRows = []; + lv.RowRender += (_, args) => receivedRows.Add (args.Row); + + lv.Draw (); + + // After scrolling, should render items 2, 3, 4 + Assert.Equal ([2, 3, 4], receivedRows); + } + + #endregion RowRender RowAttribute Override }