Port floating windows and Message Dialogs

This commit is contained in:
Ritchie Frodomar 2024-07-06 16:16:08 -04:00
parent 81ce9df56a
commit e91de52dc0
27 changed files with 1150 additions and 18 deletions

View file

@ -14,6 +14,7 @@ public struct LayoutRect
public Vector2 TopLeft => new Vector2(Left, Top);
public Vector2 Size => new Vector2(Width, Height);
public Vector2 Center => TopLeft + (Size / 2);
public LayoutRect(float left, float top, float width, float height)
{

View file

@ -59,6 +59,33 @@ public class GeometryHelper : IFontStashRenderer2
AddRoundedRectangle(rectangle, uniformRadius, uniformRadius, uniformRadius, uniformRadius, color, texture);
}
public void AddQuadOutline(LayoutRect rectangle, float thickness, Color color, Texture2D? texture = null)
{
float smallerHalf = Math.Min(rectangle.Width, rectangle.Height) / 2;
if (smallerHalf <= thickness)
{
AddQuad(rectangle, color, texture);
return;
}
var mesh = GetMeshBuilder(texture);
var tl = mesh.AddVertex(new Vector2(rectangle.Left, rectangle.Top), color);
var tr = mesh.AddVertex(new Vector2(rectangle.Right, rectangle.Top), color);
var bl = mesh.AddVertex(new Vector2(rectangle.Left, rectangle.Bottom), color);
var br = mesh.AddVertex(new Vector2(rectangle.Right, rectangle.Bottom), color);
var tlInner = mesh.AddVertex(new Vector2(rectangle.Left + thickness, rectangle.Top + thickness), color);
var trInner = mesh.AddVertex(new Vector2(rectangle.Right - thickness, rectangle.Top + thickness), color);
var blInner = mesh.AddVertex(new Vector2(rectangle.Left + thickness, rectangle.Bottom - thickness), color);
var brInner = mesh.AddVertex(new Vector2(rectangle.Right - thickness, rectangle.Bottom - thickness), color);
mesh.AddQuad(tl, tr, tlInner, trInner);
mesh.AddQuad(tl, tlInner, bl, blInner);
mesh.AddQuad(trInner, tr, brInner, br);
mesh.AddQuad(blInner, brInner, bl, br);
}
public void AddQuad(LayoutRect rectangle, Color color, Texture2D? texture = null)
{
var mesh = GetMeshBuilder(texture);

View file

@ -7,9 +7,12 @@ namespace AcidicGUI.Widgets;
public sealed class Button :
ContentWidget,
IMouseEnterHandler,
IMouseClickHandler,
IMouseLeaveHandler
{
private bool hovered;
public event Action? Clicked;
public void OnMouseEnter(MouseMoveEvent e)
{
@ -30,4 +33,13 @@ public sealed class Button :
geometry.AddQuad(ContentArea, Color.Red);
}
}
public void OnMouseClick(MouseButtonEvent e)
{
if (e.Button != MouseButton.Left)
return;
e.Handle();
Clicked?.Invoke();
}
}

View file

@ -6,7 +6,7 @@ namespace AcidicGUI.Widgets;
public sealed class Icon : Widget
{
private Color color = Color.White;
private Color? color;
private string iconString = string.Empty;
private int iconSize = 24;
@ -30,7 +30,7 @@ public sealed class Icon : Widget
}
}
public Color Color
public Color? Color
{
get => color;
set
@ -59,6 +59,6 @@ public sealed class Icon : Widget
if (font == null)
return;
font.Draw(geometry, new Vector2(x, y), color, iconString, iconSize);
font.Draw(geometry, new Vector2(x, y), color ?? Microsoft.Xna.Framework.Color.White, iconString, iconSize);
}
}

View file

@ -0,0 +1,6 @@
namespace AcidicGUI.Widgets;
public sealed class OverlayWidget : ContainerWidget
{
}

View file

@ -124,9 +124,15 @@ public partial class Widget
if (item.Parent != this.parent)
throw new InvalidOperationException("The specified widget is not a member of this parent.");
item.parent = null;
this.parent.InvalidateLayout();
return children.Remove(item);
bool wasSuccess = children.Remove(item);
if (wasSuccess)
{
item.parent = null;
this.parent.InvalidateLayout();
}
return wasSuccess;
}
public int Count => children.Count;

