Skip to content

Commit bb0de1a

Browse files
committed
Add per-border resize direction permissions
Introduce ResizeBorderDirections flags enum so each border can independently allow or block expand/contract movement. Defaults to All (existing behaviour). - WindowEnums.cs: ResizeBorderDirections with Top/Bottom/Left/Right expand+contract flags, convenience composites (ExpandOnly, ContractOnly, All) - Window.cs: AllowedResizeDirections property (default All) - WindowBuilder.cs: WithResizeDirections() builder method - InputCoordinator.cs: AllowedDirection() filters hit-test results; ClampResizeDelta() restricts drag deltas; keyboard resize arrows respect the flags - BorderRenderer.cs: resize grip only shown when bottom-right movement is permitted
1 parent 2817341 commit bb0de1a

5 files changed

Lines changed: 163 additions & 20 deletions

File tree

SharpConsoleUI/Builders/WindowBuilder.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public sealed class WindowBuilder
3939
private bool _isModal = false;
4040
private WindowState _state = WindowState.Normal;
4141
private bool _isResizable = true;
42+
private ResizeBorderDirections _resizeDirections = ResizeBorderDirections.All;
4243
private bool _isClosable = true;
4344
private bool _isMovable = true;
4445
private bool _isMinimizable = true;
@@ -257,6 +258,16 @@ public WindowBuilder Resizable(bool resizable = true)
257258
return this;
258259
}
259260

261+
/// <summary>
262+
/// Sets per-border movement permissions for resizing. Implies <c>Resizable(true)</c>.
263+
/// </summary>
264+
public WindowBuilder WithResizeDirections(ResizeBorderDirections directions)
265+
{
266+
_isResizable = true;
267+
_resizeDirections = directions;
268+
return this;
269+
}
270+
260271
/// <summary>
261272
/// Sets whether the window can be closed by the user (via close button or programmatically).
262273
/// </summary>
@@ -661,6 +672,7 @@ public Window Build()
661672
window.IsModal = _isModal;
662673
window.State = _state;
663674
window.IsResizable = _isResizable;
675+
window.AllowedResizeDirections = _resizeDirections;
664676
window.IsClosable = _isClosable;
665677
window.IsMovable = _isMovable;
666678
window.IsMinimizable = _isMinimizable;

SharpConsoleUI/Input/InputCoordinator.cs

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -319,30 +319,68 @@ private ResizeDirection GetResizeDirection(Window window, Point point)
319319
// Corner resize areas
320320
if (relativePoint.Y == 0)
321321
{
322-
if (inTitleBarCorner && onLeftBorder) return ResizeDirection.TopLeft;
323-
if (inTitleBarCorner && onRightBorder) return ResizeDirection.TopRight;
322+
if (inTitleBarCorner && onLeftBorder) return AllowedDirection(window, ResizeDirection.TopLeft);
323+
if (inTitleBarCorner && onRightBorder) return AllowedDirection(window, ResizeDirection.TopRight);
324324
}
325325
else
326326
{
327-
if (onTopBorder && onLeftBorder) return ResizeDirection.TopLeft;
328-
if (onTopBorder && onRightBorder) return ResizeDirection.TopRight;
327+
if (onTopBorder && onLeftBorder) return AllowedDirection(window, ResizeDirection.TopLeft);
328+
if (onTopBorder && onRightBorder) return AllowedDirection(window, ResizeDirection.TopRight);
329329
}
330330

331-
if (onBottomBorder && onLeftBorder) return ResizeDirection.BottomLeft;
332-
if (onBottomBorder && onRightBorder) return ResizeDirection.BottomRight;
331+
if (onBottomBorder && onLeftBorder) return AllowedDirection(window, ResizeDirection.BottomLeft);
332+
if (onBottomBorder && onRightBorder) return AllowedDirection(window, ResizeDirection.BottomRight);
333333

334334
// Resize grip at bottom-right
335-
if (IsOnResizeGrip(window, point)) return ResizeDirection.BottomRight;
335+
if (IsOnResizeGrip(window, point)) return AllowedDirection(window, ResizeDirection.BottomRight);
336336

