diff --git a/.gitattributes b/.gitattributes
index 35720cb73d..bed4b064a7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,13 +1,14 @@
-# Set the default behavior for all files.
+# Set default behavior to automatically normalize line endings.
* text=auto
-# Normalized and converts to
-# native line endings on checkout.
-*.cs text
-
-# Convert to LF line endings on checkout.
+# Explicitly declare text files you want to always be normalized and converted to native line endings on checkout.
+*.cs text eol=lf
+*.txt text eol=lf
+*.md text eol=lf
*.sh text eol=lf
+*.ps1 text eol=lf
-# Binary files.
+# Denote all files that are truly binary and should not be modified.
*.png binary
-*.jpg binary
\ No newline at end of file
+*.jpg binary
+*.gif binary
diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs
index 35e7e69dcc..a5b144f212 100644
--- a/Terminal.Gui/Drawing/Region.cs
+++ b/Terminal.Gui/Drawing/Region.cs
@@ -2,166 +2,407 @@
namespace Terminal.Gui;
-using System.Buffers;
-
///
-/// Represents a region composed of one or more rectangles, providing methods for union, intersection, exclusion, and
-/// complement operations.
+/// Represents a region composed of one or more rectangles, providing methods for geometric set operations such as
+/// union,
+/// intersection, exclusion, and complement. This class is designed for use in graphical or terminal-based user
+/// interfaces
+/// where regions need to be manipulated to manage screen areas, clipping, or drawing boundaries.
///
-public class Region : IDisposable
+///
+///
+/// This class is thread-safe. All operations are synchronized to ensure consistent state when accessed concurrently.
+///
+///
+/// The class adopts a philosophy of efficiency and flexibility, balancing performance with
+/// usability for GUI applications. It maintains a list of objects, representing disjoint
+/// (non-overlapping) rectangular areas, and supports operations inspired by set theory. These operations allow
+/// combining regions in various ways, such as merging areas ( or
+/// ),
+/// finding common areas (), or removing portions (
+/// or
+/// ).
+///
+///
+/// To achieve high performance, the class employs a sweep-line algorithm for merging rectangles, which efficiently
+/// processes large sets of rectangles in O(n log n) time by scanning along the x-axis and tracking active vertical
+/// intervals. This approach ensures scalability for typical GUI scenarios with moderate numbers of rectangles. For
+/// operations like and , an optional minimization
+/// step (
+///
+/// ) is used to reduce the number of rectangles to a minimal set, producing the smallest
+/// possible collection of non-overlapping rectangles that cover the same area. This minimization, while O(n²) in
+/// worst-case complexity, is optimized for small-to-medium collections and provides a compact representation ideal
+/// for drawing or logical operations.
+///
+///
+/// The class is immutable in its operations (returning new regions or modifying in-place via methods like
+/// ), supports nullability for robustness, and implements
+/// to manage
+/// resources by clearing internal state. Developers can choose between granular (detailed) or minimal (compact)
+/// outputs for union operations via and , catering
+/// to diverse use cases such as rendering optimization, event handling, or visualization.
+///
+///
+public class Region
{
- private List _rectangles;
+ private readonly List _rectangles = [];
+
+ // Add a single reusable list for temp operations
+ private readonly List _tempRectangles = new();
+
+ // Object used for synchronization
+ private readonly object _syncLock = new object();
///
/// Initializes a new instance of the class.
///
- public Region () { _rectangles = new (); }
+ public Region () { }
///
/// Initializes a new instance of the class with the specified rectangle.
///
/// The initial rectangle for the region.
- public Region (Rectangle rectangle) { _rectangles = new () { rectangle }; }
+ public Region (Rectangle rectangle)
+ {
+ lock (_syncLock)
+ {
+ _rectangles.Add (rectangle);
+ }
+ }
///
- /// Adds the specified rectangle to the region.
+ /// Creates an exact copy of the region.
///
- /// The rectangle to add to the region.
- public void Union (Rectangle rectangle)
+ /// A new that is a copy of this instance.
+ public Region Clone ()
{
- _rectangles.Add (rectangle);
- _rectangles = MergeRectangles (_rectangles);
+ lock (_syncLock)
+ {
+ var clone = new Region ();
+ clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity
+ clone._rectangles.AddRange (_rectangles);
+
+ return clone;
+ }
}
///
- /// Adds the specified region to this region.
+ /// Combines with the region using the specified operation.
///
- /// The region to add to this region.
- public void Union (Region region)
+ /// The rectangle to combine.
+ /// The operation to perform.
+ public void Combine (Rectangle rectangle, RegionOp operation)
{
- _rectangles.AddRange (region._rectangles);
- _rectangles = MergeRectangles (_rectangles);
+ lock (_syncLock)
+ {
+ if (rectangle.IsEmpty && operation != RegionOp.Replace)
+ {
+ if (operation == RegionOp.Intersect)
+ {
+ _rectangles.Clear ();
+ }
+
+ return;
+ }
+
+ Combine (new Region (rectangle), operation);
+ }
}
///
- /// Updates the region to be the intersection of itself with the specified rectangle.
+ /// Combines with the region using the specified operation.
///
- /// The rectangle to intersect with the region.
- public void Intersect (Rectangle rectangle)
+ /// The region to combine.
+ /// The operation to perform.
+ public void Combine (Region? region, RegionOp operation)
{
- if (_rectangles.Count == 0)
+ lock (_syncLock)
{
+ CombineInternal(region, operation);
+ }
+ }
+
+ // Private method to implement the combine logic within a lock
+ private void CombineInternal(Region? region, RegionOp operation)
+ {
+ if (region is null || region._rectangles.Count == 0)
+ {
+ if (operation is RegionOp.Intersect or RegionOp.Replace)
+ {
+ _rectangles.Clear ();
+ }
+
return;
}
- // TODO: In-place swap within the original list. Does order of intersections matter?
- // Rectangle = 4 * i32 = 16 B
- // ~128 B stack allocation
- const int maxStackallocLength = 8;
- Rectangle []? rentedArray = null;
- try
+
+ switch (operation)
{
- Span rectBuffer = _rectangles.Count <= maxStackallocLength
- ? stackalloc Rectangle[maxStackallocLength]
- : (rentedArray = ArrayPool.Shared.Rent (_rectangles.Count));
+ case RegionOp.Difference:
- _rectangles.CopyTo (rectBuffer);
- ReadOnlySpan rectangles = rectBuffer[.._rectangles.Count];
- _rectangles.Clear ();
+ // region is regionB
+ // We'll chain the difference: (regionA - rect1) - rect2 - rect3 ...
+ List newRectangles = new (_rectangles);
- foreach (var rect in rectangles)
- {
- Rectangle intersection = Rectangle.Intersect (rect, rectangle);
- if (!intersection.IsEmpty)
+ foreach (Rectangle rect in region._rectangles)
+ {
+ List temp = new ();
+
+ foreach (Rectangle r in newRectangles)
+ {
+ temp.AddRange (SubtractRectangle (r, rect));
+ }
+
+ newRectangles = temp;
+ }
+
+ _rectangles.Clear ();
+ _rectangles.AddRange (newRectangles);
+
+ break;
+
+ case RegionOp.Intersect:
+ List intersections = new (_rectangles.Count); // Pre-allocate
+
+ // Null is same as empty region
+ region ??= new ();
+
+ foreach (Rectangle rect1 in _rectangles)
{
- _rectangles.Add (intersection);
+ foreach (Rectangle rect2 in region!._rectangles)
+ {
+ Rectangle intersected = Rectangle.Intersect (rect1, rect2);
+
+ if (!intersected.IsEmpty)
+ {
+ intersections.Add (intersected);
+ }
+ }
}
- }
+
+ _rectangles.Clear ();
+ _rectangles.AddRange (intersections);
+
+ break;
+
+ case RegionOp.Union:
+ // Avoid collection initialization with spread operator
+ _tempRectangles.Clear();
+ _tempRectangles.AddRange(_rectangles);
+ if (region != null)
+ {
+ // Get the region's rectangles safely
+ lock (region._syncLock)
+ {
+ _tempRectangles.AddRange(region._rectangles);
+ }
+ }
+ List mergedUnion = MergeRectangles(_tempRectangles, false);
+ _rectangles.Clear();
+ _rectangles.AddRange(mergedUnion);
+ break;
+
+ case RegionOp.MinimalUnion:
+ // Avoid collection initialization with spread operator
+ _tempRectangles.Clear();
+ _tempRectangles.AddRange(_rectangles);
+ if (region != null)
+ {
+ // Get the region's rectangles safely
+ lock (region._syncLock)
+ {
+ _tempRectangles.AddRange(region._rectangles);
+ }
+ }
+ List mergedMinimalUnion = MergeRectangles(_tempRectangles, true);
+ _rectangles.Clear();
+ _rectangles.AddRange(mergedMinimalUnion);
+ break;
+
+ case RegionOp.XOR:
+ Exclude (region);
+ region.Combine (this, RegionOp.Difference);
+ _rectangles.AddRange (region._rectangles);
+
+ break;
+
+ case RegionOp.ReverseDifference:
+ region.Combine (this, RegionOp.Difference);
+ _rectangles.Clear ();
+ _rectangles.AddRange (region._rectangles);
+
+ break;
+
+ case RegionOp.Replace:
+ _rectangles.Clear ();
+ _rectangles.Capacity = region._rectangles.Count; // Pre-allocate
+ _rectangles.AddRange (region._rectangles);
+
+ break;
+ }
+ }
+
+ ///
+ /// Updates the region to be the complement of itself within the specified bounds.
+ ///
+ /// The bounding rectangle to use for complementing the region.
+ public void Complement (Rectangle bounds)
+ {
+ if (bounds.IsEmpty || _rectangles.Count == 0)
+ {
+ _rectangles.Clear ();
+
+ return;
}
- finally
+
+ List complementRectangles = new (4) { bounds }; // Typical max initial capacity
+
+ foreach (Rectangle rect in _rectangles)
{
- if (rentedArray != null)
- {
- ArrayPool.Shared.Return (rentedArray);
- }
+ complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
}
+
+ _rectangles.Clear ();
+ _rectangles.AddRange (complementRectangles);
}
///
- /// Updates the region to be the intersection of itself with the specified region.
+ /// Determines whether the specified point is contained within the region.
///
- /// The region to intersect with this region.
- public void Intersect (Region region)
+ /// The x-coordinate of the point.
+ /// The y-coordinate of the point.
+ /// true if the point is contained within the region; otherwise, false.
+ public bool Contains (int x, int y)
{
- List intersections = new List ();
-
- foreach (Rectangle rect1 in _rectangles)
+ lock (_syncLock)
{
- foreach (Rectangle rect2 in region._rectangles)
+ foreach (Rectangle r in _rectangles)
{
- Rectangle intersected = Rectangle.Intersect (rect1, rect2);
-
- if (!intersected.IsEmpty)
+ if (r.Contains (x, y))
{
- intersections.Add (intersected);
+ return true;
}
}
- }
- _rectangles = intersections;
+ return false;
+ }
}
///
- /// Removes the specified rectangle from the region.
+ /// Determines whether the specified rectangle is contained within the region.
///
- /// The rectangle to exclude from the region.
- public void Exclude (Rectangle rectangle) { _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rectangle)).ToList (); }
+ /// The rectangle to check for containment.
+ /// true if the rectangle is contained within the region; otherwise, false.
+ public bool Contains (Rectangle rectangle)
+ {
+ lock (_syncLock)
+ {
+ foreach (Rectangle r in _rectangles)
+ {
+ if (r.Contains (rectangle))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
///
- /// Removes the portion of the specified region from this region.
+ /// Determines whether the specified object is equal to this region.
///
- /// The region to exclude from this region.
- public void Exclude (Region region)
+ /// The object to compare with this region.
+ /// true if the objects are equal; otherwise, false.
+ public override bool Equals (object? obj) { return obj is Region other && Equals (other); }
+
+ private static bool IsRegionEmpty (List rectangles)
{
- foreach (Rectangle rect in region._rectangles)
+ if (rectangles.Count == 0)
{
- _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
+ return true;
+ }
+
+ foreach (Rectangle r in rectangles)
+ {
+ if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
+ {
+ return false;
+ }
}
+
+ return true;
}
///
- /// Updates the region to be the complement of itself within the specified bounds.
+ /// Determines whether the specified region is equal to this region.
///
- /// The bounding rectangle to use for complementing the region.
- public void Complement (Rectangle bounds)
+ /// The region to compare with this region.
+ /// true if the regions are equal; otherwise, false.
+ public bool Equals (Region? other)
{
- if (bounds.IsEmpty || _rectangles.Count == 0)
+ if (other is null)
{
- _rectangles.Clear ();
+ return false;
+ }
- return;
+ if (ReferenceEquals (this, other))
+ {
+ return true;
}
- List complementRectangles = new List { bounds };
+ // Check if either region is empty
+ bool thisEmpty = IsRegionEmpty (_rectangles);
+ bool otherEmpty = IsRegionEmpty (other._rectangles);
- foreach (Rectangle rect in _rectangles)
+ // If either is empty, they're equal only if both are empty
+ if (thisEmpty || otherEmpty)
{
- complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
+ return thisEmpty == otherEmpty;
+ }
+
+ // For non-empty regions, compare rectangle counts
+ if (_rectangles.Count != other._rectangles.Count)
+ {
+ return false;
+ }
+
+ // Compare all rectangles - order matters since we maintain canonical form
+ for (var i = 0; i < _rectangles.Count; i++)
+ {
+ if (!_rectangles [i].Equals (other._rectangles [i]))
+ {
+ return false;
+ }
}
- _rectangles = complementRectangles;
+ return true;
}
///
- /// Creates an exact copy of the region.
+ /// Removes the specified rectangle from the region.
///
- /// A new that is a copy of this instance.
- public Region Clone ()
- {
- var clone = new Region ();
- clone._rectangles = new (_rectangles);
+ ///
+ ///
+ /// This is a helper method that is equivalent to calling with
+ /// .
+ ///
+ ///
+ /// The rectangle to exclude from the region.
+ public void Exclude (Rectangle rectangle) { Combine (rectangle, RegionOp.Difference); }
- return clone;
- }
+ ///
+ /// Removes the portion of the specified region from this region.
+ ///
+ ///
+ ///
+ /// This is a helper method that is equivalent to calling with
+ /// .
+ ///
+ ///
+ /// The region to exclude from this region.
+ public void Exclude (Region? region) { Combine (region, RegionOp.Difference); }
///
/// Gets a bounding rectangle for the entire region.
@@ -174,114 +415,346 @@ public Rectangle GetBounds ()
return Rectangle.Empty;
}
- int left = _rectangles.Min (r => r.Left);
- int top = _rectangles.Min (r => r.Top);
- int right = _rectangles.Max (r => r.Right);
- int bottom = _rectangles.Max (r => r.Bottom);
+ Rectangle first = _rectangles [0];
+ int left = first.Left;
+ int top = first.Top;
+ int right = first.Right;
+ int bottom = first.Bottom;
+
+ for (var i = 1; i < _rectangles.Count; i++)
+ {
+ Rectangle r = _rectangles [i];
+ left = Math.Min (left, r.Left);
+ top = Math.Min (top, r.Top);
+ right = Math.Max (right, r.Right);
+ bottom = Math.Max (bottom, r.Bottom);
+ }
return new (left, top, right - left, bottom - top);
}
///
- /// Determines whether the region is empty.
+ /// Returns a hash code for this region.
///
- /// true if the region is empty; otherwise, false.
- public bool IsEmpty () { return !_rectangles.Any (); }
+ /// A hash code for this region.
+ public override int GetHashCode ()
+ {
+ var hash = new HashCode ();
+
+ foreach (Rectangle rect in _rectangles)
+ {
+ hash.Add (rect);
+ }
+
+ return hash.ToHashCode ();
+ }
///
- /// Determines whether the specified point is contained within the region.
+ /// Returns an array of rectangles that represent the region.
///
- /// The x-coordinate of the point.
- /// The y-coordinate of the point.
- /// true if the point is contained within the region; otherwise, false.
- public bool Contains (int x, int y)
+ /// An array of objects that make up the region.
+ public Rectangle [] GetRectangles () { return _rectangles.ToArray (); }
+
+ ///
+ /// Updates the region to be the intersection of itself with the specified rectangle.
+ ///
+ ///
+ ///
+ /// This is a helper method that is equivalent to calling with
+ /// .
+ ///
+ ///
+ /// The rectangle to intersect with the region.
+ public void Intersect (Rectangle rectangle) { Combine (rectangle, RegionOp.Intersect); }
+
+ ///
+ /// Updates the region to be the intersection of itself with the specified region.
+ ///
+ ///
+ ///
+ /// This is a helper method that is equivalent to calling with
+ /// .
+ ///
+ ///
+ /// The region to intersect with this region.
+ public void Intersect (Region? region) { Combine (region, RegionOp.Intersect); }
+
+ ///
+ /// Determines whether the region is empty.
+ ///
+ /// true if the region is empty; otherwise, false.
+ public bool IsEmpty ()
{
- foreach (var rect in _rectangles)
+ if (_rectangles.Count == 0)
{
- if (rect.Contains (x, y))
+ return true;
+ }
+
+ foreach (Rectangle r in _rectangles)
+ {
+ if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
{
- return true;
+ return false;
}
}
- return false;
+
+ return true;
}
///
- /// Determines whether the specified rectangle is contained within the region.
+ /// Translates all rectangles in the region by the specified offsets.
///
- /// The rectangle to check for containment.
- /// true if the rectangle is contained within the region; otherwise, false.
- public bool Contains (Rectangle rectangle)
+ /// The amount to offset along the x-axis.
+ /// The amount to offset along the y-axis.
+ public void Translate (int offsetX, int offsetY)
{
- foreach (var rect in _rectangles)
+ if (offsetX == 0 && offsetY == 0)
{
- if (rect.Contains (rectangle))
- {
- return true;
- }
+ return;
+ }
+
+ for (var i = 0; i < _rectangles.Count; i++)
+ {
+ Rectangle rect = _rectangles [i];
+ _rectangles [i] = rect with { X = rect.Left + offsetX, Y = rect.Top + offsetY };
}
- return false;
}
///
- /// Returns an array of rectangles that represent the region.
+ /// Adds the specified rectangle to the region. Merges all rectangles into a minimal or granular bounding shape.
///
- /// An array of objects that make up the region.
- public Rectangle [] GetRegionScans () { return _rectangles.ToArray (); }
+ /// The rectangle to add to the region.
+ public void Union (Rectangle rectangle) { Combine (rectangle, RegionOp.Union); }
///
- /// Offsets all rectangles in the region by the specified amounts.
+ /// Adds the specified region to this region. Merges all rectangles into a minimal or granular bounding shape.
///
- /// The amount to offset along the x-axis.
- /// The amount to offset along the y-axis.
- public void Offset (int offsetX, int offsetY)
+ /// The region to add to this region.
+ public void Union (Region? region) { Combine (region, RegionOp.Union); }
+
+ ///
+ /// Adds the specified rectangle to the region. Merges all rectangles into the smallest possible bounding shape.
+ ///
+ /// The rectangle to add to the region.
+ public void MinimalUnion (Rectangle rectangle) { Combine (rectangle, RegionOp.MinimalUnion); }
+
+ ///
+ /// Adds the specified region to this region. Merges all rectangles into the smallest possible bounding shape.
+ ///
+ /// The region to add to this region.
+ public void MinimalUnion (Region? region) { Combine (region, RegionOp.MinimalUnion); }
+
+ ///
+ /// Merges overlapping rectangles into a minimal or granular set of non-overlapping rectangles with a minimal bounding
+ /// shape.
+ ///
+ /// The list of rectangles to merge.
+ ///
+ /// If true, minimizes the set to the smallest possible number of rectangles; otherwise,
+ /// returns a granular set.
+ ///
+ /// A list of merged rectangles.
+ internal static List MergeRectangles (List rectangles, bool minimize)
{
- for (int i = 0; i < _rectangles.Count; i++)
+ if (rectangles.Count == 0)
{
- var rect = _rectangles [i];
- _rectangles [i] = new Rectangle (rect.Left + offsetX, rect.Top + offsetY, rect.Width, rect.Height);
+ return [];
}
+
+ // Sweep-line algorithm to merge rectangles
+ List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2); // Pre-allocate
+
+ foreach (Rectangle r in rectangles)
+ {
+ if (!r.IsEmpty)
+ {
+ events.Add ((r.Left, true, r.Top, r.Bottom)); // Start event
+ events.Add ((r.Right, false, r.Top, r.Bottom)); // End event
+ }
+ }
+
+ if (events.Count == 0)
+ {
+ return []; // Return empty list if no non-empty rectangles exist
+ }
+
+ events.Sort (
+ (a, b) =>
+ {
+ int cmp = a.x.CompareTo (b.x);
+
+ if (cmp != 0)
+ {
+ return cmp;
+ }
+
+ return a.isStart.CompareTo (b.isStart); // Start events before end events at same x
+ });
+
+ List merged = [];
+
+ SortedSet<(int yTop, int yBottom)> active = new (
+ Comparer<(int yTop, int yBottom)>.Create (
+ (a, b) =>
+ {
+ int cmp = a.yTop.CompareTo (b.yTop);
+
+ return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
+ }));
+ int lastX = events [0].x;
+
+ foreach ((int x, bool isStart, int yTop, int yBottom) evt in events)
+ {
+ // Output rectangles for the previous segment if there are active rectangles
+ if (active.Count > 0 && evt.x > lastX)
+ {
+ merged.AddRange (MergeVerticalIntervals (active, lastX, evt.x));
+ }
+
+ // Process the event
+ if (evt.isStart)
+ {
+ active.Add ((evt.yTop, evt.yBottom));
+ }
+ else
+ {
+ active.Remove ((evt.yTop, evt.yBottom));
+ }
+
+ lastX = evt.x;
+ }
+
+ return minimize ? MinimizeRectangles (merged) : merged;
}
///
- /// Merges overlapping rectangles into a minimal set of non-overlapping rectangles.
+ /// Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles.
///
- /// The list of rectangles to merge.
+ /// The set of active vertical intervals.
+ /// The starting x-coordinate for the rectangles.
+ /// The ending x-coordinate for the rectangles.
/// A list of merged rectangles.
- private List MergeRectangles (List rectangles)
+ internal static List MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX)
{
- // Simplified merging logic: this does not handle all edge cases for merging overlapping rectangles.
- // For a full implementation, a plane sweep algorithm or similar would be needed.
- List merged = new List (rectangles);
- bool mergedAny;
+ if (active.Count == 0)
+ {
+ return [];
+ }
+
+ List result = new (active.Count); // Pre-allocate
+ int? currentTop = null;
+ int? currentBottom = null;
+
+ foreach ((int yTop, int yBottom) in active)
+ {
+ if (currentTop == null)
+ {
+ currentTop = yTop;
+ currentBottom = yBottom;
+ }
+ else if (yTop <= currentBottom)
+ {
+ currentBottom = Math.Max (currentBottom.Value, yBottom);
+ }
+ else
+ {
+ result.Add (new (startX, currentTop.Value, endX - startX, currentBottom.Value - currentTop.Value));
+ currentTop = yTop;
+ currentBottom = yBottom;
+ }
+ }
+
+ if (currentTop != null)
+ {
+ result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles
+ /// by merging adjacent rectangles where possible.
+ ///
+ /// The list of rectangles to minimize.
+ /// A list of minimized rectangles.
+ internal static List MinimizeRectangles (List rectangles)
+ {
+ if (rectangles.Count <= 1)
+ {
+ return rectangles.ToList ();
+ }
+
+ List minimized = new (rectangles.Count); // Pre-allocate
+ List current = new (rectangles); // Work with a copy
+
+ bool changed;
do
{
- mergedAny = false;
+ changed = false;
+ minimized.Clear ();
+
+ // Sort by Y then X for consistent processing
+ current.Sort (
+ (a, b) =>
+ {
+ int cmp = a.Top.CompareTo (b.Top);
- for (var i = 0; i < merged.Count; i++)
+ return cmp != 0 ? cmp : a.Left.CompareTo (b.Left);
+ });
+
+ var i = 0;
+
+ while (i < current.Count)
{
- for (int j = i + 1; j < merged.Count; j++)
+ Rectangle r = current [i];
+ int j = i + 1;
+
+ while (j < current.Count)
{
- if (merged [i].IntersectsWith (merged [j]))
+ Rectangle next = current [j];
+
+ // Check if rectangles can be merged horizontally (same Y range, adjacent X)
+ if (r.Top == next.Top && r.Bottom == next.Bottom && (r.Right == next.Left || next.Right == r.Left || r.IntersectsWith (next)))
{
- merged [i] = Rectangle.Union (merged [i], merged [j]);
- merged.RemoveAt (j);
- mergedAny = true;
+ r = new (
+ Math.Min (r.Left, next.Left),
+ r.Top,
+ Math.Max (r.Right, next.Right) - Math.Min (r.Left, next.Left),
+ r.Height
+ );
+ current.RemoveAt (j);
+ changed = true;
+ }
- break;
+ // Check if rectangles can be merged vertically (same X range, adjacent Y)
+ else if (r.Left == next.Left && r.Right == next.Right && (r.Bottom == next.Top || next.Bottom == r.Top || r.IntersectsWith (next)))
+ {
+ r = new (
+ r.Left,
+ Math.Min (r.Top, next.Top),
+ r.Width,
+ Math.Max (r.Bottom, next.Bottom) - Math.Min (r.Top, next.Top)
+ );
+ current.RemoveAt (j);
+ changed = true;
+ }
+ else
+ {
+ j++;
}
}
- if (mergedAny)
- {
- break;
- }
+ minimized.Add (r);
+ i++;
}
+
+ current = minimized.ToList (); // Prepare for next iteration
}
- while (mergedAny);
+ while (changed);
- return merged;
+ return minimized;
}
///
@@ -290,8 +763,28 @@ private List MergeRectangles (List rectangles)
/// The original rectangle.
/// The rectangle to subtract from the original.
/// An enumerable collection of resulting rectangles after subtraction.
- private IEnumerable SubtractRectangle (Rectangle original, Rectangle subtract)
+ internal static IEnumerable SubtractRectangle (Rectangle original, Rectangle subtract)
{
+ // Handle empty or invalid rectangles
+ if (original.IsEmpty || original.Width <= 0 || original.Height <= 0)
+ {
+ yield break; // Return empty enumeration for empty or invalid original
+ }
+
+ if (subtract.IsEmpty || subtract.Width <= 0 || subtract.Height <= 0)
+ {
+ yield return original;
+
+ yield break;
+ }
+
+ // Check for complete overlap (subtract fully contains or equals original)
+ if (subtract.Left <= original.Left && subtract.Top <= original.Top && subtract.Right >= original.Right && subtract.Bottom >= original.Bottom)
+ {
+ yield break; // Return empty if subtract completely overlaps original
+ }
+
+ // Check for no overlap
if (!original.IntersectsWith (subtract))
{
yield return original;
@@ -299,19 +792,29 @@ private IEnumerable SubtractRectangle (Rectangle original, Rectangle
yield break;
}
- // Top segment
+ // Fragment the original rectangle into segments excluding the subtract rectangle
+
+ // Top segment (above subtract)
if (original.Top < subtract.Top)
{
- yield return new (original.Left, original.Top, original.Width, subtract.Top - original.Top);
+ yield return new (
+ original.Left,
+ original.Top,
+ original.Width,
+ subtract.Top - original.Top);
}
- // Bottom segment
+ // Bottom segment (below subtract)
if (original.Bottom > subtract.Bottom)
{
- yield return new (original.Left, subtract.Bottom, original.Width, original.Bottom - subtract.Bottom);
+ yield return new (
+ original.Left,
+ subtract.Bottom,
+ original.Width,
+ original.Bottom - subtract.Bottom);
}
- // Left segment
+ // Left segment (to the left of subtract)
if (original.Left < subtract.Left)
{
int top = Math.Max (original.Top, subtract.Top);
@@ -319,11 +822,15 @@ private IEnumerable SubtractRectangle (Rectangle original, Rectangle
if (bottom > top)
{
- yield return new (original.Left, top, subtract.Left - original.Left, bottom - top);
+ yield return new (
+ original.Left,
+ top,
+ subtract.Left - original.Left,
+ bottom - top);
}
}
- // Right segment
+ // Right segment (to the right of subtract)
if (original.Right > subtract.Right)
{
int top = Math.Max (original.Top, subtract.Top);
@@ -331,13 +838,302 @@ private IEnumerable SubtractRectangle (Rectangle original, Rectangle
if (bottom > top)
{
- yield return new (subtract.Right, top, original.Right - subtract.Right, bottom - top);
+ yield return new (
+ subtract.Right,
+ top,
+ original.Right - subtract.Right,
+ bottom - top);
}
}
}
///
- /// Releases all resources used by the .
+ /// Fills the interior of all rectangles in the region with the specified attribute and fill rune.
///
- public void Dispose () { _rectangles.Clear (); }
+ /// The attribute (color/style) to use.
+ ///
+ /// The rune to fill the interior of the rectangles with. If space will be
+ /// used.
+ ///
+ public void FillRectangles (Attribute attribute, Rune? fillRune = null)
+ {
+ if (_rectangles.Count == 0)
+ {
+ return;
+ }
+
+ foreach (Rectangle rect in _rectangles)
+ {
+ if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+ {
+ continue;
+ }
+
+ Application.Driver?.SetAttribute (attribute);
+
+ for (int y = rect.Top; y < rect.Bottom; y++)
+ {
+ for (int x = rect.Left; x < rect.Right; x++)
+ {
+ Application.Driver?.Move (x, y);
+ Application.Driver?.AddRune (fillRune ?? (Rune)' ');
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
+ /// enough.
+ ///
+ /// The canvas to draw on.
+ /// The line style to use for drawing.
+ /// The attribute (color/style) to use for the lines. If null.
+ public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attribute = null)
+ {
+ if (_rectangles.Count == 0)
+ {
+ return;
+ }
+
+ foreach (Rectangle rect in _rectangles)
+ {
+ if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+ {
+ continue;
+ }
+
+ // Only draw boundaries if the rectangle is "big enough" (e.g., width and height > 1)
+ //if (rect.Width > 2 && rect.Height > 2)
+ {
+ if (rect.Width > 1)
+ {
+ // Add horizontal lines
+ canvas.AddLine (new (rect.Left, rect.Top), rect.Width, Orientation.Horizontal, style, attribute);
+ canvas.AddLine (new (rect.Left, rect.Bottom - 1), rect.Width, Orientation.Horizontal, style, attribute);
+ }
+
+ if (rect.Height > 1)
+ {
+ // Add vertical lines
+ canvas.AddLine (new (rect.Left, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
+ canvas.AddLine (new (rect.Right - 1, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
+ }
+ }
+ }
+ }
+
+
+ // BUGBUG: DrawOuterBoundary does not work right. it draws all regions +1 too tall/wide. It should draw single width/height regions as just a line.
+ //
+ // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4).
+ // This is how they should draw:
+ //
+ // |123456789|123456789|123456789
+ // 1 │ ┌┐ ┌─┐
+ // 2 │ ││ │ │
+ // 3 │ ││ │ │
+ // 4 │ └┘ └─┘
+ //
+ // But this is what it draws:
+ // |123456789|123456789|123456789
+ // 1┌┐ ┌─┐ ┌──┐
+ // 2││ │ │ │ │
+ // 3││ │ │ │ │
+ // 4││ │ │ │ │
+ // 5└┘ └─┘ └──┘
+ //
+ // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3).
+ // This is fill - correct:
+ // |123456789
+ // 1░░░
+ // 2░░░
+ // 3░░░░░
+ // 4 ░░░
+ // 5 ░░░
+ // 6
+ //
+ // This is what DrawOuterBoundary should draw
+ // |123456789|123456789
+ // 1┌─┐
+ // 2│ │
+ // 3└─┼─┐
+ // 4 │ │
+ // 5 └─┘
+ // 6
+ //
+ // This is what DrawOuterBoundary actually draws
+ // |123456789|123456789
+ // 1┌──┐
+ // 2│ │
+ // 3│ └─┐
+ // 4└─┐ │
+ // 5 │ │
+ // 6 └──┘
+
+ ///
+ /// Draws the outer perimeter of the region to using and
+ /// .
+ /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing
+ /// boundaries and excluding internal lines.
+ ///
+ /// The LineCanvas to draw on.
+ /// The line style to use for drawing.
+ /// The attribute (color/style) to use for the lines. If null.
+ public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null)
+ {
+ if (_rectangles.Count == 0)
+ {
+ return;
+ }
+
+ // Get the bounds of the region
+ Rectangle bounds = GetBounds ();
+
+ // Add protection against extremely large allocations
+ if (bounds.Width > 1000 || bounds.Height > 1000)
+ {
+ // Fall back to drawing each rectangle's boundary
+ DrawBoundaries(lineCanvas, style, attribute);
+ return;
+ }
+
+ // Create a grid to track which cells are inside the region
+ var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1];
+
+ // Fill the grid based on rectangles
+ foreach (Rectangle rect in _rectangles)
+ {
+ if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
+ {
+ continue;
+ }
+
+ for (int x = rect.Left; x < rect.Right; x++)
+ {
+ for (int y = rect.Top; y < rect.Bottom; y++)
+ {
+ // Adjust coordinates to grid space
+ int gridX = x - bounds.Left;
+ int gridY = y - bounds.Top;
+
+ if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height)
+ {
+ insideRegion [gridX, gridY] = true;
+ }
+ }
+ }
+ }
+
+ // Find horizontal boundary lines
+ for (var y = 0; y <= bounds.Height; y++)
+ {
+ int startX = -1;
+
+ for (var x = 0; x <= bounds.Width; x++)
+ {
+ bool above = y > 0 && insideRegion [x, y - 1];
+ bool below = y < bounds.Height && insideRegion [x, y];
+
+ // A boundary exists where one side is inside and the other is outside
+ bool isBoundary = above != below;
+
+ if (isBoundary)
+ {
+ // Start a new segment or continue the current one
+ if (startX == -1)
+ {
+ startX = x;
+ }
+ }
+ else
+ {
+ // End the current segment if one exists
+ if (startX != -1)
+ {
+ int length = x - startX + 1; // Add 1 to make sure lines connect
+
+ lineCanvas.AddLine (
+ new (startX + bounds.Left, y + bounds.Top),
+ length,
+ Orientation.Horizontal,
+ style,
+ attribute
+ );
+ startX = -1;
+ }
+ }
+ }
+
+ // End any segment that reaches the right edge
+ if (startX != -1)
+ {
+ int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect
+
+ lineCanvas.AddLine (
+ new (startX + bounds.Left, y + bounds.Top),
+ length,
+ Orientation.Horizontal,
+ style,
+ attribute
+ );
+ }
+ }
+
+ // Find vertical boundary lines
+ for (var x = 0; x <= bounds.Width; x++)
+ {
+ int startY = -1;
+
+ for (var y = 0; y <= bounds.Height; y++)
+ {
+ bool left = x > 0 && insideRegion [x - 1, y];
+ bool right = x < bounds.Width && insideRegion [x, y];
+
+ // A boundary exists where one side is inside and the other is outside
+ bool isBoundary = left != right;
+
+ if (isBoundary)
+ {
+ // Start a new segment or continue the current one
+ if (startY == -1)
+ {
+ startY = y;
+ }
+ }
+ else
+ {
+ // End the current segment if one exists
+ if (startY != -1)
+ {
+ int length = y - startY + 1; // Add 1 to make sure lines connect
+
+ lineCanvas.AddLine (
+ new (x + bounds.Left, startY + bounds.Top),
+ length,
+ Orientation.Vertical,
+ style,
+ attribute
+ );
+ startY = -1;
+ }
+ }
+ }
+
+ // End any segment that reaches the bottom edge
+ if (startY != -1)
+ {
+ int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect
+
+ lineCanvas.AddLine (
+ new (x + bounds.Left, startY + bounds.Top),
+ length,
+ Orientation.Vertical,
+ style,
+ attribute
+ );
+ }
+ }
+ }
}
diff --git a/Terminal.Gui/Drawing/RegionOp.cs b/Terminal.Gui/Drawing/RegionOp.cs
new file mode 100644
index 0000000000..9bf31f285d
--- /dev/null
+++ b/Terminal.Gui/Drawing/RegionOp.cs
@@ -0,0 +1,128 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+///
+/// Specifies the operation to perform when combining two regions or a with a
+/// >, defining how their
+/// rectangular areas are merged, intersected, or subtracted.
+///
+///
+///
+/// Each operation modifies the first region's set of rectangles based on the second (op) region or rectangle,
+/// producing a new set of non-overlapping rectangles. The operations align with set theory, enabling flexible
+/// manipulation for TUI layout, clipping, or drawing. Developers can choose between granular outputs (e.g.,
+/// ) that preserve detailed rectangles or minimal outputs (e.g., )
+/// that reduce the rectangle count for compactness.
+///
+///
+public enum RegionOp
+{
+ ///
+ /// Subtracts the second (op) region or rectangle from the first region, removing any areas where the op overlaps
+ /// the first region. The result includes only the portions of the first region that do not intersect with the op.
+ ///
+ /// For example, if the first region contains rectangle A = (0,0,10,10) and the op is B = (5,5,5,5), the result
+ /// would include rectangles covering A minus the overlapping part of B, such as (0,0,10,5), (0,5,5,5), and
+ /// (5,10,5,5).
+ ///
+ ///
+ /// If the op region is empty or null, the operation has no effect unless the first region is also empty, in
+ /// which case it clears the first region.
+ ///
+ ///
+ Difference = 0,
+
+ ///
+ /// Intersects the first region with the second (op) region or rectangle, retaining only the areas where both
+ /// regions overlap. The result includes rectangles covering the common areas, excluding any parts unique to either
+ /// region.
+ ///
+ /// For example, if the first region contains A = (0,0,10,10) and the op is B = (5,5,5,5), the result would be
+ /// a single rectangle (5,5,5,5), representing the intersection.
+ ///
+ ///
+ /// If either region is empty or null, the result clears the first region, as there’s no intersection possible.
+ ///
+ ///
+ Intersect = 1,
+
+ ///
+ /// Performs a union (inclusive-or) of the first region and the second (op) region or rectangle, combining all
+ /// areas covered by either region into a single contiguous region without holes (unless explicitly subtracted).
+ ///
+ /// The formal union (∪) includes all points in at least one rectangle, producing a granular set of
+ /// non-overlapping rectangles that cover the combined area. For example, if the first region contains A =
+ /// (0,0,5,5) and the op is B = (5,0,5,5), the result might include (0,0,5,5) and (5,0,5,5) unless minimized.
+ ///
+ ///
+ /// This operation uses granular output (preserving detailed rectangles). To minimize the result use
+ /// instead.
+ ///
+ ///
+ /// If the op region is empty or null, the first region remains unchanged.
+ ///
+ ///
+ Union = 2,
+
+ ///
+ /// Performs a minimal union (inclusive-or) of the first region and the second (op) region or rectangle, merging adjacent or
+ /// overlapping rectangles into the smallest possible set of non-overlapping rectangles that cover the combined
+ /// area.
+ ///
+ /// This operation minimizes the number of rectangles, producing a more compact representation compared to
+ /// . For example, if the first region contains A = (0,0,5,5) and the op is B = (5,0,5,5),
+ /// the result would be a single rectangle (0,0,10,5), reducing redundancy.
+ ///
+ ///
+ /// This operation always minimizes the output and has lower performance than .
+ ///
+ ///
+ /// If the op region is empty or null, the first region remains unchanged.
+ ///
+ ///
+ MinimalUnion = 3,
+
+ ///
+ /// Performs an exclusive-or (XOR) of the first region and the second (op) region or rectangle, retaining only the
+ /// areas that are unique to each region—i.e., areas present in one region but not both.
+ ///
+ /// For example, if the first region contains A = (0,0,10,10) and the op is B = (5,5,5,5), the result would
+ /// include rectangles covering (0,0,10,5), (0,5,5,5), (5,10,5,5), and (5,5,5,5), excluding the intersection
+ /// (5,5,5,5).
+ ///
+ ///
+ /// If the op region is empty or null, this operation excludes the first region from itself (clearing it) or
+ /// adds the first region to the op (if op is empty), depending on the logic.
+ ///
+ ///
+ XOR = 4,
+
+ ///
+ /// Subtracts the first region from the second (op) region or rectangle, retaining only the areas of the op that do
+ /// not overlap with the first region. The result replaces the first region with these areas.
+ ///
+ /// For example, if the first region contains A = (5,5,5,5) and the op is B = (0,0,10,10), the result would
+ /// include rectangles covering B minus A, such as (0,0,10,5), (0,5,5,5), and (5,10,5,5).
+ ///
+ ///
+ /// If the first region is empty or null, the op region replaces the first region. If the op region is empty,
+ /// the first region is cleared.
+ ///
+ ///
+ ReverseDifference = 5,
+
+ ///
+ /// Replaces the first region entirely with the second (op) region or rectangle, discarding the first region's
+ /// current rectangles and adopting the op's rectangles.
+ ///
+ /// For example, if the first region contains (0,0,5,5) and the op is (10,10,5,5), the first region will be
+ /// cleared and replaced with (10,10,5,5).
+ ///
+ ///
+ /// If the op region is empty or null, the first region is cleared. This operation is useful for resetting or
+ /// overwriting region state.
+ ///
+ ///
+ Replace = 6
+}
diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs
index 6aa2088e5a..ed73b3913c 100644
--- a/Terminal.Gui/Drawing/Thickness.cs
+++ b/Terminal.Gui/Drawing/Thickness.cs
@@ -235,6 +235,19 @@ public Rectangle GetInside (Rectangle rect)
return new (x, y, width, height);
}
+ ///
+ /// Returns a region describing the thickness.
+ ///
+ /// The source rectangle
+ ///
+ public Region AsRegion (Rectangle rect)
+ {
+ Region region = new Region (rect);
+ region.Exclude (GetInside (rect));
+
+ return region;
+ }
+
///
/// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and right sides
/// of the rectangle to half the specified value.
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index d4c18bedc0..e576b83cf3 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -438,7 +438,7 @@ public void Draw (
}
}
}
-
+
///
/// Determines if the viewport width will be used or only the text width will be used,
/// If all the viewport area will be filled with whitespaces and the same background color
@@ -864,6 +864,276 @@ private T EnableNeedsFormat (T value)
return value;
}
+ ///
+ /// Calculates and returns a describing the areas where text would be output, based on the
+ /// formatting rules of .
+ ///
+ ///
+ /// Uses the same formatting logic as , including alignment, direction, word wrap, and constraints,
+ /// but does not perform actual drawing to .
+ ///
+ /// Specifies the screen-relative location and maximum size for drawing the text.
+ /// Specifies the screen-relative location and maximum container size.
+ /// A representing the areas where text would be drawn.
+ public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default)
+ {
+ Region drawnRegion = new Region ();
+
+ // With this check, we protect against subclasses with overrides of Text (like Button)
+ if (string.IsNullOrEmpty (Text))
+ {
+ return drawnRegion;
+ }
+
+ List linesFormatted = GetLines ();
+
+ bool isVertical = IsVerticalDirection (Direction);
+ Rectangle maxScreen = screen;
+
+ // INTENT: What, exactly, is the intent of this?
+ maxScreen = maximum == default (Rectangle)
+ ? screen
+ : new (
+ Math.Max (maximum.X, screen.X),
+ Math.Max (maximum.Y, screen.Y),
+ Math.Max (
+ Math.Min (maximum.Width, maximum.Right - screen.Left),
+ 0
+ ),
+ Math.Max (
+ Math.Min (
+ maximum.Height,
+ maximum.Bottom - screen.Top
+ ),
+ 0
+ )
+ );
+
+ if (maxScreen.Width == 0 || maxScreen.Height == 0)
+ {
+ return drawnRegion;
+ }
+
+ int lineOffset = !isVertical && screen.Y < 0 ? Math.Abs (screen.Y) : 0;
+
+ for (int line = lineOffset; line < linesFormatted.Count; line++)
+ {
+ if ((isVertical && line > screen.Width) || (!isVertical && line > screen.Height))
+ {
+ continue;
+ }
+
+ if ((isVertical && line >= maxScreen.Left + maxScreen.Width)
+ || (!isVertical && line >= maxScreen.Top + maxScreen.Height + lineOffset))
+ {
+ break;
+ }
+
+ Rune [] runes = linesFormatted [line].ToRunes ();
+
+ // 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;
+
+ break;
+ }
+ case Alignment.End:
+ {
+ int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+ x = screen.Right - runesWidth;
+
+ break;
+ }
+ case Alignment.Start when isVertical:
+ {
+ int runesWidth = line > 0
+ ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
+ : 0;
+ x = screen.Left + runesWidth;
+
+ break;
+ }
+ case Alignment.Start:
+ x = screen.Left;
+
+ break;
+ case Alignment.Fill when 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;
+
+ break;
+ }
+ case Alignment.Fill:
+ x = screen.Left;
+
+ break;
+ case Alignment.Center when isVertical:
+ {
+ int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
+ int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
+ x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
+
+ break;
+ }
+ case Alignment.Center:
+ {
+ int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+ x = screen.Left + (screen.Width - runesWidth) / 2;
+
+ break;
+ }
+ default:
+ Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
+
+ return drawnRegion;
+ }
+
+ switch (VerticalAlignment)
+ {
+ // Vertical Alignment
+ case Alignment.End when isVertical:
+ y = screen.Bottom - runes.Length;
+
+ break;
+ case Alignment.End:
+ y = screen.Bottom - linesFormatted.Count + line;
+
+ break;
+ case Alignment.Start when isVertical:
+ y = screen.Top;
+
+ break;
+ case Alignment.Start:
+ y = screen.Top + line;
+
+ break;
+ case Alignment.Fill when isVertical:
+ y = screen.Top;
+
+ break;
+ case Alignment.Fill:
+ {
+ 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;
+
+ break;
+ }
+ case Alignment.Center when isVertical:
+ {
+ int s = (screen.Height - runes.Length) / 2;
+ y = screen.Top + s;
+
+ break;
+ }
+ case Alignment.Center:
+ {
+ int s = (screen.Height - linesFormatted.Count) / 2;
+ y = screen.Top + line + s;
+
+ 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 ? runes.Sum (r => r.GetColumns () == 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 > runes.Length - 1)
+ {
+ break;
+ }
+
+ if ((!isVertical
+ && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
+ || (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))
+ || (isVertical
+ && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
+ || (idx < runes.Length && runes [idx].GetColumns () > screen.Width))))
+ {
+ break;
+ }
+
+ Rune rune = idx >= 0 && idx < runes.Length ? runes [idx] : (Rune)' ';
+ int runeWidth = GetRuneWidth (rune, TabWidth);
+
+ if (isVertical)
+ {
+ if (runeWidth > 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 += runeWidth;
+ }
+
+ current += isVertical && runeWidth > 0 ? 1 : runeWidth;
+
+ int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length
+ ? runes [idx + 1].GetColumns ()
+ : 0;
+
+ if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
+ {
+ break;
+ }
+ }
+
+ // Add the line's drawn region to the overall region
+ if (lineWidth > 0 && lineHeight > 0)
+ {
+ drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+ }
+ }
+
+ return drawnRegion;
+ }
+
#region Static Members
/// Check if it is a horizontal direction
diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs
index 3625506a35..d872db769c 100644
--- a/Terminal.Gui/View/Adornment/Adornment.cs
+++ b/Terminal.Gui/View/Adornment/Adornment.cs
@@ -182,6 +182,7 @@ protected override bool OnClearingViewport ()
///
protected override bool OnDrawingSubviews () { return Thickness == Thickness.Empty; }
+
/// Does nothing for Adornment
///
protected override bool OnRenderingLineCanvas () { return true; }
@@ -196,9 +197,6 @@ public override bool SuperViewRendersLineCanvas
set => throw new InvalidOperationException (@"Adornment can only render to their Parent or Parent's Superview.");
}
- ///
- protected override void OnDrawComplete () { }
-
///
/// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness.
///
diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs
index 289de1e7b5..566559b2c9 100644
--- a/Terminal.Gui/View/Adornment/Border.cs
+++ b/Terminal.Gui/View/Adornment/Border.cs
@@ -313,7 +313,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
// Adornment.Contains takes Parent SuperView=relative coords.
if (Contains (new (mouseEvent.Position.X + Parent.Frame.X + Frame.X, mouseEvent.Position.Y + Parent.Frame.Y + Frame.Y)))
{
- if (_arranging != ViewArrangement.Fixed)
+ if (Arranging != ViewArrangement.Fixed)
{
EndArrangeMode ();
}
@@ -480,7 +480,7 @@ protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
int minWidth = Thickness.Horizontal + Parent!.Margin!.Thickness.Right;
// TODO: This code can be refactored to be more readable and maintainable.
- switch (_arranging)
+ switch (Arranging)
{
case ViewArrangement.Movable:
@@ -979,7 +979,7 @@ private static void GetAppealingGradientColors (out List stops, out List<
steps = [15];
}
- private ViewArrangement _arranging;
+ internal ViewArrangement Arranging { get; set; }
private Button? _moveButton; // always top-left
private Button? _allSizeButton;
@@ -1000,7 +1000,7 @@ private static void GetAppealingGradientColors (out List stops, out List<
///
public bool? EnterArrangeMode (ViewArrangement arrangement)
{
- Debug.Assert (_arranging == ViewArrangement.Fixed);
+ Debug.Assert (Arranging == ViewArrangement.Fixed);
if (!Parent!.Arrangement.HasFlag (ViewArrangement.Movable)
&& !Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable)
@@ -1163,16 +1163,16 @@ private static void GetAppealingGradientColors (out List stops, out List<
_allSizeButton!.Visible = true;
}
- _arranging = ViewArrangement.Movable;
+ Arranging = ViewArrangement.Movable;
CanFocus = true;
SetFocus ();
}
else
{
// Mouse mode
- _arranging = arrangement;
+ Arranging = arrangement;
- switch (_arranging)
+ switch (Arranging)
{
case ViewArrangement.Movable:
_moveButton!.Visible = true;
@@ -1247,13 +1247,13 @@ private static void GetAppealingGradientColors (out List stops, out List<
}
}
- if (_arranging != ViewArrangement.Fixed)
+ if (Arranging != ViewArrangement.Fixed)
{
if (arrangement == ViewArrangement.Fixed)
{
// Keyboard mode - enable nav
// TODO: Keyboard mode only supports sizing from bottom/right.
- _arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
+ Arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
}
return true;
@@ -1278,12 +1278,12 @@ private void AddArrangeModeKeyBindings ()
return false;
}
- if (_arranging == ViewArrangement.Movable)
+ if (Arranging == ViewArrangement.Movable)
{
Parent!.Y = Parent.Y - 1;
}
- if (_arranging == ViewArrangement.Resizable)
+ if (Arranging == ViewArrangement.Resizable)
{
if (Parent!.Viewport.Height > 0)
{
@@ -1303,12 +1303,12 @@ private void AddArrangeModeKeyBindings ()
return false;
}
- if (_arranging == ViewArrangement.Movable)
+ if (Arranging == ViewArrangement.Movable)
{
Parent!.Y = Parent.Y + 1;
}
- if (_arranging == ViewArrangement.Resizable)
+ if (Arranging == ViewArrangement.Resizable)
{
Parent!.Height = Parent.Height! + 1;
}
@@ -1325,12 +1325,12 @@ private void AddArrangeModeKeyBindings ()
return false;
}
- if (_arranging == ViewArrangement.Movable)
+ if (Arranging == ViewArrangement.Movable)
{
Parent!.X = Parent.X - 1;
}
- if (_arranging == ViewArrangement.Resizable)
+ if (Arranging == ViewArrangement.Resizable)
{
if (Parent!.Viewport.Width > 0)
{
@@ -1350,12 +1350,12 @@ private void AddArrangeModeKeyBindings ()
return false;
}
- if (_arranging == ViewArrangement.Movable)
+ if (Arranging == ViewArrangement.Movable)
{
Parent!.X = Parent.X + 1;
}
- if (_arranging == ViewArrangement.Resizable)
+ if (Arranging == ViewArrangement.Resizable)
{
Parent!.Width = Parent.Width! + 1;
}
@@ -1373,7 +1373,7 @@ private void AddArrangeModeKeyBindings ()
// BUGBUG: the view hierachy.
AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
- _arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
+ Arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
return true; // Always eat
});
@@ -1383,7 +1383,7 @@ private void AddArrangeModeKeyBindings ()
() =>
{
AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
- _arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
+ Arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
return true; // Always eat
});
@@ -1419,7 +1419,7 @@ private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e)
private bool? EndArrangeMode ()
{
// Debug.Assert (_arranging != ViewArrangement.Fixed);
- _arranging = ViewArrangement.Fixed;
+ Arranging = ViewArrangement.Fixed;
Application.MouseEvent -= ApplicationOnMouseEvent;
diff --git a/Terminal.Gui/View/DrawContext.cs b/Terminal.Gui/View/DrawContext.cs
new file mode 100644
index 0000000000..36b60c4c4d
--- /dev/null
+++ b/Terminal.Gui/View/DrawContext.cs
@@ -0,0 +1,54 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Tracks the region that has been drawn during . This is primarily
+/// in support of .
+///
+public class DrawContext
+{
+ private readonly Region _drawnRegion = new Region ();
+
+ ///
+ /// Gets a copy of the region drawn so far in this context.
+ ///
+ public Region GetDrawnRegion () => _drawnRegion.Clone ();
+
+ ///
+ /// Reports that a rectangle has been drawn.
+ ///
+ /// The rectangle that was drawn.
+ public void AddDrawnRectangle (Rectangle rect)
+ {
+ _drawnRegion.Combine (rect, RegionOp.Union);
+ }
+
+ ///
+ /// Reports that a region has been drawn.
+ ///
+ /// The region that was drawn.
+ public void AddDrawnRegion (Region region)
+ {
+ _drawnRegion.Combine (region, RegionOp.Union);
+ }
+
+ ///
+ /// Clips (intersects) the drawn region with the specified rectangle.
+ /// This modifies the internal drawn region directly.
+ ///
+ /// The clipping rectangle.
+ public void ClipDrawnRegion (Rectangle clipRect)
+ {
+ _drawnRegion.Intersect (clipRect);
+ }
+
+ ///
+ /// Clips (intersects) the drawn region with the specified region.
+ /// This modifies the internal drawn region directly.
+ ///
+ /// The clipping region.
+ public void ClipDrawnRegion (Region clipRegion)
+ {
+ _drawnRegion.Intersect (clipRegion);
+ }
+}
diff --git a/Terminal.Gui/View/DrawEventArgs.cs b/Terminal.Gui/View/DrawEventArgs.cs
index f3cfc7747a..5aabd2e660 100644
--- a/Terminal.Gui/View/DrawEventArgs.cs
+++ b/Terminal.Gui/View/DrawEventArgs.cs
@@ -1,4 +1,5 @@
-using System.ComponentModel;
+#nullable enable
+using System.ComponentModel;
namespace Terminal.Gui;
@@ -14,10 +15,16 @@ public class DrawEventArgs : CancelEventArgs
/// The Content-relative rectangle describing the old visible viewport into the
/// .
///
- public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport)
+ ///
+ /// Add any regions that have been drawn to during operations to this context. This is
+ /// primarily
+ /// in support of .
+ ///
+ public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport, DrawContext? drawContext)
{
NewViewport = newViewport;
OldViewport = oldViewport;
+ DrawContext = drawContext;
}
/// Gets the Content-relative rectangle describing the old visible viewport into the .
@@ -25,4 +32,11 @@ public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport)
/// Gets the Content-relative rectangle describing the currently visible viewport into the .
public Rectangle NewViewport { get; }
+
+ ///
+ /// Add any regions that have been drawn to during operations to this context. This is
+ /// primarily
+ /// in support of .
+ ///
+ public DrawContext? DrawContext { get; }
}
diff --git a/Terminal.Gui/View/View.Content.cs b/Terminal.Gui/View/View.Content.cs
index 3f3790dddd..b187e01407 100644
--- a/Terminal.Gui/View/View.Content.cs
+++ b/Terminal.Gui/View/View.Content.cs
@@ -385,7 +385,7 @@ void ApplySettings (ref Rectangle newViewport)
private void RaiseViewportChangedEvent (Rectangle oldViewport)
{
- var args = new DrawEventArgs (IsInitialized ? Viewport : Rectangle.Empty, oldViewport);
+ var args = new DrawEventArgs (IsInitialized ? Viewport : Rectangle.Empty, oldViewport, null);
OnViewportChanged (args);
ViewportChanged?.Invoke (this, args);
}
@@ -430,6 +430,20 @@ public Point ViewportToScreen (in Point viewportLocation)
return screen.Location;
}
+ ///
+ /// Gets the Viewport rectangle with a screen-relative location.
+ ///
+ /// Screen-relative location and size.
+ public Rectangle ViewportToScreen ()
+ {
+ // Translate bounds to Frame (our SuperView's Viewport-relative coordinates)
+ Rectangle screen = FrameToScreen ();
+ Point viewportOffset = GetViewportOffsetFromFrame ();
+ screen.Offset (viewportOffset.X, viewportOffset.Y);
+
+ return screen;
+ }
+
/// Converts a screen-relative coordinate to a Viewport-relative coordinate.
/// The coordinate relative to this view's .
///
diff --git a/Terminal.Gui/View/View.Drawing.Clipping.cs b/Terminal.Gui/View/View.Drawing.Clipping.cs
index 30f8b703c6..3b131a614c 100644
--- a/Terminal.Gui/View/View.Drawing.Clipping.cs
+++ b/Terminal.Gui/View/View.Drawing.Clipping.cs
@@ -11,8 +11,7 @@ public partial class View
/// There is a single clip region for the entire application.
///
///
- /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, it is
- /// recommended to clone it first.
+ /// This method returns the current clip region, not a clone. If there is a need to modify the clip region, clone it first.
///
///
/// The current Clip.
@@ -74,6 +73,17 @@ public static void SetClip (Region? region)
///
public static void ExcludeFromClip (Rectangle rectangle) { Driver?.Clip?.Exclude (rectangle); }
+ ///
+ /// Removes the specified rectangle from the Clip.
+ ///
+ ///
+ ///
+ /// There is a single clip region for the entire application.
+ ///
+ ///
+ ///
+ public static void ExcludeFromClip (Region? region) { Driver?.Clip?.Exclude (region); }
+
///
/// Changes the Clip to the intersection of the current Clip and the of this View.
///
@@ -86,7 +96,7 @@ public static void SetClip (Region? region)
///
/// The current Clip, which can be then re-applied
///
- internal Region? ClipFrame ()
+ internal Region? AddFrameToClip ()
{
if (Driver is null)
{
@@ -133,7 +143,7 @@ public static void SetClip (Region? region)
///
/// The current Clip, which can be then re-applied
///
- public Region? ClipViewport ()
+ public Region? AddViewportToClip ()
{
if (Driver is null)
{
diff --git a/Terminal.Gui/View/View.Drawing.Primitives.cs b/Terminal.Gui/View/View.Drawing.Primitives.cs
index a7f1c8522d..153f12e644 100644
--- a/Terminal.Gui/View/View.Drawing.Primitives.cs
+++ b/Terminal.Gui/View/View.Drawing.Primitives.cs
@@ -137,7 +137,7 @@ public void FillRect (Rectangle rect, Color? color = null)
return;
}
- Region prevClip = ClipViewport ();
+ Region prevClip = AddViewportToClip ();
Rectangle toClear = ViewportToScreen (rect);
Attribute prev = SetAttribute (new (color ?? GetNormalColor ().Background));
Driver.FillRect (toClear);
@@ -155,7 +155,7 @@ public void FillRect (Rectangle rect, Rune rune)
return;
}
- Region prevClip = ClipViewport ();
+ Region prevClip = AddViewportToClip ();
Rectangle toClear = ViewportToScreen (rect);
Driver.FillRect (toClear, rune);
SetClip (prevClip);
diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs
index 2e33706e14..aa77e3f05a 100644
--- a/Terminal.Gui/View/View.Drawing.cs
+++ b/Terminal.Gui/View/View.Drawing.cs
@@ -14,6 +14,9 @@ internal static void Draw (IEnumerable views, bool force)
{
IEnumerable viewsArray = views as View [] ?? views.ToArray ();
+ // The draw context is used to track the region drawn by each view.
+ DrawContext context = new DrawContext ();
+
foreach (View view in viewsArray)
{
if (force)
@@ -21,7 +24,7 @@ internal static void Draw (IEnumerable views, bool force)
view.SetNeedsDraw ();
}
- view.Draw ();
+ view.Draw (context);
}
Margin.DrawMargins (viewsArray);
@@ -40,94 +43,90 @@ internal static void Draw (IEnumerable views, bool force)
/// See the View Drawing Deep Dive for more information: .
///
///
- public void Draw ()
+ public void Draw (DrawContext? context = null)
{
if (!CanBeVisible (this))
{
return;
}
+ Region? originalClip = GetClip ();
- Region? saved = GetClip ();
-
- // TODO: This can be further optimized by checking NeedsDraw below and only clearing, drawing text, drawing content, etc. if it is true.
+ // TODO: This can be further optimized by checking NeedsDraw below and only
+ // TODO: clearing, drawing text, drawing content, etc. if it is true.
if (NeedsDraw || SubViewNeedsDraw)
{
+ // ------------------------------------
// Draw the Border and Padding.
- // We clip to the frame to prevent drawing outside the frame.
- saved = ClipFrame ();
-
- DoDrawBorderAndPadding ();
- SetClip (saved);
+ // Note Margin is special-cased and drawn in a separate pass to support
+ // transparent shadows.
+ DoDrawBorderAndPadding (originalClip);
+ SetClip (originalClip);
- // Draw the content within the Viewport
+ // ------------------------------------
+ // Clear the Viewport
// By default, we clip to the viewport preventing drawing outside the viewport
// We also clip to the content, but if a developer wants to draw outside the viewport, they can do
// so via settings. SetClip honors the ViewportSettings.DisableVisibleContentClipping flag.
// Get our Viewport in screen coordinates
+ originalClip = AddViewportToClip ();
- saved = ClipViewport ();
+ // If no context ...
+ context ??= new DrawContext ();
- // Clear the viewport
// TODO: Simplify/optimize SetAttribute system.
DoSetAttribute ();
DoClearViewport ();
- // Draw the subviews only if needed
+ // ------------------------------------
+ // Draw the subviews first (order matters: Subviews, Text, Content)
if (SubViewNeedsDraw)
{
DoSetAttribute ();
- DoDrawSubviews ();
+ DoDrawSubviews (context);
}
+ // ------------------------------------
// Draw the text
DoSetAttribute ();
- DoDrawText ();
+ DoDrawText (context);
+ // ------------------------------------
// Draw the content
DoSetAttribute ();
- DoDrawContent ();
+ DoDrawContent (context);
+ // ------------------------------------
+ // Draw the line canvas
// Restore the clip before rendering the line canvas and adornment subviews
// because they may draw outside the viewport.
- SetClip (saved);
-
- saved = ClipFrame ();
-
- // Draw the line canvas
+ SetClip (originalClip);
+ originalClip = AddFrameToClip ();
DoRenderLineCanvas ();
+ // ------------------------------------
// Re-draw the border and padding subviews
// HACK: This is a hack to ensure that the border and padding subviews are drawn after the line canvas.
DoDrawBorderAndPaddingSubViews ();
+ // ------------------------------------
// Advance the diagnostics draw indicator
Border?.AdvanceDrawIndicator ();
ClearNeedsDraw ();
}
+ // ------------------------------------
// This causes the Margin to be drawn in a second pass
// PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop.
Margin?.CacheClip ();
- // We're done drawing
- DoDrawComplete ();
-
- // QUESTION: Should this go before DoDrawComplete? What is more correct?
- SetClip (saved);
-
- // Exclude this view (not including Margin) from the Clip
- if (this is not Adornment)
- {
- Rectangle borderFrame = FrameToScreen ();
-
- if (Border is { })
- {
- borderFrame = Border.FrameToScreen ();
- }
+ // ------------------------------------
+ // Reset the clip to what it was when we started
+ SetClip (originalClip);
- ExcludeFromClip (borderFrame);
- }
+ // ------------------------------------
+ // We're done drawing - The Clip is reset to what it was before we started.
+ DoDrawComplete (context);
}
#region DrawAdornments
@@ -144,10 +143,10 @@ private void DoDrawBorderAndPaddingSubViews ()
subview.SetNeedsDraw ();
}
- LineCanvas.Exclude (new (subview.FrameToScreen()));
+ LineCanvas.Exclude (new (subview.FrameToScreen ()));
}
- Region? saved = Border?.ClipFrame ();
+ Region? saved = Border?.AddFrameToClip ();
Border?.DoDrawSubviews ();
SetClip (saved);
}
@@ -159,17 +158,33 @@ private void DoDrawBorderAndPaddingSubViews ()
subview.SetNeedsDraw ();
}
- Region? saved = Padding?.ClipFrame ();
+ Region? saved = Padding?.AddFrameToClip ();
Padding?.DoDrawSubviews ();
SetClip (saved);
}
}
- private void DoDrawBorderAndPadding ()
+ private void DoDrawBorderAndPadding (Region? originalClip)
{
+ if (this is Adornment)
+ {
+ AddFrameToClip ();
+ }
+ else
+ {
+ // Set the clip to be just the thicknesses of the adornments
+ // TODO: Put this union logic in a method on View?
+ Region? clipAdornments = Margin!.Thickness.AsRegion (Margin!.FrameToScreen ());
+ clipAdornments?.Combine (Border!.Thickness.AsRegion (Border!.FrameToScreen ()), RegionOp.Union);
+ clipAdornments?.Combine (Padding!.Thickness.AsRegion (Padding!.FrameToScreen ()), RegionOp.Union);
+ clipAdornments?.Combine (originalClip, RegionOp.Intersect);
+ SetClip (clipAdornments);
+ }
+
if (Margin?.NeedsLayout == true)
{
Margin.NeedsLayout = false;
+ // BUGBUG: This should not use ClearFrame as that clears the insides too
Margin?.ClearFrame ();
Margin?.Parent?.SetSubViewNeedsDraw ();
}
@@ -291,27 +306,31 @@ public void SetNormalAttribute ()
#region ClearViewport
- private void DoClearViewport ()
+ internal void DoClearViewport ()
{
- if (OnClearingViewport ())
+ if (ViewportSettings.HasFlag (ViewportSettings.Transparent))
{
return;
}
- var dev = new DrawEventArgs (Viewport, Rectangle.Empty);
- ClearingViewport?.Invoke (this, dev);
-
- if (dev.Cancel)
+ if (OnClearingViewport ())
{
return;
}
- if (!NeedsDraw)
+ var dev = new DrawEventArgs (Viewport, Rectangle.Empty, null);
+ ClearingViewport?.Invoke (this, dev);
+
+ if (dev.Cancel)
{
+ SetNeedsDraw ();
return;
}
ClearViewport ();
+
+ OnClearedViewport ();
+ ClearedViewport?.Invoke (this, new (Viewport, Viewport, null));
}
///
@@ -320,7 +339,7 @@ private void DoClearViewport ()
/// to stop further clearing.
protected virtual bool OnClearingViewport () { return false; }
- /// Event invoked when the content area of the View is to be drawn.
+ /// Event invoked when the is to be cleared.
///
/// Will be invoked before any subviews added with have been drawn.
///
@@ -330,6 +349,14 @@ private void DoClearViewport ()
///
public event EventHandler? ClearingViewport;
+ ///
+ /// Called when the has been cleared
+ ///
+ protected virtual void OnClearedViewport () { }
+
+ /// Event invoked when the has been cleared.
+ public event EventHandler? ClearedViewport;
+
/// Clears with the normal background.
///
///
@@ -368,35 +395,41 @@ public void ClearViewport ()
#region DrawText
- private void DoDrawText ()
+ private void DoDrawText (DrawContext? context = null)
{
- if (OnDrawingText ())
+ if (OnDrawingText (context))
{
return;
}
- var dev = new DrawEventArgs (Viewport, Rectangle.Empty);
- DrawingText?.Invoke (this, dev);
-
- if (dev.Cancel)
+ if (OnDrawingText ())
{
return;
}
- if (!NeedsDraw)
+ var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context);
+ DrawingText?.Invoke (this, dev);
+
+ if (dev.Cancel)
{
return;
}
- DrawText ();
+ DrawText (context);
}
///
/// Called when the of the View is to be drawn.
///
+ /// The draw context to report drawn areas to.
/// to stop further drawing of .
- protected virtual bool OnDrawingText () { return false; }
+ protected virtual bool OnDrawingText (DrawContext? context) { return false; }
+ ///
+ /// Called when the of the View is to be drawn.
+ ///
+ /// to stop further drawing of .
+ protected virtual bool OnDrawingText () { return false; }
/// Raised when the of the View is to be drawn.
///
@@ -408,16 +441,27 @@ private void DoDrawText ()
///
/// Draws the of the View using the .
///
- public void DrawText ()
+ /// The draw context to report drawn areas to.
+ public void DrawText (DrawContext? context = null)
{
if (!string.IsNullOrEmpty (TextFormatter.Text))
{
TextFormatter.NeedsFormat = true;
}
- // TODO: If the output is not in the Viewport, do nothing
var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ());
+ // Use GetDrawRegion to get precise drawn areas
+ Region textRegion = TextFormatter.GetDrawRegion (drawRect);
+
+ // Report the drawn area to the context
+ context?.AddDrawnRegion (textRegion);
+
+ if (!NeedsDraw)
+ {
+ return;
+ }
+
TextFormatter?.Draw (
drawRect,
HasFocus ? GetFocusColor () : GetNormalColor (),
@@ -430,34 +474,45 @@ public void DrawText ()
}
#endregion DrawText
-
#region DrawContent
- private void DoDrawContent ()
+ private void DoDrawContent (DrawContext? context = null)
{
+ if (OnDrawingContent (context))
+ {
+ return;
+ }
+
if (OnDrawingContent ())
{
return;
}
- var dev = new DrawEventArgs (Viewport, Rectangle.Empty);
+ var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context);
DrawingContent?.Invoke (this, dev);
if (dev.Cancel)
- { }
+ {
+ return;
+ }
- // Do nothing.
+ // No default drawing; let event handlers or overrides handle it
}
///
/// Called when the View's content is to be drawn. The default implementation does nothing.
///
- ///
- ///
+ /// The draw context to report drawn areas to.
+ /// to stop further drawing content.
+ protected virtual bool OnDrawingContent (DrawContext? context = null) { return false; }
+
+ ///
+ /// Called when the View's content is to be drawn. The default implementation does nothing.
+ ///
/// to stop further drawing content.
protected virtual bool OnDrawingContent () { return false; }
- /// Raised when the View's content is to be drawn.
+ /// Raised when the View's content is to be drawn.
///
/// Will be invoked before any subviews added with have been drawn.
///
@@ -471,14 +526,19 @@ private void DoDrawContent ()
#region DrawSubviews
- private void DoDrawSubviews ()
+ private void DoDrawSubviews (DrawContext? context = null)
{
+ if (OnDrawingSubviews (context))
+ {
+ return;
+ }
+
if (OnDrawingSubviews ())
{
return;
}
- var dev = new DrawEventArgs (Viewport, Rectangle.Empty);
+ var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context);
DrawingSubviews?.Invoke (this, dev);
if (dev.Cancel)
@@ -491,15 +551,21 @@ private void DoDrawSubviews ()
return;
}
- DrawSubviews ();
+ DrawSubviews (context);
}
///
/// Called when the are to be drawn.
///
+ /// The draw context to report drawn areas to, or null if not tracking.
/// to stop further drawing of .
- protected virtual bool OnDrawingSubviews () { return false; }
+ protected virtual bool OnDrawingSubviews (DrawContext? context) { return false; }
+ ///
+ /// Called when the are to be drawn.
+ ///
+ /// to stop further drawing of .
+ protected virtual bool OnDrawingSubviews () { return false; }
/// Raised when the are to be drawn.
///
@@ -513,7 +579,8 @@ private void DoDrawSubviews ()
///
/// Draws the .
///
- public void DrawSubviews ()
+ /// The draw context to report drawn areas to, or null if not tracking.
+ public void DrawSubviews (DrawContext? context = null)
{
if (_subviews is null)
{
@@ -523,12 +590,12 @@ public void DrawSubviews ()
// Draw the subviews in reverse order to leverage clipping.
foreach (View view in _subviews.Where (view => view.Visible).Reverse ())
{
- // TODO: HACK - This enables auto line join to work, but is brute force.
- if (view.SuperViewRendersLineCanvas)
+ // TODO: HACK - This forcing of SetNeedsDraw with SuperViewRendersLineCanvas enables auto line join to work, but is brute force.
+ if (view.SuperViewRendersLineCanvas || view.ViewportSettings.HasFlag (ViewportSettings.Transparent))
{
view.SetNeedsDraw ();
}
- view.Draw ();
+ view.Draw (context);
if (view.SuperViewRendersLineCanvas)
{
@@ -609,19 +676,56 @@ public void RenderLineCanvas ()
#region DrawComplete
- private void DoDrawComplete ()
+ private void DoDrawComplete (DrawContext? context)
{
- OnDrawComplete ();
+ OnDrawComplete (context);
+ DrawComplete?.Invoke (this, new (Viewport, Viewport, context));
+
+ // Now, update the clip to exclude this view (not including Margin)
+ if (this is not Adornment)
+ {
+ if (ViewportSettings.HasFlag (ViewportSettings.Transparent))
+ {
+ // context!.DrawnRegion is the region that was drawn by this view. It may include regions outside
+ // the Viewport. We need to clip it to the Viewport.
+ context!.ClipDrawnRegion (ViewportToScreen (Viewport));
- DrawComplete?.Invoke (this, new (Viewport, Viewport));
+ // Exclude the drawn region from the clip
+ ExcludeFromClip (context!.GetDrawnRegion ());
- // Default implementation does nothing.
+ // Exclude the Border and Padding from the clip
+ ExcludeFromClip (Border?.Thickness.AsRegion (FrameToScreen ()));
+ ExcludeFromClip (Padding?.Thickness.AsRegion (FrameToScreen ()));
+ }
+ else
+ {
+ // Exclude this view (not including Margin) from the Clip
+ Rectangle borderFrame = FrameToScreen ();
+
+ if (Border is { })
+ {
+ borderFrame = Border.FrameToScreen ();
+ }
+
+ // In the non-transparent (typical case), we want to exclude the entire view area (borderFrame) from the clip
+ ExcludeFromClip (borderFrame);
+
+ // Update context.DrawnRegion to include the entire view (borderFrame), but clipped to our SuperView's viewport
+ // This enables the SuperView to know what was drawn by this view.
+ context?.AddDrawnRectangle (borderFrame);
+ }
+ }
+
+ // TODO: Determine if we need another event that conveys the FINAL DrawContext
}
///
/// Called when the View is completed drawing.
///
- protected virtual void OnDrawComplete () { }
+ ///
+ /// The parameter provides the drawn region of the View.
+ ///
+ protected virtual void OnDrawComplete (DrawContext? context) { }
/// Raised when the View is completed drawing.
///
diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs
index f7d6d57a22..691c10d8a8 100644
--- a/Terminal.Gui/View/View.Mouse.cs
+++ b/Terminal.Gui/View/View.Mouse.cs
@@ -763,7 +763,7 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle)
///
///
///
- internal static List GetViewsUnderMouse (in Point location)
+ internal static List GetViewsUnderMouse (in Point location, bool ignoreTransparent = false)
{
List viewsUnderMouse = new ();
@@ -810,7 +810,8 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle)
for (int i = start.InternalSubviews.Count - 1; i >= 0; i--)
{
if (start.InternalSubviews [i].Visible
- && start.InternalSubviews [i].Contains (new (startOffsetX + start.Viewport.X, startOffsetY + start.Viewport.Y)))
+ && start.InternalSubviews [i].Contains (new (startOffsetX + start.Viewport.X, startOffsetY + start.Viewport.Y))
+ && (!ignoreTransparent || !start.InternalSubviews [i].ViewportSettings.HasFlag (ViewportSettings.TransparentMouse)))
{
subview = start.InternalSubviews [i];
currentLocation.X = startOffsetX + start.Viewport.X;
@@ -823,6 +824,15 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle)
if (subview is null)
{
+ if (start.ViewportSettings.HasFlag (ViewportSettings.TransparentMouse))
+ {
+ viewsUnderMouse.AddRange (View.GetViewsUnderMouse (location, true));
+
+ // De-dupe viewsUnderMouse
+ HashSet dedupe = [..viewsUnderMouse];
+ viewsUnderMouse = [..dedupe];
+ }
+
// No subview was found that's under the mouse, so we're done
return viewsUnderMouse;
}
diff --git a/Terminal.Gui/View/ViewportSettings.cs b/Terminal.Gui/View/ViewportSettings.cs
index db2c23f4fe..bedc9a43ca 100644
--- a/Terminal.Gui/View/ViewportSettings.cs
+++ b/Terminal.Gui/View/ViewportSettings.cs
@@ -1,7 +1,7 @@
namespace Terminal.Gui;
///
-/// Settings for how the behaves relative to the View's Content area.
+/// Settings for how the behaves.
///
///
/// See the Layout Deep Dive for more information:
@@ -13,7 +13,7 @@ public enum ViewportSettings
///
/// No settings.
///
- None = 0,
+ None = 0b_0000,
///
/// If set, .X can be set to negative values enabling scrolling beyond the left of
@@ -23,7 +23,7 @@ public enum ViewportSettings
/// When not set, .X is constrained to positive values.
///
///
- AllowNegativeX = 1,
+ AllowNegativeX = 0b_0001,
///
/// If set, .Y can be set to negative values enabling scrolling beyond the top of the
@@ -32,7 +32,7 @@ public enum ViewportSettings
/// When not set, .Y is constrained to positive values.
///
///
- AllowNegativeY = 2,
+ AllowNegativeY = 0b_0010,
///
/// If set, .Size can be set to negative coordinates enabling scrolling beyond the
@@ -58,7 +58,7 @@ public enum ViewportSettings
/// The practical effect of this is that the last column of the content will always be visible.
///
///
- AllowXGreaterThanContentWidth = 4,
+ AllowXGreaterThanContentWidth = 0b_0100,
///
/// If set, .Y can be set values greater than
@@ -74,7 +74,7 @@ public enum ViewportSettings
/// The practical effect of this is that the last row of the content will always be visible.
///
///
- AllowYGreaterThanContentHeight = 8,
+ AllowYGreaterThanContentHeight = 0b_1000,
///
/// If set, .Location can be set values greater than
@@ -102,7 +102,7 @@ public enum ViewportSettings
/// This can be useful in infinite scrolling scenarios.
///
///
- AllowNegativeXWhenWidthGreaterThanContentWidth = 16,
+ AllowNegativeXWhenWidthGreaterThanContentWidth = 0b_0001_0000,
///
/// If set and .Height is greater than
@@ -117,7 +117,7 @@ public enum ViewportSettings
/// This can be useful in infinite scrolling scenarios.
///
///
- AllowNegativeYWhenHeightGreaterThanContentHeight = 32,
+ AllowNegativeYWhenHeightGreaterThanContentHeight = 0b_0010_0000,
///
/// The combination of and
@@ -129,7 +129,7 @@ public enum ViewportSettings
/// By default, clipping is applied to the . Setting this flag will cause clipping to be
/// applied to the visible content area.
///
- ClipContentOnly = 64,
+ ClipContentOnly = 0b_0100_0000,
///
/// If set will clear only the portion of the content
@@ -138,5 +138,27 @@ public enum ViewportSettings
/// must be set for this setting to work (clipping beyond the visible area must be
/// disabled).
///
- ClearContentOnly = 128
+ ClearContentOnly = 0b_1000_0000,
+
+ ///
+ /// If set the View will be transparent: The will not be cleared when the View is drawn and the clip region
+ /// will be set to clip the View's and .
+ ///
+ /// Only the topmost View in a Subview Hierarchy can be transparent. Any subviews of the topmost transparent view
+ /// will have indeterminate draw behavior.
+ ///
+ ///
+ /// Combine this with to get a view that is both visually transparent and transparent to the mouse.
+ ///
+ ///
+ Transparent = 0b_0001_0000_0000,
+
+ ///
+ /// If set the View will be transparent to mouse events: Any mouse event that occurs over the View (and it's Subviews) will be passed to the
+ /// Views below it.
+ ///
+ /// Combine this with to get a view that is both visually transparent and transparent to the mouse.
+ ///
+ ///
+ TransparentMouse = 0b_0010_0000_0000,
}
diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs
index f3afab7596..e15a732dc2 100644
--- a/Terminal.Gui/Views/CharMap/CharMap.cs
+++ b/Terminal.Gui/Views/CharMap/CharMap.cs
@@ -96,8 +96,8 @@ public CharMap ()
};
// Set up the vertical scrollbar. Turn off AutoShow since it's always visible.
- VerticalScrollBar.AutoShow = false;
- VerticalScrollBar.Visible = true; // Force always visible
+ VerticalScrollBar.AutoShow = true;
+ VerticalScrollBar.Visible = false;
VerticalScrollBar.X = Pos.AnchorEnd ();
VerticalScrollBar.Y = HEADER_HEIGHT; // Header
}
diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs
index 2a834aa925..c2693f1914 100644
--- a/Terminal.Gui/Views/TileView.cs
+++ b/Terminal.Gui/Views/TileView.cs
@@ -171,8 +171,6 @@ public int IndexOf (View toFind, bool recursive = false)
///
public bool IsRootTileView () { return _parentTileView == null; }
- // BUG: v2 fix this hack
- // QUESTION: Does this need to be fixed before events are refactored?
/// Overridden so no Frames get drawn
///
protected override bool OnDrawingBorderAndPadding () { return true; }
@@ -181,7 +179,7 @@ public int IndexOf (View toFind, bool recursive = false)
protected override bool OnRenderingLineCanvas () { return false; }
///
- protected override void OnDrawComplete ()
+ protected override void OnDrawComplete (DrawContext? context)
{
if (ColorScheme is { })
{
diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings
index 8b9178c393..07d849e84f 100644
--- a/Terminal.sln.DotSettings
+++ b/Terminal.sln.DotSettings
@@ -386,6 +386,7 @@
UIULUR
+ XORFalse<Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" />
diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs
index d982b2b899..c6f9136796 100644
--- a/UICatalog/Scenarios/AllViewsTester.cs
+++ b/UICatalog/Scenarios/AllViewsTester.cs
@@ -18,10 +18,9 @@ public class AllViewsTester : Scenario
private Dictionary? _viewClasses;
private ListView? _classListView;
private AdornmentsEditor? _adornmentsEditor;
-
private ArrangementEditor? _arrangementEditor;
-
private LayoutEditor? _layoutEditor;
+ private ViewportSettingsEditor? _viewportSettingsEditor;
private FrameView? _settingsPane;
private RadioGroup? _orientation;
private string _demoText = "This, that, and the other thing.";
@@ -133,13 +132,27 @@ public override void Main ()
AutoSelectAdornments = false,
SuperViewRendersLineCanvas = true
};
- _layoutEditor.Border!.Thickness = new (1);
+ _layoutEditor.Border!.Thickness = new (1, 1, 1, 0);
+
+ _viewportSettingsEditor = new ()
+ {
+ Title = "ViewportSettings [_5]",
+ X = Pos.Right (_arrangementEditor) - 1,
+ Y = Pos.Bottom (_layoutEditor) - Pos.Func (() => _layoutEditor.Frame.Height == 1 ? 0 : 1),
+ Width = Dim.Width (_layoutEditor),
+ Height = Dim.Auto (),
+ CanFocus = true,
+ AutoSelectViewToEdit = false,
+ AutoSelectAdornments = false,
+ SuperViewRendersLineCanvas = true
+ };
+ _viewportSettingsEditor.Border!.Thickness = new (1, 1, 1, 1);
_settingsPane = new ()
{
- Title = "Settings [_5]",
+ Title = "Misc Settings [_6]",
X = Pos.Right (_adornmentsEditor) - 1,
- Y = Pos.Bottom (_layoutEditor) - Pos.Func (() => _layoutEditor.Frame.Height == 1 ? 0 : 1),
+ Y = Pos.Bottom (_viewportSettingsEditor) - Pos.Func (() => _viewportSettingsEditor.Frame.Height == 1 ? 0 : 1),
Width = Dim.Width (_layoutEditor),
Height = Dim.Auto (),
CanFocus = true,
@@ -237,7 +250,7 @@ public override void Main ()
_hostPane.Padding.Diagnostics = ViewDiagnosticFlags.Ruler;
_hostPane.Padding.ColorScheme = app.ColorScheme;
- app.Add (_classListView, _adornmentsEditor, _arrangementEditor, _layoutEditor, _settingsPane, _eventLog, _hostPane);
+ app.Add (_classListView, _adornmentsEditor, _arrangementEditor, _layoutEditor, _viewportSettingsEditor, _settingsPane, _eventLog, _hostPane);
app.Initialized += App_Initialized;
@@ -306,6 +319,7 @@ private void CreateCurrentView (Type type)
_hostPane!.Add (_curView);
_layoutEditor!.ViewToEdit = _curView;
+ _viewportSettingsEditor!.ViewToEdit = _curView;
_arrangementEditor!.ViewToEdit = _curView;
_curView.SetNeedsLayout ();
}
@@ -318,6 +332,7 @@ private void DisposeCurrentView ()
_curView.SubviewsLaidOut -= CurrentView_LayoutComplete;
_hostPane!.Remove (_curView);
_layoutEditor!.ViewToEdit = null;
+ _viewportSettingsEditor!.ViewToEdit = null;
_arrangementEditor!.ViewToEdit = null;
_curView.Dispose ();
diff --git a/UICatalog/Scenarios/Arrangement.cs b/UICatalog/Scenarios/Arrangement.cs
index 33ac9ee84c..403e6f5baf 100644
--- a/UICatalog/Scenarios/Arrangement.cs
+++ b/UICatalog/Scenarios/Arrangement.cs
@@ -49,11 +49,14 @@ public override void Main ()
FrameView testFrame = new ()
{
Title = "_1 Test Frame",
+ Text = "This is the text of the Test Frame.\nLine 2.\nLine 3.",
X = Pos.Right (arrangementEditor),
Y = 0,
Width = Dim.Fill (),
Height = Dim.Fill ()
};
+ testFrame.TextAlignment = Alignment.Center;
+ testFrame.VerticalTextAlignment = Alignment.Center;
app.Add (testFrame);
@@ -64,16 +67,15 @@ public override void Main ()
tiledView3.Height = Dim.Height (tiledView1);
View tiledView4 = CreateTiledView (3, Pos.Left (tiledView1), Pos.Bottom (tiledView1) - 1);
tiledView4.Width = Dim.Func (() => tiledView3.Frame.Width + tiledView2.Frame.Width + tiledView1.Frame.Width - 2);
- testFrame.Add (tiledView4, tiledView3, tiledView2, tiledView1);
- View overlappedView1 = CreateOverlappedView (2, 0, 13);
- overlappedView1.Title = "Movable _& Sizable";
+ View movableSizeableWithProgress = CreateOverlappedView (2, 10, 8);
+ movableSizeableWithProgress.Title = "Movable _& Sizable";
View tiledSubView = CreateTiledView (4, 0, 2);
tiledSubView.Arrangement = ViewArrangement.Fixed;
- overlappedView1.Add (tiledSubView);
+ movableSizeableWithProgress.Add (tiledSubView);
tiledSubView = CreateTiledView (5, Pos.Right (tiledSubView), Pos.Top (tiledSubView));
tiledSubView.Arrangement = ViewArrangement.Fixed;
- overlappedView1.Add (tiledSubView);
+ movableSizeableWithProgress.Add (tiledSubView);
ProgressBar progressBar = new ()
{
@@ -81,7 +83,7 @@ public override void Main ()
Width = Dim.Fill (),
Id = "progressBar"
};
- overlappedView1.Add (progressBar);
+ movableSizeableWithProgress.Add (progressBar);
Timer timer = new (10)
{
@@ -168,9 +170,6 @@ public override void Main ()
overlappedView2.Add (colorPicker);
overlappedView2.Width = 50;
- testFrame.Add (overlappedView1);
- testFrame.Add (overlappedView2);
-
DatePicker datePicker = new ()
{
X = 30,
@@ -183,12 +182,27 @@ public override void Main ()
TabStop = TabBehavior.TabGroup,
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped
};
+
+ TransparentView transparentView = new ()
+ {
+ Id = "transparentView",
+ X = 30,
+ Y = 5,
+ Width = 35,
+ Height = 15
+ };
+
+ testFrame.Add (tiledView4, tiledView3, tiledView2, tiledView1);
+ testFrame.Add (overlappedView2);
testFrame.Add (datePicker);
+ testFrame.Add (movableSizeableWithProgress);
+ testFrame.Add (transparentView);
adornmentsEditor.AutoSelectSuperView = testFrame;
arrangementEditor.AutoSelectSuperView = testFrame;
testFrame.SetFocus ();
+
Application.Run (app);
timer.Close ();
app.Dispose ();
@@ -299,3 +313,57 @@ public override List GetDemoKeyStrokes ()
return keys;
}
}
+
+public class TransparentView : FrameView
+{
+ public TransparentView ()
+ {
+ Title = "Transparent View";
+ base.Text = "View.Text.\nThis should be opaque.\nNote how clipping works?";
+ TextFormatter.Alignment = Alignment.Center;
+ TextFormatter.VerticalAlignment = Alignment.Center;
+ Arrangement = ViewArrangement.Overlapped | ViewArrangement.Resizable | ViewArrangement.Movable;
+ ViewportSettings |= Terminal.Gui.ViewportSettings.Transparent | Terminal.Gui.ViewportSettings.TransparentMouse;
+ BorderStyle = LineStyle.RoundedDotted;
+ base.ColorScheme = Colors.ColorSchemes ["Menu"];
+
+ var transparentSubView = new View ()
+ {
+ Text = "Sizable/Movable View with border. Should be opaque. The shadow should be semi-opaque.",
+ Id = "transparentSubView",
+ X = 4,
+ Y = 8,
+ Width = 20,
+ Height = 8,
+ BorderStyle = LineStyle.Dashed,
+ Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
+ ShadowStyle = ShadowStyle.Transparent,
+ //ViewportSettings = Terminal.Gui.ViewportSettings.Transparent
+ };
+ transparentSubView.Border.Thickness = new (1, 1, 1, 1);
+ transparentSubView.ColorScheme = Colors.ColorSchemes ["Dialog"];
+
+ Button button = new Button ()
+ {
+ Title = "_Opaque Shadows No Worky",
+ X = Pos.Center (),
+ Y = 4,
+ ColorScheme = Colors.ColorSchemes ["Dialog"],
+ };
+
+ button.ClearingViewport += (sender, args) =>
+ {
+ args.Cancel = true;
+ };
+
+
+ base.Add (button);
+ base.Add (transparentSubView);
+ }
+
+ ///
+ protected override bool OnClearingViewport () { return false; }
+
+ ///
+ protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { return false; }
+}
diff --git a/UICatalog/Scenarios/Editors/ViewportSettingsEditor.cs b/UICatalog/Scenarios/Editors/ViewportSettingsEditor.cs
new file mode 100644
index 0000000000..011ca212fc
--- /dev/null
+++ b/UICatalog/Scenarios/Editors/ViewportSettingsEditor.cs
@@ -0,0 +1,371 @@
+#nullable enable
+using System;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+///
+/// Provides an editor UI for View.ViewportSettings.
+///
+public sealed class ViewportSettingsEditor : EditorBase
+{
+ public ViewportSettingsEditor ()
+ {
+ Title = "ViewportSettingsEditor";
+ TabStop = TabBehavior.TabGroup;
+
+ Initialized += ViewportSettingsEditor_Initialized;
+ }
+
+ protected override void OnUpdateSettings ()
+ {
+ foreach (View subview in Subviews)
+ {
+ subview.Enabled = ViewToEdit is not Adornment;
+ }
+
+ if (ViewToEdit is null)
+ { }
+ }
+
+ protected override void OnViewToEditChanged ()
+ {
+ if (ViewToEdit is { } and not Adornment)
+ {
+ //ViewToEdit.VerticalScrollBar.AutoShow = true;
+ //ViewToEdit.HorizontalScrollBar.AutoShow = true;
+
+ _contentSizeWidth!.Value = ViewToEdit.GetContentSize ().Width;
+ _contentSizeHeight!.Value = ViewToEdit.GetContentSize ().Height;
+
+ _cbAllowNegativeX!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.AllowNegativeX)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbAllowNegativeY!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.AllowNegativeY)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbAllowXGreaterThanContentWidth!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbAllowYGreaterThanContentHeight!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbClearContentOnly!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.ClearContentOnly)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbClipContentOnly!.CheckedState = ViewToEdit.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.ClipContentOnly)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbTransparent!.CheckedState = ViewToEdit.ViewportSettings.HasFlag(Terminal.Gui.ViewportSettings.Transparent)
+ ? CheckState.Checked
+ : CheckState.UnChecked;
+
+ _cbVerticalScrollBar!.CheckedState = ViewToEdit.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked;
+ _cbAutoShowVerticalScrollBar!.CheckedState = ViewToEdit.VerticalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked;
+ _cbHorizontalScrollBar!.CheckedState = ViewToEdit.HorizontalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked;
+ _cbAutoShowHorizontalScrollBar!.CheckedState = ViewToEdit.HorizontalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked;
+ }
+ }
+
+ private CheckBox? _cbAllowNegativeX;
+ private CheckBox? _cbAllowNegativeY;
+ private CheckBox? _cbAllowXGreaterThanContentWidth;
+ private CheckBox? _cbAllowYGreaterThanContentHeight;
+ private NumericUpDown? _contentSizeWidth;
+ private NumericUpDown? _contentSizeHeight;
+ private CheckBox? _cbClearContentOnly;
+ private CheckBox? _cbClipContentOnly;
+ private CheckBox? _cbTransparent;
+ private CheckBox? _cbVerticalScrollBar;
+ private CheckBox? _cbAutoShowVerticalScrollBar;
+ private CheckBox? _cbHorizontalScrollBar;
+ private CheckBox? _cbAutoShowHorizontalScrollBar;
+
+ private void ViewportSettingsEditor_Initialized (object? s, EventArgs e)
+ {
+ _cbAllowNegativeX = new()
+ {
+ Title = "Allow X < 0",
+ CanFocus = true
+ };
+
+ Add (_cbAllowNegativeX);
+
+ _cbAllowNegativeY = new()
+ {
+ Title = "Allow Y < 0",
+ CanFocus = true
+ };
+
+ Add (_cbAllowNegativeY);
+
+ _cbAllowXGreaterThanContentWidth = new()
+ {
+ Title = "Allow X > Content Width",
+ Y = Pos.Bottom (_cbAllowNegativeX),
+ CanFocus = true
+ };
+
+ _cbAllowNegativeX.CheckedStateChanging += AllowNegativeXToggle;
+ _cbAllowXGreaterThanContentWidth.CheckedStateChanging += AllowXGreaterThanContentWidthToggle;
+
+ Add (_cbAllowXGreaterThanContentWidth);
+
+ void AllowNegativeXToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowNegativeX;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowNegativeX;
+ }
+ }
+
+ void AllowXGreaterThanContentWidthToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth;
+ }
+ }
+
+ _cbAllowYGreaterThanContentHeight = new()
+ {
+ Title = "Allow Y > Content Height",
+ X = Pos.Right (_cbAllowXGreaterThanContentWidth) + 1,
+ Y = Pos.Bottom (_cbAllowNegativeX),
+ CanFocus = true
+ };
+
+ _cbAllowNegativeY.CheckedStateChanging += AllowNegativeYToggle;
+
+ _cbAllowYGreaterThanContentHeight.CheckedStateChanging += AllowYGreaterThanContentHeightToggle;
+
+ Add (_cbAllowYGreaterThanContentHeight);
+
+ void AllowNegativeYToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowNegativeY;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowNegativeY;
+ }
+ }
+
+ void AllowYGreaterThanContentHeightToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+ }
+ }
+
+ _cbAllowNegativeY.X = Pos.Left (_cbAllowYGreaterThanContentHeight);
+
+ var labelContentSize = new Label
+ {
+ Title = "ContentSize:",
+ Y = Pos.Bottom (_cbAllowYGreaterThanContentHeight)
+ };
+
+ _contentSizeWidth = new()
+ {
+ X = Pos.Right (labelContentSize) + 1,
+ Y = Pos.Top (labelContentSize),
+ CanFocus = true
+ };
+ _contentSizeWidth.ValueChanging += ContentSizeWidthValueChanged;
+
+ void ContentSizeWidthValueChanged (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue < 0)
+ {
+ e.Cancel = true;
+
+ return;
+ }
+
+ // BUGBUG: set_ContentSize is supposed to be `protected`.
+ ViewToEdit!.SetContentSize (ViewToEdit.GetContentSize () with { Width = e.NewValue });
+ }
+
+ var labelComma = new Label
+ {
+ Title = ",",
+ X = Pos.Right (_contentSizeWidth),
+ Y = Pos.Top (labelContentSize)
+ };
+
+ _contentSizeHeight = new()
+ {
+ X = Pos.Right (labelComma) + 1,
+ Y = Pos.Top (labelContentSize),
+ CanFocus = true
+ };
+ _contentSizeHeight.ValueChanging += ContentSizeHeightValueChanged;
+
+ void ContentSizeHeightValueChanged (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue < 0)
+ {
+ e.Cancel = true;
+
+ return;
+ }
+
+ // BUGBUG: set_ContentSize is supposed to be `protected`.
+ ViewToEdit?.SetContentSize (ViewToEdit.GetContentSize () with { Height = e.NewValue });
+ }
+
+ _cbClearContentOnly = new()
+ {
+ Title = "ClearContentOnly",
+ X = 0,
+ Y = Pos.Bottom (labelContentSize),
+ CanFocus = true
+ };
+ _cbClearContentOnly.CheckedStateChanging += ClearContentOnlyToggle;
+
+ void ClearContentOnlyToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.ClearContentOnly;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.ClearContentOnly;
+ }
+ }
+
+ _cbClipContentOnly = new()
+ {
+ Title = "ClipContentOnly",
+ X = Pos.Right (_cbClearContentOnly) + 1,
+ Y = Pos.Bottom (labelContentSize),
+ CanFocus = true
+ };
+ _cbClipContentOnly.CheckedStateChanging += ClipContentOnlyToggle;
+
+ void ClipContentOnlyToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.ClipContentOnly;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.ClipContentOnly;
+ }
+ }
+
+ _cbTransparent = new ()
+ {
+ Title = "Transparent",
+ X = Pos.Right (_cbClipContentOnly) + 1,
+ Y = Pos.Bottom (labelContentSize),
+ CanFocus = true
+ };
+ _cbTransparent.CheckedStateChanging += TransparentToggle;
+
+ void TransparentToggle (object? sender, CancelEventArgs e)
+ {
+ if (e.NewValue == CheckState.Checked)
+ {
+ ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewportSettings.Transparent;
+ }
+ else
+ {
+ ViewToEdit!.ViewportSettings &= ~Terminal.Gui.ViewportSettings.Transparent;
+ }
+ }
+
+ _cbVerticalScrollBar = new()
+ {
+ Title = "VerticalScrollBar",
+ X = 0,
+ Y = Pos.Bottom (_cbClearContentOnly),
+ CanFocus = false
+ };
+ _cbVerticalScrollBar.CheckedStateChanging += VerticalScrollBarToggle;
+
+ void VerticalScrollBarToggle (object? sender, CancelEventArgs e)
+ {
+ ViewToEdit!.VerticalScrollBar.Visible = e.NewValue == CheckState.Checked;
+ }
+
+ _cbAutoShowVerticalScrollBar = new()
+ {
+ Title = "AutoShow",
+ X = Pos.Right (_cbVerticalScrollBar) + 1,
+ Y = Pos.Top (_cbVerticalScrollBar),
+ CanFocus = false
+ };
+ _cbAutoShowVerticalScrollBar.CheckedStateChanging += AutoShowVerticalScrollBarToggle;
+
+ void AutoShowVerticalScrollBarToggle (object? sender, CancelEventArgs e)
+ {
+ ViewToEdit!.VerticalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+ }
+
+ _cbHorizontalScrollBar = new()
+ {
+ Title = "HorizontalScrollBar",
+ X = 0,
+ Y = Pos.Bottom (_cbVerticalScrollBar),
+ CanFocus = false
+ };
+ _cbHorizontalScrollBar.CheckedStateChanging += HorizontalScrollBarToggle;
+
+ void HorizontalScrollBarToggle (object? sender, CancelEventArgs e)
+ {
+ ViewToEdit!.HorizontalScrollBar.Visible = e.NewValue == CheckState.Checked;
+ }
+
+ _cbAutoShowHorizontalScrollBar = new()
+ {
+ Title = "AutoShow ",
+ X = Pos.Right (_cbHorizontalScrollBar) + 1,
+ Y = Pos.Top (_cbHorizontalScrollBar),
+ CanFocus = false
+ };
+ _cbAutoShowHorizontalScrollBar.CheckedStateChanging += AutoShowHorizontalScrollBarToggle;
+
+ void AutoShowHorizontalScrollBarToggle (object? sender, CancelEventArgs e)
+ {
+ ViewToEdit!.HorizontalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+ }
+
+ Add (
+ labelContentSize,
+ _contentSizeWidth,
+ labelComma,
+ _contentSizeHeight,
+ _cbClearContentOnly,
+ _cbClipContentOnly,
+ _cbTransparent,
+ _cbVerticalScrollBar,
+ _cbHorizontalScrollBar,
+ _cbAutoShowVerticalScrollBar,
+ _cbAutoShowHorizontalScrollBar);
+ }
+}
diff --git a/UICatalog/Scenarios/RegionScenario.cs b/UICatalog/Scenarios/RegionScenario.cs
new file mode 100644
index 0000000000..9b2e29c1a0
--- /dev/null
+++ b/UICatalog/Scenarios/RegionScenario.cs
@@ -0,0 +1,397 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+using UICatalog;
+using UICatalog.Scenarios;
+
+///
+/// Demonstrates creating and drawing regions through mouse dragging.
+///
+[ScenarioMetadata ("Regions", "Region Tester")]
+[ScenarioCategory ("Mouse and Keyboard")]
+[ScenarioCategory ("Drawing")]
+public class RegionScenario : Scenario
+{
+ private readonly Region _region = new ();
+ private Point? _dragStart;
+ private bool _isDragging;
+
+
+ public Rune? _previewFillRune = Glyphs.Stipple;
+
+ public Rune? _fillRune = Glyphs.Dot;
+
+ private RegionDrawStyles _drawStyle;
+ private RegionOp _regionOp;
+
+ public override void Main ()
+ {
+ Application.Init ();
+
+ Window app = new ()
+ {
+ Title = GetQuitKeyAndName (),
+ TabStop = TabBehavior.TabGroup,
+
+ };
+ app.Padding.Thickness = new (1);
+
+ var tools = new ToolsView { Title = "Tools", X = Pos.AnchorEnd (), Y = 2 };
+
+ tools.CurrentAttribute = app.ColorScheme.HotNormal;
+
+ tools.SetStyle += b =>
+ {
+ _drawStyle = (RegionDrawStyles)b;
+ app.SetNeedsDraw();
+ };
+
+ tools.RegionOpChanged += (s, e) =>
+ {
+ _regionOp = e;
+ };
+ //tools.AddLayer += () => canvas.AddLayer ();
+
+ app.Add (tools);
+
+ // Add drag handling to window
+ app.MouseEvent += (s, e) =>
+ {
+ if (e.Flags.HasFlag (MouseFlags.Button1Pressed))
+ {
+ if (!e.Flags.HasFlag (MouseFlags.ReportMousePosition))
+ { // Start drag
+ _dragStart = e.ScreenPosition;
+ _isDragging = true;
+ }
+ else
+ {
+ // Drag
+ if (_isDragging && _dragStart.HasValue)
+ {
+ app.SetNeedsDraw ();
+ }
+ }
+ }
+
+ if (e.Flags.HasFlag (MouseFlags.Button1Released))
+ {
+ if (_isDragging && _dragStart.HasValue)
+ {
+ // Add the new region
+ AddRectangleFromPoints (_dragStart.Value, e.ScreenPosition, _regionOp);
+ _isDragging = false;
+ _dragStart = null;
+ }
+
+ app.SetNeedsDraw ();
+ }
+ };
+
+ // Draw the regions
+ app.DrawingContent += (s, e) =>
+ {
+ // Draw all regions with single line style
+ //_region.FillRectangles (_attribute.Value, _fillRune);
+ switch (_drawStyle)
+ {
+ case RegionDrawStyles.FillOnly:
+ _region.FillRectangles (tools.CurrentAttribute!.Value, _previewFillRune);
+
+ break;
+
+ case RegionDrawStyles.InnerBoundaries:
+ _region.DrawBoundaries (app.LineCanvas, LineStyle.Single, tools.CurrentAttribute);
+ _region.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' ');
+
+ break;
+
+ case RegionDrawStyles.OuterBoundary:
+ _region.DrawOuterBoundary (app.LineCanvas, LineStyle.Single, tools.CurrentAttribute);
+ _region.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' ');
+
+ break;
+ }
+
+ // If currently dragging, draw preview rectangle
+ if (_isDragging && _dragStart.HasValue)
+ {
+ Point currentMousePos = Application.GetLastMousePosition ()!.Value;
+ var previewRect = GetRectFromPoints (_dragStart.Value, currentMousePos);
+ var previewRegion = new Region (previewRect);
+
+ previewRegion.FillRectangles (tools.CurrentAttribute!.Value, (Rune)' ');
+
+ previewRegion.DrawBoundaries (app.LineCanvas, LineStyle.Dashed, new (tools.CurrentAttribute!.Value.Foreground.GetHighlightColor(), tools.CurrentAttribute!.Value.Background));
+ }
+ };
+
+ Application.Run (app);
+
+ // Clean up
+ app.Dispose ();
+ Application.Shutdown ();
+ }
+
+ private void AddRectangleFromPoints (Point start, Point end, RegionOp op)
+ {
+ var rect = GetRectFromPoints (start, end);
+ var region = new Region (rect);
+ _region.Combine (region, op); // Or RegionOp.MinimalUnion if you want minimal rectangles
+ }
+
+ private Rectangle GetRectFromPoints (Point start, Point end)
+ {
+ int left = Math.Min (start.X, end.X);
+ int top = Math.Min (start.Y, end.Y);
+ int right = Math.Max (start.X, end.X);
+ int bottom = Math.Max (start.Y, end.Y);
+
+ // Ensure minimum width and height of 1
+ int width = Math.Max (1, right - left + 1);
+ int height = Math.Max (1, bottom - top + 1);
+
+ return new Rectangle (left, top, width, height);
+ }
+}
+
+public enum RegionDrawStyles
+{
+ FillOnly = 0,
+
+ InnerBoundaries = 1,
+
+ OuterBoundary = 2
+
+}
+
+public class ToolsView : Window
+{
+ //private Button _addLayerBtn;
+ private readonly AttributeView _attributeView = new ();
+ private RadioGroup _stylePicker;
+ private RegionOpSelector _regionOpSelector;
+
+ public Attribute? CurrentAttribute
+ {
+ get => _attributeView.Value;
+ set => _attributeView.Value = value;
+ }
+
+ public ToolsView ()
+ {
+ BorderStyle = LineStyle.Dotted;
+ Border!.Thickness = new (1, 2, 1, 1);
+ Height = Dim.Auto ();
+ Width = Dim.Auto ();
+
+ }
+
+ //public event Action AddLayer;
+
+ public override void BeginInit ()
+ {
+ base.BeginInit ();
+
+ _attributeView.ValueChanged += (s, e) => AttributeChanged?.Invoke (this, e);
+
+ _stylePicker = new ()
+ {
+ Width=Dim.Fill(),
+ X = 0, Y = Pos.Bottom (_attributeView) + 1, RadioLabels = Enum.GetNames ().Select (n => n = "_" + n).ToArray()
+ };
+ _stylePicker.BorderStyle = LineStyle.Single;
+ _stylePicker.Border.Thickness = new (0, 1, 0, 0);
+ _stylePicker.Title = "Draw Style";
+
+ _stylePicker.SelectedItemChanged += (s, a) => { SetStyle?.Invoke ((LineStyle)a.SelectedItem); };
+ _stylePicker.SelectedItem = (int)RegionDrawStyles.FillOnly;
+
+ _regionOpSelector = new ()
+ {
+ X = 0,
+ Y = Pos.Bottom (_stylePicker) + 1
+ };
+ _regionOpSelector.SelectedItemChanged += (s, a) => { RegionOpChanged?.Invoke (this, a); };
+ _regionOpSelector.SelectedItem = RegionOp.MinimalUnion;
+
+ //_addLayerBtn = new () { Text = "New Layer", X = Pos.Center (), Y = Pos.Bottom (_stylePicker) };
+
+ //_addLayerBtn.Accepting += (s, a) => AddLayer?.Invoke ();
+ Add (_attributeView, _stylePicker, _regionOpSelector);//, _addLayerBtn);
+ }
+
+ public event EventHandler? AttributeChanged;
+ public event EventHandler? RegionOpChanged;
+ public event Action? SetStyle;
+}
+
+public class RegionOpSelector : View
+{
+ private RadioGroup _radioGroup;
+ public RegionOpSelector ()
+ {
+ Width = Dim.Auto ();
+ Height = Dim.Auto ();
+
+ BorderStyle = LineStyle.Single;
+ Border.Thickness = new (0, 1, 0, 0);
+ Title = "RegionOp";
+
+ _radioGroup = new ()
+ {
+ X = 0,
+ Y = 0,
+ RadioLabels = Enum.GetNames ().Select (n => n = "_" + n).ToArray ()
+ };
+ _radioGroup.SelectedItemChanged += (s, a) => { SelectedItemChanged?.Invoke (this, (RegionOp)a.SelectedItem); };
+ Add (_radioGroup);
+ }
+ public event EventHandler? SelectedItemChanged;
+
+ public RegionOp SelectedItem
+ {
+ get => (RegionOp)_radioGroup.SelectedItem;
+ set => _radioGroup.SelectedItem = (int) value;
+ }
+
+}
+
+public class AttributeView : View
+{
+ public event EventHandler? ValueChanged;
+ private Attribute? _value;
+
+ public Attribute? Value
+ {
+ get => _value;
+ set
+ {
+ _value = value;
+ ValueChanged?.Invoke (this, value);
+ }
+ }
+
+ private static readonly HashSet<(int, int)> _foregroundPoints =
+ [
+ (0, 0), (1, 0), (2, 0),
+ (0, 1), (1, 1), (2, 1)
+ ];
+
+ private static readonly HashSet<(int, int)> _backgroundPoints =
+ [
+ (3, 1),
+ (1, 2), (2, 2), (3, 2)
+ ];
+
+ public AttributeView ()
+ {
+ Width = Dim.Fill();
+ Height = 4;
+
+ BorderStyle = LineStyle.Single;
+ Border.Thickness = new (0, 1, 0, 0);
+ Title = "Attribute";
+ }
+
+ ///
+ protected override bool OnDrawingContent ()
+ {
+ Color fg = Value?.Foreground ?? Color.Black;
+ Color bg = Value?.Background ?? Color.Black;
+
+ bool isTransparentFg = fg == GetNormalColor ().Background;
+ bool isTransparentBg = bg == GetNormalColor ().Background;
+
+ SetAttribute (new (fg, isTransparentFg ? Color.Gray : fg));
+
+ // Square of foreground color
+ foreach ((int, int) point in _foregroundPoints)
+ {
+ // Make pattern like this when it is same color as background of control
+ /*▓▒
+ ▒▓*/
+ Rune rune;
+
+ if (isTransparentFg)
+ {
+ rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
+ }
+ else
+ {
+ rune = (Rune)'█';
+ }
+
+ AddRune (point.Item1, point.Item2, rune);
+ }
+
+ SetAttribute (new (bg, isTransparentBg ? Color.Gray : bg));
+
+ // Square of background color
+ foreach ((int, int) point in _backgroundPoints)
+ {
+ // Make pattern like this when it is same color as background of control
+ /*▓▒
+ ▒▓*/
+ Rune rune;
+
+ if (isTransparentBg)
+ {
+ rune = (Rune)(point.Item1 % 2 == point.Item2 % 2 ? '▓' : '▒');
+ }
+ else
+ {
+ rune = (Rune)'█';
+ }
+
+ AddRune (point.Item1, point.Item2, rune);
+ }
+
+ return true;
+ }
+
+ ///
+ protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+ {
+ if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
+ {
+ if (IsForegroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
+ {
+ ClickedInForeground ();
+ }
+ else if (IsBackgroundPoint (mouseEvent.Position.X, mouseEvent.Position.Y))
+ {
+ ClickedInBackground ();
+ }
+
+ }
+ mouseEvent.Handled = true;
+
+ return mouseEvent.Handled;
+ }
+
+ private bool IsForegroundPoint (int x, int y) { return _foregroundPoints.Contains ((x, y)); }
+
+ private bool IsBackgroundPoint (int x, int y) { return _backgroundPoints.Contains ((x, y)); }
+
+ private void ClickedInBackground ()
+ {
+ if (LineDrawing.PromptForColor ("Background", Value!.Value.Background, out Color newColor))
+ {
+ Value = new (Value!.Value.Foreground, newColor);
+ SetNeedsDraw ();
+ }
+ }
+
+ private void ClickedInForeground ()
+ {
+ if (LineDrawing.PromptForColor ("Foreground", Value!.Value.Foreground, out Color newColor))
+ {
+ Value = new (newColor, Value!.Value.Background);
+ SetNeedsDraw ();
+ }
+ }
+}
\ No newline at end of file
diff --git a/UICatalog/Scenarios/TextAlignmentAndDirection.cs b/UICatalog/Scenarios/TextAlignmentAndDirection.cs
index 7f62072685..36a1adecbe 100644
--- a/UICatalog/Scenarios/TextAlignmentAndDirection.cs
+++ b/UICatalog/Scenarios/TextAlignmentAndDirection.cs
@@ -10,6 +10,16 @@ namespace UICatalog.Scenarios;
[ScenarioCategory ("Text and Formatting")]
public class TextAlignmentAndDirection : Scenario
{
+
+ internal class AlignmentAndDirectionView : View
+ {
+ public AlignmentAndDirectionView()
+ {
+ ViewportSettings = Terminal.Gui.ViewportSettings.Transparent;
+ BorderStyle = LineStyle.Dotted;
+ }
+ }
+
public override void Main ()
{
Application.Init ();
@@ -24,8 +34,8 @@ public override void Main ()
var color1 = new ColorScheme { Normal = new (Color.Black, Color.Gray) };
var color2 = new ColorScheme { Normal = new (Color.Black, Color.DarkGray) };
- List