View file

@ -201,6 +201,9 @@ public partial class Widget
contentSize.X += margin.Horizontal;
contentSize.Y += margin.Vertical;
contentSize.X += padding.Horizontal;
contentSize.Y += padding.Vertical;
if (maximumSize.X > 0)
{

View file

@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace SociallyDistant.Core.Shell.Common
@ -9,7 +10,7 @@ namespace SociallyDistant.Core.Shell.Common
{
public string textIcon;
public Texture2D? spriteIcon;
public ShellColor iconColor;
public Color? iconColor;
public static implicit operator CompositeIcon(string unicodeTextIcon)
{
@ -17,7 +18,7 @@ namespace SociallyDistant.Core.Shell.Common
{
textIcon = unicodeTextIcon,
spriteIcon = null,
iconColor = new ShellColor(1, 1, 1, 1)
iconColor = null
};
}
}

View file

@ -2,6 +2,7 @@
using SociallyDistant.Core.Shell.Common;
using SociallyDistant.Core.Shell.InfoPanel;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.Core.Shell
{
@ -15,5 +16,7 @@ namespace SociallyDistant.Core.Shell
IInfoPanelService InfoPanelService { get; }
Task ShowInfoDialog(string title, string message);
IMessageDialog CreateMessageDialog(string title);
}
}

View file

