Skip to content

Commit 1814e3b

Browse files
committed
fix(portals): add BufferOrigin to prevent submenu Y-clipping at desktop bottom
Portal submenus near the bottom of the screen were clipped because the buffer coordinate system had no room above the portal. Add BufferOrigin property (defaults to Bounds.Location for zero behavior change) that lets the Start Menu map buffer (0,0) to the desktop top-left, giving submenus space to shift upward. Update all buffer-to-screen mappings in RenderCoordinator, HitTest, and CalculateDropdownBounds to use BufferOrigin instead of Bounds for coordinate translation.
1 parent ff7e41b commit 1814e3b

5 files changed

Lines changed: 38 additions & 21 deletions

File tree

SharpConsoleUI/Controls/MenuControl/MenuControl.DropdownManagement.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,17 @@ private Rectangle CalculateDropdownBounds(MenuItem item)
220220
}
221221
else if (Container is Core.DesktopPortalContainer dpc)
222222
{
223-
// Desktop portal context — clamp to available screen space from portal origin.
224-
// Buffer pos (bx, by) → screen pos (Bounds.X+bx, Bounds.Y+by).
225-
// Rendering clips to desktopBottomRight — use the same reference point.
223+
// Desktop portal context — clamp to available screen space in buffer coordinates.
224+
// Item bounds are in buffer space (layout positions relative to buffer origin).
225+
// Using BufferOrigin (instead of Bounds) ensures the range covers the full buffer,
226+
// allowing submenus to use space above the portal when BufferOrigin < Bounds.Y.
226227
var ws = Container.GetConsoleWindowSystem!;
227228
var desktopBR = ws.DesktopBottomRight;
228-
var portalScreenX = dpc.Portal.Bounds.X;
229-
var portalScreenY = dpc.Portal.Bounds.Y;
229+
var originX = dpc.Portal.BufferOrigin.X;
230+
var originY = dpc.Portal.BufferOrigin.Y;
230231
screenBounds = new Rectangle(0, 0,
231-
desktopBR.X + 1 - portalScreenX,
232-
desktopBR.Y + 1 - portalScreenY);
232+
desktopBR.X + 1 - originX,
233+
desktopBR.Y + 1 - originY);
233234
}
234235
else
235236
{

SharpConsoleUI/Core/DesktopPortal.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace SharpConsoleUI.Core
2222
/// <param name="DimBackground">Whether to dim the screen behind the portal.</param>
2323
/// <param name="OnDismiss">Callback invoked when the portal is dismissed.</param>
2424
/// <param name="Owner">The control that owns this portal (for identification/cleanup).</param>
25+
/// <param name="BufferSize">Buffer size for rendering. Defaults to Bounds size.</param>
26+
/// <param name="BufferOrigin">Screen coordinate that buffer (0,0) maps to. Defaults to Bounds.Location.</param>
2527
public record DesktopPortalOptions(
2628
IWindowControl Content,
2729
Rectangle Bounds,
@@ -30,7 +32,8 @@ public record DesktopPortalOptions(
3032
bool DimBackground = false,
3133
Action? OnDismiss = null,
3234
IWindowControl? Owner = null,
33-
Size? BufferSize = null
35+
Size? BufferSize = null,
36+
Point? BufferOrigin = null
3437
);
3538

3639
/// <summary>
@@ -72,6 +75,13 @@ public class DesktopPortal
7275
/// </summary>
7376
public Size BufferSize { get; }
7477

78+
/// <summary>
79+
/// Screen coordinate that buffer position (0,0) maps to.
80+
/// Defaults to Bounds.Location for backwards compatibility.
81+
/// Set to DesktopUpperLeft when the buffer needs to cover space above the portal (e.g., Start Menu).
82+
/// </summary>
83+
public Point BufferOrigin { get; }
84+
7585
/// <summary>Stacking order among portals (higher = on top).</summary>
7686
public int ZOrder { get; }
7787

@@ -117,6 +127,7 @@ internal DesktopPortal(DesktopPortalOptions options, int zOrder, ConsoleWindowSy
117127
IsDirty = true;
118128
CreatedAt = DateTime.Now;
119129
BufferSize = options.BufferSize ?? new Size(Bounds.Width, Bounds.Height);
130+
BufferOrigin = options.BufferOrigin ?? new Point(Bounds.X, Bounds.Y);
120131

121132
Container = new DesktopPortalContainer(this);
122133

@@ -131,7 +142,9 @@ internal DesktopPortal(DesktopPortalOptions options, int zOrder, ConsoleWindowSy
131142

132143
// Arrange at portal bounds so the root control fills the declared area.
133144
// Controls that want to be smaller will respect their own sizing within this rect.
134-
RootNode.Arrange(new LayoutRect(0, 0, Bounds.Width, Bounds.Height));
145+
int rootOffsetX = Bounds.X - BufferOrigin.X;
146+
int rootOffsetY = Bounds.Y - BufferOrigin.Y;
147+
RootNode.Arrange(new LayoutRect(rootOffsetX, rootOffsetY, Bounds.Width, Bounds.Height));
135148

136149
// Compute initial control bounds for hit-testing before first render
137150
RootNode.Visit(node =>

SharpConsoleUI/Core/DesktopPortalService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,8 @@ public bool AnyPortalDirty()
184184
// Check against control bounds, not full portal bounds
185185
foreach (var bounds in portal.ControlBounds)
186186
{
187-
int screenX = portal.Bounds.X + bounds.X;
188-
int screenY = portal.Bounds.Y + bounds.Y;
187+
int screenX = portal.BufferOrigin.X + bounds.X;
188+
int screenY = portal.BufferOrigin.Y + bounds.Y;
189189
var screenRect = new Rectangle(screenX, screenY, bounds.Width, bounds.Height);
190190
if (screenRect.Contains(point))
191191
return portal;

SharpConsoleUI/Dialogs/StartMenuDialog.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ public static void Show(ConsoleWindowSystem windowSystem)
118118
ConsumeClickOnDismiss: true,
119119
DimBackground: false,
120120
Owner: menu,
121-
BufferSize: new Size(desktopDims.Width, desktopDims.Height)));
121+
BufferSize: new Size(desktopDims.Width, desktopDims.Height),
122+
BufferOrigin: new Point(desktopUpperLeft.X, desktopUpperLeft.Y)));
122123
}
123124

124125
private static void BuildSystemCategory(MenuBuilder menuBuilder, ConsoleWindowSystem windowSystem)

SharpConsoleUI/Rendering/RenderCoordinator.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ public void RestorePortalRegions(Core.DesktopPortal portal)
176176
foreach (var bounds in portal.ControlBounds)
177177
{
178178
var screenRect = new Rectangle(
179-
portal.Bounds.X + bounds.X,
180-
portal.Bounds.Y + bounds.Y,
179+
portal.BufferOrigin.X + bounds.X,
180+
portal.BufferOrigin.Y + bounds.Y,
181181
bounds.Width,
182182
bounds.Height);
183183
RestoreScreenRegion(screenRect, belowPortal: portal);
@@ -429,8 +429,8 @@ private void RestoreScreenRegion(Rectangle screenRect, Core.DesktopPortal? below
429429

430430
foreach (var bounds in portal.ControlBounds)
431431
{
432-
int portalScreenX = portal.Bounds.X + bounds.X;
433-
int portalScreenY = portal.Bounds.Y + bounds.Y;
432+
int portalScreenX = portal.BufferOrigin.X + bounds.X;
433+
int portalScreenY = portal.BufferOrigin.Y + bounds.Y;
434434
var portalScreenRect = new Rectangle(portalScreenX, portalScreenY, bounds.Width, bounds.Height);
435435

436436
if (!portalScreenRect.IntersectsWith(clippedRect))
@@ -500,7 +500,9 @@ private void RenderDesktopPortals()
500500
// Arrange at portal bounds so root control fills the declared area
501501
var constraints = Layout.LayoutConstraints.Loose(bufSize.Width, bufSize.Height);
502502
portal.RootNode.Measure(constraints);
503-
portal.RootNode.Arrange(new Layout.LayoutRect(0, 0, portal.Bounds.Width, portal.Bounds.Height));
503+
int rootOffX = portal.Bounds.X - portal.BufferOrigin.X;
504+
int rootOffY = portal.Bounds.Y - portal.BufferOrigin.Y;
505+
portal.RootNode.Arrange(new Layout.LayoutRect(rootOffX, rootOffY, portal.Bounds.Width, portal.Bounds.Height));
504506

505507
// Paint DOM to buffer (clip to full buffer so submenus can render beyond content bounds)
506508
var clipRect = new Layout.LayoutRect(0, 0, bufSize.Width, bufSize.Height);
@@ -560,8 +562,8 @@ private void RestorePortalDelta(Core.DesktopPortal portal, List<Layout.LayoutRec
560562
{
561563
// Convert to screen space and restore
562564
var screenRect = new Rectangle(
563-
portal.Bounds.X + prevRect.X,
564-
portal.Bounds.Y + prevRect.Y,
565+
portal.BufferOrigin.X + prevRect.X,
566+
portal.BufferOrigin.Y + prevRect.Y,
565567
prevRect.Width,
566568
prevRect.Height);
567569
RestoreScreenRegion(screenRect, belowPortal: portal);
@@ -583,8 +585,8 @@ private void WritePortalControlRegions(Core.DesktopPortal portal)
583585

584586
foreach (var bounds in portal.ControlBounds)
585587
{
586-
int screenX = portal.Bounds.X + bounds.X;
587-
int screenY = portal.Bounds.Y + bounds.Y;
588+
int screenX = portal.BufferOrigin.X + bounds.X;
589+
int screenY = portal.BufferOrigin.Y + bounds.Y;
588590

589591
// Clip to desktop area
590592
int clipLeft = Math.Max(screenX, desktopUpperLeft.X);

0 commit comments

Comments
 (0)