Improve Paper UI, allow an to entity configure how it's UI looks (#13494)

Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
This commit is contained in:
eoineoineoin
2023-01-17 08:32:46 +00:00
committed by GitHub
parent 741991602f
commit bda5f8248f
48 changed files with 2212 additions and 47 deletions

View File

@@ -19,14 +19,18 @@ namespace Content.Client.Paper.UI
protected override void Open()
{
base.Open();
_window = new PaperWindow
{
Title = IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(Owner.Owner).EntityName,
};
var entityMgr = IoCManager.Resolve<IEntityManager>();
_window = new PaperWindow();
_window.OnClose += Close;
_window.Input.OnTextEntered += Input_OnTextEntered;
_window.OpenCentered();
if (entityMgr.TryGetComponent<PaperVisualsComponent>(Owner.Owner, out var visuals))
{
_window.InitVisuals(visuals);
}
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -1,6 +1,99 @@
namespace Content.Client.Paper;
namespace Content.Client.Paper;
[RegisterComponent]
public sealed class PaperVisualsComponent : Component
{
/// <summary>
/// The path to the image which will be used as a background for the paper itself
/// </summary>
[DataField("backgroundImagePath")]
public string? BackgroundImagePath;
/// <summary>
/// An optional patch to configure tiling stretching of the background. Used to set
/// the PatchMargin in a <code>StyleBoxTexture</code>
/// </summary>
[DataField("backgroundPatchMargin")]
public Box2 BackgroundPatchMargin = default;
/// <summary>
/// Modulate the background image by this color. Can be used to add colorful
/// variants of images, without having to create new textures.
/// </summary>
[DataField("backgroundModulate")]
public Color BackgroundModulate = Color.White;
/// <summary>
/// Should the background image tile, or be streched? Sets <code>StyleBoxTexture.StrechMode</code>
/// </summary>
[DataField("backgroundImageTile")]
public bool BackgroundImageTile = false;
/// <summary>
/// An additional scale to apply to the background image
/// </summary>
[DataField("backgroundScale")]
public Vector2 BackgroundScale = Vector2.One;
/// <summary>
/// A path to an image which will be used as a header on the paper
/// </summary>
[DataField("headerImagePath")]
public string? HeaderImagePath;
/// <summary>
/// Modulate the header image by this color
/// </summary>
[DataField("headerImageModulate")]
public Color HeaderImageModulate = Color.White;
/// <summary>
/// Any additional margin to add around the header
/// </summary>
[DataField("headerMargin")]
public Box2 HeaderMargin = default;
/// <summary>
/// Path to an image to use as the background to the "content" of the paper
/// The header and actual written text will use this as a background. The
/// image will be tiled vertically with the property that the bottom of the
/// written text will line up with the bottom of this image.
/// </summary>
[DataField("contentImagePath")]
public string? ContentImagePath;
/// <summary>
/// Modulate the content image by this color
/// </summary>
[DataField("contentImageModulate")]
public Color ContentImageModulate = Color.White;
/// <summary>
/// An additional margin around the content (including header)
/// </summary>
[DataField("contentMargin")]
public Box2 ContentMargin = default;
/// <summary>
/// The number of lines that the content image represents. The
/// content image will be vertically tiled after this many lines
/// of text.
/// </summary>
[DataField("contentImageNumLines")]
public int ContentImageNumLines = 1;
/// <summary>
/// Modulate the style's font by this color
/// </summary>
[DataField("fontAccentColor")]
public Color FontAccentColor = new Color(0x25, 0x25, 0x2a);
/// <summary>
/// This can enforce that your paper has a limited area to write in.
/// If you wish to constrain only one direction, the other direction
/// can be unlimited by specifying a value of zero.
/// This will be scaled according to UI scale.
/// </summary>
[DataField("maxWritableArea")]
public Vector2? MaxWritableArea = null;
}

View File

@@ -1,10 +1,31 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="300 300"
SetSize="300 300">
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="Label" />
<LineEdit Name="Input"
Access="Public"
Visible="False" />
<paper:PaperWindow xmlns="https://spacestation14.io"
xmlns:paper="clr-namespace:Content.Client.Paper.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MouseFilter="Stop" Resizable="True" MinSize="150 150"
SetSize="300 400"> <!-- Provide some reasonable sizes by default. Can be changed by the component -->
<BoxContainer Name="ContentsRoot" Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="6">
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"/>
</PanelContainer>
<PanelContainer Name="PaperBackground" StyleClasses="PaperDefaultBorder" VerticalExpand="True" HorizontalExpand="True">
<ScrollContainer Name="ScrollingContents" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="False">
<PanelContainer Name="PaperContent" VerticalExpand="True" HorizontalExpand="True" MaxWidth="600">
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
<TextureButton Name="HeaderImage" HorizontalAlignment="Center" VerticalAlignment="Top" MouseFilter="Ignore"/>
<Control Name="TextAlignmentPadding" VerticalAlignment="Top" />
<RichTextLabel Name="BlankPaperIndicator" StyleClasses="LabelSecondaryColor"
VerticalAlignment="Top" HorizontalAlignment="Center"/>
<RichTextLabel StyleClasses="PaperWrittenText" Name="WrittenTextLabel" VerticalAlignment="Top"/>
<PanelContainer Name="InputContainer" StyleClasses="TransparentBorderedWindowPanel"
VerticalAlignment="Top" HorizontalExpand="True">
<LineEdit Name="Input" StyleClasses="PaperLineEdit" Access="Public" />
</PanelContainer>
</BoxContainer>
<BoxContainer Name="StampDisplay" Orientation="Vertical" VerticalAlignment="Bottom" Margin="6"/>
</PanelContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
</DefaultWindow>
</paper:PaperWindow>

View File

@@ -1,6 +1,7 @@
using Content.Shared.Paper;
using Content.Shared.Paper;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -8,22 +9,223 @@ using Robust.Shared.Utility;
namespace Content.Client.Paper.UI
{
[GenerateTypedNameReferences]
public sealed partial class PaperWindow : DefaultWindow
public sealed partial class PaperWindow : BaseWindow
{
// <summary>
// Size of resize handles around the paper
private const int DRAG_MARGIN_SIZE = 16;
// We keep a reference to the paper content texture that we create
// so that we can modify it later.
private StyleBoxTexture _paperContentTex = new();
// The number of lines that the content image represents.
// See PaperVisualsComponent.ContentImageNumLines.
private float _paperContentLineScale = 1.0f;
// If paper limits the size in one or both axes, it'll affect whether
// we're able to resize this UI or not. Default to everything enabled:
private DragMode _allowedResizeModes = ~DragMode.None;
public PaperWindow()
{
RobustXamlLoader.Load(this);
// We can't configure the RichTextLabel contents from xaml, so do it here:
BlankPaperIndicator.SetMessage(Loc.GetString("paper-ui-blank-page-message"));
// Hook up the close button:
CloseButton.OnPressed += _ => Close();
}
/// <summary>
/// Initialize this UI according to <code>visuals</code> Initializes
/// textures, recalculates sizes, and applies some layout rules.
/// </summary>
public void InitVisuals(PaperVisualsComponent visuals)
{
var resCache = IoCManager.Resolve<IResourceCache>();
/// Initialize the background:
PaperBackground.ModulateSelfOverride = visuals.BackgroundModulate;
var backgroundImage = visuals.BackgroundImagePath != null? resCache.GetResource<TextureResource>(visuals.BackgroundImagePath) : null;
if (backgroundImage != null)
{
var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
var backgroundPatchMargin = visuals.BackgroundPatchMargin;
PaperBackground.PanelOverride = new StyleBoxTexture
{
Texture = backgroundImage,
TextureScale = visuals.BackgroundScale,
Mode = backgroundImageMode,
PatchMarginLeft = backgroundPatchMargin.Left,
PatchMarginBottom = backgroundPatchMargin.Bottom,
PatchMarginRight = backgroundPatchMargin.Right,
PatchMarginTop = backgroundPatchMargin.Top
};
}
else
{
PaperBackground.PanelOverride = null;
}
// Then the header:
if (visuals.HeaderImagePath != null)
{
HeaderImage.TexturePath = visuals.HeaderImagePath;
HeaderImage.MinSize = HeaderImage.TextureNormal?.Size ?? Vector2.Zero;
}
HeaderImage.ModulateSelfOverride = visuals.HeaderImageModulate;
HeaderImage.Margin = new Thickness(visuals.HeaderMargin.Left, visuals.HeaderMargin.Top,
visuals.HeaderMargin.Right, visuals.HeaderMargin.Bottom);
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;
var contentImage = visuals.ContentImagePath != null ? resCache.GetResource<TextureResource>(visuals.ContentImagePath) : null;
if (contentImage != null)
{
// Setup the paper content texture, but keep a reference to it, as we can't set
// some font-related properties here. We'll fix those up later, in Draw()
_paperContentTex = new StyleBoxTexture
{
Texture = contentImage,
Mode = StyleBoxTexture.StretchMode.Tile,
};
PaperContent.PanelOverride = _paperContentTex;
_paperContentLineScale = visuals.ContentImageNumLines;
}
PaperContent.Margin = new Thickness(
visuals.ContentMargin.Left, visuals.ContentMargin.Top,
visuals.ContentMargin.Right, visuals.ContentMargin.Bottom);
if (visuals.MaxWritableArea != null)
{
var a = (Vector2)visuals.MaxWritableArea;
// Paper has requested that this has a maximum area that you can write on.
// So, we'll make the window non-resizable and fix the size of the content.
// Ideally, would like to be able to allow resizing only one direction.
ScrollingContents.MinSize = Vector2.Zero;
ScrollingContents.MinSize = (Vector2)(a);
if (a.X > 0.0f)
{
ScrollingContents.MaxWidth = a.X;
_allowedResizeModes &= ~(DragMode.Left | DragMode.Right);
// Since this dimension has been specified by the user, we
// need to undo the SetSize which was configured in the xaml.
// Controls use NaNs to indicate unset for this value.
// This is leaky - there should be a method for this
SetWidth = float.NaN;
}
if (a.Y > 0.0f)
{
ScrollingContents.MaxHeight = a.Y;
_allowedResizeModes &= ~(DragMode.Top | DragMode.Bottom);
SetHeight = float.NaN;
}
}
}
/// <summary>
/// Control interface. We'll mostly rely on the children to do the drawing
/// but in order to get lines on paper to match up with the rich text labels,
/// we need to do a small calculation to sync them up.
/// </summary>
protected override void Draw(DrawingHandleScreen handle)
{
// Now do the deferred setup of the written area. At the point
// that InitVisuals runs, the label hasn't had it's style initialized
// so we need to get some info out now:
if (WrittenTextLabel.TryGetStyleProperty<Font>("font", out var font))
{
float fontLineHeight = font.GetLineHeight(UIScale);
// This positions the texture so the font baseline is on the bottom:
_paperContentTex.ExpandMarginTop = font.GetDescent(UIScale);
// And this scales the texture so that it's a single text line:
var scaleY = (_paperContentLineScale * fontLineHeight) / _paperContentTex.Texture?.Height ?? fontLineHeight;
_paperContentTex.TextureScale = new Vector2(1, scaleY);
// Now, we might need to add some padding to the text to ensure
// that, even if a header is specified, the text will line up with
// where the content image expects the font to be rendered (i.e.,
// adjusting the height of the header image shouldn't cause the
// text to be offset from a line)
{
var headerHeight = HeaderImage.Size.Y + HeaderImage.Margin.Top + HeaderImage.Margin.Bottom;
var headerInLines = headerHeight / (fontLineHeight * _paperContentLineScale);
var paddingRequiredInLines = (float)Math.Ceiling(headerInLines) - headerInLines;
var verticalMargin = fontLineHeight * paddingRequiredInLines * _paperContentLineScale;
TextAlignmentPadding.Margin = new Thickness(0.0f, verticalMargin, 0.0f, 0.0f);
}
}
base.Draw(handle);
}
/// <summary>
/// Initialize the paper contents, i.e. the text typed by the
/// user and any stamps that have peen put on the page.
/// </summary>
public void Populate(SharedPaperComponent.PaperBoundUserInterfaceState state)
{
if (state.Mode == SharedPaperComponent.PaperAction.Write)
{
Input.Visible = true;
}
bool isEditing = state.Mode == SharedPaperComponent.PaperAction.Write;
InputContainer.Visible = isEditing;
var msg = new FormattedMessage();
msg.AddMarkupPermissive(state.Text);
Label.SetMessage(msg);
// Remove any newlines from the end of the message. There can be a trailing
// new line at the end of user input, and we would like to display the input
// box immediately on the next line.
msg.AddMarkupPermissive(state.Text.TrimEnd('\r', '\n'));
WrittenTextLabel.SetMessage(msg);
WrittenTextLabel.Visible = state.Text.Length > 0;
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
StampDisplay.RemoveAllChildren();
foreach(var stamper in state.StampedBy)
{
StampDisplay.AddChild(new StampWidget{ Stamper = stamper });
}
}
/// <summary>
/// BaseWindow interface. Allow users to drag UI around by grabbing
/// anywhere on the page (like FancyWindow) but try to calculate
/// reasonable dragging bounds because this UI can have round corners,
/// and it can be hard to judge where to click to resize.
/// </summary>
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
var mode = DragMode.Move;
// Be quite generous with resize margins:
if (relativeMousePos.Y < DRAG_MARGIN_SIZE)
{
mode |= DragMode.Top;
}
else if (relativeMousePos.Y > Size.Y - DRAG_MARGIN_SIZE)
{
mode |= DragMode.Bottom;
}
if (relativeMousePos.X < DRAG_MARGIN_SIZE)
{
mode |= DragMode.Left;
}
else if (relativeMousePos.X > Size.X - DRAG_MARGIN_SIZE)
{
mode |= DragMode.Right;
}
return mode & _allowedResizeModes;
}
}
}