@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
namespace SociallyDistant.Core.Shell.Windowing
{
[Obsolete("IFloatingGui is being replaced with IWindowWithClient<T>.")]
public interface IFloatingGui : IWindow, IContentHolder
{
Vector2 MinimumSize { get; set; }

View file

@ -3,12 +3,18 @@ using SociallyDistant.Core.Shell.Common;
namespace SociallyDistant.Core.Shell.Windowing
{
public interface IWindowTab
{
bool Active { get; }
string Title { get; }
}
public interface IWindow : ICloseable
{
public event Action<IWindow>? WindowClosed;
WindowHints Hints { get; }
IWorkspaceDefinition? Workspace { get; set; }
IWorkspaceDefinition Workspace { get; }
CompositeIcon Icon { get; set; }
bool IsActive { get; }

View file

@ -5,6 +5,5 @@
IReadOnlyList < IWindow > WindowList { get; }
IFloatingGui CreateFloatingGui(string title);
IMessageDialog CreateMessageDialog(string title);
}
}

View file

@ -0,0 +1,30 @@
using Microsoft.Xna.Framework;
using AcidicGUI.Widgets;
namespace SociallyDistant.Core.UI.Common;
public sealed class DecorativeBlock : Widget
{
private Color? color;
private bool opaque;
public bool Opaque
{
get => opaque;
set
{
opaque = value;
InvalidateGeometry();
}
}
public Color? BoxColor
{
get => color;
set
{
color = value;
InvalidateGeometry();
}
}
}

View file

@ -0,0 +1,36 @@
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
namespace SociallyDistant.Core.UI.Common;
public class TextButton : Widget
{
private readonly Button button = new();
private readonly TextWidget textWidget = new();
public Action? ClickCallback { get; set; }
public string Text
{
get => textWidget.Text;
set => textWidget.Text = value;
}
public TextButton()
{
Children.Add(button);
button.Content = textWidget;
button.Margin = new Padding(15, 0);
textWidget.HorizontalAlignment = HorizontalAlignment.Center;
textWidget.VerticalAlignment = VerticalAlignment.Middle;
button.Clicked += HandleClicked;
}
private void HandleClicked()
{
ClickCallback?.Invoke();
}
}

View file

@ -6,6 +6,8 @@ using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SociallyDistant.Core.Modules;
using SociallyDistant.Core.Shell.Windowing;
using SociallyDistant.Core.UI.Common;
namespace SociallyDistant.Core.UI.VisualStyles;
@ -14,7 +16,16 @@ public class SociallyDistantVisualStyle : IVisualStyle
private readonly IGameContext game;
private readonly Color mainBackground = new Color(0x11, 0x13, 0x15);
private readonly Color statusBarColor = new Color(0x01,0x22, 0x37, 0xff);
private readonly Color accentPrimary = new Color(0x13, 0x85, 0xC3, 0xff);
// Tabs - Inactive
private readonly Color tabInactiveBackgroundDefault = new Color(0x44, 0x44, 0x44, 191);
private readonly Color tabInactiveBorderDefault = new Color(0x44, 0x44, 0x44);
// Tabs - Active
private readonly Color tabActiveBackgroundDefault = new Color(0x16, 0x93, 0xD6, 190);
private readonly Color tabActiveBorderDefault = new Color(0x16, 0x93, 0xD6);
private Font iconFont;
private Font defaultFont = null!;
@ -79,11 +90,78 @@ public class SociallyDistantVisualStyle : IVisualStyle
}
}
}
private void DrawWindowTab(Widget widget, IWindowTab tab, GeometryHelper geometry)
{
var thickness = 1;
var background = tabInactiveBackgroundDefault;
var border = tabInactiveBorderDefault;
if (tab.Active)
{
background = tabActiveBackgroundDefault;
border = tabActiveBorderDefault;
}
geometry.AddRoundedRectangle(
widget.ContentArea,
3,
3,
0,
0,
background
);
geometry.AddRoundedRectangleOutline(
widget.ContentArea,
thickness,
3,
3,
0,
0,
border
);
}
private void DrawDecorativeBlock(DecorativeBlock widget, GeometryHelper geometry)
{
var color = (widget.BoxColor ?? accentPrimary);
if (widget.Opaque)
{
geometry.AddRoundedRectangle(
widget.ContentArea,
3,
color
);
}
else
{
geometry.AddRoundedRectangle(
widget.ContentArea,
3,
color * 0.5f
);
geometry.AddRoundedRectangleOutline(
widget.ContentArea,
2,
3,
color
);
}
}
public void DrawWidgetBackground(Widget widget, GeometryHelper geometryHelper)
{
if (widget is InputField inputField)
DrawInputField(inputField, geometryHelper);
else if (widget is IWindowTab tab)
DrawWindowTab(widget, tab, geometryHelper);
else if (widget is DecorativeBlock box)
DrawDecorativeBlock(box, geometryHelper);
else
{
WidgetBackgrounds background = widget.GetCustomProperty<WidgetBackgrounds>();
@ -93,9 +171,18 @@ public class SociallyDistantVisualStyle : IVisualStyle
case WidgetBackgrounds.StatusBar:
DrawStatusBar(widget, geometryHelper);
break;
case WidgetBackgrounds.Overlay:
geometryHelper.AddQuad(widget.ContentArea, mainBackground * 0.67f);
break;
case WidgetBackgrounds.Dock:
DrawDock(widget, geometryHelper);
break;
case WidgetBackgrounds.WindowClient:
geometryHelper.AddQuad(widget.ContentArea, mainBackground);
break;
case WidgetBackgrounds.WindowBorder:
geometryHelper.AddQuadOutline(widget.ContentArea, 1, accentPrimary);
break;
}
}
}
@ -139,6 +226,9 @@ public enum InputFieldStyle
public enum WidgetBackgrounds
{
None,
Overlay,
StatusBar,
Dock
Dock,
WindowClient,
WindowBorder,
}

View file

@ -0,0 +1,55 @@
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using SociallyDistant.Core.Shell.Common;
namespace SociallyDistant.UI.Common;
public class CompositeIconWidget : Widget
{
private readonly Image image = new();
private readonly Icon textIcon = new();
private CompositeIcon compositeIcon;
private int iconSize = 24;
public int IconSize
{
get => iconSize;
set
{
iconSize = value;
UpdateIconSize();
}
}
public CompositeIcon Icon
{
get => compositeIcon;
set
{
compositeIcon = value;
UpdateIcon();
}
}
private void UpdateIconSize()
{
image.MaximumSize = new Vector2(iconSize, iconSize);
image.MinimumSize = image.MaximumSize;
textIcon.IconSize = iconSize;
}
public CompositeIconWidget()
{
Children.Add(image);
Children.Add(textIcon);
}
private void UpdateIcon()
{
image.Texture = compositeIcon.spriteIcon;
textIcon.IconString = compositeIcon.textIcon;
textIcon.Color = compositeIcon.iconColor;
}
}