337337
// Edge resize areas
338-
if (onTopBorder) return ResizeDirection.Top;
339-
if (onBottomBorder) return ResizeDirection.Bottom;
340-
if (onLeftBorder) return ResizeDirection.Left;
341-
if (onRightBorder) return ResizeDirection.Right;
338+
if (onTopBorder) return AllowedDirection(window, ResizeDirection.Top);
339+
if (onBottomBorder) return AllowedDirection(window, ResizeDirection.Bottom);
340+
if (onLeftBorder) return AllowedDirection(window, ResizeDirection.Left);
341+
if (onRightBorder) return AllowedDirection(window, ResizeDirection.Right);
342342

343343
return ResizeDirection.None;
344344
}
345345

346+
/// <summary>
347+
/// Returns <paramref name="direction"/> if it is permitted by the window's
348+
/// <see cref="Window.AllowedResizeDirections"/> (a border is active when at least one of
349+
/// its two movement directions is enabled); otherwise <see cref="ResizeDirection.None"/>.
350+
/// </summary>
351+
private static ResizeDirection AllowedDirection(Window window, ResizeDirection direction)
352+
{
353+
var d = window.AllowedResizeDirections;
354+
bool topActive = d.HasFlag(ResizeBorderDirections.TopExpand) || d.HasFlag(ResizeBorderDirections.TopContract);
355+
bool bottomActive = d.HasFlag(ResizeBorderDirections.BottomExpand) || d.HasFlag(ResizeBorderDirections.BottomContract);
356+
bool leftActive = d.HasFlag(ResizeBorderDirections.LeftExpand) || d.HasFlag(ResizeBorderDirections.LeftContract);
357+
bool rightActive = d.HasFlag(ResizeBorderDirections.RightExpand) || d.HasFlag(ResizeBorderDirections.RightContract);
358+
359+
bool allowed = direction switch
360+
{
361+
ResizeDirection.Top => topActive,
362+
ResizeDirection.Bottom => bottomActive,
363+
ResizeDirection.Left => leftActive,
364+
ResizeDirection.Right => rightActive,
365+
ResizeDirection.TopLeft => topActive && leftActive,
366+
ResizeDirection.TopRight => topActive && rightActive,
367+
ResizeDirection.BottomLeft => bottomActive && leftActive,
368+
ResizeDirection.BottomRight => bottomActive && rightActive,
369+
_ => true
370+
};
371+
return allowed ? direction : ResizeDirection.None;
372+
}
373+
374+
/// <summary>
375+
/// Clamps a resize delta to zero when the movement direction is not permitted.
376+
/// </summary>
377+
private static int ClampResizeDelta(int delta, bool allowNegative, bool allowPositive)
378+
{
379+
if (delta < 0 && !allowNegative) return 0;
380+
if (delta > 0 && !allowPositive) return 0;
381+
return delta;
382+
}
383+
346384
/// <summary>
347385
/// Checks if a point is in the draggable title bar area.
348386
/// </summary>
@@ -509,40 +547,57 @@ private void HandleWindowResize(Point currentMousePos)
509547
int newWidth = currentResize.StartWindowSize.Width;
510548
int newHeight = currentResize.StartWindowSize.Height;
511549

