XGUI/Window.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Linq;
namespace XGUI;
public partial class Window : XGUIPanel
{
public string Title = "Window";
public TitleBar TitleBar { get; set; }
public Vector2 Position = new Vector2( 22, 22 );
public Vector2 Size;
public Vector2 MinSize = new Vector2();
public int ZIndex;
public bool HasControls = true;
public bool HasTitleBar = true;
public bool HasMinimise = false;
public bool HasMaximise = false;
public bool HasClose = true;
public bool IsResizable = true;
public bool IsDraggable = true;
public bool AutoFocus = true;
public Button ControlsClose { get; set; } = new Button();
public Button ControlsMinimise { get; set; } = new Button();
public Button ControlsMaximise { get; set; } = new Button();
public Panel WindowContent;
public Vector2? InitalInnerSize = null;
public Window()
{
if ( HasTitleBar )
{
TitleBar = new TitleBar();
TitleBar.ParentWindow = this;
AddChild( TitleBar );
}
AddClass( "Panel" );
AddClass( "Window" );
Style.Position = PositionMode.Absolute;
Style.FlexDirection = FlexDirection.Column;
}
bool hasInitInnerSize = false;
protected override void OnAfterTreeRender( bool firstTime )
{
base.OnAfterTreeRender( firstTime );
if ( firstTime )
{
// warn if we dont have a child with class window-content
if ( Children.Where( x => x.HasClass( "window-content" ) ).FirstOrDefault() is Panel contentpnl )
{
WindowContent = contentpnl;
}
else
{
Log.Warning( $"The window {this} does not have a child with class window-content, this is standard practice as of XGUI-3" );
}
CreateTitleBar();
this.AddEventListener( "onmousedown", ResizeDown );
this.AddEventListener( "onmouseup", ResizeUp );
this.AddEventListener( "onmousemove", ResizeMove );
OverrideButtons();
if ( AutoFocus )
{
FocusWindow();
AutoFocus = false;
}
// If size is set, set the width and height
if ( Size != Vector2.Zero )
{
Style.Width = Size.x;
Style.Height = Size.y;
}
}
if ( TitleBar.IsValid )
SetChildIndex( TitleBar, 0 );
}
private void TryInitInnerSize()
{
if ( hasInitInnerSize ) return;
if ( InitalInnerSize.HasValue )
{
float currentWindowWidth = Box.Rect.Width;
float currentWindowHeight = Box.Rect.Height;
float currentWindowContentWidth = WindowContent.Box.Rect.Width;
float currentWindowContentHeight = WindowContent.Box.Rect.Height;
if ( currentWindowContentHeight == 0 && currentWindowContentWidth == 0 )
{
Log.Info( $"hi {currentWindowContentHeight}" );
return;
}
float chromeWidth = currentWindowWidth - currentWindowContentWidth;
float chromeHeight = currentWindowHeight - currentWindowContentHeight;
Log.Info( $"Window: {this} - Size: {Box.Rect} - WindowContentSize {WindowContent.Box.Rect} - Chrome: {chromeWidth}, {chromeHeight}" );
Size = new Vector2( InitalInnerSize.Value.x + chromeWidth, InitalInnerSize.Value.y + chromeHeight );
Style.Width = Size.x;
Style.Height = Size.y;
hasInitInnerSize = true;
}
}
public Panel CreateWindowContentPanel()
{
// Create a new panel to hold the window content
var contentPanel = AddChild<Panel>( "window-content" );
return contentPanel;
}
// theres no way by default to make buttons focusable so hack it in
public void OverrideButtons()
{
foreach ( Panel button in Descendants.OfType<Button>() )
{
var focusallowed = button.GetAttribute( "focus", "0" );
if ( focusallowed == "1" )
{
button.AcceptsFocus = true;
}
var autofocus = button.GetAttribute( "autofocus", "0" );
if ( autofocus == "1" )
{
button.Focus();
button.AddClass( "autofocused" );
}
}
}
Panel LastFocus;
public void FocusUpdate()
{
if ( InputFocus.Current == null || InputFocus.Current is Window ) return;
if ( InputFocus.Current != LastFocus )
{
if ( LastFocus != null )
{
LastFocus.SetClass( "focus", false );
}
LastFocus = InputFocus.Current;
LastFocus.SetClass( "focus", true );
}
}
public void CreateTitleBar()
{
if ( !HasTitleBar ) return;
/*
<style>
Window {
pointer-events:all;
position: absolute;
flex-direction:column;
.TitleBar {
.TitleIcon {
}
.TitleSpacer {
flex-grow: 1;
background-color: rgba(0,0,0,1);
}
.Control {
}
}
}
</style>
<div class="TitleBar" @ref=TitleBar>
<div class="TitleIcon" @ref=TitleIcon></div>
<div>@Title</div>
<div class="TitleSpacer" onmousedown=@DragBarDown onmouseup=@DragBarUp onmousemove=@Drag></div>
<button class="Control" @ref=ControlsClose onclick=@Close>X</button>
</div>
*/
AddChild( TitleBar );
var bg = TitleBar.AddChild<Panel>( "TitleBackground" );
//TitleBar.Style.ZIndex = 100; // Unsure why we had this, but now it causes issues, so remove it.
// The "0", "1" and "r" are for the marlett/webdings font
// Ideally i want these to be set from the theming CSS space
// but unfortunately s&box does not support the css content property
ControlsMinimise.AddEventListener( "onclick", Minimise );
ControlsMinimise.Text = "0";
ControlsMaximise.AddEventListener( "onclick", Maximise );
ControlsMaximise.Text = "1";
ControlsClose.AddEventListener( "onclick", Close );
ControlsClose.Text = "r";
}
public static event Action<Window> OnMinimised;
public static event Action<Window> OnRestored;
public bool IsMinimised = false;
Vector2 PreMinimisedSize;
Vector2 PreMinimisedPos;
public void Minimise()
{
if ( !IsMinimised )
{
PreMinimisedSize = Box.Rect.Size;
PreMinimisedPos = Position;
var offset = 0;
// offset x for other minimised windows
foreach ( Window window in Parent.Children.OfType<Window>() )
{
if ( window.IsMinimised )
{
offset += 180;
}
}
Position.x = 0 + offset;
var newheight = TitleBar.Box.Rect.Size.y + ((TitleBar.Box.Rect.Position.y - Box.Rect.Position.y) * 2);
Log.Info( newheight );
Position.y = Parent.Box.Rect.Size.y - newheight;
Style.Height = newheight;
Style.Width = 180;
IsMinimised = true;
OnMinimised?.Invoke( this );
}
else
{
IsMinimised = false;
Style.Width = PreMinimisedSize.x;
Style.Height = PreMinimisedSize.y;
Position = PreMinimisedPos;
OnRestored?.Invoke( this );
}
Log.Info( "minimise" );
}
public bool IsMaximised = false;
Vector2 PreMaximisedSize;
Vector2 PreMaximisedPos;
public void Maximise()
{
if ( !IsMaximised )
{
PreMaximisedSize = Box.Rect.Size;
PreMaximisedPos = Position;
Position = 0;
Style.Height = Parent.Box.Rect.Size.y;
Style.Width = Parent.Box.Rect.Size.x;
IsMaximised = true;
}
else
{
IsMaximised = false;
Style.Width = PreMaximisedSize.x;
Style.Height = PreMaximisedSize.y;
Position = PreMaximisedPos;
}
Log.Info( "maximise" );
}
public void Close()
{
Log.Info( "close" );
OnClose();
OnCloseAction?.Invoke();
Delete();
}
// onclose action too
public Action OnCloseAction;
public virtual void OnClose()
{
// Override this to do something when the window closes
}
public override void Tick()
{
base.Tick();
TryInitInnerSize();
// Todo - use something nicer that doesn't rely on this being named Attack1
if ( Input.Released( "Attack1" ) )
{
Dragging = false;
ResizeUp();
}
Drag();
if ( Style.Left == null )
{
Style.Left = 0;
Style.Top = 0;
}
Style.Position = PositionMode.Absolute;
Style.Left = Position.x * ScaleFromScreen;
Style.Top = Position.y * ScaleFromScreen;
Style.ZIndex = (Parent.ChildrenCount - Parent.GetChildIndex( this )) * 10;
SetClass( "minimised", this.IsMinimised );
SetClass( "maximised", this.IsMaximised );
SetClass( "unfocused", !this.HasFocus );
FocusUpdate();
}
public void FocusWindow()
{
AcceptsFocus = true;
if ( !HasFocus )
Focus();
Parent.SetChildIndex( this, 0 );
}
Vector2 MousePos()
{
if ( FindRootPanel().IsWorldPanel && Game.ActiveScene.IsValid() && Game.ActiveScene.IsValid() )
{
Ray ray = Game.ActiveScene.Camera.ScreenPixelToRay( Mouse.Position );
FindRootPanel().RayToLocalPosition( ray, out var pos, out var distance );
return pos;
}
return FindRootPanel().MousePosition;
}
Vector2 LocalMousePos()
{
return Parent.MousePosition;
}
// -------------
// Dragging
// -------------
bool Dragging = false;
float xoff = 0;
float yoff = 0;
public void Drag()
{
if ( !Dragging ) return;
var mousePos = LocalMousePos();
Position.x = ((mousePos.x) - xoff);
Position.y = ((mousePos.y) - yoff);
// Window edge to edge snapping
foreach ( Window window in Parent.Children.OfType<Window>() )
{
var snapDistance = 10;
var window1leftpos = Position.x;
var window1rightpos = Position.x + Box.Rect.Size.x;
var window1uppos = Position.y;
var window1downpos = Position.y + Box.Rect.Size.y;
var window2leftpos = window.Position.x;
var window2rightpos = window.Position.x + window.Box.Rect.Size.x;
var window2uppos = window.Position.y;
var window2downpos = window.Position.y + window.Box.Rect.Size.y;
if ( !(window1downpos < window2uppos || window1uppos > window2downpos) )
{
if ( window1leftpos.AlmostEqual( window2rightpos, snapDistance ) ) Position.x -= window1leftpos - window2rightpos;
if ( window1rightpos.AlmostEqual( window2leftpos, snapDistance ) ) Position.x += window2leftpos - window1rightpos;
}
if ( !(window1rightpos < window2leftpos || window1leftpos > window2rightpos) )
{
if ( window1uppos.AlmostEqual( window2downpos, snapDistance ) ) Position.y -= window1uppos - window2downpos;
if ( window1downpos.AlmostEqual( window2uppos, snapDistance ) ) Position.y += window2uppos - window1downpos;
}
}
}
public void DragBarDown()
{
if ( !IsDraggable ) return;
var mousePos = MousePos();
Log.Info( this.Parent.MousePosition );
xoff = (float)((mousePos.x) - Box.Rect.Left);
yoff = (float)((mousePos.y) - Box.Rect.Top);
Dragging = true;
}
public void DragBarUp()
{
Dragging = false;
}
// -------------
// -------------
// Focusing
// -------------
protected override void OnMouseDown( MousePanelEvent e )
{
FocusWindow();
//Parent.SortChildren( x => x.HasFocus ? 1 : 0 );
base.OnMouseDown( e );
}
// -------------
// Resizing
// -------------
// I feel like everything about resizing sucks.
internal bool draggingR = false;
internal bool draggingL = false;
internal bool draggingT = false;
internal bool draggingB = false;
public void ResizeDown()
{
if ( !IsResizable ) return;
// TODO FIXME: Don't resize if were dragging a window by the title bar!!
var Distance = 5;
var mousePos = MousePos();
if ( mousePos.y.AlmostEqual( this.Box.Rect.Bottom, Distance ) ) draggingB = true;
if ( mousePos.x.AlmostEqual( this.Box.Rect.Right, Distance ) ) draggingR = true;
if ( mousePos.y.AlmostEqual( this.Box.Rect.Top, Distance ) ) draggingT = true;
if ( mousePos.x.AlmostEqual( this.Box.Rect.Left, Distance ) ) draggingL = true;
//draggingT = true;
//draggingL = true;
xoff1 = (float)((mousePos.x) - this.Box.Rect.Right);
yoff1 = (float)((mousePos.y) - this.Box.Rect.Bottom);
xoff2 = (float)((mousePos.x) - this.Box.Rect.Left);
yoff2 = (float)((mousePos.y) - this.Box.Rect.Top);
}
public void ResizeUp()
{
draggingB = false;
draggingR = false;
draggingT = false;
draggingL = false;
}
internal float xoff1 = 0;
internal float yoff1 = 0;
internal float xoff2 = 0;
internal float yoff2 = 0;
public void ResizeMove()
{
var mousePos = MousePos();
var mousePosLocal = LocalMousePos();
if ( IsResizable )
{
var Distance = 5;
var almostbottom = mousePos.y.AlmostEqual( this.Box.Rect.Bottom, Distance );
var almostright = mousePos.x.AlmostEqual( this.Box.Rect.Right, Distance );
var almosttop = mousePos.y.AlmostEqual( this.Box.Rect.Top, Distance );
var almostleft = mousePos.x.AlmostEqual( this.Box.Rect.Left, Distance );
if ( (almostleft && almostbottom) || (draggingL && draggingB) ) Style.Cursor = "nesw-resize";
else if ( (almostright && almosttop) || (draggingR && draggingT) ) Style.Cursor = "nesw-resize";
else if ( (almostright && almostbottom) || (draggingR && draggingB) ) Style.Cursor = "nwse-resize";
else if ( (almostleft && almosttop) || (draggingL && draggingT) ) Style.Cursor = "nwse-resize";
else if ( almostbottom || draggingB ) Style.Cursor = "ns-resize";
else if ( almostright || draggingR ) Style.Cursor = "ew-resize";
else if ( almosttop || draggingT ) Style.Cursor = "ns-resize";
else if ( almostleft || draggingL ) Style.Cursor = "ew-resize";
else Style.Cursor = "unset";
}
/*if ( Mouse )
{
ResizeUp( e );
}*/
// This sucks.
if ( draggingB )
{
//Parent.Style.Width = (FindRootPanel().MousePosition.x - Parent.Box.Rect.Left) - xoff;
var newheight = (mousePos.y - Box.Rect.Top) - yoff1;
if ( newheight > MinSize.y )
{
Style.Height = newheight;
}
}
if ( draggingR )
{
var newwidth = (mousePos.x - Box.Rect.Left) - xoff1;
if ( newwidth > MinSize.x )
{
Style.Width = newwidth;
}
//Parent.Style.Height = (FindRootPanel().MousePosition.y - Parent.Box.Rect.Top) - yoff;
}
if ( draggingT )
{
var newheight = Box.Rect.Height - ((mousePos.y - yoff2) - Box.Rect.Top);
if ( newheight > MinSize.y )
{
Style.Height = newheight;
Position.y = mousePosLocal.y - yoff2;
}
}
if ( draggingL )
{
var newwidth = Box.Rect.Width - ((mousePos.x - xoff2) - Box.Rect.Left);
if ( newwidth > MinSize.x )
{
Style.Width = newwidth;
Position.x = mousePosLocal.x - xoff2;
}
}
}
// -------------
public override void SetProperty( string name, string value )
{
switch ( name )
{
case "title":
{
Title = value;
return;
}
case "hastitlebar":
{
HasTitleBar = bool.Parse( value );
if ( !HasTitleBar )
{
TitleBar.Delete();
}
this.SetClass( "notitlebar", !HasTitleBar );
return;
}
case "hasminimise":
{
HasMinimise = bool.Parse( value );
return;
}
case "hasmaximise":
{
HasMaximise = bool.Parse( value );
return;
}
case "hasclose":
{
HasClose = bool.Parse( value );
return;
}
case "isresizable":
{
IsResizable = bool.Parse( value );
return;
}
case "isdraggable":
{
IsDraggable = bool.Parse( value );
return;
}
case "autofocus":
{
AutoFocus = bool.Parse( value );
return;
}
case "width":
{
Style.Width = Length.Parse( value );
return;
}
case "height":
{
Style.Height = Length.Parse( value );
return;
}
case "x":
{
Position.x = Length.Parse( value ).Value.Value;
return;
}
case "y":
{
Position.y = Length.Parse( value ).Value.Value;
return;
}
case "minwidth":
{
MinSize.x = Length.Parse( value ).Value.Value;
return;
}
case "minheight":
{
MinSize.y = Length.Parse( value ).Value.Value;
return;
}
default:
{
base.SetProperty( name, value );
return;
}
}
}
}