View file

@ -0,0 +1,46 @@
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using SociallyDistant.Core.UI.Common;
namespace SociallyDistant.UI.Common;
public sealed class InfoBox : Widget
{
private readonly StackPanel root = new();
private readonly DecorativeBlock decorativeBlock = new();
private readonly StackPanel contentStack = new();
private readonly TextWidget title = new();
private readonly Box contentBox = new();
public string TitleText
{
get => title.Text;
set => title.Text = value;
}
public Widget? Content
{
get => contentBox.Content;
set => contentBox.Content = value;
}
public InfoBox()
{
Children.Add(root);
root.ChildWidgets.Add(decorativeBlock);
root.ChildWidgets.Add(contentStack);
contentStack.ChildWidgets.Add(title);
contentStack.ChildWidgets.Add(contentBox);
root.Padding = 3;
root.Spacing = 3;
root.Direction = Direction.Horizontal;
contentStack.Spacing = 3;
contentStack.Padding = new Padding(0, 3);
contentStack.Direction = Direction.Vertical;
decorativeBlock.MinimumSize = new Vector2(18, 0);
}
}

View file

@ -0,0 +1,219 @@
using System.Collections;
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell;
using SociallyDistant.Core.Shell.Common;
using SociallyDistant.Core.Shell.Windowing;
using SociallyDistant.Core.UI.Common;
using SociallyDistant.UI.Windowing;
namespace SociallyDistant.UI.Common;
public sealed class MessageDialog : IMessageDialog
{
private readonly Window window;
private readonly InfoBox infoBox = new();
private readonly StackPanel contentStack = new();
private readonly TextWidget message = new();
private readonly WrapPanel buttonsPanel = new();
private readonly ButtonList buttonList;
private readonly List<TextButton> views = new();
private MessageDialogResult dismissResult = MessageDialogResult.Cancel;
public MessageDialog(Window window)
{
buttonList = new ButtonList(this);
this.window = window;
this.window.SetClient(infoBox);
infoBox.Content = contentStack;
contentStack.ChildWidgets.Add(message);
contentStack.ChildWidgets.Add(buttonsPanel);
contentStack.Spacing = 6;
buttonsPanel.SpacingX = 3;
buttonsPanel.SpacingY = 3;
buttonsPanel.Direction = Direction.Horizontal;
}
public bool CanClose
{
get => window.CanClose;
set => window.CanClose = value;
}
public void Close()
{
DismissCallback?.Invoke(dismissResult);
window.ForceClose();
}
public void ForceClose()
{
Close();
}
public event Action<IWindow>? WindowClosed
{
add
{
window.WindowClosed += value;
}
remove
{
window.WindowClosed -= value;
}
}
public WindowHints Hints => window.Hints;
public IWorkspaceDefinition Workspace => window.Workspace;
public CompositeIcon Icon
{
get => window.Icon;
set => window.Icon = value;
}
public bool IsActive => window.IsActive;
public IWorkspaceDefinition CreateWindowOverlay()
{
return window.CreateWindowOverlay();
}
public void SetWindowHints(WindowHints hints)
{
window.SetWindowHints(hints);
}
public string Title
{
get => infoBox.TitleText;
set => infoBox.TitleText = value;
}
public string Message
{
get => message.Text;
set => message.Text = value;
}
public CommonColor Color { get; set; }
public MessageBoxType MessageType { get; set; }
public IList<MessageBoxButtonData> Buttons => buttonList;
public Action<MessageDialogResult>? DismissCallback { get; set; }
private void Dismiss(MessageDialogResult result)
{
dismissResult = result;
Close();
}
private void RefreshButtons()
{
while (views.Count > buttonList.Count)
{
buttonsPanel.ChildWidgets.Remove(views[^1]);
views.RemoveAt(views.Count-1);
}
while (views.Count < buttonList.Count)
{
var button = new TextButton();
views.Add(button);
buttonsPanel.ChildWidgets.Add(button);
}
for (var i = 0; i < buttonList.Count; i++)
{
var button = buttonList[i];
var view = views[i];
view.Text = button.Text;
view.ClickCallback = () =>
{
Dismiss(button.Result);
};
}
}
private class ButtonList : IList<MessageBoxButtonData>
{
private readonly MessageDialog dialog;
private readonly List<MessageBoxButtonData> data = new();
public ButtonList(MessageDialog dialog)
{
this.dialog = dialog;
}
public IEnumerator<MessageBoxButtonData> GetEnumerator()
{
return data.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(MessageBoxButtonData item)
{
data.Add(item);
dialog.RefreshButtons();
}
public void Clear()
{
data.Clear();
dialog.RefreshButtons();
}
public bool Contains(MessageBoxButtonData item)
{
return data.Contains(item);
}
public void CopyTo(MessageBoxButtonData[] array, int arrayIndex)
{
data.CopyTo(array, arrayIndex);
}
public bool Remove(MessageBoxButtonData item)
{
bool result = data.Remove(item);
if (result)
dialog.RefreshButtons();
return result;
}
public int Count => data.Count;
public bool IsReadOnly => false;
public int IndexOf(MessageBoxButtonData item)
{
return data.IndexOf(item);
}
public void Insert(int index, MessageBoxButtonData item)
{
data.Insert(index, item);
dialog.RefreshButtons();
}
public void RemoveAt(int index)
{
data.RemoveAt(index);
dialog.RefreshButtons();
}
public MessageBoxButtonData this[int index]
{
get => data[index];
set
{
data[index] = value;
dialog.RefreshButtons();
}
}
}
}

View file

@ -1,19 +1,23 @@
using System.Text;
using AcidicGUI.CustomProperties;
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Serilog;
using SociallyDistant.Core.Core;
using SociallyDistant.Core.Modules;
using SociallyDistant.Core.OS.Devices;
using SociallyDistant.Core.Shell;
using SociallyDistant.Core.Shell.Common;
using SociallyDistant.Core.Shell.InfoPanel;
using SociallyDistant.Core.Shell.Windowing;
using SociallyDistant.Core.UI;
using SociallyDistant.Player;
using SociallyDistant.UI.Common;
using SociallyDistant.UI.InfoWidgets;
using SociallyDistant.UI.Shell;
using SociallyDistant.UI.Windowing;
namespace SociallyDistant.UI;
@ -25,7 +29,9 @@ public class GuiController :
private readonly InfoPanelController infoPanel = new();
private readonly FlexPanel mainPanel = new();
private readonly StatusBar statusBar = new();
private readonly OverlayWidget workArea = new();
private readonly Box mainBox = new();
private readonly FloatingWorkspace floatingWindowArea = new();
private readonly GuiService guiService;
private readonly PlayerManager playerManager;
private readonly DesktopController desktopController;
@ -48,14 +54,37 @@ public class GuiController :
guiService.GuiRoot.TopLevels.Add(mainPanel);
mainPanel.ChildWidgets.Add(statusBar);
mainPanel.ChildWidgets.Add(mainBox);
mainPanel.ChildWidgets.Add(workArea);
mainBox.GetCustomProperties<FlexPanelProperties>().Mode = FlexMode.Proportional;
workArea.ChildWidgets.Add(mainBox);
workArea.ChildWidgets.Add(floatingWindowArea);
workArea.GetCustomProperties<FlexPanelProperties>().Mode = FlexMode.Proportional;
}
public async Task ShowExceptionMessage(Exception ex)
public Task ShowExceptionMessage(Exception ex)
{
// TODO
var dialog = CreateMessageDialog("System Error");
var completionSource = new TaskCompletionSource<MessageDialogResult>();
var messageBuilder = new StringBuilder();
messageBuilder.AppendLine("Socially Distant has encountered an unexpected problem.");
messageBuilder.AppendLine();
messageBuilder.AppendLine("<b>What happened?</b>");
messageBuilder.AppendLine(
$"An unhandled .NET runtime exception ({ex.GetType().FullName}) occurred. {ex.Message}");
messageBuilder.AppendLine();
messageBuilder.AppendLine(
"This generally happens as a result of a broken mod or a bug in the game. Details have been logged to the game's log.");
Log.Error(ex.ToString());
dialog.Message = messageBuilder.ToString();
dialog.Buttons.Add("OK");
return completionSource.Task;
}
private void OnGameModeChanged(GameMode gameMode)
@ -64,7 +93,7 @@ public class GuiController :
{
this.desktop = new Desktop(desktopController);
this.mainBox.Content = desktop;
desktopController.Login();
}
else
@ -76,9 +105,41 @@ public class GuiController :
public INotificationManager NotificationManager => notificationManager;
public IInfoPanelService InfoPanelService => infoPanel;
public async Task ShowInfoDialog(string title, string message)
public Task ShowInfoDialog(string title, string message)
{
// TODO
var completionSource = new TaskCompletionSource<MessageDialogResult>();
var dialog = CreateMessageDialog(title);
dialog.Message = message;
dialog.DismissCallback = completionSource.SetResult;
dialog.Buttons.Add("OK");
return completionSource.Task;
}
private OverlayWorkspace CreateOverlayWorkspace()
{
var workspace = new OverlayWorkspace();
this.workArea.ChildWidgets.Add(workspace);
return workspace;
}
public IMessageDialog CreateMessageDialog(string title)
{
var overlay = CreateOverlayWorkspace();
var window = overlay.CreateWindow(title);
var dialog = new MessageDialog(window);
dialog.Title = title;
dialog.WindowClosed += HandleDialogClosed;
return dialog;
void HandleDialogClosed(IWindow obj)
{
workArea.ChildWidgets.Remove(overlay);
}
}
}