512-
// Apply resize based on direction
550+
// Apply resize based on direction, clamping each axis to allowed movement directions
551+
var dirs = window.AllowedResizeDirections;
513552
switch (currentResize.Direction)
514553
{
515554
case ResizeDirection.Left:
555+
// deltaX < 0 = left border expands left; deltaX > 0 = contracts right
556+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.LeftExpand), dirs.HasFlag(ResizeBorderDirections.LeftContract));
516557
newLeft += deltaX;
517558
newWidth -= deltaX;
518559
break;
519560
case ResizeDirection.Right:
561+
// deltaX > 0 = right border expands right; deltaX < 0 = contracts left
562+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.RightContract), dirs.HasFlag(ResizeBorderDirections.RightExpand));
520563
newWidth += deltaX;
521564
break;
522565
case ResizeDirection.Top:
566+
// deltaY < 0 = top border expands up; deltaY > 0 = contracts down
567+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.TopExpand), dirs.HasFlag(ResizeBorderDirections.TopContract));
523568
newTop += deltaY;
524569
newHeight -= deltaY;
525570
break;
526571
case ResizeDirection.Bottom:
572+
// deltaY > 0 = bottom border expands down; deltaY < 0 = contracts up
573+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.BottomContract), dirs.HasFlag(ResizeBorderDirections.BottomExpand));
527574
newHeight += deltaY;
528575
break;
529576
case ResizeDirection.TopLeft:
577+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.LeftExpand), dirs.HasFlag(ResizeBorderDirections.LeftContract));
578+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.TopExpand), dirs.HasFlag(ResizeBorderDirections.TopContract));
530579
newLeft += deltaX;
531580
newWidth -= deltaX;
532581
newTop += deltaY;
533582
newHeight -= deltaY;
534583
break;
535584
case ResizeDirection.TopRight:
585+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.RightContract), dirs.HasFlag(ResizeBorderDirections.RightExpand));
586+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.TopExpand), dirs.HasFlag(ResizeBorderDirections.TopContract));
536587
newWidth += deltaX;
537588
newTop += deltaY;
538589
newHeight -= deltaY;
539590
break;
540591
case ResizeDirection.BottomLeft:
592+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.LeftExpand), dirs.HasFlag(ResizeBorderDirections.LeftContract));
593+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.BottomContract), dirs.HasFlag(ResizeBorderDirections.BottomExpand));
541594
newLeft += deltaX;
542595
newWidth -= deltaX;
543596
newHeight += deltaY;
544597
break;
545598
case ResizeDirection.BottomRight:
599+
deltaX = ClampResizeDelta(deltaX, dirs.HasFlag(ResizeBorderDirections.RightContract), dirs.HasFlag(ResizeBorderDirections.RightExpand));
600+
deltaY = ClampResizeDelta(deltaY, dirs.HasFlag(ResizeBorderDirections.BottomContract), dirs.HasFlag(ResizeBorderDirections.BottomExpand));
546601
newWidth += deltaX;
547602
newHeight += deltaY;
548603
break;
@@ -626,25 +681,32 @@ private bool HandleMoveInput(ConsoleKeyInfo key)
626681
/// </summary>
627682
private bool HandleResizeInput(ConsoleKeyInfo key)
628683
{
629-
var activeWindow = _context.ActiveWindow;
630-
if (activeWindow == null) return false;
684+
var w = _context.ActiveWindow;
685+
if (w == null) return false;
686+
687+
var dirs = w.AllowedResizeDirections;
631688

689+
// Each arrow key moves the natural border outward (expand only)
632690
switch (key.Key)
633691
{
634692
case ConsoleKey.UpArrow:
635-
_context.Positioning.ResizeWindowBy(activeWindow, 0, -1);
693+
if (!dirs.HasFlag(ResizeBorderDirections.TopExpand)) return false;
694+
_context.Positioning.ResizeWindowTo(w, w.Left, w.Top - 1, w.Width, w.Height + 1);
636695
return true;
637696

638697
case ConsoleKey.DownArrow:
639-
_context.Positioning.ResizeWindowBy(activeWindow, 0, 1);
698+
if (!dirs.HasFlag(ResizeBorderDirections.BottomExpand)) return false;
699+
_context.Positioning.ResizeWindowTo(w, w.Left, w.Top, w.Width, w.Height + 1);
640700
return true;
641701

642702
case ConsoleKey.LeftArrow:
643-
_context.Positioning.ResizeWindowBy(activeWindow, -1, 0);
703+
if (!dirs.HasFlag(ResizeBorderDirections.LeftExpand)) return false;
704+
_context.Positioning.ResizeWindowTo(w, w.Left - 1, w.Top, w.Width + 1, w.Height);
644705
return true;
645706

646707
case ConsoleKey.RightArrow:
647-
_context.Positioning.ResizeWindowBy(activeWindow, 1, 0);
708+
if (!dirs.HasFlag(ResizeBorderDirections.RightExpand)) return false;
709+
_context.Positioning.ResizeWindowTo(w, w.Left, w.Top, w.Width + 1, w.Height);
648710
return true;
649711

650712
default:

SharpConsoleUI/Models/WindowEnums.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,65 @@ public enum WindowTopologyAction
3434
Move
3535
}
3636

37+
/// <summary>
38+
/// Per-border movement permissions for window resizing.
39+
/// Each border has an Expand direction (border moves outward, window grows)
40+
/// and a Contract direction (border moves inward, window shrinks).
41+
/// </summary>
42+
/// <remarks>
43+
/// Sign conventions when dragging or pressing keyboard shortcuts:
44+
/// <list type="bullet">
45+
/// <item>Top border — Expand: border moves up; Contract: border moves down</item>
46+
/// <item>Bottom border — Expand: border moves down; Contract: border moves up</item>
47+
/// <item>Left border — Expand: border moves left; Contract: border moves right</item>
48+
/// <item>Right border — Expand: border moves right; Contract: border moves left</item>
49+
/// </list>
50+
/// Keyboard shortcuts (Shift+arrows) only trigger Expand movement on the natural border.
51+
/// </remarks>
52+
[Flags]
53+
public enum ResizeBorderDirections
54+
{
55+
/// <summary>No resize movement is permitted.</summary>
56+
None = 0,
57+
58+
/// <summary>Top border moves up — window grows taller.</summary>
59+
TopExpand = 1 << 0,
60+
/// <summary>Top border moves down — window shrinks from the top.</summary>
61+
TopContract = 1 << 1,
62+
63+
/// <summary>Bottom border moves down — window grows taller.</summary>
64+
BottomExpand = 1 << 2,
65+
/// <summary>Bottom border moves up — window shrinks from the bottom.</summary>
66+
BottomContract = 1 << 3,
67+
68+
/// <summary>Left border moves left — window grows wider.</summary>
69+
LeftExpand = 1 << 4,
70+
/// <summary>Left border moves right — window shrinks from the left.</summary>
71+
LeftContract = 1 << 5,
72+
73+
/// <summary>Right border moves right — window grows wider.</summary>
74+
RightExpand = 1 << 6,
75+
/// <summary>Right border moves left — window shrinks from the right.</summary>
76+
RightContract = 1 << 7,
77+
78+
/// <summary>Both expand and contract on the top border.</summary>
79+
Top = TopExpand | TopContract,
80+
/// <summary>Both expand and contract on the bottom border.</summary>
81+
Bottom = BottomExpand | BottomContract,
82+
/// <summary>Both expand and contract on the left border.</summary>
83+
Left = LeftExpand | LeftContract,
84+
/// <summary>Both expand and contract on the right border.</summary>
85+
Right = RightExpand | RightContract,
86+
87+
/// <summary>All borders may expand outward only.</summary>
88+
ExpandOnly = TopExpand | BottomExpand | LeftExpand | RightExpand,
89+
/// <summary>All borders may contract inward only.</summary>
90+
ContractOnly = TopContract | BottomContract | LeftContract | RightContract,
91+
92+
/// <summary>All borders and all directions are resizable.</summary>
93+
All = Top | Bottom | Left | Right
94+
}
95+
3796
/// <summary>
3897
/// Specifies the direction from which a window is being resized.
3998
/// </summary>

SharpConsoleUI/Window.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,12 @@ public bool IsDirty
551551
/// </summary>
552552
public bool IsResizable { get; set; } = true;
553553

554+
/// <summary>
555+
/// Gets or sets per-border movement permissions for resizing. Only meaningful when
556+
/// <see cref="IsResizable"/> is <c>true</c>. Defaults to <see cref="ResizeBorderDirections.All"/>.
557+
/// </summary>
558+
public ResizeBorderDirections AllowedResizeDirections { get; set; } = ResizeBorderDirections.All;
559+
554560
/// <summary>
555561
/// Gets or sets a value indicating whether the window content is scrollable.
556562
/// </summary>

SharpConsoleUI/Windows/BorderRenderer.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,12 @@ private void DrawVisibleBorders(List<Rectangle> visibleRegions)
165165
var closeButton = (_window.IsClosable && _window.ShowCloseButton) ? $"{closeButtonColor}[X]" : "";
166166
var windowButtons = $"{minimizeButton}{maximizeButton}{closeButton}{resetColor}";
167167

168-
// Resize grip replaces bottom-right corner when window is resizable: ◢
169-
var bottomRightChar = _window.IsResizable ? "◢" : bottomRightCorner.ToString();
168+
// Resize grip replaces bottom-right corner when bottom-right dragging is allowed: ◢
169+
var resizeDirs = _window.AllowedResizeDirections;
170+
bool showResizeGrip = _window.IsResizable &&
171+
(resizeDirs.HasFlag(ResizeBorderDirections.BottomExpand) || resizeDirs.HasFlag(ResizeBorderDirections.BottomContract)) &&
172+
(resizeDirs.HasFlag(ResizeBorderDirections.RightExpand) || resizeDirs.HasFlag(ResizeBorderDirections.RightContract));
173+
var bottomRightChar = showResizeGrip ? "◢" : bottomRightCorner.ToString();
170174

171175
// Build title section (only if ShowTitle is true and title is not empty)
172176
string title;

0 commit comments

Comments
 (0)