// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Standard Library using System; // Unity using UnityEditor; using UnityEngine; // GameKit using AWS.GameKit.Editor.FileStructure; namespace AWS.GameKit.Editor.GUILayoutExtensions { /// /// A GUI element which displays a link followed by an external icon, optionally prepended with a label. /// public class LinkWidget : IDrawable { // Data private readonly string _prependedText; private readonly float _spaceAfterPrependedText; private readonly string _linkLabel; private readonly Options _options; private readonly Action _onClick; // Styles private GUIStyle _linkTextStyle; private GUIStyle _prependedTextStyle; private GUIStyle _iconStyle; private GUIStyle _iconStyleWithoutOffset; /// /// Create a new LinkWidget. /// /// The link text to display. /// The action to execute when clicking on the link. /// Styling options to apply. public LinkWidget(string linkLabel, Action onClick, Options options = null) { _linkLabel = linkLabel; _onClick = onClick; _options = options ?? new Options(); } /// /// Create a new LinkWidget with a label placed before it. /// /// The text to place directly before the linkLabel. /// The link text to display. /// The action to execute when clicking on the link. /// Styling options to apply. /// The amount of space to place between the prependedText and linkLabel. public LinkWidget(string prependedText, string linkLabel, Action onClick, Options options = null, float spaceAfterPrependedText = 0f) : this(linkLabel, onClick, options) { _prependedText = prependedText; _spaceAfterPrependedText = spaceAfterPrependedText; } /// /// Create a new LinkWidget. /// /// The text to display in place of the URL. /// The link that clicking the label text will route to. /// Styling options to apply. public LinkWidget(string linkLabel, string url, Options options = null) : this(linkLabel, () => Application.OpenURL(url), options) { } /// /// Create a new LinkWidget with a label placed before it. /// /// The text to place directly before the linkLabel. /// The text to display in place of the URL. /// The link that clicking the label text will route to. /// Styling options to apply. /// The amount of space to place between the prependedText and linkLabel. public LinkWidget(string prependedText, string linkLabel, string url, Options options = null, float spaceAfterPrependedText = 0f) : this(linkLabel, url, options) { _prependedText = prependedText; _spaceAfterPrependedText = spaceAfterPrependedText; } /// /// Styling options for the LinkWidget class. /// public class Options { /// /// A GUIStyle to apply to the horizontal layout section which contains the entire link widget (the prepended text, link label, and icon). /// public GUIStyle OverallStyle = GUIStyle.none; /// /// Whether to enable/disable word wrapping for the linkLabel parameter's text. /// public bool ShouldWordWrapLinkLabel = true; /// /// Number of pixels to shift the entire link widget (prepended text, link label, and icon). /// public Vector2 ContentOffset = Vector2.zero; /// /// Alignment of the entire link widget (prepended text, link label, and icon). /// public Alignment Alignment = Alignment.Left; /// /// The tooltip to show when the mouse is hovering over the link. By default no tooltip is shown. /// public string Tooltip = string.Empty; /// /// Whether to draw the external icon after the link label. /// public bool ShouldDrawExternalIcon = true; } /// /// The alignment of the entire link widget (prepended text, link label, and icon). /// public enum Alignment { /// /// The LinkWidget is aligned to the left, by placing a GUILayout.FlexibleSpace() at the end of it's horizontal layout group. /// Left, /// /// The LinkWidget is aligned to the right by placing a GUILayout.FlexibleSpace() at the beginning of it's horizontal layout group. /// Right, /// /// The LinkWidget is placed in the default manner for a horizontal layout group. /// It does not use a GUILayout.FlexibleSpace() to position itself. /// None, } /// /// Draw the LinkWidget. /// public void OnGUI() { SetGUIStyles(); using (new EditorGUILayout.HorizontalScope(_options.OverallStyle)) { if (_options.Alignment == Alignment.Right) { GUILayout.FlexibleSpace(); } DrawPrependedText(); DrawLinkWithExternalIcon(); if (_options.Alignment == Alignment.Left) { GUILayout.FlexibleSpace(); } } } // Avoid a null-reference exception by creating the GUIStyles during OnGUI instead of the constructor. // The exception happens because EditorStyles (ex: EditorStyles.label) are null until the first frame of the GUI. private void SetGUIStyles() { _linkTextStyle = new GUIStyle(LinkWidgetStyles.LinkLabelStyle) { wordWrap = _options.ShouldWordWrapLinkLabel }; Vector2 totalContentOffset = _options.ContentOffset + LinkWidgetStyles.BaselineContentOffset; _linkTextStyle = CommonGUIStyles.AddContentOffset(_linkTextStyle, totalContentOffset); _linkTextStyle.fontSize = _options.OverallStyle.fontSize; _prependedTextStyle = CommonGUIStyles.AddContentOffset(LinkWidgetStyles.PrependedTextStyle, totalContentOffset); _iconStyleWithoutOffset = _linkTextStyle.fontSize == 10 ? LinkWidgetStyles.IconStyleSmall : LinkWidgetStyles.IconStyleNormal; _iconStyle = CommonGUIStyles.AddContentOffset(_iconStyleWithoutOffset, totalContentOffset); } private void DrawPrependedText() { if (string.IsNullOrEmpty(_prependedText)) { return; } EditorGUILayout.LabelField(_prependedText, _prependedTextStyle); if (_spaceAfterPrependedText != 0) { // We only create this Space() element if non-zero, because Space(0) actually creates a non zero-width space. EditorGUILayout.Space(_spaceAfterPrependedText); } } /// /// Draws a button, that is styled to look like a hyperlink, followed by an external link Icon. /// private void DrawLinkWithExternalIcon() { GUILayout.Label(_linkLabel, _linkTextStyle); Rect buttonRect = GUILayoutUtility.GetLastRect(); buttonRect.x += _options.ContentOffset.x; buttonRect.y += _options.ContentOffset.y; buttonRect.width = _linkTextStyle.CalcSize(new GUIContent(_linkLabel)).x; if (_options.ShouldDrawExternalIcon) { // Draw icon GUILayout.Box(EditorResources.Textures.SettingsWindow.ExternalLinkIcon.Get(), _iconStyle); buttonRect.width += GUILayoutUtility.GetLastRect().width + _iconStyleWithoutOffset.fixedWidth / 3f; } // Create a button with no text but spans the length of both the text and the icon (if drawn) GUIContent buttonContent = new GUIContent(string.Empty, _options.Tooltip); if (GUI.Button(buttonRect, buttonContent, GUIStyle.none)) { _onClick(); } // Change mouse cursor to a "pointer" when hovering over the button EditorGUIUtility.AddCursorRect(buttonRect, MouseCursor.Link); } /// /// Subclass containing all specific styles for the LinkWidget. /// private static class LinkWidgetStyles { // The link widget is 2 pixels higher than other text on it's same line. // This offset brings it back down to where it should be. public static readonly Vector2 BaselineContentOffset = new Vector2(0, 2); private static readonly GUIStyleState LinkLabelStyleState = new GUIStyleState() { background = EditorStyles.label.normal.background, scaledBackgrounds = EditorStyles.label.normal.scaledBackgrounds, textColor = EditorStyles.linkLabel.normal.textColor }; public static readonly GUIStyle LinkLabelStyle = new GUIStyle(EditorStyles.label) { active = EditorStyles.linkLabel.active, hover = EditorStyles.linkLabel.hover, normal = LinkLabelStyleState, stretchWidth = false, }; public static readonly GUIStyle PrependedTextStyle = new GUIStyle(EditorStyles.label) { wordWrap = true }; public static readonly GUIStyle IconStyleNormal = new GUIStyle() { contentOffset = new Vector2(2, 3), fixedWidth = 14, fixedHeight = 14 }; public static readonly GUIStyle IconStyleSmall = new GUIStyle() { contentOffset = new Vector2(0, 2), fixedWidth = 10, fixedHeight = 10 }; } } }