View file

@ -0,0 +1,138 @@
using AcidicGUI;
using AcidicGUI.CustomProperties;
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using SociallyDistant.Core.Shell.Windowing;
using SociallyDistant.Core.UI.VisualStyles;
namespace SociallyDistant.UI.Windowing;
public class OverlayWorkspace :
Widget,
IClientWorkspaceDefinition<Window, Widget?>
{
private readonly FloatingWorkspace workspace = new();
public OverlayWorkspace()
{
Children.Add(workspace);
this.SetCustomProperty(WidgetBackgrounds.Overlay);
}
public Window CreateWindow(string title, Widget? client = default)
{
return workspace.CreateWindow(title, client);
}
public IReadOnlyList<IWindow> WindowList => workspace.WindowList;
public IFloatingGui CreateFloatingGui(string title)
{
throw new NotImplementedException();
}
}
public class FloatingWorkspace :
Widget,
IClientWorkspaceDefinition<Window, Widget?>,
INotifyCloseWorkspace
{
private readonly List<WindowBase> windows = new();
public FloatingWorkspace()
{
LayoutRoot = this;
}
public Window CreateWindow(string title, Widget? client = default)
{
var win = new Window(this);
win.SetClient(client);
this.Children.Add(win);
windows.Add(win);
return win;
}
public void OnCloseWindow(WindowBase window)
{
windows.Remove(window);
Children.Remove(window);
}
public IReadOnlyList<IWindow> WindowList => windows;
public IFloatingGui CreateFloatingGui(string title)
{
throw new NotImplementedException();
}
public IMessageDialog CreateMessageDialog(string title)
{
throw new NotImplementedException();
}
protected override Vector2 GetContentSize(Vector2 availableSize)
{
return availableSize;
}
protected override void ArrangeChildren(IGuiContext context, LayoutRect availableSpace)
{
foreach (Widget win in Children)
{
var childSize = win.GetCachedContentSize(availableSpace.Size);
var windowSettings = win.GetCustomProperties<WindowSettings>();
if (windowSettings.Maximized)
{
win.UpdateLayout(context, availableSpace);
}
else
{
var positionInWorkspace = availableSpace.Center + windowSettings.Position;
var layoutPosition = positionInWorkspace - (childSize / 2);
win.UpdateLayout(context, new LayoutRect(
layoutPosition.X,
layoutPosition.Y,
childSize.X,
childSize.Y
));
}
}
}
}
public sealed class WindowSettings : CustomPropertyObject
{
private Vector2 position;
private bool maximized;
public bool Maximized
{
get => maximized;
set
{
maximized = value;
Widget.InvalidateLayout();
}
}
public Vector2 Position
{
get => position;
set
{
position = value;
Widget.InvalidateLayout();
}
}
public WindowSettings(Widget owner) : base(owner)
{
}
}