View File

@@ -0,0 +1,31 @@
<paper:StampWidget xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:style="clr-namespace:Content.Client.Stylesheets"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:paper="clr-namespace:Content.Client.Paper.UI" HorizontalAlignment="Center" Margin="6">
<!--
<TextureButton Margin="6 6 6 2" MinSize="24 12"
TexturePath="/Textures/Interface/Nano/nano_stamp.192dpi.png">
-->
<BoxContainer Orientation="Vertical">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
</PanelContainer.PanelOverride>
<Control MinSize="3 3" />
</PanelContainer>
<Label Name="StampedByLabel" StyleClasses="LabelHeadingBigger" FontColorOverride="{x:Static style:StyleNano.DangerousRedFore}" Margin="12 6 12 6"/>
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.DangerousRedFore}" />
</PanelContainer.PanelOverride>
<Control MinSize="3 3" />
</PanelContainer>
</BoxContainer>
</paper:StampWidget>

View File

@@ -0,0 +1,23 @@
using Content.Shared.Paper;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.Paper.UI
{
[GenerateTypedNameReferences]
public sealed partial class StampWidget : Container
{
public string? Stamper {
get => StampedByLabel.Text;
set => StampedByLabel.Text = value;
}
public StampWidget()
{
RobustXamlLoader.Load(this);
}
}
}

