#nullable enable
using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.ViewBase;
// Border Arrange Mode
public partial class Border
{
internal ViewArrangement Arranging { get; set; }
private Button? _moveButton; // always top-left
private Button? _allSizeButton;
private Button? _leftSizeButton;
private Button? _rightSizeButton;
private Button? _topSizeButton;
private Button? _bottomSizeButton;
///
/// Starts "Arrange Mode" where can be moved and/or resized using the mouse
/// or keyboard. If is keyboard mode is enabled.
///
///
/// Arrange Mode is exited by the user pressing , , or by
/// clicking the mouse out of the 's Frame.
///
///
public bool? EnterArrangeMode (ViewArrangement arrangement)
{
Debug.Assert (Arranging == ViewArrangement.Fixed);
if (!HasAnyArrangementOptions ())
{
return false;
}
MouseState |= MouseState.Pressed;
// Add Commands and KeyBindings - Note it's ok these get added each time. KeyBindings are cleared in EndArrange()
AddArrangeModeKeyBindings ();
Application.MouseEvent += ApplicationOnMouseEvent;
// Create all necessary arrangement buttons
CreateArrangementButtons ();
if (arrangement == ViewArrangement.Fixed)
{
// Keyboard mode
SetVisibilityForKeyboardMode ();
Arranging = ViewArrangement.Movable;
CanFocus = true;
SetFocus ();
}
else
{
// Mouse mode
Arranging = arrangement;
SetVisibilityForMouseMode (arrangement);
}
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);
}
return true;
}
// Hack for now
EndArrangeMode ();
return false;
}
///
/// Checks if the parent view has any arrangement options enabled
///
private bool HasAnyArrangementOptions ()
{
return Parent!.Arrangement.HasFlag (ViewArrangement.Movable)
|| Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable)
|| Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable)
|| Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable)
|| Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable);
}
///
/// Creates all the buttons required for the arrange mode based on allowed arrangement options
///
private void CreateArrangementButtons ()
{
if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable))
{
_moveButton = CreateArrangementButton ("moveButton", Glyphs.Move, 0, 0, ViewArrangement.Movable);
}
if (Parent!.Arrangement.HasFlag (ViewArrangement.Resizable))
{
_allSizeButton = CreateArrangementButton (
"allSizeButton",
Glyphs.SizeBottomRight,
Pos.AnchorEnd (),
Pos.AnchorEnd (),
ViewArrangement.Resizable);
}
if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable))
{
_topSizeButton = CreateArrangementButton (
"topSizeButton",
Glyphs.SizeVertical,
Pos.Center () + Parent!.Margin!.Thickness.Horizontal,
0,
ViewArrangement.TopResizable);
}
if (Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable))
{
_rightSizeButton = CreateArrangementButton (
"rightSizeButton",
Glyphs.SizeHorizontal,
Pos.AnchorEnd (),
Pos.Center () + Parent!.Margin!.Thickness.Vertical / 2,
ViewArrangement.RightResizable);
}
if (Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable))
{
_leftSizeButton = CreateArrangementButton (
"leftSizeButton",
Glyphs.SizeHorizontal,
0,
Pos.Center () + Parent!.Margin!.Thickness.Vertical / 2,
ViewArrangement.LeftResizable);
}
if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable))
{
_bottomSizeButton = CreateArrangementButton (
"bottomSizeButton",
Glyphs.SizeVertical,
Pos.Center () + Parent!.Margin!.Thickness.Horizontal / 2,
Pos.AnchorEnd (),
ViewArrangement.BottomResizable);
}
}
///
/// Factory method to create a standardized arrangement button
///
private Button CreateArrangementButton (string id, Rune glyph, Pos x, Pos y, ViewArrangement arrangement)
{
var button = new Button
{
Id = id,
CanFocus = true,
Width = 1,
Height = 1,
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.None,
Text = $"{glyph}",
X = x,
Y = y,
Visible = false,
Data = arrangement
};
Add (button);
return button;
}
///
/// Sets button visibility for keyboard arrangement mode
///
private void SetVisibilityForKeyboardMode ()
{
if (_moveButton != null)
{
_moveButton.Visible = true;
}
if (_allSizeButton != null)
{
_allSizeButton.Visible = true;
}
}
///
/// Sets button visibility based on the specified mouse arrangement mode
///
private void SetVisibilityForMouseMode (ViewArrangement arrangement)
{
switch (arrangement)
{
case ViewArrangement.Movable:
SetVisibleButton (_moveButton);
break;
case ViewArrangement.RightResizable | ViewArrangement.BottomResizable:
case ViewArrangement.Resizable:
SetVisibleButton (_rightSizeButton);
SetVisibleButton (_bottomSizeButton);
if (_allSizeButton != null)
{
_allSizeButton.X = Pos.AnchorEnd ();
_allSizeButton.Y = Pos.AnchorEnd ();
_allSizeButton.Visible = true;
}
break;
case ViewArrangement.LeftResizable:
SetVisibleButton (_leftSizeButton);
break;
case ViewArrangement.RightResizable:
SetVisibleButton (_rightSizeButton);
break;
case ViewArrangement.TopResizable:
SetVisibleButton (_topSizeButton);
break;
case ViewArrangement.BottomResizable:
SetVisibleButton (_bottomSizeButton);
break;
case ViewArrangement.LeftResizable | ViewArrangement.BottomResizable:
SetVisibleButton (_leftSizeButton);
SetVisibleButton (_bottomSizeButton);
if (_allSizeButton != null)
{
_allSizeButton.X = 0;
_allSizeButton.Y = Pos.AnchorEnd ();
_allSizeButton.Visible = true;
}
break;
case ViewArrangement.LeftResizable | ViewArrangement.TopResizable:
SetVisibleButton (_leftSizeButton);
SetVisibleButton (_topSizeButton);
break;
case ViewArrangement.RightResizable | ViewArrangement.TopResizable:
SetVisibleButton (_rightSizeButton);
SetVisibleButton (_topSizeButton);
if (_allSizeButton != null)
{
_allSizeButton.X = Pos.AnchorEnd ();
_allSizeButton.Y = 0;
_allSizeButton.Visible = true;
}
break;
}
}
///
/// Helper method to make a button visible if it's not null
///
private void SetVisibleButton (Button? button)
{
if (button != null)
{
button.Visible = true;
}
}
private void AddArrangeModeKeyBindings ()
{
AddCommand (Command.Quit, EndArrangeMode);
AddCommand (
Command.Up,
() =>
{
if (Parent is null)
{
return false;
}
if (Arranging == ViewArrangement.Movable)
{
Parent.Y = Parent.Y - 1;
}
if (Arranging == ViewArrangement.Resizable)
{
if (Parent.Viewport.Height > 0)
{
Parent.Height = Parent.Height! - 1;
}
}
return true;
});
AddCommand (
Command.Down,
() =>
{
if (Parent is null)
{
return false;
}
if (Arranging == ViewArrangement.Movable)
{
Parent.Y = Parent.Y + 1;
}
if (Arranging == ViewArrangement.Resizable)
{
Parent.Height = Parent.Height! + 1;
}
return true;
});
AddCommand (
Command.Left,
() =>
{
if (Parent is null)
{
return false;
}
if (Arranging == ViewArrangement.Movable)
{
Parent.X = Parent.X - 1;
}
if (Arranging == ViewArrangement.Resizable)
{
if (Parent.Viewport.Width > 0)
{
Parent.Width = Parent.Width! - 1;
}
}
return true;
});
AddCommand (
Command.Right,
() =>
{
if (Parent is null)
{
return false;
}
if (Arranging == ViewArrangement.Movable)
{
Parent.X = Parent.X + 1;
}
if (Arranging == ViewArrangement.Resizable)
{
Parent.Width = Parent.Width! + 1;
}
return true;
});
AddCommand (
Command.Tab,
() =>
{
// BUGBUG: If an arrangeable view has only arrangeable subviews, it's not possible to activate
// BUGBUG: ArrangeMode with keyboard for the superview.
// BUGBUG: AdvanceFocus should be wise to this and when in ArrangeMode, should move across
// BUGBUG: the view hierarchy.
AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
Arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
return true; // Always eat
});
AddCommand (
Command.BackTab,
() =>
{
AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
Arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed);
return true; // Always eat
});
HotKeyBindings.Add (Key.Esc, Command.Quit);
HotKeyBindings.Add (Application.ArrangeKey, Command.Quit);
HotKeyBindings.Add (Key.CursorUp, Command.Up);
HotKeyBindings.Add (Key.CursorDown, Command.Down);
HotKeyBindings.Add (Key.CursorLeft, Command.Left);
HotKeyBindings.Add (Key.CursorRight, Command.Right);
HotKeyBindings.Add (Key.Tab, Command.Tab);
HotKeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
}
private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e)
{
if (e.Flags != MouseFlags.Button1Clicked)
{
return;
}
// If mouse click is outside of Border.Thickness then exit Arrange Mode
// e.Position is screen relative
Point framePos = ScreenToFrame (e.ScreenPosition);
if (!Thickness.Contains (Frame, framePos))
{
EndArrangeMode ();
}
}
private bool? EndArrangeMode ()
{
// Debug.Assert (_arranging != ViewArrangement.Fixed);
Arranging = ViewArrangement.Fixed;
MouseState &= ~MouseState.Pressed;
Application.MouseEvent -= ApplicationOnMouseEvent;
if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue)
{
Application.Mouse.UngrabMouse ();
}
// Clean up all arrangement buttons
DisposeSizeButton (ref _moveButton);
DisposeSizeButton (ref _allSizeButton);
DisposeSizeButton (ref _leftSizeButton);
DisposeSizeButton (ref _rightSizeButton);
DisposeSizeButton (ref _topSizeButton);
DisposeSizeButton (ref _bottomSizeButton);
HotKeyBindings.Clear ();
if (CanFocus)
{
CanFocus = false;
}
return true;
}
///
/// Helper method to dispose and remove a button
///
private void DisposeSizeButton (ref Button? button)
{
if (button != null)
{
Remove (button);
button.Dispose ();
button = null;
}
}
#region Mouse Support
private Point? _dragPosition;
private Point _startGrabPoint;
///
protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
{
// BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3312
if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
{
Parent!.SetFocus ();
if (!HasAnyArrangementOptions ())
{
return false;
}
// Only start grabbing if the user clicks in the Thickness area
// 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)
{
EndArrangeMode ();
}
// Set the start grab point to the Frame coords
_startGrabPoint = new (mouseEvent.Position.X + Frame.X, mouseEvent.Position.Y + Frame.Y);
_dragPosition = mouseEvent.Position;
Application.Mouse.GrabMouse (this);
// Determine the mode based on where the click occurred
ViewArrangement arrangeMode = DetermineArrangeModeFromClick ();
EnterArrangeMode (arrangeMode);
// BUGBUG: Should we return the result of EnterArrangeMode?
return true;
}
return true;
}
if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && Application.Mouse.MouseGrabView == this)
{
if (_dragPosition.HasValue)
{
HandleDragOperation (mouseEvent);
return true;
}
}
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue)
{
_dragPosition = null;
Application.Mouse.UngrabMouse ();
EndArrangeMode ();
return true;
}
return false;
}
///
/// Determines the arrangement mode based on where the mouse was clicked
///
internal ViewArrangement DetermineArrangeModeFromClick ()
{
Rectangle sideRect;
// Check for left resizable region
if (Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable))
{
sideRect = new (Frame.X, Frame.Y + Thickness.Top, Thickness.Left, Frame.Height - Thickness.Top - Thickness.Bottom);
if (sideRect.Contains (_startGrabPoint))
{
return ViewArrangement.LeftResizable;
}
}
// Check for right resizable region
if (Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable))
{
sideRect = new (
Frame.X + Frame.Width - Thickness.Right,
Frame.Y + Thickness.Top,
Thickness.Right,
Frame.Height - Thickness.Top - Thickness.Bottom);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.RightResizable);
}
}
// Check for top resizable region (only if not movable)
if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && !Parent!.Arrangement.HasFlag (ViewArrangement.Movable))
{
sideRect = new (Frame.X + Thickness.Left, Frame.Y, Frame.Width - Thickness.Left - Thickness.Right, Thickness.Top);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.TopResizable);
}
}
// Check for bottom resizable region
if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable))
{
sideRect = new (
Frame.X + Thickness.Left,
Frame.Y + Frame.Height - Thickness.Bottom,
Frame.Width - Thickness.Left - Thickness.Right,
Thickness.Bottom);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.BottomResizable);
}
}
// Check for bottom-left corner region
if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable))
{
sideRect = new (Frame.X, Frame.Height - Thickness.Top, Thickness.Left, Thickness.Bottom);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.BottomResizable | ViewArrangement.LeftResizable);
}
}
// Check for bottom-right corner region
if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable))
{
sideRect = new (Frame.X + Frame.Width - Thickness.Right, Frame.Height - Thickness.Top, Thickness.Right, Thickness.Bottom);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.BottomResizable | ViewArrangement.RightResizable);
}
}
// Check for top-right corner region
if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable))
{
sideRect = new (Frame.X + Frame.Width - Thickness.Right, Frame.Y, Thickness.Right, Thickness.Top);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.TopResizable | ViewArrangement.RightResizable);
}
}
// Check for top-left corner region
if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable))
{
sideRect = new (Frame.X, Frame.Y, Thickness.Left, Thickness.Top);
if (sideRect.Contains (_startGrabPoint))
{
return (ViewArrangement.TopResizable | ViewArrangement.LeftResizable);
}
}
// Default to movable if enabled
if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable))
{
return ViewArrangement.Movable;
}
return ViewArrangement.Fixed;
}
///
/// Handles drag operations for moving and resizing
///
internal void HandleDragOperation (MouseEventArgs mouseEvent)
{
if (Parent!.SuperView is null)
{
// Redraw the entire app window.
Application.Top!.SetNeedsDraw ();
}
else
{
Parent.SuperView.SetNeedsDraw ();
}
_dragPosition = mouseEvent.Position;
Point parentLoc = Parent!.SuperView?.ScreenToViewport (new (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y))
?? mouseEvent.ScreenPosition;
int minHeight = Thickness.Vertical + Parent!.Margin!.Thickness.Bottom;
int minWidth = Thickness.Horizontal + Parent!.Margin!.Thickness.Right;
switch (Arranging)
{
case ViewArrangement.Movable:
Parent.X = parentLoc.X - _startGrabPoint.X;
Parent.Y = parentLoc.Y - _startGrabPoint.Y;
break;
case ViewArrangement.TopResizable:
// Get how much the mouse has moved since the start of the drag
// and adjust the height of the parent by that amount
int deltaY = parentLoc.Y - Parent.Frame.Y;
int newHeight = Math.Max (minHeight, Parent.Frame.Height - deltaY);
if (newHeight != Parent.Frame.Height)
{
Parent.Height = newHeight;
Parent.Y = parentLoc.Y - _startGrabPoint.Y;
}
break;
case ViewArrangement.BottomResizable:
Parent.Height = Math.Max (minHeight, parentLoc.Y - Parent.Frame.Y + Parent!.Margin!.Thickness.Bottom + 1);
break;
case ViewArrangement.LeftResizable:
// Get how much the mouse has moved since the start of the drag
// and adjust the width of the parent by that amount
int deltaX = parentLoc.X - Parent.Frame.X;
int newWidth = Math.Max (minWidth, Parent.Frame.Width - deltaX);
if (newWidth != Parent.Frame.Width)
{
Parent.Width = newWidth;
Parent.X = parentLoc.X - _startGrabPoint.X;
}
break;
case ViewArrangement.RightResizable:
Parent.Width = Math.Max (minWidth, parentLoc.X - Parent.Frame.X + Parent!.Margin!.Thickness.Right + 1);
break;
case ViewArrangement.BottomResizable | ViewArrangement.RightResizable:
Parent.Width = Math.Max (minWidth, parentLoc.X - Parent.Frame.X + Parent!.Margin!.Thickness.Right + 1);
Parent.Height = Math.Max (minHeight, parentLoc.Y - Parent.Frame.Y + Parent!.Margin!.Thickness.Bottom + 1);
break;
case ViewArrangement.BottomResizable | ViewArrangement.LeftResizable:
int dX = parentLoc.X - Parent.Frame.X;
int newW = Math.Max (minWidth, Parent.Frame.Width - dX);
if (newW != Parent.Frame.Width)
{
Parent.Width = newW;
Parent.X = parentLoc.X - _startGrabPoint.X;
}
Parent.Height = Math.Max (minHeight, parentLoc.Y - Parent.Frame.Y + Parent!.Margin!.Thickness.Bottom + 1);
break;
case ViewArrangement.TopResizable | ViewArrangement.RightResizable:
int dY = parentLoc.Y - Parent.Frame.Y;
int newH = Math.Max (minHeight, Parent.Frame.Height - dY);
if (newH != Parent.Frame.Height)
{
Parent.Height = newH;
Parent.Y = parentLoc.Y - _startGrabPoint.Y;
}
Parent.Width = Math.Max (minWidth, parentLoc.X - Parent.Frame.X + Parent!.Margin!.Thickness.Right + 1);
break;
case ViewArrangement.TopResizable | ViewArrangement.LeftResizable:
int dY2 = parentLoc.Y - Parent.Frame.Y;
int newH2 = Math.Max (minHeight, Parent.Frame.Height - dY2);
if (newH2 != Parent.Frame.Height)
{
Parent.Height = newH2;
Parent.Y = parentLoc.Y - _startGrabPoint.Y;
}
int dX2 = parentLoc.X - Parent.Frame.X;
int newW2 = Math.Max (minWidth, Parent.Frame.Width - dX2);
if (newW2 != Parent.Frame.Width)
{
Parent.Width = newW2;
Parent.X = parentLoc.X - _startGrabPoint.X;
}
break;
}
}
private void Application_GrabbingMouse (object? sender, GrabMouseEventArgs e)
{
if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue)
{
e.Cancel = true;
}
}
private void Application_UnGrabbingMouse (object? sender, GrabMouseEventArgs e)
{
if (Application.Mouse.MouseGrabView == this && _dragPosition.HasValue)
{
e.Cancel = true;
}
}
#endregion Mouse Support
///
protected override void Dispose (bool disposing)
{
Application.Mouse.GrabbingMouse -= Application_GrabbingMouse;
Application.Mouse.UnGrabbingMouse -= Application_UnGrabbingMouse;
_dragPosition = null;
base.Dispose (disposing);
}
}