View file

@ -0,0 +1,9 @@
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.UI.Windowing;
internal interface INotifyCloseWorkspace : IWorkspaceDefinition
{
void OnCloseWindow(WindowBase window);
}

View file

@ -0,0 +1,23 @@
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.UI.Windowing;
public sealed class Window :
WindowBase,
IWindowWithClient<Widget?>
{
private readonly ITabDefinition tab;
internal Window(INotifyCloseWorkspace workspace) : base(workspace)
{
tab = Tabs.CreateTab();
tab.Active = true;
}
public Widget? Client => Decorations.Client;
public void SetClient(Widget? newClient)
{
Decorations.Client = newClient;
}
}

View file

@ -0,0 +1,58 @@
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell.Common;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.UI.Windowing;
public abstract class WindowBase :
Widget,
IWindow
{
private readonly INotifyCloseWorkspace workspace;
private readonly WindowDecoration decoration;
public bool CanClose { get; set; }
protected WindowDecoration Decorations => decoration;
protected WindowTabList Tabs => decoration.Tabs;
internal WindowBase(INotifyCloseWorkspace workspace)
{
decoration = new WindowDecoration(this);
this.workspace = workspace;
Children.Add(decoration);
}
public void Close()
{
throw new NotImplementedException();
}
public void ForceClose()
{
workspace.OnCloseWindow(this);
WindowClosed?.Invoke(this);
}
public CompositeIcon Icon
{
get => decoration.Icon;
set => decoration.Icon = value;
}
public event Action<IWindow>? WindowClosed;
public WindowHints Hints => decoration.Hints;
public IWorkspaceDefinition Workspace => workspace;
public bool IsActive => IsChildFocused;
public IWorkspaceDefinition CreateWindowOverlay()
{
throw new NotImplementedException();
}
public void SetWindowHints(WindowHints hints)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,87 @@
using AcidicGUI.CustomProperties;
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using SociallyDistant.Core.Shell;
using SociallyDistant.Core.Shell.Common;
using SociallyDistant.Core.Shell.Windowing;
using SociallyDistant.Core.UI.VisualStyles;
using SociallyDistant.UI.Common;
namespace SociallyDistant.UI.Windowing;
public class WindowDecoration : Widget
{
private readonly FlexPanel flexPanel = new();
private readonly FlexPanel titleBar = new();
private readonly Box borderBox = new();
private readonly Box clientBox = new();
private readonly WindowBase window;
private readonly WindowDragSurface dragSurface;
private readonly CompositeIconWidget titleIcon = new();
private readonly WindowTabList tabList = new();
private WindowHints hints;
public WindowHints Hints => hints;
public CompositeIcon Icon
{
get => titleIcon.Icon;
set => titleIcon.Icon = value;
}
public Widget? Client
{
get => clientBox.Content;
set => clientBox.Content = value;
}
public WindowTabList Tabs => tabList;
public WindowDecoration(WindowBase window)
{
this.window = window;
this.dragSurface = new WindowDragSurface((window));
Children.Add(flexPanel);
flexPanel.ChildWidgets.Add(dragSurface);
flexPanel.ChildWidgets.Add(borderBox);
dragSurface.Content = titleBar;
titleBar.Direction = Direction.Horizontal;
titleBar.Padding = new Padding(3, 0);
titleBar.Spacing = 3;
titleBar.ChildWidgets.Add(titleIcon);
titleBar.ChildWidgets.Add(tabList);
tabList.GetCustomProperties<FlexPanelProperties>().Mode = FlexMode.Proportional;
borderBox.Margin = 1;
borderBox.Content = clientBox;
titleIcon.VerticalAlignment = VerticalAlignment.Bottom;
titleIcon.Padding = new Padding(0, 3);
borderBox.SetCustomProperty(WidgetBackgrounds.WindowBorder);
clientBox.SetCustomProperty(WidgetBackgrounds.WindowClient);
clientBox.GetCustomProperties<FlexPanelProperties>().Mode = FlexMode.Proportional;
tabList.VerticalAlignment = VerticalAlignment.Bottom;
Icon = new CompositeIcon()
{
textIcon = MaterialIcons.Window
};
}
}
public interface ITabDefinition
{
string Title { get; set; }
bool Closeable { get; set; }
bool Active { get; set; }
}

View file

@ -0,0 +1,57 @@
using AcidicGUI.Events;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
namespace SociallyDistant.UI.Windowing;
public sealed class WindowDragSurface :
ContentWidget,
IDragStartHandler,
IDragHandler,
IDragEndHandler
{
private readonly WindowBase window;
private bool dragging;
public WindowDragSurface(WindowBase window)
{
MinimumSize = new Vector2(0, 24);
this.window = window;
}
public void OnDragStart(MouseButtonEvent e)
{
if (e.Button != MouseButton.Left)
return;
e.RequestFocus();
dragging = true;
}
public void OnDrag(MouseButtonEvent e)
{
if (!dragging)
return;
var settings = window.GetCustomProperties<WindowSettings>();
if (settings.Maximized)
return;
settings.Position += e.Movement;
e.RequestFocus();
}
public void OnDragEnd(MouseButtonEvent e)
{
if (!dragging)
return;
if (e.Button != MouseButton.Left)
return;
dragging = false;
}
}

View file

@ -0,0 +1,49 @@
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.UI.Windowing;
public sealed class WindowTab :
Widget,
IWindowTab
{
private readonly StackPanel stackPanel = new();
private readonly TextWidget titleText = new();
private readonly Button closeButton = new();
private readonly Icon closeIcon = new();
private bool active = false;
public string Title
{
get => titleText.Text;
set => titleText.Text = value;
}
public bool Active
{
get => active;
set
{
active = value;
InvalidateGeometry();
}
}
public WindowTab()
{
Children.Add(stackPanel);
stackPanel.ChildWidgets.Add(titleText);
stackPanel.ChildWidgets.Add(closeButton);
closeButton.Content = closeIcon;
closeIcon.IconSize = 18;
closeIcon.IconString = MaterialIcons.Close;
stackPanel.Direction = Direction.Horizontal;
stackPanel.Spacing = 3;
stackPanel.Margin = 3;
}
}

View file

@ -0,0 +1,109 @@
using AcidicGUI.Layout;
using AcidicGUI.Widgets;
using SociallyDistant.Core.Shell.Windowing;
namespace SociallyDistant.UI.Windowing;
public sealed class WindowTabList : Widget
{
private readonly WrapPanel wrapPanel = new();
private readonly List<WindowTab> views = new();
private readonly List<Definition> definitions = new();
private bool showNewTab = false;
public WindowTabList()
{
Children.Add(wrapPanel);
wrapPanel.Direction = Direction.Horizontal;
}
public ITabDefinition CreateTab()
{
var definition = new Definition(this);
definitions.Add(definition);
UpdateViews();
return definition;
}
public void RemoveTab(ITabDefinition definition)
{
if (definition is not Definition actualDefinition)
return;
definitions.Remove(actualDefinition);
UpdateViews();
}
private void UpdateViews()
{
while (views.Count > definitions.Count)
{
wrapPanel.ChildWidgets.Remove(views[^1]);
views.RemoveAt(views.Count-1);
}
while (views.Count < definitions.Count)
{
var view = new WindowTab();
views.Add(view);
wrapPanel.ChildWidgets.Add(view);
}
for (var i = 0; i < definitions.Count; i++)
{
Definition definition = definitions[i];
WindowTab view = views[i];
view.Title = definition.Title;
view.Active = definition.Active;
}
}
internal class Definition : ITabDefinition
{
private readonly WindowTabList list;
private string title = "Tab title";
private bool closeable;
private bool active;
public Definition(WindowTabList list)
{
this.list = list;
}
public string Title
{
get => title;
set
{
title = value;
list.UpdateViews();
}
}
public bool Closeable
{
get => closeable;
set
{
closeable = value;
list.UpdateViews();
}
}
public bool Active
{
get => active;
set
{
active = value;
list.UpdateViews();
}
}
}
}