diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index e7ea27671c..7becbe1dd7 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -5,9 +5,9 @@ using Terminal.Gui.App; using Terminal.Gui.Configuration; +using Terminal.Gui.Input; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -using Terminal.Gui.Input; // Override the default configuration for the application to use the Amber Phosphor theme ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }"""; diff --git a/Terminal.sln b/Terminal.sln index 627570beb0..b4cb7cbf11 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -151,8 +151,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScenarioRunner", "Examples\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A589126F-C71A-4FEE-B7EA-2DCA1ADF6A46}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShortcutTest", "Examples\ShortcutTest\ShortcutTest.csproj", "{0EECEC4F-AD8D-1076-1B40-562926DE1CB3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -239,10 +237,6 @@ Global {DB0337E6-BBC0-4ECB-9F18-F7310705BDAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB0337E6-BBC0-4ECB-9F18-F7310705BDAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB0337E6-BBC0-4ECB-9F18-F7310705BDAF}.Release|Any CPU.Build.0 = Release|Any CPU - {0EECEC4F-AD8D-1076-1B40-562926DE1CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0EECEC4F-AD8D-1076-1B40-562926DE1CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0EECEC4F-AD8D-1076-1B40-562926DE1CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0EECEC4F-AD8D-1076-1B40-562926DE1CB3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -270,7 +264,6 @@ Global {26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {566AFB59-FF8C-FFF4-C1F4-049B6246E4A7} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} {DB0337E6-BBC0-4ECB-9F18-F7310705BDAF} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} - {0EECEC4F-AD8D-1076-1B40-562926DE1CB3} = {3DD033C0-E023-47BF-A808-9CCE30873C3E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03} diff --git a/docfx/scripts/OutputView/OutputView.cs b/docfx/scripts/OutputView/OutputView.cs index 374caeb497..ca726ab9c1 100644 --- a/docfx/scripts/OutputView/OutputView.cs +++ b/docfx/scripts/OutputView/OutputView.cs @@ -1,8 +1,9 @@ #nullable enable +using AnsiConsoleToHtml; using Terminal.Gui.App; -using Terminal.Gui.Drivers; using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; +using Terminal.Gui.Drivers; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; using Attribute = Terminal.Gui.Drawing.Attribute; @@ -30,8 +31,8 @@ string? outputFile = null; string [] commandArgs = Environment.GetCommandLineArgs (); -bool ansi = false; -bool addBorderFrame = false; +var ansi = false; +var addBorderFrame = false; for (var i = 0; i < commandArgs.Length; i++) { @@ -103,7 +104,7 @@ if (ansi) { - output = AnsiConsoleToHtml.AnsiConsole.ToHtml (output); + output = AnsiConsole.ToHtml (output); } // Write to file or console @@ -128,13 +129,13 @@ public ViewDemoWindow () Height = 20; // Use only white on black - SetScheme (new (new Attribute (ColorName16.White, ColorName16.Black))); + SetScheme (new Scheme (new Attribute (ColorName16.White, ColorName16.Black))); BorderStyle = LineStyle.None; } public static bool AddBorderFrame { get; set; } - /// + /// protected override void OnIsRunningChanged (bool newIsRunning) { base.OnIsRunningChanged (newIsRunning); @@ -218,6 +219,7 @@ protected override void OnIsRunningChanged (bool newIsRunning) { view.Text = "This is some demo text."; } + //view.Title = $"View: {type.Name}"; return view; diff --git a/docfx/scripts/OutputView/Properties/launchSettings.json b/docfx/scripts/OutputView/Properties/launchSettings.json index 893246195e..f69ff05414 100644 --- a/docfx/scripts/OutputView/Properties/launchSettings.json +++ b/docfx/scripts/OutputView/Properties/launchSettings.json @@ -6,7 +6,7 @@ }, "OutputView ": { "commandName": "Project", - "commandLineArgs": "--view=GraphView" + "commandLineArgs": "--view=AttributePicker" }, "OutputView --ansi": { "commandName": "Project", diff --git a/plans/fix-border-subview-linecanvas-clipping.md b/plans/fix-border-subview-linecanvas-clipping.md deleted file mode 100644 index dc73ab5f24..0000000000 --- a/plans/fix-border-subview-linecanvas-clipping.md +++ /dev/null @@ -1,239 +0,0 @@ -# Fix: Border SubView LineCanvas Lines Not Clipped at Parent Bounds - -## Bug Summary - -When a SubView of a Border has `SuperViewRendersLineCanvas = true` and its own border -(`BorderStyle != None`), and the SubView's frame extends past the parent Border's bounds, -the SubView's border lines bleed into the parent's border columns. For example, a `║` -becomes `╫` because the SubView's `─` merges unclipped into the parent's LineCanvas. - -**Failing test:** `AdornmentSubViewLineCanvasTests.BorderSubView_WithBorder_ClippedWhenExceedingParentBounds` - -## Root Cause - -The merge at `View.Drawing.Adornments.cs:50` is unclipped: - -```csharp -// Line 43: clip set to border's frame (only affects raster drawing via Driver.Clip) -Region? saved = borderView.AddFrameToClip (); -// Line 44: subviews are drawn (their LineCanvas lines are generated) -borderView.DoDrawSubViews (); - -// Line 50: ALL lines from borderView's LineCanvas are merged — NO BOUNDS CHECK -LineCanvas.Merge (borderView.LineCanvas); -``` - -`LineCanvas.Merge()` (LineCanvas.cs:510-524) copies every `StraightLine` unconditionally. -`Driver.Clip` (set by `AddFrameToClip`) only restricts raster output (`AddStr`, `Move`), -not LineCanvas data. The merged lines participate in intersection resolution and produce -corrupted junction glyphs where they cross the parent's border lines. - -The class docs (LineCanvas.cs:44-48) describe a `Merge(LineCanvas, Region?)` overload for -clipped merging, but **this overload does not exist**. - -## Draw Pipeline Context - -``` -View.Draw(): - 1. DoDrawAdornments() — Parent's border adds lines to this.LineCanvas - 2. AddViewportToClip() — Clip to viewport (raster only) - 3. DoDrawSubViews() — Content subviews drawn - 4. SetClip → AddFrameToClip() — Clip to frame (raster only) - 5. DoDrawAdornmentsSubViews() — Border subview lines merged into this.LineCanvas ← BUG - 6. DoRenderLineCanvas() — Resolves all lines and renders to screen -``` - -The merge in step 5 must restrict lines to the border's content area before they enter -the parent's LineCanvas in step 6. - -## Fix Options - -### Option A: Clipped `Merge` overload on LineCanvas - -Implement the documented but missing `Merge(LineCanvas, Rectangle clipBounds)` overload. -It would trim or discard each incoming `StraightLine` to fit within `clipBounds` before -adding it. - -**Where to change:** -- `LineCanvas.cs` — Add `Merge(LineCanvas, Rectangle)` that clips each line using - `StraightLineExtensions`-style logic (trim Start/Length to stay within bounds). -- `View.Drawing.Adornments.cs:50` — Pass the border view's frame rect: - ```csharp - Rectangle borderBounds = borderView.FrameToScreen (); - LineCanvas.Merge (borderView.LineCanvas, borderBounds); - ``` -- Same pattern for Padding merge at line 84. - -**Pros:** -- Clean, self-contained — clipping logic lives in LineCanvas where it belongs. -- The documentation already describes this overload; just implement it. -- Lines are trimmed *before* intersection resolution, so no corrupted junctions. -- `StraightLineExtensions.Exclude` already has line-splitting logic that can be reused - to clip lines against a rectangle boundary. - -**Cons:** -- Trimming lines can produce different junction types at the clip boundary (the docs - warn about this). A line that was `PassOverHorizontal` may become `StartRight` after - clipping, which could change the resolved glyph. This is acceptable — the clipped - edge is at the parent's border, which already has its own lines providing the correct - junction context. -- Must handle both horizontal and vertical lines, and both positive/negative lengths. - -**Complexity:** Medium. The line-trimming math is straightforward (clamp start/end to -bounds, recompute length). `StraightLineExtensions` already demonstrates the pattern. - ---- - -### Option B: Exclude-based approach — add exclusion region to parent LineCanvas - -Instead of clipping lines before merge, merge everything, then exclude the out-of-bounds -cells from the parent's LineCanvas output. - -**Where to change:** -- `View.Drawing.Adornments.cs:50` — After merge, compute the region outside the border - view's frame and call `LineCanvas.Exclude()` on those areas. - -**Pros:** -- Simpler implementation — no line-splitting math. -- Uses existing `Exclude` API. - -**Cons:** -- **Does not fix the bug.** `Exclude` hides cells from `GetCellMap` output but lines - still participate in intersection resolution. The out-of-bounds `─` still crosses the - parent's `║` during resolution, producing `╫` — even though the `╫` cell at the - parent's border column would be excluded, the parent's own `║` line at that position - would ALSO be excluded because exclusion is position-based, not line-based. -- Would need careful region math to only exclude the *SubView's* cells outside bounds - without excluding the parent's own border cells at those positions. -- Fragile and semantically wrong — the problem is that lines exist where they shouldn't, - not that their output needs hiding. - -**Verdict: Not viable** without significant additional work to make exclusion line-aware. - ---- - -### Option C: Clip in `DoDrawSubViews` — restrict the SubView's own LineCanvas generation - -Prevent the SubView from generating LineCanvas lines outside the border's frame in the -first place, by clipping the SubView's layout/frame before it draws. - -**Where to change:** -- `View.Drawing.Adornments.cs:44` or the SubView's own `Draw()` — Constrain the - SubView's effective frame to the intersection of its frame and the border view's frame - before drawing. - -**Pros:** -- Fixes the problem at the source — lines are never generated outside bounds. -- No post-hoc filtering or trimming needed. - -**Cons:** -- Changing the SubView's frame/layout is invasive and could have side effects on hit - testing, mouse events, and other layout-dependent behavior. -- The SubView's `BorderView.OnDrawingContent` adds lines based on the SubView's - `FrameToScreen()`. Changing the frame changes the border geometry, not just clips it. -- Would need to be undone after drawing, adding complexity. -- Conceptually wrong — layout shouldn't change during draw. - -**Verdict: Too invasive.** Mixing layout mutation with draw is a design smell. - ---- - -### Option D: Filter during `RenderLineCanvas` — clip at output time - -Instead of clipping during merge, filter the resolved `cellMap` in `RenderLineCanvas` -to only include cells within the view's frame. - -**Where to change:** -- `View.Drawing.LineCanvas.cs:48-60` — Skip cells outside `FrameToScreen()`. - -**Pros:** -- Simple one-line check in the render loop. -- No changes to LineCanvas data structure. - -**Cons:** -- **Does not fix junction corruption.** The out-of-bounds lines still participate in - intersection resolution. Even if the corrupted `╫` cell is not rendered, the parent's - `║` line at that position may resolve differently because of the intersecting `─`. - The resolved glyph at the parent's border column would be wrong even if we skip - rendering out-of-bounds cells. -- Only addresses the symptom (rendering) not the cause (unclipped lines in the canvas). - -**Verdict: Insufficient.** Junction corruption happens during resolution, not rendering. - -## Recommendation - -**Option A** is the correct fix. It addresses the root cause (unclipped lines entering -the parent's LineCanvas), uses the existing documented API contract, and produces correct -junction glyphs because the parent's own border lines are the only lines at the boundary -during intersection resolution. - -### Implementation sketch - -```csharp -// LineCanvas.cs — new overload -public void Merge (LineCanvas lineCanvas, Rectangle clipBounds) -{ - foreach (StraightLine line in lineCanvas._lines) - { - // Clip the line to clipBounds; may produce 0 or 1 clipped line - StraightLine? clipped = ClipLine (line, clipBounds); - - if (clipped is { }) - { - AddLine (clipped); - } - } - - // Exclusion regions are position-based — intersect with clipBounds - if (lineCanvas._exclusionRegion is { }) - { - Region clippedExclusion = lineCanvas._exclusionRegion.Clone (); - clippedExclusion.Intersect (clipBounds); - _exclusionRegion ??= new Region (); - _exclusionRegion.Union (clippedExclusion); - } -} - -private static StraightLine? ClipLine (StraightLine line, Rectangle bounds) -{ - Rectangle lineBounds = line.Bounds; - Rectangle clipped = Rectangle.Intersect (lineBounds, bounds); - - if (clipped.IsEmpty) - { - return null; - } - - // Recompute Start and Length from the clipped rectangle - Point newStart = line.Orientation == Orientation.Horizontal - ? new Point (clipped.X, clipped.Y) - : new Point (clipped.X, clipped.Y); - - int newLength = line.Orientation == Orientation.Horizontal - ? clipped.Width - : clipped.Height; - - // Preserve direction (sign of Length) - if (line.Length < 0) - { - newLength = -newLength; - // Adjust start for negative-direction lines - // ... (handle negative length start offset) - } - - return new StraightLine (newStart, newLength, line.Orientation, line.Style, line.Attribute); -} -``` - -Call site in `View.Drawing.Adornments.cs`: - -```csharp -if (borderView.LineCanvas.Bounds != Rectangle.Empty) -{ - Rectangle clipBounds = borderView.FrameToScreen (); - LineCanvas.Merge (borderView.LineCanvas, clipBounds); - borderView.LineCanvas.Clear (); -} -``` - -Same for the Padding merge at line 82-86. diff --git a/plans/refactor-border-tab-to-borderview.md b/plans/refactor-border-tab-to-borderview.md deleted file mode 100644 index c3b84e3250..0000000000 --- a/plans/refactor-border-tab-to-borderview.md +++ /dev/null @@ -1,359 +0,0 @@ -# Plan: Move Tab-Related Functionality from Border to BorderView - -## Problem - -`Border` is instantiated on **every** `View` — it should be as lightweight as possible. Currently it carries tab-related members (`TabSide`, `TabOffset`, `TabLength`, `TabEnd`, `EffectiveTabLength`, `SettingsChanged`) that are only meaningful when `BorderSettings.Tab` is active. This functionality belongs on `BorderView`, which is lazily created only when needed. - -## Goals - -1. **Minimize Border's footprint** — Border stores only `Thickness`, `LineStyle`, and `Settings`. -2. **Move tab configuration to BorderView** — `TabSide`, `TabOffset`, `TabLength`, `EffectiveTabLength` become properties on `BorderView`. `TabEnd` is deleted (zero consumers). -3. **Update all consumers** — backwards compatibility is not a concern. All call sites change from `view.Border.TabSide` to `((BorderView)view.Border.View!).TabSide` (or use a helper/local). -4. **No behavioral changes** — rendering, tests, and UICatalog scenarios produce identical output. - -## Approach - -**Option B: Move completely, remove from Border.** All tab properties are deleted from `Border` and added to `BorderView`. Every consumer is updated. This gives the cleanest separation. - ---- - -## Current State Inventory - -### Members on `Border` today - -| Member | Kind | Tab-Only? | Consumers | -|--------|------|-----------|-----------| -| `Thickness` | inherited | No | Everywhere — **stays** | -| `LineStyle` | property | No | Everywhere — **stays** | -| `Settings` | property | No (but triggers tab setup) | Everywhere — **stays** | -| `SettingsChanged` | event | Yes (only subscriber: BorderView) | 1 internal — **remove** | -| `TabSide` | property | **Yes** | ~48 locations — **move** | -| `TabOffset` | property | **Yes** | ~80 locations — **move** | -| `TabLength` | property | **Yes** | ~13 locations — **move** | -| `TabEnd` | computed property | **Yes** | 0 consumers — **delete** | -| `EffectiveTabLength` | internal property | **Yes** | ~11 locations — **move** | - -### Members on `BorderView` today - -BorderView already has all the tab **rendering** logic. It reads tab configuration from `Border` via its `Adornment` reference. After this refactor, it owns the configuration too. - ---- - -## Execution Order - -Work in three phases, building green after each. - -### Phase 1: `EffectiveTabLength` and `TabEnd` - -Low-risk warm-up. `EffectiveTabLength` is `internal` and `TabEnd` is dead code. - -### Phase 2: `TabSide`, `TabOffset`, `TabLength` - -The bulk of the work — these are public properties with many consumers. - -### Phase 3: `SettingsChanged` event - -Cleanup — replace the event with a direct call. - ---- - -## Phase 1: Move `EffectiveTabLength`, Delete `TabEnd` - -### Step 1.1: Add `EffectiveTabLength` to `BorderView` - -Add to `BorderView.cs` (tab support region): - -```csharp -internal int EffectiveTabLength -{ - get - { - if (TabLength is { } explicitLength) - { - return explicitLength; - } - - if (TitleView is not (ITitleView itv and View tv)) - { - return 0; - } - - if (itv.MeasuredTabLength > 0) - { - return itv.MeasuredTabLength; - } - - // TitleView hasn't been laid out yet — set text and orientation, then measure. - tv.Text = Adornment?.Parent?.Title ?? string.Empty; - itv.Orientation = TabSide is Side.Left or Side.Right ? Orientation.Vertical : Orientation.Horizontal; - - int measured = TabSide is Side.Top or Side.Bottom ? tv.GetAutoWidth () : tv.GetAutoHeight (); - itv.MeasuredTabLength = measured; - - return measured; - } -} -``` - -Note: This initially reads `TabSide` and `TabLength` from `Border` (via `Adornment`). After Phase 2, these become local properties and the reads simplify. - -### Step 1.2: Delete `EffectiveTabLength` from `Border` - -Remove the full `EffectiveTabLength` property from `Border.cs`. - -### Step 1.3: Update consumers of `EffectiveTabLength` - -All consumers currently access `border.EffectiveTabLength` or `tab.Border.EffectiveTabLength`. - -**Library code (`Tabs.cs`)** — 4 reads. Pattern: `tab.Border.EffectiveTabLength`. Change to: - -```csharp -((BorderView)tab.Border.View!).EffectiveTabLength -``` - -Or introduce a local helper in `Tabs.cs`: - -```csharp -private static BorderView GetBorderView (View tab) => (BorderView)tab.Border.View!; -``` - -Then: `GetBorderView (tab).EffectiveTabLength` - -**Library code (`BorderView.cs`)** — 1 read in `DrawTabBorder`. Change `border.EffectiveTabLength` → `EffectiveTabLength` (now local). - -**Tests** — ~5 assertions. Change `view.Border.EffectiveTabLength` → cast and access. - -### Step 1.4: Delete `TabEnd` from `Border` - -Remove the `TabEnd` computed property entirely. It has **zero consumers**. - -### Step 1.5: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -``` - ---- - -## Phase 2: Move `TabSide`, `TabOffset`, `TabLength` - -### Step 2.1: Add properties to `BorderView` - -Add to `BorderView.cs` (tab support region): - -```csharp -public Side TabSide -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} = Side.Top; - -public int TabOffset -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} - -public int? TabLength -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} -``` - -### Step 2.2: Delete properties from `Border` - -Remove `TabSide`, `TabOffset`, `TabLength` (including backing fields, setters, and XML docs) from `Border.cs`. - -### Step 2.3: Update `BorderView` internal reads - -All reads in `BorderView.cs` that go through `border.TabSide`, `border.TabOffset`, `border.TabLength` change to `TabSide`, `TabOffset`, `TabLength` (now `this`). Affected methods: - -| Method | Properties read | -|--------|----------------| -| `ConfigureForTabMode()` | `border.TabSide` → `TabSide` | -| `UpdateTitleViewLayout()` | `border.TabSide`, `border.TabOffset`, `border.TabLength` → local | -| `GetTabBorderBounds()` | `border.TabSide` → `TabSide` | -| `DrawTabBorder()` | `border.TabSide`, `border.TabOffset`, `border.EffectiveTabLength` → local | -| `GetTabDepth()` | Uses `Adornment.Thickness` only — **no change** | -| `IsFocusedOrLastTab()` | No tab config reads — **no change** | - -Also update `EffectiveTabLength` getter (from Phase 1) to read `TabSide`/`TabLength` from `this` instead of from Border. - -### Step 2.4: Update `Tabs.cs` - -This is the primary external consumer. All patterns are `view.Border.TabSide`, `tab.Border.TabOffset`, etc. - -Add a static helper (or extension) to reduce cast noise: - -```csharp -// In Tabs.cs (private helper) -private static BorderView GetBorderView (View tab) => (BorderView)tab.Border.View!; -``` - -Then update all call sites: - -| Old | New | -|-----|-----| -| `view.Border.TabSide = _tabSide` | `GetBorderView (view).TabSide = _tabSide` | -| `tab.Border.TabOffset = offset` | `GetBorderView (tab).TabOffset = offset` | -| `tab.Border.TabLength = null` | `GetBorderView (tab).TabLength = null` | -| `tab.Border.EffectiveTabLength` | `GetBorderView (tab).EffectiveTabLength` | - -Approximate count: ~15 sites in `Tabs.cs`. - -### Step 2.5: Update `BorderEditor.cs` - -`BorderEditor.cs` in `Examples/UICatalog/Scenarios/EditorsAndHelpers/` casts to `Border` and reads/writes `TabSide`, `TabOffset`. Change to cast to `BorderView` via `AdornmentToEdit.View`: - -```csharp -// Old: -((Border)AdornmentToEdit).TabSide -// New: -((BorderView)AdornmentToEdit.View!).TabSide -``` - -Approximate count: ~5 sites. - -### Step 2.6: Update `Adornments.cs` scenario - -`Examples/UICatalog/Scenarios/Adornments.cs` reads `window.Border.TabSide`, `window.Border.TabOffset`, `window.Border.TabLength`. Change to access via `BorderView`: - -```csharp -BorderView bv = (BorderView)window.Border.View!; -bv.TabSide ... -bv.TabOffset ... -``` - -Approximate count: ~5 sites. - -### Step 2.7: Update `UICatalogRunnable.cs` - -Has 2 commented-out references to `Border.TabSide`. Update or remove the comments. - -### Step 2.8: Update tests - -Tests that access `view.Border.TabSide`, `view.Border.TabOffset`, `view.Border.TabLength` need updating: - -| Test file | Approx sites | -|-----------|-------------| -| `TabsTests.cs` | ~10 | -| `TabsScrollingTests.cs` | ~55 | -| `BorderViewTests.cs` | ~30 | -| `TitleViewTests.cs` | ~5 | -| `TabCompositionTests.cs` | ~3 | -| `AdornmentSubViewLineCanvasTests.cs` | ~2 | - -Pattern: add a local helper or inline cast. For test files with many accesses, a helper at the top of the class: - -```csharp -private static BorderView Bv (View v) => (BorderView)v.Border.View!; -``` - -### Step 2.9: Update docs - -- `docfx/docs/borders.md` — update "Key Properties" table and code examples to use `BorderView` access pattern. -- `Border.cs` XML docs — remove tab-related examples and references. -- `BorderView.cs` XML docs — add docs for the new properties. -- `BorderSettings.cs` — update `Tab` doc to reference `BorderView.TabSide` etc. instead of `Border.TabSide`. - -### Step 2.10: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build -``` - ---- - -## Phase 3: Remove `SettingsChanged` Event - -### Step 3.1: Replace event with direct call - -In `Border.Settings` setter, replace: - -```csharp -SettingsChanged?.Invoke (this, EventArgs.Empty); -``` - -with: - -```csharp -(View as BorderView)?.ConfigureForTabMode (); -``` - -Make `ConfigureForTabMode` `internal` (currently `private`). - -### Step 3.2: Remove event + subscription - -- Delete `public event EventHandler? SettingsChanged;` from `Border.cs`. -- Delete `border.SettingsChanged += OnSettingsChanged;` from `BorderView` constructor. -- Delete the `OnSettingsChanged` bridge method from `BorderView`. - -### Step 3.3: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -``` - ---- - -## Files Changed - -| File | Phase | Change | -|------|-------|--------| -| `Terminal.Gui/ViewBase/Adornment/Border.cs` | 1,2,3 | Remove `TabSide`, `TabOffset`, `TabLength`, `TabEnd`, `EffectiveTabLength`, `SettingsChanged`; update `Settings` setter | -| `Terminal.Gui/ViewBase/Adornment/BorderView.cs` | 1,2 | Add `TabSide`, `TabOffset`, `TabLength`, `EffectiveTabLength`; update all internal reads to use local props; make `ConfigureForTabMode` internal | -| `Terminal.Gui/ViewBase/Adornment/BorderSettings.cs` | 2 | Update XML doc for `Tab` to reference `BorderView` | -| `Terminal.Gui/Views/Tabs.cs` | 1,2 | Add helper; update ~15 call sites | -| `Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs` | 2 | Update ~5 call sites | -| `Examples/UICatalog/Scenarios/Adornments.cs` | 2 | Update ~5 call sites | -| `Examples/UICatalog/UICatalogRunnable.cs` | 2 | Update 2 commented-out references | -| `Tests/.../TabsTests.cs` | 2 | Update ~10 sites | -| `Tests/.../TabsScrollingTests.cs` | 2 | Update ~55 sites | -| `Tests/.../BorderViewTests.cs` | 1,2 | Update ~30 sites | -| `Tests/.../TitleViewTests.cs` | 2 | Update ~5 sites | -| `Tests/.../TabCompositionTests.cs` | 2 | Update ~3 sites | -| `Tests/.../AdornmentSubViewLineCanvasTests.cs` | 2 | Update ~2 sites | -| `docfx/docs/borders.md` | 2 | Update property table and examples | - ---- - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| `Tabs.cs` accesses tab properties before `BorderView` exists | `Tabs.cs` always sets `Border.Settings = Tab \| Title` first, which triggers `GetOrCreateView()` — `BorderView` exists before any tab property access | -| Tests that set `Border.TabOffset` without first enabling `BorderSettings.Tab` | These tests must also set `Border.Settings` to include `Tab` (which creates `BorderView`) before accessing tab properties. Fix in test updates. | -| Forgot a consumer — compile error | Good: compile errors are easy to find and fix. No silent runtime breakage. | -| `ConfigureForTabMode` visibility change | Making it `internal` is safe — it's only called from within the assembly |