View File

@@ -485,6 +485,14 @@ namespace Content.Client.Stylesheets
};
insetBack.SetPatchMargin(StyleBox.Margin.All, 10);
// Default paper background:
var paperBackground = new StyleBoxTexture
{
Texture = resCache.GetTexture("/Textures/Interface/Paper/paper_background_default.svg.96dpi.png"),
Modulate = Color.FromHex("#eaedde"), // A light cream
};
paperBackground.SetPatchMargin(StyleBox.Margin.All, 16.0f);
var contextMenuExpansionTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
var verbMenuConfirmationTexture = resCache.GetTexture("/Textures/Interface/VerbIcons/group.svg.192dpi.png");
@@ -1323,6 +1331,16 @@ namespace Content.Client.Stylesheets
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#753131")),
// ---
// The default look of paper in UIs. Pages can have components which override this
Element<PanelContainer>().Class("PaperDefaultBorder")
.Prop(PanelContainer.StylePropertyPanel, paperBackground),
Element<RichTextLabel>().Class("PaperWrittenText")
.Prop(Label.StylePropertyFont, notoSans12)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#111111")),
Element<LineEdit>().Class("PaperLineEdit")
.Prop(LineEdit.StylePropertyStyleBox, new StyleBoxEmpty()),
// Red Button ---
Element<Button>().Class("ButtonColorRed")
.Prop(Control.StylePropertyModulateSelf, ButtonColorDefaultRed),