using System.Numerics; using Content.Shared.Paper; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Utility; using Robust.Client.UserInterface.RichText; using Robust.Shared.Input; namespace Content.Client.Paper.UI { [GenerateTypedNameReferences] public sealed partial class PaperWindow : BaseWindow { [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IResourceCache _resCache = default!; private static Color DefaultTextColor = new(25, 25, 25); // // 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; private readonly Type[] _allowedTags = new Type[] { typeof(BoldItalicTag), typeof(BoldTag), typeof(BulletTag), typeof(ColorTag), typeof(HeadingTag), typeof(ItalicTag) }; public event Action? OnSaved; public PaperWindow() { IoCManager.InjectDependencies(this); 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"), null, DefaultTextColor); // Hook up the close button: CloseButton.OnPressed += _ => Close(); Input.OnKeyBindDown += args => // Solution while TextEdit don't have events { if (args.Function == EngineKeyFunctions.MultilineTextSubmit) { RunOnSaved(); args.Handle(); } }; SaveButton.OnPressed += _ => { RunOnSaved(); }; SaveButton.Text = Loc.GetString("paper-ui-save-button", ("keybind", _inputManager.GetKeyFunctionButtonString(EngineKeyFunctions.MultilineTextSubmit))); } /// /// Initialize this UI according to visuals Initializes /// textures, recalculates sizes, and applies some layout rules. /// public void InitVisuals(EntityUid entity, PaperVisualsComponent visuals) { // Randomize the placement of any stamps based on the entity UID // so that there's some variety in different papers. StampDisplay.PlacementSeed = (int)entity; // Initialize the background: PaperBackground.ModulateSelfOverride = visuals.BackgroundModulate; var backgroundImage = visuals.BackgroundImagePath != null? _resCache.GetResource(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(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 = 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; } } } /// /// 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. /// 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", out var font)) { float fontLineHeight = font.GetLineHeight(1.0f); // 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); } /// /// Initialize the paper contents, i.e. the text typed by the /// user and any stamps that have peen put on the page. /// public void Populate(PaperComponent.PaperBoundUserInterfaceState state) { bool isEditing = state.Mode == PaperComponent.PaperAction.Write; bool wasEditing = InputContainer.Visible; InputContainer.Visible = isEditing; EditButtons.Visible = isEditing; var msg = new FormattedMessage(); msg.AddMarkupPermissive(state.Text); // For premade documents, we want to be able to edit them rather than // replace them. var shouldCopyText = 0 == Input.TextLength && 0 != state.Text.Length; if (!wasEditing || shouldCopyText) { // We can get repeated messages with state.Mode == Write if another // player opens the UI for reading. In this case, don't update the // text input, as this player is currently writing new text and we // don't want to lose any text they already input. Input.TextRope = Rope.Leaf.Empty; Input.CursorPosition = new TextEdit.CursorPos(); Input.InsertAtCursor(state.Text); } for (var i = 0; i <= state.StampedBy.Count * 3 + 1; i++) { msg.AddMarkupPermissive("\r\n"); } WrittenTextLabel.SetMessage(msg, _allowedTags, DefaultTextColor); WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0; BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0; StampDisplay.RemoveAllChildren(); StampDisplay.RemoveStamps(); foreach(var stamper in state.StampedBy) { StampDisplay.AddStamp(new StampWidget{ StampInfo = stamper }); } } /// /// 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. /// protected override DragMode GetDragModeFor(Vector2 relativeMousePos) { var mode = DragMode.None; // 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; } if((mode & _allowedResizeModes) == DragMode.None) { return DragMode.Move; } return mode & _allowedResizeModes; } private void RunOnSaved() { OnSaved?.Invoke(Rope.Collapse(Input.TextRope)); } } }