Skip to content

Commit a8a3081

Browse files
committed
Add built-in bordered portal rendering and point-anchored positioning
1 parent 55ded28 commit a8a3081

2 files changed

Lines changed: 77 additions & 2 deletions

File tree

SharpConsoleUI/Controls/PortalContentBase.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// -----------------------------------------------------------------------
88

99
using System.Drawing;
10+
using SharpConsoleUI.Drawing;
1011
using SharpConsoleUI.Events;
1112
using SharpConsoleUI.Layout;
1213
using Color = Spectre.Console.Color;
@@ -27,6 +28,19 @@ public abstract class PortalContentBase : IWindowControl, IDOMPaintable, IMouseA
2728
private int _actualWidth;
2829
private int _actualHeight;
2930

31+
/// <summary>
32+
/// When set, PaintDOM draws a border using these characters and shrinks the
33+
/// inner bounds by 1 on each side before calling <see cref="PaintPortalContent"/>.
34+
/// Mouse coordinates are automatically adjusted by (-1,-1) for the border offset.
35+
/// </summary>
36+
public BoxChars? BorderStyle { get; set; }
37+
38+
/// <summary>Foreground color for the border characters. Falls back to the default foreground.</summary>
39+
public Color? BorderColor { get; set; }
40+
41+
/// <summary>Background color for the border and fill area. Falls back to the default background.</summary>
42+
public Color? BorderBackgroundColor { get; set; }
43+
3044
#region IHasPortalBounds
3145

3246
/// <summary>
@@ -79,7 +93,25 @@ public abstract class PortalContentBase : IWindowControl, IDOMPaintable, IMouseA
7993
public event EventHandler<MouseEventArgs>? MouseMove;
8094
#pragma warning restore CS0067
8195

82-
/// <inheritdoc/>
96+
/// <summary>
97+
/// Explicit interface implementation that adjusts mouse coordinates for the border
98+
/// offset before delegating to the public virtual <see cref="ProcessMouseEvent"/>.
99+
/// </summary>
100+
bool IMouseAwareControl.ProcessMouseEvent(MouseEventArgs args)
101+
{
102+
if (BorderStyle.HasValue)
103+
{
104+
var adjusted = args.WithPosition(
105+
new System.Drawing.Point(args.Position.X - 1, args.Position.Y - 1));
106+
return ProcessMouseEvent(adjusted);
107+
}
108+
return ProcessMouseEvent(args);
109+
}
110+
111+
/// <summary>
112+
/// Processes a mouse event. When <see cref="BorderStyle"/> is set, coordinates
113+
/// are already adjusted for the border offset.
114+
/// </summary>
83115
public abstract bool ProcessMouseEvent(MouseEventArgs args);
84116

85117
#endregion
@@ -167,7 +199,18 @@ public void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipR
167199
_actualWidth = bounds.Width;
168200
_actualHeight = bounds.Height;
169201

170-
PaintPortalContent(buffer, bounds, clipRect, defaultFg, defaultBg);
202+
if (BorderStyle is { } border)
203+
{
204+
buffer.DrawBox(bounds, border, BorderColor ?? defaultFg,
205+
BorderBackgroundColor ?? defaultBg);
206+
var inner = new LayoutRect(bounds.X + 1, bounds.Y + 1,
207+
Math.Max(0, bounds.Width - 2), Math.Max(0, bounds.Height - 2));
208+
PaintPortalContent(buffer, inner, clipRect, defaultFg, defaultBg);
209+
}
210+
else
211+
{
212+
PaintPortalContent(buffer, bounds, clipRect, defaultFg, defaultBg);
213+
}
171214
}
172215

173216
/// <summary>

SharpConsoleUI/Layout/PortalPositioner.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,38 @@ public static PortalPositionResult Calculate(PortalPositionRequest request)
160160
);
161161
}
162162

163+
/// <summary>
164+
/// Calculates the optimal position for a portal anchored at a single point
165+
/// (e.g. a cursor position) rather than a rectangle.
166+
/// </summary>
167+
/// <param name="anchor">The point anchor (e.g. cursor position).</param>
168+
/// <param name="contentSize">The desired size of the portal content.</param>
169+
/// <param name="screenBounds">The available screen/window area to position within.</param>
170+
/// <param name="placement">Preferred placement direction (default: BelowOrAbove).</param>
171+
/// <param name="minSize">Minimum dimensions to enforce on the result.</param>
172+
/// <returns>The calculated position result.</returns>
173+
public static PortalPositionResult CalculateFromPoint(
174+
Point anchor, Size contentSize, Rectangle screenBounds,
175+
PortalPlacement placement = PortalPlacement.BelowOrAbove,
176+
Size minSize = default)
177+
{
178+
// Point anchor → zero-width, 1-height rect (cursor occupies 1 row)
179+
var anchorRect = new Rectangle(anchor.X, anchor.Y, 0, 1);
180+
var result = Calculate(new PortalPositionRequest(anchorRect, contentSize, screenBounds, placement));
181+
182+
// Enforce minimum dimensions
183+
if (minSize.Width > 0 || minSize.Height > 0)
184+
{
185+
var b = result.Bounds;
186+
int w = Math.Max(b.Width, minSize.Width);
187+
int h = Math.Max(b.Height, minSize.Height);
188+
if (w != b.Width || h != b.Height)
189+
return new PortalPositionResult(
190+
new Rectangle(b.X, b.Y, w, h), result.ActualPlacement, result.WasClamped);
191+
}
192+
return result;
193+
}
194+
163195
/// <summary>
164196
/// Resolves a composite placement (e.g. BelowOrAbove) into a concrete direction
165197
/// based on available space.

0 commit comments

Comments
 (0)