Initial commit
This commit is contained in:
32
mod/PLibUI/IDynamicSizable.cs
Normal file
32
mod/PLibUI/IDynamicSizable.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A UI component which can be dynamically resized for its content.
|
||||
/// </summary>
|
||||
public interface IDynamicSizable : IUIComponent {
|
||||
/// <summary>
|
||||
/// Whether the component should dynamically resize for its content. This adds more
|
||||
/// components and more layout depth, so should only be enabled if necessary.
|
||||
///
|
||||
/// Defaults to false. Must be set to true for components with a nonzero flex size.
|
||||
/// </summary>
|
||||
bool DynamicSize { get; set; }
|
||||
}
|
||||
}
|
||||
36
mod/PLibUI/ISettableFlexSize.cs
Normal file
36
mod/PLibUI/ISettableFlexSize.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Describes a UI component whose flexible size can be mutated.
|
||||
/// </summary>
|
||||
internal interface ISettableFlexSize : ILayoutGroup {
|
||||
/// <summary>
|
||||
/// The flexible width of the completed layout group can be set.
|
||||
/// </summary>
|
||||
float flexibleWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible height of the completed layout group can be set.
|
||||
/// </summary>
|
||||
float flexibleHeight { get; set; }
|
||||
}
|
||||
}
|
||||
27
mod/PLibUI/ITooltipListableOption.cs
Normal file
27
mod/PLibUI/ITooltipListableOption.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
public interface ITooltipListableOption : IListableOption {
|
||||
/// <summary>
|
||||
/// Retrieves the tool tip text for this option.
|
||||
/// </summary>
|
||||
/// <returns>The text to be shown on the tool tip.</returns>
|
||||
string GetToolTipText();
|
||||
}
|
||||
}
|
||||
43
mod/PLibUI/IUIComponent.cs
Normal file
43
mod/PLibUI/IUIComponent.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Implemented by PLib UI components.
|
||||
/// </summary>
|
||||
public interface IUIComponent {
|
||||
/// <summary>
|
||||
/// The component name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a physical game object embodying this component.
|
||||
/// </summary>
|
||||
/// <returns>The game object representing this UI component. Multiple invocations return
|
||||
/// unique objects.</returns>
|
||||
GameObject Build();
|
||||
|
||||
/// <summary>
|
||||
/// Actions invoked when the UI component is actually realized.
|
||||
/// </summary>
|
||||
event PUIDelegates.OnRealize OnRealize;
|
||||
}
|
||||
}
|
||||
36
mod/PLibUI/ImageTransform.cs
Normal file
36
mod/PLibUI/ImageTransform.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// An enumeration describing how to transform the image in a label.
|
||||
///
|
||||
/// Rotations are counterclockwise from 0 (straight up).
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ImageTransform : uint {
|
||||
None = 0,
|
||||
FlipHorizontal = 1,
|
||||
FlipVertical = 2,
|
||||
Rotate90 = 4,
|
||||
Rotate180 = 8,
|
||||
Rotate270 = Rotate90 | Rotate180
|
||||
}
|
||||
}
|
||||
88
mod/PLibUI/LayoutSizes.cs
Normal file
88
mod/PLibUI/LayoutSizes.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A class representing the size sets of a particular component.
|
||||
/// </summary>
|
||||
internal struct LayoutSizes {
|
||||
/// <summary>
|
||||
/// The flexible dimension value.
|
||||
/// </summary>
|
||||
public float flexible;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this component should be ignored completely.
|
||||
/// </summary>
|
||||
public bool ignore;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum dimension value.
|
||||
/// </summary>
|
||||
public float min;
|
||||
|
||||
/// <summary>
|
||||
/// The preferred dimension value.
|
||||
/// </summary>
|
||||
public float preferred;
|
||||
|
||||
/// <summary>
|
||||
/// The source of these values.
|
||||
/// </summary>
|
||||
public readonly GameObject source;
|
||||
|
||||
internal LayoutSizes(GameObject source) : this(source, 0.0f, 0.0f, 0.0f) { }
|
||||
|
||||
internal LayoutSizes(GameObject source, float min, float preferred,
|
||||
float flexible) {
|
||||
ignore = false;
|
||||
this.source = source;
|
||||
this.flexible = flexible;
|
||||
this.min = min;
|
||||
this.preferred = preferred;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds another set of layout sizes to this one.
|
||||
/// </summary>
|
||||
/// <param name="other">The size values to add.</param>
|
||||
public void Add(LayoutSizes other) {
|
||||
flexible += other.flexible;
|
||||
min += other.min;
|
||||
preferred += other.preferred;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enlarges this layout size, if necessary, using the values from another.
|
||||
/// </summary>
|
||||
/// <param name="other">The minimum size values to enforce.</param>
|
||||
public void Max(LayoutSizes other) {
|
||||
flexible = Math.Max(flexible, other.flexible);
|
||||
min = Math.Max(min, other.min);
|
||||
preferred = Math.Max(preferred, other.preferred);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("LayoutSizes[min={0:F2},preferred={1:F2},flexible={2:F2}]",
|
||||
min, preferred, flexible);
|
||||
}
|
||||
}
|
||||
}
|
||||
239
mod/PLibUI/Layouts/AbstractLayoutGroup.cs
Normal file
239
mod/PLibUI/Layouts/AbstractLayoutGroup.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// The abstract parent of most layout groups.
|
||||
/// </summary>
|
||||
public abstract class AbstractLayoutGroup : UIBehaviour, ISettableFlexSize, ILayoutElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets an object's layout dirty on the next frame.
|
||||
/// </summary>
|
||||
/// <param name="transform">The transform to set dirty.</param>
|
||||
/// <returns>A coroutine to set it dirty.</returns>
|
||||
internal static IEnumerator DelayedSetDirty(RectTransform transform) {
|
||||
yield return null;
|
||||
LayoutRebuilder.MarkLayoutForRebuild(transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and destroys any PLib layouts on the component. They will be replaced with
|
||||
/// a static LayoutElement containing the old size of the component.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to cleanse.</param>
|
||||
internal static void DestroyAndReplaceLayout(GameObject component) {
|
||||
if (component != null && component.TryGetComponent(out AbstractLayoutGroup
|
||||
layoutGroup)) {
|
||||
var replacement = component.AddOrGet<LayoutElement>();
|
||||
replacement.flexibleHeight = layoutGroup.flexibleHeight;
|
||||
replacement.flexibleWidth = layoutGroup.flexibleWidth;
|
||||
replacement.layoutPriority = layoutGroup.layoutPriority;
|
||||
replacement.minHeight = layoutGroup.minHeight;
|
||||
replacement.minWidth = layoutGroup.minWidth;
|
||||
replacement.preferredHeight = layoutGroup.preferredHeight;
|
||||
replacement.preferredWidth = layoutGroup.preferredWidth;
|
||||
DestroyImmediate(layoutGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public float minWidth {
|
||||
get {
|
||||
return mMinWidth;
|
||||
}
|
||||
set {
|
||||
mMinWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
public float preferredWidth {
|
||||
get {
|
||||
return mPreferredWidth;
|
||||
}
|
||||
set {
|
||||
mPreferredWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flexible width of the completed layout group can be set.
|
||||
/// </summary>
|
||||
public float flexibleWidth {
|
||||
get {
|
||||
return mFlexibleWidth;
|
||||
}
|
||||
set {
|
||||
mFlexibleWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
public float minHeight {
|
||||
get {
|
||||
return mMinHeight;
|
||||
}
|
||||
set {
|
||||
mMinHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
public float preferredHeight {
|
||||
get {
|
||||
return mPreferredHeight;
|
||||
}
|
||||
set {
|
||||
mPreferredHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flexible height of the completed layout group can be set.
|
||||
/// </summary>
|
||||
public float flexibleHeight {
|
||||
get {
|
||||
return mFlexibleHeight;
|
||||
}
|
||||
set {
|
||||
mFlexibleHeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The priority of this layout group.
|
||||
/// </summary>
|
||||
public int layoutPriority {
|
||||
get {
|
||||
return mLayoutPriority;
|
||||
}
|
||||
set {
|
||||
mLayoutPriority = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected RectTransform rectTransform {
|
||||
get {
|
||||
if (cachedTransform == null)
|
||||
cachedTransform = gameObject.rectTransform();
|
||||
return cachedTransform;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the layout is currently locked.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
protected bool locked;
|
||||
|
||||
// The backing fields must be annotated with the attribute to have prefabbed versions
|
||||
// successfully copy the values
|
||||
[SerializeField]
|
||||
private float mMinWidth, mMinHeight, mPreferredWidth, mPreferredHeight;
|
||||
[SerializeField]
|
||||
private float mFlexibleWidth, mFlexibleHeight;
|
||||
[SerializeField]
|
||||
private int mLayoutPriority;
|
||||
|
||||
/// <summary>
|
||||
/// The cached rect transform to speed up layout.
|
||||
/// </summary>
|
||||
private RectTransform cachedTransform;
|
||||
|
||||
protected AbstractLayoutGroup() {
|
||||
cachedTransform = null;
|
||||
locked = false;
|
||||
mLayoutPriority = 1;
|
||||
}
|
||||
|
||||
public abstract void CalculateLayoutInputHorizontal();
|
||||
|
||||
public abstract void CalculateLayoutInputVertical();
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a layout with the current parent, and then locks the layout size. Further
|
||||
/// attempts to automatically lay out the component, unless UnlockLayout is called,
|
||||
/// will not trigger any action.
|
||||
///
|
||||
/// The resulting layout has very good performance, but cannot adapt to changes in the
|
||||
/// size of its children or its own size.
|
||||
/// </summary>
|
||||
/// <returns>The computed size of this component when locked.</returns>
|
||||
public virtual Vector2 LockLayout() {
|
||||
var rt = gameObject.rectTransform();
|
||||
if (rt != null) {
|
||||
locked = false;
|
||||
CalculateLayoutInputHorizontal();
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, minWidth);
|
||||
SetLayoutHorizontal();
|
||||
CalculateLayoutInputVertical();
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, minHeight);
|
||||
SetLayoutVertical();
|
||||
locked = true;
|
||||
}
|
||||
return new Vector2(minWidth, minHeight);
|
||||
}
|
||||
|
||||
protected override void OnDidApplyAnimationProperties() {
|
||||
base.OnDidApplyAnimationProperties();
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
protected override void OnDisable() {
|
||||
base.OnDisable();
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
protected override void OnEnable() {
|
||||
base.OnEnable();
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
protected override void OnRectTransformDimensionsChange() {
|
||||
base.OnRectTransformDimensionsChange();
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets this layout as dirty.
|
||||
/// </summary>
|
||||
protected virtual void SetDirty() {
|
||||
if (gameObject != null && IsActive()) {
|
||||
if (CanvasUpdateRegistry.IsRebuildingLayout())
|
||||
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
|
||||
else
|
||||
StartCoroutine(DelayedSetDirty(rectTransform));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void SetLayoutHorizontal();
|
||||
|
||||
public abstract void SetLayoutVertical();
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks the layout, allowing it to again dynamically resize when component sizes
|
||||
/// are changed.
|
||||
/// </summary>
|
||||
public virtual void UnlockLayout() {
|
||||
locked = false;
|
||||
LayoutRebuilder.MarkLayoutForRebuild(gameObject.rectTransform());
|
||||
}
|
||||
}
|
||||
}
|
||||
266
mod/PLibUI/Layouts/BoxLayoutGroup.cs
Normal file
266
mod/PLibUI/Layouts/BoxLayoutGroup.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
//#define DEBUG_LAYOUT
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A freezable, flexible layout manager that fixes the issues I am having with
|
||||
/// HorizontalLayoutGroup and VerticalLayoutGroup. You get a content size fitter for
|
||||
/// free too!
|
||||
///
|
||||
/// Intended to work something like Java's BoxLayout...
|
||||
/// </summary>
|
||||
public sealed class BoxLayoutGroup : AbstractLayoutGroup {
|
||||
/// <summary>
|
||||
/// Calculates the size of the box layout container.
|
||||
/// </summary>
|
||||
/// <param name="obj">The container to lay out.</param>
|
||||
/// <param name="args">The parameters to use for layout.</param>
|
||||
/// <param name="direction">The direction which is being calculated.</param>
|
||||
/// <returns>The minimum and preferred box layout size.</returns>
|
||||
private static BoxLayoutResults Calc(GameObject obj, BoxLayoutParams args,
|
||||
PanelDirection direction) {
|
||||
var transform = obj.AddOrGet<RectTransform>();
|
||||
int n = transform.childCount;
|
||||
var result = new BoxLayoutResults(direction, n);
|
||||
var components = ListPool<Component, BoxLayoutGroup>.Allocate();
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i)?.gameObject;
|
||||
if (child != null && child.activeInHierarchy) {
|
||||
// Only on active game objects
|
||||
components.Clear();
|
||||
child.GetComponents(components);
|
||||
var hc = PUIUtils.CalcSizes(child, direction, components);
|
||||
if (!hc.ignore) {
|
||||
if (args.Direction == direction)
|
||||
result.Accum(hc, args.Spacing);
|
||||
else
|
||||
result.Expand(hc);
|
||||
result.children.Add(hc);
|
||||
}
|
||||
}
|
||||
}
|
||||
components.Recycle();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lays out components in the box layout container.
|
||||
/// </summary>
|
||||
/// <param name="args">The parameters to use for layout.</param>
|
||||
/// <param name="required">The calculated minimum and preferred sizes.</param>
|
||||
/// <param name="size">The total available size in this dimension.</param>
|
||||
private static void DoLayout(BoxLayoutParams args, BoxLayoutResults required,
|
||||
float size) {
|
||||
if (required == null)
|
||||
throw new ArgumentNullException(nameof(required));
|
||||
var direction = required.direction;
|
||||
var status = new BoxLayoutStatus(direction, args.Margin ?? new RectOffset(), size);
|
||||
if (args.Direction == direction)
|
||||
DoLayoutLinear(required, args, status);
|
||||
else
|
||||
DoLayoutPerp(required, args, status);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lays out components in the box layout container parallel to the layout axis.
|
||||
/// </summary>
|
||||
/// <param name="required">The calculated minimum and preferred sizes.</param>
|
||||
/// <param name="args">The parameters to use for layout.</param>
|
||||
/// <param name="status">The current status of layout.</param>
|
||||
private static void DoLayoutLinear(BoxLayoutResults required, BoxLayoutParams args,
|
||||
BoxLayoutStatus status) {
|
||||
var total = required.total;
|
||||
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
|
||||
var direction = args.Direction;
|
||||
// Determine flex size ratio
|
||||
float size = status.size, prefRatio = 0.0f, minSize = total.min, prefSize =
|
||||
total.preferred, excess = Math.Max(0.0f, size - prefSize), flexTotal = total.
|
||||
flexible, offset = status.offset, spacing = args.Spacing;
|
||||
if (size > minSize && prefSize > minSize)
|
||||
// Do not divide by 0
|
||||
prefRatio = Math.Min(1.0f, (size - minSize) / (prefSize - minSize));
|
||||
if (excess > 0.0f && flexTotal == 0.0f)
|
||||
// If no components can be expanded, offset all
|
||||
offset += PUIUtils.GetOffset(args.Alignment, status.direction, excess);
|
||||
foreach (var child in required.children) {
|
||||
var obj = child.source;
|
||||
// Active objects only
|
||||
if (obj != null && obj.activeInHierarchy) {
|
||||
float compSize = child.min;
|
||||
if (prefRatio > 0.0f)
|
||||
compSize += (child.preferred - child.min) * prefRatio;
|
||||
if (excess > 0.0f && flexTotal > 0.0f)
|
||||
compSize += excess * child.flexible / flexTotal;
|
||||
// Place and size component
|
||||
obj.AddOrGet<RectTransform>().SetInsetAndSizeFromParentEdge(status.edge,
|
||||
offset, compSize);
|
||||
offset += compSize + ((compSize > 0.0f) ? spacing : 0.0f);
|
||||
// Invoke SetLayout on dependents
|
||||
components.Clear();
|
||||
obj.GetComponents(components);
|
||||
foreach (var component in components)
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
component.SetLayoutHorizontal();
|
||||
else // if (direction == PanelDirection.Vertical)
|
||||
component.SetLayoutVertical();
|
||||
}
|
||||
}
|
||||
components.Recycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lays out components in the box layout container against the layout axis.
|
||||
/// </summary>
|
||||
/// <param name="required">The calculated minimum and preferred sizes.</param>
|
||||
/// <param name="args">The parameters to use for layout.</param>
|
||||
/// <param name="status">The current status of layout.</param>
|
||||
private static void DoLayoutPerp(BoxLayoutResults required, BoxLayoutParams args,
|
||||
BoxLayoutStatus status) {
|
||||
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
|
||||
var direction = args.Direction;
|
||||
float size = status.size;
|
||||
foreach (var child in required.children) {
|
||||
var obj = child.source;
|
||||
// Active objects only
|
||||
if (obj != null && obj.activeInHierarchy) {
|
||||
float compSize = size;
|
||||
if (child.flexible <= 0.0f)
|
||||
// Does not expand to all
|
||||
compSize = Math.Min(compSize, child.preferred);
|
||||
float offset = (size > compSize) ? PUIUtils.GetOffset(args.Alignment,
|
||||
status.direction, size - compSize) : 0.0f;
|
||||
// Place and size component
|
||||
obj.AddOrGet<RectTransform>().SetInsetAndSizeFromParentEdge(status.edge,
|
||||
offset + status.offset, compSize);
|
||||
// Invoke SetLayout on dependents
|
||||
components.Clear();
|
||||
obj.GetComponents(components);
|
||||
foreach (var component in components)
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
component.SetLayoutVertical();
|
||||
else // if (direction == PanelDirection.Vertical)
|
||||
component.SetLayoutHorizontal();
|
||||
}
|
||||
}
|
||||
components.Recycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The parameters used to set up this box layout.
|
||||
/// </summary>
|
||||
public BoxLayoutParams Params {
|
||||
get {
|
||||
return parameters;
|
||||
}
|
||||
set {
|
||||
parameters = value ?? throw new ArgumentNullException(nameof(Params));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Results from the horizontal calculation pass.
|
||||
/// </summary>
|
||||
private BoxLayoutResults horizontal;
|
||||
|
||||
/// <summary>
|
||||
/// The parameters used to set up this box layout.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private BoxLayoutParams parameters;
|
||||
|
||||
/// <summary>
|
||||
/// Results from the vertical calculation pass.
|
||||
/// </summary>
|
||||
private BoxLayoutResults vertical;
|
||||
|
||||
internal BoxLayoutGroup() {
|
||||
horizontal = null;
|
||||
layoutPriority = 1;
|
||||
parameters = new BoxLayoutParams();
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputHorizontal() {
|
||||
if (!locked) {
|
||||
var margin = parameters.Margin;
|
||||
float gap = (margin == null) ? 0.0f : margin.left + margin.right;
|
||||
horizontal = Calc(gameObject, parameters, PanelDirection.Horizontal);
|
||||
var hTotal = horizontal.total;
|
||||
minWidth = hTotal.min + gap;
|
||||
preferredWidth = hTotal.preferred + gap;
|
||||
#if DEBUG_LAYOUT
|
||||
PUIUtils.LogUIDebug("CalculateLayoutInputHorizontal for {0} preferred {1:F2}".
|
||||
F(gameObject.name, preferredWidth));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputVertical() {
|
||||
if (!locked) {
|
||||
var margin = parameters.Margin;
|
||||
float gap = (margin == null) ? 0.0f : margin.top + margin.bottom;
|
||||
vertical = Calc(gameObject, parameters, PanelDirection.Vertical);
|
||||
var vTotal = vertical.total;
|
||||
minHeight = vTotal.min + gap;
|
||||
preferredHeight = vTotal.preferred + gap;
|
||||
#if DEBUG_LAYOUT
|
||||
PUIUtils.LogUIDebug("CalculateLayoutInputVertical for {0} preferred {1:F2}".F(
|
||||
gameObject.name, preferredHeight));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisable() {
|
||||
base.OnDisable();
|
||||
horizontal = null;
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
protected override void OnEnable() {
|
||||
base.OnEnable();
|
||||
horizontal = null;
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
public override void SetLayoutHorizontal() {
|
||||
if (horizontal != null && !locked) {
|
||||
#if DEBUG_LAYOUT
|
||||
PUIUtils.LogUIDebug("SetLayoutHorizontal for {0} resolved width to {1:F2}".F(
|
||||
gameObject.name, rectTransform.rect.width));
|
||||
#endif
|
||||
DoLayout(parameters, horizontal, rectTransform.rect.width);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutVertical() {
|
||||
if (vertical != null && !locked) {
|
||||
#if DEBUG_LAYOUT
|
||||
PUIUtils.LogUIDebug("SetLayoutVertical for {0} resolved height to {1:F2}".F(
|
||||
gameObject.name, rectTransform.rect.height));
|
||||
#endif
|
||||
DoLayout(parameters, vertical, rectTransform.rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
mod/PLibUI/Layouts/BoxLayoutParams.cs
Normal file
61
mod/PLibUI/Layouts/BoxLayoutParams.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// The parameters used for laying out a box layout.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class BoxLayoutParams {
|
||||
/// <summary>
|
||||
/// The alignment to use for components that are not big enough to fit and have no
|
||||
/// flexible width.
|
||||
/// </summary>
|
||||
public TextAnchor Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The direction of layout.
|
||||
/// </summary>
|
||||
public PanelDirection Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin between the children and the component edge.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The spacing between components.
|
||||
/// </summary>
|
||||
public float Spacing { get; set; }
|
||||
|
||||
public BoxLayoutParams() {
|
||||
Alignment = TextAnchor.MiddleCenter;
|
||||
Direction = PanelDirection.Horizontal;
|
||||
Margin = null;
|
||||
Spacing = 0.0f;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("BoxLayoutParams[Alignment={0},Direction={1},Spacing={2:F2}]",
|
||||
Alignment, Direction, Spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
mod/PLibUI/Layouts/BoxLayoutResults.cs
Normal file
147
mod/PLibUI/Layouts/BoxLayoutResults.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// A class which stores the results of a single box layout calculation pass.
|
||||
/// </summary>
|
||||
internal sealed class BoxLayoutResults {
|
||||
/// <summary>
|
||||
/// The components which were laid out.
|
||||
/// </summary>
|
||||
public readonly ICollection<LayoutSizes> children;
|
||||
|
||||
/// <summary>
|
||||
/// The current direction of flow.
|
||||
/// </summary>
|
||||
public readonly PanelDirection direction;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any spaces have been added yet for minimum size.
|
||||
/// </summary>
|
||||
private bool haveMinSpace;
|
||||
|
||||
/// <summary>
|
||||
/// Whether any spaces have been added yet for preferred size.
|
||||
/// </summary>
|
||||
private bool havePrefSpace;
|
||||
|
||||
/// <summary>
|
||||
/// The total sizes.
|
||||
/// </summary>
|
||||
public LayoutSizes total;
|
||||
|
||||
internal BoxLayoutResults(PanelDirection direction, int presize) {
|
||||
children = new List<LayoutSizes>(presize);
|
||||
this.direction = direction;
|
||||
haveMinSpace = false;
|
||||
havePrefSpace = false;
|
||||
total = new LayoutSizes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accumulates another component into the results.
|
||||
/// </summary>
|
||||
/// <param name="sizes">The size of the component to add.</param>
|
||||
/// <param name="spacing">The component spacing.</param>
|
||||
public void Accum(LayoutSizes sizes, float spacing) {
|
||||
float newMin = sizes.min, newPreferred = sizes.preferred;
|
||||
if (newMin > 0.0f) {
|
||||
// Skip one space
|
||||
if (haveMinSpace)
|
||||
newMin += spacing;
|
||||
haveMinSpace = true;
|
||||
}
|
||||
total.min += newMin;
|
||||
if (newPreferred > 0.0f) {
|
||||
// Skip one space
|
||||
if (havePrefSpace)
|
||||
newPreferred += spacing;
|
||||
havePrefSpace = true;
|
||||
}
|
||||
total.preferred += newPreferred;
|
||||
total.flexible += sizes.flexible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the results around another component.
|
||||
/// </summary>
|
||||
/// <param name="sizes">The size of the component to expand to.</param>
|
||||
public void Expand(LayoutSizes sizes) {
|
||||
float newMin = sizes.min, newPreferred = sizes.preferred, newFlexible = sizes.
|
||||
flexible;
|
||||
if (newMin > total.min)
|
||||
total.min = newMin;
|
||||
if (newPreferred > total.preferred)
|
||||
total.preferred = newPreferred;
|
||||
if (newFlexible > total.flexible)
|
||||
total.flexible = newFlexible;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return direction + " " + total;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maintains the status of a layout in progress.
|
||||
/// </summary>
|
||||
internal sealed class BoxLayoutStatus {
|
||||
/// <summary>
|
||||
/// The current direction of flow.
|
||||
/// </summary>
|
||||
public readonly PanelDirection direction;
|
||||
|
||||
/// <summary>
|
||||
/// The edge from where layout started.
|
||||
/// </summary>
|
||||
public readonly RectTransform.Edge edge;
|
||||
|
||||
/// <summary>
|
||||
/// The next component's offset.
|
||||
/// </summary>
|
||||
public readonly float offset;
|
||||
|
||||
/// <summary>
|
||||
/// The component size in that direction minus margins.
|
||||
/// </summary>
|
||||
public readonly float size;
|
||||
|
||||
internal BoxLayoutStatus(PanelDirection direction, RectOffset margins, float size) {
|
||||
this.direction = direction;
|
||||
switch (direction) {
|
||||
case PanelDirection.Horizontal:
|
||||
edge = RectTransform.Edge.Left;
|
||||
offset = margins.left;
|
||||
this.size = size - offset - margins.right;
|
||||
break;
|
||||
case PanelDirection.Vertical:
|
||||
edge = RectTransform.Edge.Top;
|
||||
offset = margins.top;
|
||||
this.size = size - offset - margins.bottom;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("direction");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
208
mod/PLibUI/Layouts/CardLayoutGroup.cs
Normal file
208
mod/PLibUI/Layouts/CardLayoutGroup.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A freezable layout manager that displays one of its contained objects at a time.
|
||||
/// Unlike other layout groups, even inactive children are considered for sizing.
|
||||
/// </summary>
|
||||
public sealed class CardLayoutGroup : AbstractLayoutGroup {
|
||||
/// <summary>
|
||||
/// Calculates the size of the card layout container.
|
||||
/// </summary>
|
||||
/// <param name="obj">The container to lay out.</param>
|
||||
/// <param name="args">The parameters to use for layout.</param>
|
||||
/// <param name="direction">The direction which is being calculated.</param>
|
||||
/// <returns>The minimum and preferred box layout size.</returns>
|
||||
private static CardLayoutResults Calc(GameObject obj, PanelDirection direction) {
|
||||
var transform = obj.AddOrGet<RectTransform>();
|
||||
int n = transform.childCount;
|
||||
var result = new CardLayoutResults(direction, n);
|
||||
var components = ListPool<Component, BoxLayoutGroup>.Allocate();
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i)?.gameObject;
|
||||
if (child != null) {
|
||||
bool active = child.activeInHierarchy;
|
||||
// Not only on active game objects
|
||||
components.Clear();
|
||||
child.GetComponents(components);
|
||||
child.SetActive(true);
|
||||
var hc = PUIUtils.CalcSizes(child, direction, components);
|
||||
if (!hc.ignore) {
|
||||
result.Expand(hc);
|
||||
result.children.Add(hc);
|
||||
}
|
||||
child.SetActive(active);
|
||||
}
|
||||
}
|
||||
components.Recycle();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lays out components in the card layout container.
|
||||
/// </summary>
|
||||
/// <param name="margin">The margin to allow around the components.</param>
|
||||
/// <param name="required">The calculated minimum and preferred sizes.</param>
|
||||
/// <param name="size">The total available size in this dimension.</param>
|
||||
private static void DoLayout(RectOffset margin, CardLayoutResults required, float size)
|
||||
{
|
||||
if (required == null)
|
||||
throw new ArgumentNullException(nameof(required));
|
||||
var direction = required.direction;
|
||||
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
|
||||
// Compensate for margins
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
size -= margin.left + margin.right;
|
||||
else
|
||||
size -= margin.top + margin.bottom;
|
||||
foreach (var child in required.children) {
|
||||
var obj = child.source;
|
||||
if (obj != null) {
|
||||
float compSize = PUIUtils.GetProperSize(child, size);
|
||||
// Place and size component
|
||||
var transform = obj.AddOrGet<RectTransform>();
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
transform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left,
|
||||
margin.left, compSize);
|
||||
else
|
||||
transform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top,
|
||||
margin.top, compSize);
|
||||
// Invoke SetLayout on dependents
|
||||
components.Clear();
|
||||
obj.GetComponents(components);
|
||||
foreach (var component in components)
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
component.SetLayoutHorizontal();
|
||||
else // if (direction == PanelDirection.Vertical)
|
||||
component.SetLayoutVertical();
|
||||
}
|
||||
}
|
||||
components.Recycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the components as a whole.
|
||||
/// </summary>
|
||||
public RectOffset Margin {
|
||||
get {
|
||||
return margin;
|
||||
}
|
||||
set {
|
||||
margin = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Results from the horizontal calculation pass.
|
||||
/// </summary>
|
||||
private CardLayoutResults horizontal;
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the components as a whole.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private RectOffset margin;
|
||||
|
||||
/// <summary>
|
||||
/// Results from the vertical calculation pass.
|
||||
/// </summary>
|
||||
private CardLayoutResults vertical;
|
||||
|
||||
internal CardLayoutGroup() {
|
||||
horizontal = null;
|
||||
layoutPriority = 1;
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputHorizontal() {
|
||||
if (!locked) {
|
||||
var margin = Margin;
|
||||
float gap = (margin == null) ? 0.0f : margin.left + margin.right;
|
||||
horizontal = Calc(gameObject, PanelDirection.Horizontal);
|
||||
var hTotal = horizontal.total;
|
||||
minWidth = hTotal.min + gap;
|
||||
preferredWidth = hTotal.preferred + gap;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputVertical() {
|
||||
if (!locked) {
|
||||
var margin = Margin;
|
||||
float gap = (margin == null) ? 0.0f : margin.top + margin.bottom;
|
||||
vertical = Calc(gameObject, PanelDirection.Vertical);
|
||||
var vTotal = vertical.total;
|
||||
minHeight = vTotal.min + gap;
|
||||
preferredHeight = vTotal.preferred + gap;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisable() {
|
||||
base.OnDisable();
|
||||
horizontal = null;
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
protected override void OnEnable() {
|
||||
base.OnEnable();
|
||||
horizontal = null;
|
||||
vertical = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the active card.
|
||||
/// </summary>
|
||||
/// <param name="card">The child to make active, or null to inactivate all children.</param>
|
||||
public void SetActiveCard(GameObject card) {
|
||||
int n = transform.childCount;
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i)?.gameObject;
|
||||
if (child != null)
|
||||
child.SetActive(child == card);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the active card.
|
||||
/// </summary>
|
||||
/// <param name="index">The child index to make active, or -1 to inactivate all children.</param>
|
||||
public void SetActiveCard(int index) {
|
||||
int n = transform.childCount;
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i)?.gameObject;
|
||||
if (child != null)
|
||||
child.SetActive(i == index);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutHorizontal() {
|
||||
if (horizontal != null && !locked)
|
||||
DoLayout(Margin ?? new RectOffset(), horizontal, rectTransform.rect.width);
|
||||
}
|
||||
|
||||
public override void SetLayoutVertical() {
|
||||
if (vertical != null && !locked)
|
||||
DoLayout(Margin ?? new RectOffset(), vertical, rectTransform.rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
mod/PLibUI/Layouts/CardLayoutResults.cs
Normal file
66
mod/PLibUI/Layouts/CardLayoutResults.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// A class which stores the results of a single card layout calculation pass.
|
||||
/// </summary>
|
||||
internal sealed class CardLayoutResults {
|
||||
/// <summary>
|
||||
/// The components which were laid out.
|
||||
/// </summary>
|
||||
public readonly ICollection<LayoutSizes> children;
|
||||
|
||||
/// <summary>
|
||||
/// The current direction of flow.
|
||||
/// </summary>
|
||||
public readonly PanelDirection direction;
|
||||
|
||||
/// <summary>
|
||||
/// The total sizes.
|
||||
/// </summary>
|
||||
public LayoutSizes total;
|
||||
|
||||
internal CardLayoutResults(PanelDirection direction, int presize) {
|
||||
children = new List<LayoutSizes>(presize);
|
||||
this.direction = direction;
|
||||
total = new LayoutSizes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands the results around another component.
|
||||
/// </summary>
|
||||
/// <param name="sizes">The size of the component to expand to.</param>
|
||||
public void Expand(LayoutSizes sizes) {
|
||||
float newMin = sizes.min, newPreferred = sizes.preferred, newFlexible =
|
||||
sizes.flexible;
|
||||
if (newMin > total.min)
|
||||
total.min = newMin;
|
||||
if (newPreferred > total.preferred)
|
||||
total.preferred = newPreferred;
|
||||
if (newFlexible > total.flexible)
|
||||
total.flexible = newFlexible;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return direction + " " + total;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
mod/PLibUI/Layouts/GridComponent.cs
Normal file
42
mod/PLibUI/Layouts/GridComponent.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A component in the grid with its placement information.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal sealed class GridComponent<T> : GridComponentSpec where T : class {
|
||||
/// <summary>
|
||||
/// The object to place here.
|
||||
/// </summary>
|
||||
public T Item { get; }
|
||||
|
||||
internal GridComponent(GridComponentSpec spec, T item) {
|
||||
Alignment = spec.Alignment;
|
||||
Item = item;
|
||||
Column = spec.Column;
|
||||
ColumnSpan = spec.ColumnSpan;
|
||||
Margin = spec.Margin;
|
||||
Row = spec.Row;
|
||||
RowSpan = spec.RowSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
166
mod/PLibUI/Layouts/GridComponentSpec.cs
Normal file
166
mod/PLibUI/Layouts/GridComponentSpec.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Stores the state of a component in a grid layout.
|
||||
/// </summary>
|
||||
public class GridComponentSpec {
|
||||
/// <summary>
|
||||
/// The alignment of the component.
|
||||
/// </summary>
|
||||
public TextAnchor Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The column of the component.
|
||||
/// </summary>
|
||||
public int Column { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of columns this component spans.
|
||||
/// </summary>
|
||||
public int ColumnSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin to allocate around each component.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The row of the component.
|
||||
/// </summary>
|
||||
public int Row { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows this component spans.
|
||||
/// </summary>
|
||||
public int RowSpan { get; set; }
|
||||
|
||||
internal GridComponentSpec() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new grid component specification. While the row and column are mandatory,
|
||||
/// the other attributes can be optionally specified in the initializer.
|
||||
/// </summary>
|
||||
/// <param name="row">The row to place the component.</param>
|
||||
/// <param name="column">The column to place the component.</param>
|
||||
public GridComponentSpec(int row, int column) {
|
||||
if (row < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(row));
|
||||
if (column < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(column));
|
||||
Alignment = TextAnchor.MiddleCenter;
|
||||
Row = row;
|
||||
Column = column;
|
||||
Margin = null;
|
||||
RowSpan = 1;
|
||||
ColumnSpan = 1;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("GridComponentSpec[Row={0:D},Column={1:D},RowSpan={2:D},ColumnSpan={3:D}]",
|
||||
Row, Column, RowSpan, ColumnSpan);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The specifications for one column in a grid layout.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class GridColumnSpec {
|
||||
/// <summary>
|
||||
/// The flexible width of this grid column. If there is space left after all
|
||||
/// columns get their nominal width, each column will get a fraction of the space
|
||||
/// left proportional to their FlexWidth value as a ratio to the total flexible
|
||||
/// width values.
|
||||
/// </summary>
|
||||
public float FlexWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The nominal width of this grid column. If zero, the preferred width of the
|
||||
/// largest component is used. If there are no components in this column (possibly
|
||||
/// because the only components in this row all have column spans from other
|
||||
/// columns), the width will be zero!
|
||||
/// </summary>
|
||||
public float Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new grid column specification.
|
||||
/// </summary>
|
||||
/// <param name="width">The column's base width, or 0 to auto-size the column to the
|
||||
/// preferred width of its largest component.</param>
|
||||
/// <param name="flex">The percentage of the leftover width the column should occupy.</param>
|
||||
public GridColumnSpec(float width = 0.0f, float flex = 0.0f) {
|
||||
if (width.IsNaNOrInfinity() || width < 0.0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(width));
|
||||
if (flex.IsNaNOrInfinity() || flex < 0.0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(flex));
|
||||
Width = width;
|
||||
FlexWidth = flex;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("GridColumnSpec[Width={0:F2}]", Width);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The specifications for one row in a grid layout.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class GridRowSpec {
|
||||
/// <summary>
|
||||
/// The flexible height of this grid row. If there is space left after all rows
|
||||
/// get their nominal height, each row will get a fraction of the space left
|
||||
/// proportional to their FlexHeight value as a ratio to the total flexible
|
||||
/// height values.
|
||||
/// </summary>
|
||||
public float FlexHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The nominal height of this grid row. If zero, the preferred height of the
|
||||
/// largest component is used. If there are no components in this row (possibly
|
||||
/// because the only components in this row all have row spans from other rows),
|
||||
/// the height will be zero!
|
||||
/// </summary>
|
||||
public float Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new grid row specification.
|
||||
/// </summary>
|
||||
/// <param name="height">The row's base width, or 0 to auto-size the row to the
|
||||
/// preferred height of its largest component.</param>
|
||||
/// <param name="flex">The percentage of the leftover height the row should occupy.</param>
|
||||
public GridRowSpec(float height = 0.0f, float flex = 0.0f) {
|
||||
if (height.IsNaNOrInfinity() || height < 0.0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(height));
|
||||
if (flex.IsNaNOrInfinity() || flex < 0.0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(flex));
|
||||
Height = height;
|
||||
FlexHeight = flex;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("GridRowSpec[Height={0:F2}]", Height);
|
||||
}
|
||||
}
|
||||
}
|
||||
304
mod/PLibUI/Layouts/GridLayoutResults.cs
Normal file
304
mod/PLibUI/Layouts/GridLayoutResults.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// A class which stores the results of a single grid layout calculation pass.
|
||||
/// </summary>
|
||||
internal sealed class GridLayoutResults {
|
||||
/// <summary>
|
||||
/// Builds a matrix of the components at each given location. Components only are
|
||||
/// entered at their origin cell (ignoring row and column span).
|
||||
/// </summary>
|
||||
/// <param name="rows">The maximum number of rows.</param>
|
||||
/// <param name="columns">The maximum number of columns.</param>
|
||||
/// <param name="components">The components to add.</param>
|
||||
/// <returns>A 2-D array of the components at a given row/column location.</returns>
|
||||
private static ICollection<SizedGridComponent>[,] GetMatrix(int rows, int columns,
|
||||
ICollection<SizedGridComponent> components) {
|
||||
var spec = new ICollection<SizedGridComponent>[rows, columns];
|
||||
foreach (var component in components) {
|
||||
int x = component.Row, y = component.Column;
|
||||
if (x >= 0 && x < rows && y >= 0 && y < columns) {
|
||||
// Multiple components are allowed at a given cell
|
||||
var atLocation = spec[x, y];
|
||||
if (atLocation == null)
|
||||
spec[x, y] = atLocation = new List<SizedGridComponent>(8);
|
||||
atLocation.Add(component);
|
||||
}
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The columns in the grid.
|
||||
/// </summary>
|
||||
public IList<GridColumnSpec> ColumnSpecs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The components in the grid, in order of addition.
|
||||
/// </summary>
|
||||
public ICollection<SizedGridComponent> Components { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of columns in the grid.
|
||||
/// </summary>
|
||||
public int Columns { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The columns in the grid with their calculated widths.
|
||||
/// </summary>
|
||||
public IList<GridColumnSpec> ComputedColumnSpecs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The rows in the grid with their calculated heights.
|
||||
/// </summary>
|
||||
public IList<GridRowSpec> ComputedRowSpecs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum total height.
|
||||
/// </summary>
|
||||
public float MinHeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum total width.
|
||||
/// </summary>
|
||||
public float MinWidth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The components which were laid out.
|
||||
/// </summary>
|
||||
public ICollection<SizedGridComponent>[,] Matrix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The rows in the grid.
|
||||
/// </summary>
|
||||
public IList<GridRowSpec> RowSpecs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows in the grid.
|
||||
/// </summary>
|
||||
public int Rows { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total flexible height weights.
|
||||
/// </summary>
|
||||
public float TotalFlexHeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total flexible width weights.
|
||||
/// </summary>
|
||||
public float TotalFlexWidth { get; private set; }
|
||||
|
||||
internal GridLayoutResults(IList<GridRowSpec> rows, IList<GridColumnSpec> columns,
|
||||
ICollection<GridComponent<GameObject>> components) {
|
||||
if (rows == null)
|
||||
throw new ArgumentNullException(nameof(rows));
|
||||
if (columns == null)
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
if (components == null)
|
||||
throw new ArgumentNullException(nameof(components));
|
||||
Columns = columns.Count;
|
||||
Rows = rows.Count;
|
||||
ColumnSpecs = columns;
|
||||
MinHeight = MinWidth = 0.0f;
|
||||
RowSpecs = rows;
|
||||
ComputedColumnSpecs = new List<GridColumnSpec>(Columns);
|
||||
ComputedRowSpecs = new List<GridRowSpec>(Rows);
|
||||
TotalFlexHeight = TotalFlexWidth = 0.0f;
|
||||
// Populate alive components
|
||||
Components = new List<SizedGridComponent>(Math.Max(components.Count, 4));
|
||||
foreach (var component in components) {
|
||||
var item = component.Item;
|
||||
if (item != null)
|
||||
Components.Add(new SizedGridComponent(component, item));
|
||||
}
|
||||
Matrix = GetMatrix(Rows, Columns, Components);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the base height of each row, the minimum it gets before extra space
|
||||
/// is distributed.
|
||||
/// </summary>
|
||||
internal void CalcBaseHeights() {
|
||||
int rows = Rows;
|
||||
MinHeight = TotalFlexHeight = 0.0f;
|
||||
ComputedRowSpecs.Clear();
|
||||
for (int row = 0; row < rows; row++) {
|
||||
var spec = RowSpecs[row];
|
||||
float height = spec.Height, flex = spec.FlexHeight;
|
||||
if (height <= 0.0f)
|
||||
// Auto height
|
||||
for (int i = 0; i < Columns; i++)
|
||||
height = Math.Max(height, PreferredHeightAt(row, i));
|
||||
if (flex > 0.0f)
|
||||
TotalFlexHeight += flex;
|
||||
ComputedRowSpecs.Add(new GridRowSpec(height, flex));
|
||||
}
|
||||
foreach (var component in Components)
|
||||
if (component.RowSpan > 1)
|
||||
ExpandMultiRow(component);
|
||||
// Min height is calculated after all multirow components are distributed
|
||||
for (int row = 0; row < rows; row++)
|
||||
MinHeight += ComputedRowSpecs[row].Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the base width of each row, the minimum it gets before extra space
|
||||
/// is distributed.
|
||||
/// </summary>
|
||||
internal void CalcBaseWidths() {
|
||||
int columns = Columns;
|
||||
MinWidth = TotalFlexWidth = 0.0f;
|
||||
ComputedColumnSpecs.Clear();
|
||||
for (int column = 0; column < columns; column++) {
|
||||
var spec = ColumnSpecs[column];
|
||||
float width = spec.Width, flex = spec.FlexWidth;
|
||||
if (width <= 0.0f)
|
||||
// Auto width
|
||||
for (int i = 0; i < Rows; i++)
|
||||
width = Math.Max(width, PreferredWidthAt(i, column));
|
||||
if (flex > 0.0f)
|
||||
TotalFlexWidth += flex;
|
||||
ComputedColumnSpecs.Add(new GridColumnSpec(width, flex));
|
||||
}
|
||||
foreach (var component in Components)
|
||||
if (component.ColumnSpan > 1)
|
||||
ExpandMultiColumn(component);
|
||||
// Min width is calculated after all multicolumn components are distributed
|
||||
for (int column = 0; column < columns; column++)
|
||||
MinWidth += ComputedColumnSpecs[column].Width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a multicolumn component, ratiometrically splits up any excess preferred size
|
||||
/// among the columns in its span that have a flexible width.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to reallocate sizes.</param>
|
||||
private void ExpandMultiColumn(SizedGridComponent component) {
|
||||
float need = component.HorizontalSize.preferred, totalFlex = 0.0f;
|
||||
int start = component.Column, end = start + component.ColumnSpan;
|
||||
for (int i = start; i < end; i++) {
|
||||
var spec = ComputedColumnSpecs[i];
|
||||
if (spec.FlexWidth > 0.0f)
|
||||
totalFlex += spec.FlexWidth;
|
||||
need -= spec.Width;
|
||||
}
|
||||
if (need > 0.0f && totalFlex > 0.0f)
|
||||
// No flex = we can do nothing about it
|
||||
for (int i = start; i < end; i++) {
|
||||
var spec = ComputedColumnSpecs[i];
|
||||
float flex = spec.FlexWidth;
|
||||
if (flex > 0.0f)
|
||||
ComputedColumnSpecs[i] = new GridColumnSpec(spec.Width + flex * need /
|
||||
totalFlex, flex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For a multirow component, ratiometrically splits up any excess preferred size
|
||||
/// among the rows in its span that have a flexible height.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to reallocate sizes.</param>
|
||||
private void ExpandMultiRow(SizedGridComponent component) {
|
||||
float need = component.VerticalSize.preferred, totalFlex = 0.0f;
|
||||
int start = component.Row, end = start + component.RowSpan;
|
||||
for (int i = start; i < end; i++) {
|
||||
var spec = ComputedRowSpecs[i];
|
||||
if (spec.FlexHeight > 0.0f)
|
||||
totalFlex += spec.FlexHeight;
|
||||
need -= spec.Height;
|
||||
}
|
||||
if (need > 0.0f && totalFlex > 0.0f)
|
||||
// No flex = we can do nothing about it
|
||||
for (int i = start; i < end; i++) {
|
||||
var spec = ComputedRowSpecs[i];
|
||||
float flex = spec.FlexHeight;
|
||||
if (flex > 0.0f)
|
||||
ComputedRowSpecs[i] = new GridRowSpec(spec.Height + flex * need /
|
||||
totalFlex, flex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the preferred height of a cell.
|
||||
/// </summary>
|
||||
/// <param name="row">The cell's row.</param>
|
||||
/// <param name="column">The cell's column.</param>
|
||||
/// <returns>The preferred height.</returns>
|
||||
private float PreferredHeightAt(int row, int column) {
|
||||
float size = 0.0f;
|
||||
var atLocation = Matrix[row, column];
|
||||
if (atLocation != null && atLocation.Count > 0)
|
||||
foreach (var component in atLocation) {
|
||||
var sizes = component.VerticalSize;
|
||||
if (component.RowSpan < 2)
|
||||
size = Math.Max(size, sizes.preferred);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the preferred width of a cell.
|
||||
/// </summary>
|
||||
/// <param name="row">The cell's row.</param>
|
||||
/// <param name="column">The cell's column.</param>
|
||||
/// <returns>The preferred width.</returns>
|
||||
private float PreferredWidthAt(int row, int column) {
|
||||
float size = 0.0f;
|
||||
var atLocation = Matrix[row, column];
|
||||
if (atLocation != null && atLocation.Count > 0)
|
||||
foreach (var component in atLocation) {
|
||||
var sizes = component.HorizontalSize;
|
||||
if (component.ColumnSpan < 2)
|
||||
size = Math.Max(size, sizes.preferred);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A component in the grid with its sizes computed.
|
||||
/// </summary>
|
||||
internal sealed class SizedGridComponent : GridComponentSpec {
|
||||
/// <summary>
|
||||
/// The object and its computed horizontal sizes.
|
||||
/// </summary>
|
||||
public LayoutSizes HorizontalSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The object and its computed vertical sizes.
|
||||
/// </summary>
|
||||
public LayoutSizes VerticalSize { get; set; }
|
||||
|
||||
internal SizedGridComponent(GridComponentSpec spec, GameObject item) {
|
||||
Alignment = spec.Alignment;
|
||||
Column = spec.Column;
|
||||
ColumnSpan = spec.ColumnSpan;
|
||||
Margin = spec.Margin;
|
||||
Row = spec.Row;
|
||||
RowSpan = spec.RowSpan;
|
||||
HorizontalSize = new LayoutSizes(item);
|
||||
VerticalSize = new LayoutSizes(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
349
mod/PLibUI/Layouts/PGridLayoutGroup.cs
Normal file
349
mod/PLibUI/Layouts/PGridLayoutGroup.cs
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Implements a flexible version of the base GridLayout.
|
||||
/// </summary>
|
||||
public sealed class PGridLayoutGroup : AbstractLayoutGroup {
|
||||
/// <summary>
|
||||
/// Calculates all column widths.
|
||||
/// </summary>
|
||||
/// <param name="results">The results from layout.</param>
|
||||
/// <param name="width">The current container width.</param>
|
||||
/// <param name="margin">The margins within the borders.</param>
|
||||
/// <returns>The column widths.</returns>
|
||||
private static float[] GetColumnWidths(GridLayoutResults results, float width,
|
||||
RectOffset margin) {
|
||||
int columns = results.Columns;
|
||||
// Find out how much flexible size can be given out
|
||||
float position = margin?.left ?? 0, right = margin?.right ?? 0;
|
||||
float actualWidth = width - position - right, totalFlex = results.TotalFlexWidth,
|
||||
excess = (totalFlex > 0.0f) ? (actualWidth - results.MinWidth) / totalFlex :
|
||||
0.0f;
|
||||
float[] colX = new float[columns + 1];
|
||||
// Determine start of columns
|
||||
for (int i = 0; i < columns; i++) {
|
||||
var spec = results.ComputedColumnSpecs[i];
|
||||
colX[i] = position;
|
||||
position += spec.Width + spec.FlexWidth * excess;
|
||||
}
|
||||
colX[columns] = position;
|
||||
return colX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates all row heights.
|
||||
/// </summary>
|
||||
/// <param name="results">The results from layout.</param>
|
||||
/// <param name="height">The current container height.</param>
|
||||
/// <param name="margin">The margins within the borders.</param>
|
||||
/// <returns>The row heights.</returns>
|
||||
private static float[] GetRowHeights(GridLayoutResults results, float height,
|
||||
RectOffset margin) {
|
||||
int rows = results.Rows;
|
||||
// Find out how much flexible size can be given out
|
||||
float position = margin?.bottom ?? 0, top = margin?.top ?? 0;
|
||||
float actualWidth = height - position - top, totalFlex = results.TotalFlexHeight,
|
||||
excess = (totalFlex > 0.0f) ? (actualWidth - results.MinHeight) / totalFlex :
|
||||
0.0f;
|
||||
float[] rowY = new float[rows + 1];
|
||||
// Determine start of rows
|
||||
for (int i = 0; i < rows; i++) {
|
||||
var spec = results.ComputedRowSpecs[i];
|
||||
rowY[i] = position;
|
||||
position += spec.Height + spec.FlexHeight * excess;
|
||||
}
|
||||
rowY[rows] = position;
|
||||
return rowY;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the final height of this component and applies it to the component.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to calculate.</param>
|
||||
/// <param name="rowY">The row locations from GetRowHeights.</param>
|
||||
/// <returns>true if the height was applied, or false if the component was not laid out
|
||||
/// due to being disposed or set to ignore layout.</returns>
|
||||
private static bool SetFinalHeight(SizedGridComponent component, float[] rowY) {
|
||||
var margin = component.Margin;
|
||||
var sizes = component.VerticalSize;
|
||||
var target = sizes.source;
|
||||
bool ok = !sizes.ignore && target != null;
|
||||
if (ok) {
|
||||
int rows = rowY.Length - 1;
|
||||
// Clamp first and last row occupied by this object
|
||||
int first = component.Row, last = first + component.RowSpan;
|
||||
first = first.InRange(0, rows - 1);
|
||||
last = last.InRange(1, rows);
|
||||
// Align correctly in the cell box
|
||||
float y = rowY[first], rowHeight = rowY[last] - y;
|
||||
if (margin != null) {
|
||||
float border = margin.top + margin.bottom;
|
||||
y += margin.top;
|
||||
rowHeight -= border;
|
||||
sizes.min -= border;
|
||||
sizes.preferred -= border;
|
||||
}
|
||||
float actualHeight = PUIUtils.GetProperSize(sizes, rowHeight);
|
||||
// Take alignment into account
|
||||
y += PUIUtils.GetOffset(component.Alignment, PanelDirection.Vertical,
|
||||
rowHeight - actualHeight);
|
||||
target.rectTransform().SetInsetAndSizeFromParentEdge(RectTransform.Edge.
|
||||
Top, y, actualHeight);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the final width of this component and applies it to the component.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to calculate.</param>
|
||||
/// <param name="colX">The column locations from GetColumnWidths.</param>
|
||||
/// <returns>true if the width was applied, or false if the component was not laid out
|
||||
/// due to being disposed or set to ignore layout.</returns>
|
||||
private static bool SetFinalWidth(SizedGridComponent component, float[] colX) {
|
||||
var margin = component.Margin;
|
||||
var sizes = component.HorizontalSize;
|
||||
var target = sizes.source;
|
||||
bool ok = !sizes.ignore && target != null;
|
||||
if (ok) {
|
||||
int columns = colX.Length - 1;
|
||||
// Clamp first and last column occupied by this object
|
||||
int first = component.Column, last = first + component.ColumnSpan;
|
||||
first = first.InRange(0, columns - 1);
|
||||
last = last.InRange(1, columns);
|
||||
// Align correctly in the cell box
|
||||
float x = colX[first], colWidth = colX[last] - x;
|
||||
if (margin != null) {
|
||||
float border = margin.left + margin.right;
|
||||
x += margin.left;
|
||||
colWidth -= border;
|
||||
sizes.min -= border;
|
||||
sizes.preferred -= border;
|
||||
}
|
||||
float actualWidth = PUIUtils.GetProperSize(sizes, colWidth);
|
||||
// Take alignment into account
|
||||
x += PUIUtils.GetOffset(component.Alignment, PanelDirection.Horizontal,
|
||||
colWidth - actualWidth);
|
||||
target.rectTransform().SetInsetAndSizeFromParentEdge(RectTransform.Edge.
|
||||
Left, x, actualWidth);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the components as a whole.
|
||||
/// </summary>
|
||||
public RectOffset Margin {
|
||||
get {
|
||||
return margin;
|
||||
}
|
||||
set {
|
||||
margin = value;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0044 // Cannot be readonly for Unity serialization to work
|
||||
/// <summary>
|
||||
/// The children of this panel.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private IList<GridComponent<GameObject>> children;
|
||||
|
||||
/// <summary>
|
||||
/// The columns in this panel.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private IList<GridColumnSpec> columns;
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the components as a whole.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private RectOffset margin;
|
||||
|
||||
/// <summary>
|
||||
/// The current layout status.
|
||||
/// </summary>
|
||||
private GridLayoutResults results;
|
||||
|
||||
/// <summary>
|
||||
/// The rows in this panel.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private IList<GridRowSpec> rows;
|
||||
#pragma warning restore IDE0044
|
||||
|
||||
internal PGridLayoutGroup() {
|
||||
children = new List<GridComponent<GameObject>>(16);
|
||||
columns = new List<GridColumnSpec>(16);
|
||||
rows = new List<GridRowSpec>(16);
|
||||
layoutPriority = 1;
|
||||
Margin = null;
|
||||
results = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to this grid layout.
|
||||
/// </summary>
|
||||
/// <param name="column">The specification for that column.</param>
|
||||
public void AddColumn(GridColumnSpec column) {
|
||||
if (column == null)
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
columns.Add(column);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a component to this layout. Components added through other means to the
|
||||
/// transform will not be laid out at all!
|
||||
/// </summary>
|
||||
/// <param name="child">The child to add.</param>
|
||||
/// <param name="spec">The location where the child will be placed.</param>
|
||||
public void AddComponent(GameObject child, GridComponentSpec spec) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
if (spec == null)
|
||||
throw new ArgumentNullException(nameof(spec));
|
||||
children.Add(new GridComponent<GameObject>(spec, child));
|
||||
child.SetParent(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to this grid layout.
|
||||
/// </summary>
|
||||
/// <param name="row">The specification for that row.</param>
|
||||
public void AddRow(GridRowSpec row) {
|
||||
if (row == null)
|
||||
throw new ArgumentNullException(nameof(row));
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputHorizontal() {
|
||||
if (!locked) {
|
||||
results = new GridLayoutResults(rows, columns, children);
|
||||
var elements = ListPool<Component, PGridLayoutGroup>.Allocate();
|
||||
foreach (var component in results.Components) {
|
||||
// Cache size of children
|
||||
var obj = component.HorizontalSize.source;
|
||||
var margin = component.Margin;
|
||||
elements.Clear();
|
||||
obj.GetComponents(elements);
|
||||
var sz = PUIUtils.CalcSizes(obj, PanelDirection.Horizontal, elements);
|
||||
if (!sz.ignore) {
|
||||
// Add borders
|
||||
int border = (margin == null) ? 0 : margin.left + margin.right;
|
||||
sz.min += border;
|
||||
sz.preferred += border;
|
||||
}
|
||||
component.HorizontalSize = sz;
|
||||
}
|
||||
elements.Recycle();
|
||||
// Calculate columns sizes and our size
|
||||
results.CalcBaseWidths();
|
||||
float width = results.MinWidth;
|
||||
if (Margin != null)
|
||||
width += Margin.left + Margin.right;
|
||||
minWidth = preferredWidth = width;
|
||||
flexibleWidth = (results.TotalFlexWidth > 0.0f) ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputVertical() {
|
||||
if (results != null && !locked) {
|
||||
var elements = ListPool<Component, PGridLayoutGroup>.Allocate();
|
||||
foreach (var component in results.Components) {
|
||||
// Cache size of children
|
||||
var obj = component.VerticalSize.source;
|
||||
var margin = component.Margin;
|
||||
elements.Clear();
|
||||
obj.GetComponents(elements);
|
||||
var sz = PUIUtils.CalcSizes(obj, PanelDirection.Vertical, elements);
|
||||
if (!sz.ignore) {
|
||||
// Add borders
|
||||
int border = (margin == null) ? 0 : margin.top + margin.bottom;
|
||||
sz.min += border;
|
||||
sz.preferred += border;
|
||||
}
|
||||
component.VerticalSize = sz;
|
||||
}
|
||||
elements.Recycle();
|
||||
// Calculate row sizes and our size
|
||||
results.CalcBaseHeights();
|
||||
float height = results.MinHeight;
|
||||
if (Margin != null)
|
||||
height += Margin.bottom + Margin.top;
|
||||
minHeight = preferredHeight = height;
|
||||
flexibleHeight = (results.TotalFlexHeight > 0.0f) ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisable() {
|
||||
base.OnDisable();
|
||||
results = null;
|
||||
}
|
||||
|
||||
protected override void OnEnable() {
|
||||
base.OnEnable();
|
||||
results = null;
|
||||
}
|
||||
|
||||
public override void SetLayoutHorizontal() {
|
||||
var obj = gameObject;
|
||||
if (results != null && obj != null && results.Columns > 0 && !locked) {
|
||||
float[] colX = GetColumnWidths(results, rectTransform.rect.width, Margin);
|
||||
// All components lay out
|
||||
var controllers = ListPool<ILayoutController, PGridLayoutGroup>.Allocate();
|
||||
foreach (var component in results.Components)
|
||||
if (SetFinalWidth(component, colX)) {
|
||||
// Lay out all children
|
||||
controllers.Clear();
|
||||
component.HorizontalSize.source.GetComponents(controllers);
|
||||
foreach (var controller in controllers)
|
||||
controller.SetLayoutHorizontal();
|
||||
}
|
||||
controllers.Recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutVertical() {
|
||||
var obj = gameObject;
|
||||
if (results != null && obj != null && results.Rows > 0 && !locked) {
|
||||
float[] rowY = GetRowHeights(results, rectTransform.rect.height, Margin);
|
||||
// All components lay out
|
||||
var controllers = ListPool<ILayoutController, PGridLayoutGroup>.Allocate();
|
||||
foreach (var component in results.Components)
|
||||
if (!SetFinalHeight(component, rowY)) {
|
||||
// Lay out all children
|
||||
controllers.Clear();
|
||||
component.VerticalSize.source.GetComponents(controllers);
|
||||
foreach (var controller in controllers)
|
||||
controller.SetLayoutVertical();
|
||||
}
|
||||
controllers.Recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
292
mod/PLibUI/Layouts/RelativeLayoutGroup.cs
Normal file
292
mod/PLibUI/Layouts/RelativeLayoutGroup.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A layout group based on the constraints defined in RelativeLayout. Allows the same
|
||||
/// fast relative positioning that RelativeLayout does, but can respond to changes in the
|
||||
/// size of its containing components.
|
||||
/// </summary>
|
||||
public sealed class RelativeLayoutGroup : AbstractLayoutGroup,
|
||||
ISerializationCallbackReceiver {
|
||||
/// <summary>
|
||||
/// The margin added around all components in the layout. This is in addition to any
|
||||
/// margins around the components.
|
||||
///
|
||||
/// Note that this margin is not taken into account with percentage based anchors.
|
||||
/// Items anchored to the extremes will always work fine. Items anchored in the middle
|
||||
/// will use the middle <b>before</b> margins are effective.
|
||||
/// </summary>
|
||||
public RectOffset Margin {
|
||||
get {
|
||||
return margin;
|
||||
}
|
||||
set {
|
||||
margin = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constraints for each object are stored here.
|
||||
/// </summary>
|
||||
private readonly IDictionary<GameObject, RelativeLayoutParams> locConstraints;
|
||||
|
||||
/// <summary>
|
||||
/// The serialized constraints.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private IList<KeyValuePair<GameObject, RelativeLayoutParams>> serialConstraints;
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the components as a whole.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private RectOffset margin;
|
||||
|
||||
/// <summary>
|
||||
/// The results of the layout in progress.
|
||||
/// </summary>
|
||||
private readonly IList<RelativeLayoutResults> results;
|
||||
|
||||
internal RelativeLayoutGroup() {
|
||||
layoutPriority = 1;
|
||||
locConstraints = new Dictionary<GameObject, RelativeLayoutParams>(32);
|
||||
results = new List<RelativeLayoutResults>(32);
|
||||
serialConstraints = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the parameters for a child game object. Creates an entry if none exists
|
||||
/// for this component.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to look up.</param>
|
||||
/// <returns>The parameters for that object.</returns>
|
||||
private RelativeLayoutParams AddOrGet(GameObject item) {
|
||||
if (!locConstraints.TryGetValue(item, out RelativeLayoutParams param))
|
||||
locConstraints[item] = param = new RelativeLayoutParams();
|
||||
return param;
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup AnchorXAxis(GameObject item, float anchor = 0.5f) {
|
||||
SetLeftEdge(item, fraction: anchor);
|
||||
return SetRightEdge(item, fraction: anchor);
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup AnchorYAxis(GameObject item, float anchor = 0.5f) {
|
||||
SetTopEdge(item, fraction: anchor);
|
||||
return SetBottomEdge(item, fraction: anchor);
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputHorizontal() {
|
||||
var all = gameObject?.rectTransform();
|
||||
if (all != null && !locked) {
|
||||
int ml, mr, passes, limit;
|
||||
if (Margin == null)
|
||||
ml = mr = 0;
|
||||
else {
|
||||
ml = Margin.left;
|
||||
mr = Margin.right;
|
||||
}
|
||||
// X layout
|
||||
results.CalcX(all, locConstraints);
|
||||
if (results.Count > 0) {
|
||||
limit = 2 * results.Count;
|
||||
for (passes = 0; passes < limit && !results.RunPassX(); passes++) ;
|
||||
if (passes >= limit)
|
||||
results.ThrowUnresolvable(passes, PanelDirection.Horizontal);
|
||||
}
|
||||
minWidth = preferredWidth = results.GetMinSizeX() + ml + mr;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputVertical() {
|
||||
var all = gameObject?.rectTransform();
|
||||
if (all != null && !locked) {
|
||||
int passes, limit = 2 * results.Count, mt, mb;
|
||||
if (Margin == null)
|
||||
mt = mb = 0;
|
||||
else {
|
||||
mt = Margin.top;
|
||||
mb = Margin.bottom;
|
||||
}
|
||||
// Y layout
|
||||
if (results.Count > 0) {
|
||||
results.CalcY();
|
||||
for (passes = 0; passes < limit && !results.RunPassY(); passes++) ;
|
||||
if (passes >= limit)
|
||||
results.ThrowUnresolvable(passes, PanelDirection.Vertical);
|
||||
}
|
||||
minHeight = preferredHeight = results.GetMinSizeY() + mt + mb;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports the data from RelativeLayout for compatibility.
|
||||
/// </summary>
|
||||
/// <param name="values">The raw data to import.</param>
|
||||
internal void Import(IDictionary<GameObject, RelativeLayoutParams> values) {
|
||||
locConstraints.Clear();
|
||||
foreach (var pair in values)
|
||||
locConstraints[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize() {
|
||||
int n = locConstraints.Count;
|
||||
if (n > 0) {
|
||||
serialConstraints = new List<KeyValuePair<GameObject, RelativeLayoutParams>>(n);
|
||||
foreach (var pair in locConstraints)
|
||||
serialConstraints.Add(pair);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAfterDeserialize() {
|
||||
if (serialConstraints != null) {
|
||||
locConstraints.Clear();
|
||||
foreach (var pair in serialConstraints)
|
||||
locConstraints[pair.Key] = pair.Value;
|
||||
serialConstraints = null;
|
||||
}
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup OverrideSize(GameObject item, Vector2 size) {
|
||||
if (item != null)
|
||||
AddOrGet(item).OverrideSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup SetBottomEdge(GameObject item, float fraction = -1.0f,
|
||||
GameObject above = null) {
|
||||
if (item != null) {
|
||||
if (above == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(AddOrGet(item).BottomEdge, fraction, above);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override void SetDirty() {
|
||||
if (!locked)
|
||||
base.SetDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a component's edge constraint.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to set.</param>
|
||||
/// <param name="fraction">The fraction of the parent to anchor.</param>
|
||||
/// <param name="child">The other component to anchor.</param>
|
||||
private void SetEdge(RelativeLayoutParams.EdgeStatus edge, float fraction,
|
||||
GameObject child) {
|
||||
if (fraction >= 0.0f && fraction <= 1.0f) {
|
||||
edge.Constraint = RelativeConstraintType.ToAnchor;
|
||||
edge.FromAnchor = fraction;
|
||||
edge.FromComponent = null;
|
||||
} else if (child != null) {
|
||||
edge.Constraint = RelativeConstraintType.ToComponent;
|
||||
edge.FromComponent = child;
|
||||
} else {
|
||||
edge.Constraint = RelativeConstraintType.Unconstrained;
|
||||
edge.FromComponent = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutHorizontal() {
|
||||
if (!locked && results.Count > 0) {
|
||||
var components = ListPool<ILayoutController, RelativeLayoutGroup>.Allocate();
|
||||
int ml, mr;
|
||||
if (Margin == null)
|
||||
ml = mr = 0;
|
||||
else {
|
||||
ml = Margin.left;
|
||||
mr = Margin.right;
|
||||
}
|
||||
// Lay out children
|
||||
results.ExecuteX(components, ml, mr);
|
||||
components.Recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutVertical() {
|
||||
if (!locked && results.Count > 0) {
|
||||
var components = ListPool<ILayoutController, RelativeLayoutGroup>.Allocate();
|
||||
int mt, mb;
|
||||
if (Margin == null)
|
||||
mt = mb = 0;
|
||||
else {
|
||||
mt = Margin.top;
|
||||
mb = Margin.bottom;
|
||||
}
|
||||
// Lay out children
|
||||
results.ExecuteY(components, mt, mb);
|
||||
components.Recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup SetLeftEdge(GameObject item, float fraction = -1.0f,
|
||||
GameObject toRight = null) {
|
||||
if (item != null) {
|
||||
if (toRight == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(AddOrGet(item).LeftEdge, fraction, toRight);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup SetMargin(GameObject item, RectOffset insets) {
|
||||
if (item != null)
|
||||
AddOrGet(item).Insets = insets;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all layout parameters of an object at once.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to configure.</param>
|
||||
/// <param name="rawParams">The raw parameters to use.</param>
|
||||
internal void SetRaw(GameObject item, RelativeLayoutParams rawParams) {
|
||||
if (item != null && rawParams != null)
|
||||
locConstraints[item] = rawParams;
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup SetRightEdge(GameObject item, float fraction = -1.0f,
|
||||
GameObject toLeft = null) {
|
||||
if (item != null) {
|
||||
if (toLeft == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(AddOrGet(item).RightEdge, fraction, toLeft);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public RelativeLayoutGroup SetTopEdge(GameObject item, float fraction = -1.0f,
|
||||
GameObject below = null) {
|
||||
if (item != null) {
|
||||
if (below == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(AddOrGet(item).TopEdge, fraction, below);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
185
mod/PLibUI/Layouts/RelativeLayoutParams.cs
Normal file
185
mod/PLibUI/Layouts/RelativeLayoutParams.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// Stores constraints applied to a game object in a relative layout.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal sealed class RelativeLayoutParams : RelativeLayoutParamsBase<GameObject> { }
|
||||
|
||||
/// <summary>
|
||||
/// Stores constraints applied to an object in a relative layout.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the target object.</typeparam>
|
||||
internal class RelativeLayoutParamsBase<T> {
|
||||
/// <summary>
|
||||
/// The anchored position of the bottom edge.
|
||||
/// </summary>
|
||||
internal EdgeStatus BottomEdge { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The insets. If null, insets are all zero.
|
||||
/// </summary>
|
||||
internal RectOffset Insets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The anchored position of the left edge.
|
||||
/// </summary>
|
||||
internal EdgeStatus LeftEdge { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the size of the component if set.
|
||||
/// </summary>
|
||||
internal Vector2 OverrideSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The anchored position of the right edge.
|
||||
/// </summary>
|
||||
internal EdgeStatus RightEdge { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The anchored position of the top edge.
|
||||
/// </summary>
|
||||
internal EdgeStatus TopEdge { get; }
|
||||
|
||||
internal RelativeLayoutParamsBase() {
|
||||
Insets = null;
|
||||
BottomEdge = new EdgeStatus();
|
||||
LeftEdge = new EdgeStatus();
|
||||
RightEdge = new EdgeStatus();
|
||||
TopEdge = new EdgeStatus();
|
||||
OverrideSize = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The edge position determined for a component.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal sealed class EdgeStatus {
|
||||
/// <summary>
|
||||
/// The type of constraint to use for this relative layout.
|
||||
/// </summary>
|
||||
internal RelativeConstraintType Constraint;
|
||||
|
||||
/// <summary>
|
||||
/// The anchor position in the component that sets the relative anchor.
|
||||
///
|
||||
/// 0.0f is the bottom/left, 1.0f is the top/right.
|
||||
/// </summary>
|
||||
internal float FromAnchor;
|
||||
|
||||
/// <summary>
|
||||
/// The component to which this edge is anchored.
|
||||
/// </summary>
|
||||
internal T FromComponent;
|
||||
|
||||
/// <summary>
|
||||
/// The offset in pixels from the anchor. + is upwards/rightwards, - is downwards/
|
||||
/// leftwards.
|
||||
/// </summary>
|
||||
internal float Offset;
|
||||
|
||||
/// <summary>
|
||||
/// True if the position has been locked down in the code.
|
||||
/// Locked should only be set by the layout manager, crashes may occur otherwise.
|
||||
/// </summary>
|
||||
public bool Locked {
|
||||
get {
|
||||
return Constraint == RelativeConstraintType.Locked;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the position is not constrained to anything.
|
||||
/// </summary>
|
||||
public bool Unconstrained {
|
||||
get {
|
||||
return Constraint == RelativeConstraintType.Unconstrained;
|
||||
}
|
||||
}
|
||||
|
||||
internal EdgeStatus() {
|
||||
FromAnchor = 0.0f;
|
||||
FromComponent = default;
|
||||
Offset = 0.0f;
|
||||
Constraint = RelativeConstraintType.Unconstrained;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies data from another edge status object.
|
||||
/// </summary>
|
||||
/// <param name="other">The object to copy.</param>
|
||||
internal void CopyFrom(EdgeStatus other) {
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
switch (Constraint = other.Constraint) {
|
||||
case RelativeConstraintType.ToComponent:
|
||||
FromComponent = other.FromComponent;
|
||||
break;
|
||||
case RelativeConstraintType.ToAnchor:
|
||||
FromAnchor = other.FromAnchor;
|
||||
break;
|
||||
case RelativeConstraintType.Locked:
|
||||
FromAnchor = other.FromAnchor;
|
||||
Offset = other.Offset;
|
||||
break;
|
||||
case RelativeConstraintType.Unconstrained:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
// Caution: floats are imprecise
|
||||
return obj is EdgeStatus other && other.FromAnchor == FromAnchor && other.
|
||||
Offset == Offset && Constraint == other.Constraint;
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return 37 * (37 * FromAnchor.GetHashCode() + Offset.GetHashCode()) +
|
||||
Constraint.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets these offsets to unlocked.
|
||||
/// </summary>
|
||||
public void Reset() {
|
||||
Constraint = RelativeConstraintType.Unconstrained;
|
||||
Offset = 0.0f;
|
||||
FromAnchor = 0.0f;
|
||||
FromComponent = default;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("EdgeStatus[Constraints={2},Anchor={0:F2},Offset={1:F2}]",
|
||||
FromAnchor, Offset, Constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The types of constraints which can be applied to components in a relative layout.
|
||||
/// </summary>
|
||||
internal enum RelativeConstraintType {
|
||||
Unconstrained, ToComponent, ToAnchor, Locked
|
||||
}
|
||||
}
|
||||
132
mod/PLibUI/Layouts/RelativeLayoutResults.cs
Normal file
132
mod/PLibUI/Layouts/RelativeLayoutResults.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// Parameters used to store the dynamic data of an object during a relative layout.
|
||||
/// </summary>
|
||||
internal sealed class RelativeLayoutResults : RelativeLayoutParamsBase<GameObject> {
|
||||
/// <summary>
|
||||
/// A set of insets that are always zero.
|
||||
/// </summary>
|
||||
private static readonly RectOffset ZERO = new RectOffset();
|
||||
|
||||
/// <summary>
|
||||
/// The instance parameters of the bottom edge's component.
|
||||
/// </summary>
|
||||
internal RelativeLayoutResults BottomParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The height of the component plus its margin box.
|
||||
/// </summary>
|
||||
internal float EffectiveHeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the component plus its margin box.
|
||||
/// </summary>
|
||||
internal float EffectiveWidth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The instance parameters of the left edge's component.
|
||||
/// </summary>
|
||||
internal RelativeLayoutResults LeftParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred height at which this component will be laid out, unless both
|
||||
/// edges are constrained.
|
||||
/// </summary>
|
||||
internal float PreferredHeight {
|
||||
get {
|
||||
return prefSize.y;
|
||||
}
|
||||
set {
|
||||
prefSize.y = value;
|
||||
EffectiveHeight = value + Insets.top + Insets.bottom;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The preferred width at which this component will be laid out, unless both
|
||||
/// edges are constrained.
|
||||
/// </summary>
|
||||
internal float PreferredWidth {
|
||||
get {
|
||||
return prefSize.x;
|
||||
}
|
||||
set {
|
||||
prefSize.x = value;
|
||||
EffectiveWidth = value + Insets.left + Insets.right;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The instance parameters of the right edge's component.
|
||||
/// </summary>
|
||||
internal RelativeLayoutResults RightParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The instance parameters of the top edge's component.
|
||||
/// </summary>
|
||||
internal RelativeLayoutResults TopParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The object to lay out.
|
||||
/// </summary>
|
||||
internal RectTransform Transform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the size delta should be used in the X direction (as opposed to offsets).
|
||||
/// </summary>
|
||||
internal bool UseSizeDeltaX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the size delta should be used in the Y direction (as opposed to offsets).
|
||||
/// </summary>
|
||||
internal bool UseSizeDeltaY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred size of this component.
|
||||
/// </summary>
|
||||
private Vector2 prefSize;
|
||||
|
||||
internal RelativeLayoutResults(RectTransform transform, RelativeLayoutParams other) {
|
||||
Transform = transform ?? throw new ArgumentNullException(nameof(transform));
|
||||
if (other != null) {
|
||||
BottomEdge.CopyFrom(other.BottomEdge);
|
||||
TopEdge.CopyFrom(other.TopEdge);
|
||||
RightEdge.CopyFrom(other.RightEdge);
|
||||
LeftEdge.CopyFrom(other.LeftEdge);
|
||||
Insets = other.Insets ?? ZERO;
|
||||
OverrideSize = other.OverrideSize;
|
||||
} else
|
||||
Insets = ZERO;
|
||||
BottomParams = LeftParams = TopParams = RightParams = null;
|
||||
PreferredWidth = PreferredHeight = 0.0f;
|
||||
UseSizeDeltaX = UseSizeDeltaY = false;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
var go = Transform.gameObject;
|
||||
return string.Format("component={0} {1:F2}x{2:F2}", (go == null) ? "null" : go.
|
||||
name, prefSize.x, prefSize.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
401
mod/PLibUI/Layouts/RelativeLayoutUtil.cs
Normal file
401
mod/PLibUI/Layouts/RelativeLayoutUtil.cs
Normal file
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using EdgeStatus = PeterHan.PLib.UI.Layouts.RelativeLayoutParams.EdgeStatus;
|
||||
|
||||
namespace PeterHan.PLib.UI.Layouts {
|
||||
/// <summary>
|
||||
/// A helper class for RelativeLayout.
|
||||
/// </summary>
|
||||
internal static class RelativeLayoutUtil {
|
||||
/// <summary>
|
||||
/// Initializes and computes horizontal sizes for the components in this relative
|
||||
/// layout.
|
||||
/// </summary>
|
||||
/// <param name="children">The location to store information about these components.</param>
|
||||
/// <param name="all">The components to lay out.</param>
|
||||
/// <param name="constraints">The constraints defined for these components.</param>
|
||||
internal static void CalcX(this ICollection<RelativeLayoutResults> children,
|
||||
RectTransform all, IDictionary<GameObject, RelativeLayoutParams> constraints) {
|
||||
var comps = ListPool<Component, RelativeLayoutGroup>.Allocate();
|
||||
var paramMap = DictionaryPool<GameObject, RelativeLayoutResults,
|
||||
RelativeLayoutGroup>.Allocate();
|
||||
int n = all.childCount;
|
||||
children.Clear();
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = all.GetChild(i)?.gameObject;
|
||||
if (child != null) {
|
||||
comps.Clear();
|
||||
// Calculate the preferred size using all layout components
|
||||
child.GetComponents(comps);
|
||||
var horiz = PUIUtils.CalcSizes(child, PanelDirection.Horizontal, comps);
|
||||
if (!horiz.ignore) {
|
||||
RelativeLayoutResults ip;
|
||||
float w = horiz.preferred;
|
||||
if (constraints.TryGetValue(child, out RelativeLayoutParams cons)) {
|
||||
ip = new RelativeLayoutResults(child.rectTransform(), cons);
|
||||
// Set override size by axis if necessary
|
||||
var overrideSize = ip.OverrideSize;
|
||||
if (overrideSize.x > 0.0f)
|
||||
w = overrideSize.x;
|
||||
paramMap[child] = ip;
|
||||
} else
|
||||
// Default its layout to fill all
|
||||
ip = new RelativeLayoutResults(child.rectTransform(), null);
|
||||
ip.PreferredWidth = w;
|
||||
children.Add(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Resolve object references to other children
|
||||
foreach (var ip in children) {
|
||||
ip.TopParams = InitResolve(ip.TopEdge, paramMap);
|
||||
ip.BottomParams = InitResolve(ip.BottomEdge, paramMap);
|
||||
ip.LeftParams = InitResolve(ip.LeftEdge, paramMap);
|
||||
ip.RightParams = InitResolve(ip.RightEdge, paramMap);
|
||||
// All of these will die simultaneously when the list is recycled
|
||||
}
|
||||
paramMap.Recycle();
|
||||
comps.Recycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes vertical sizes for the components in this relative layout.
|
||||
/// </summary>
|
||||
/// <param name="children">The location to store information about these components.</param>
|
||||
internal static void CalcY(this ICollection<RelativeLayoutResults> children) {
|
||||
var comps = ListPool<Component, RelativeLayoutGroup>.Allocate();
|
||||
foreach (var ip in children) {
|
||||
var child = ip.Transform.gameObject;
|
||||
var overrideSize = ip.OverrideSize;
|
||||
comps.Clear();
|
||||
// Calculate the preferred size using all layout components
|
||||
child.gameObject.GetComponents(comps);
|
||||
float h = PUIUtils.CalcSizes(child, PanelDirection.Vertical, comps).preferred;
|
||||
// Set override size by axis if necessary
|
||||
if (overrideSize.y > 0.0f)
|
||||
h = overrideSize.y;
|
||||
ip.PreferredHeight = h;
|
||||
}
|
||||
comps.Recycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the minimum size the component must be to support a specific child
|
||||
/// component.
|
||||
/// </summary>
|
||||
/// <param name="min">The lower edge constraint.</param>
|
||||
/// <param name="max">The upper edge constraint.</param>
|
||||
/// <param name="effective">The component size in that dimension plus margins.</param>
|
||||
/// <returns>The minimum parent component size to fit the child.</returns>
|
||||
internal static float ElbowRoom(EdgeStatus min, EdgeStatus max, float effective) {
|
||||
float aMin = min.FromAnchor, aMax = max.FromAnchor, result, offMin = min.Offset,
|
||||
offMax = max.Offset;
|
||||
if (aMax > aMin)
|
||||
// "Elbow room" method
|
||||
result = (effective + offMin - offMax) / (aMax - aMin);
|
||||
else
|
||||
// Anchors are together
|
||||
result = Math.Max(effective, Math.Max(Math.Abs(offMin), Math.Abs(offMax)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the horizontal layout.
|
||||
/// </summary>
|
||||
/// <param name="children">The components to lay out.</param>
|
||||
/// <param name="scratch">The location where components will be temporarily stored.</param>
|
||||
/// <param name="mLeft">The left margin.</param>
|
||||
/// <param name="mRight">The right margin.</param>
|
||||
internal static void ExecuteX(this IEnumerable<RelativeLayoutResults> children,
|
||||
List<ILayoutController> scratch, float mLeft = 0.0f, float mRight = 0.0f) {
|
||||
foreach (var child in children) {
|
||||
var rt = child.Transform;
|
||||
var insets = child.Insets;
|
||||
EdgeStatus l = child.LeftEdge, r = child.RightEdge;
|
||||
// Set corner positions
|
||||
rt.anchorMin = new Vector2(l.FromAnchor, 0.0f);
|
||||
rt.anchorMax = new Vector2(r.FromAnchor, 1.0f);
|
||||
// If anchored by pivot, resize with current anchors
|
||||
if (child.UseSizeDeltaX)
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, child.
|
||||
PreferredWidth);
|
||||
else {
|
||||
// Left
|
||||
rt.offsetMin = new Vector2(l.Offset + insets.left + (l.FromAnchor <= 0.0f ?
|
||||
mLeft : 0.0f), rt.offsetMin.y);
|
||||
// Right
|
||||
rt.offsetMax = new Vector2(r.Offset - insets.right - (r.FromAnchor >=
|
||||
1.0f ? mRight : 0.0f), rt.offsetMax.y);
|
||||
}
|
||||
// Execute layout controllers if present
|
||||
scratch.Clear();
|
||||
rt.gameObject.GetComponents(scratch);
|
||||
foreach (var component in scratch)
|
||||
component.SetLayoutHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the vertical layout.
|
||||
/// </summary>
|
||||
/// <param name="children">The components to lay out.</param>
|
||||
/// <param name="scratch">The location where components will be temporarily stored.</param>
|
||||
/// <param name="mBottom">The bottom margin.</param>
|
||||
/// <param name="mTop">The top margin.</param>
|
||||
internal static void ExecuteY(this IEnumerable<RelativeLayoutResults> children,
|
||||
List<ILayoutController> scratch, float mBottom = 0.0f, float mTop = 0.0f) {
|
||||
foreach (var child in children) {
|
||||
var rt = child.Transform;
|
||||
var insets = child.Insets;
|
||||
EdgeStatus t = child.TopEdge, b = child.BottomEdge;
|
||||
// Set corner positions
|
||||
rt.anchorMin = new Vector2(rt.anchorMin.x, b.FromAnchor);
|
||||
rt.anchorMax = new Vector2(rt.anchorMax.x, t.FromAnchor);
|
||||
// If anchored by pivot, resize with current anchors
|
||||
if (child.UseSizeDeltaY)
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, child.
|
||||
PreferredHeight);
|
||||
else {
|
||||
// Bottom
|
||||
rt.offsetMin = new Vector2(rt.offsetMin.x, b.Offset + insets.bottom +
|
||||
(b.FromAnchor <= 0.0f ? mBottom : 0.0f));
|
||||
// Top
|
||||
rt.offsetMax = new Vector2(rt.offsetMax.x, t.Offset - insets.top -
|
||||
(t.FromAnchor >= 1.0f ? mTop : 0.0f));
|
||||
}
|
||||
// Execute layout controllers if present
|
||||
scratch.Clear();
|
||||
rt.gameObject.GetComponents(scratch);
|
||||
foreach (var component in scratch)
|
||||
component.SetLayoutVertical();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the minimum size in the X direction.
|
||||
/// </summary>
|
||||
/// <param name="children">The components to lay out.</param>
|
||||
/// <returns>The minimum horizontal size.</returns>
|
||||
internal static float GetMinSizeX(this IEnumerable<RelativeLayoutResults> children) {
|
||||
float maxWidth = 0.0f;
|
||||
foreach (var child in children) {
|
||||
float width = ElbowRoom(child.LeftEdge, child.RightEdge, child.EffectiveWidth);
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the minimum size in the Y direction.
|
||||
/// </summary>
|
||||
/// <param name="children">The components to lay out.</param>
|
||||
/// <returns>The minimum vertical size.</returns>
|
||||
internal static float GetMinSizeY(this IEnumerable<RelativeLayoutResults> children) {
|
||||
float maxHeight = 0.0f;
|
||||
foreach (var child in children) {
|
||||
float height = ElbowRoom(child.BottomEdge, child.TopEdge, child.
|
||||
EffectiveHeight);
|
||||
if (height > maxHeight) maxHeight = height;
|
||||
}
|
||||
return maxHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a component reference if needed.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to resolve.</param>
|
||||
/// <param name="lookup">The location where the component can be looked up.</param>
|
||||
/// <returns>The linked parameters for that edge if needed.</returns>
|
||||
private static RelativeLayoutResults InitResolve(EdgeStatus edge,
|
||||
IDictionary<GameObject, RelativeLayoutResults> lookup) {
|
||||
RelativeLayoutResults result = null;
|
||||
if (edge.Constraint == RelativeConstraintType.ToComponent)
|
||||
if (!lookup.TryGetValue(edge.FromComponent, out result))
|
||||
edge.Constraint = RelativeConstraintType.Unconstrained;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks both edges if they are constrained to the same anchor.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to check.</param>
|
||||
/// <param name="otherEdge">The other edge to check.</param>
|
||||
/// <returns>true if it was able to lock, or false otherwise.</returns>
|
||||
private static bool LockEdgeAnchor(EdgeStatus edge, EdgeStatus otherEdge) {
|
||||
bool useDelta = edge.Constraint == RelativeConstraintType.ToAnchor && otherEdge.
|
||||
Constraint == RelativeConstraintType.ToAnchor && edge.FromAnchor == otherEdge.
|
||||
FromAnchor;
|
||||
if (useDelta) {
|
||||
edge.Constraint = RelativeConstraintType.Locked;
|
||||
otherEdge.Constraint = RelativeConstraintType.Locked;
|
||||
edge.Offset = 0.0f;
|
||||
otherEdge.Offset = 0.0f;
|
||||
}
|
||||
return useDelta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks an edge if it is constrained to an anchor.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to check.</param>
|
||||
private static void LockEdgeAnchor(EdgeStatus edge) {
|
||||
if (edge.Constraint == RelativeConstraintType.ToAnchor) {
|
||||
edge.Constraint = RelativeConstraintType.Locked;
|
||||
edge.Offset = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks an edge if it can be determined from another component.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to check.</param>
|
||||
/// <param name="offset">The component's offset in that direction.</param>
|
||||
/// <param name="otherEdge">The opposing edge of the referenced component.</param>
|
||||
private static void LockEdgeComponent(EdgeStatus edge, EdgeStatus otherEdge) {
|
||||
if (edge.Constraint == RelativeConstraintType.ToComponent && otherEdge.Locked) {
|
||||
edge.Constraint = RelativeConstraintType.Locked;
|
||||
edge.FromAnchor = otherEdge.FromAnchor;
|
||||
edge.Offset = otherEdge.Offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks an edge if it can be determined from the other edge.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to check.</param>
|
||||
/// <param name="size">The component's effective size in that direction.</param>
|
||||
/// <param name="opposing">The component's other edge.</param>
|
||||
private static void LockEdgeRelative(EdgeStatus edge, float size, EdgeStatus opposing)
|
||||
{
|
||||
if (edge.Constraint == RelativeConstraintType.Unconstrained) {
|
||||
if (opposing.Locked) {
|
||||
edge.Constraint = RelativeConstraintType.Locked;
|
||||
edge.FromAnchor = opposing.FromAnchor;
|
||||
edge.Offset = opposing.Offset + size;
|
||||
} else if (opposing.Constraint == RelativeConstraintType.Unconstrained) {
|
||||
// Both unconstrained, full size
|
||||
edge.Constraint = RelativeConstraintType.Locked;
|
||||
edge.FromAnchor = 0.0f;
|
||||
edge.Offset = 0.0f;
|
||||
opposing.Constraint = RelativeConstraintType.Locked;
|
||||
opposing.FromAnchor = 1.0f;
|
||||
opposing.Offset = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a layout pass in the X direction, resolving edges that can be resolved.
|
||||
/// </summary>
|
||||
/// <param name="children">The children to resolve.</param>
|
||||
/// <returns>true if all children have all X edges constrained, or false otherwise.</returns>
|
||||
internal static bool RunPassX(this IEnumerable<RelativeLayoutResults> children) {
|
||||
bool done = true;
|
||||
foreach (var child in children) {
|
||||
float width = child.EffectiveWidth;
|
||||
EdgeStatus l = child.LeftEdge, r = child.RightEdge;
|
||||
if (LockEdgeAnchor(l, r))
|
||||
child.UseSizeDeltaX = true;
|
||||
LockEdgeAnchor(l);
|
||||
LockEdgeAnchor(r);
|
||||
LockEdgeRelative(l, -width, r);
|
||||
LockEdgeRelative(r, width, l);
|
||||
// Lock to other components
|
||||
if (child.LeftParams != null)
|
||||
LockEdgeComponent(l, child.LeftParams.RightEdge);
|
||||
if (child.RightParams != null)
|
||||
LockEdgeComponent(r, child.RightParams.LeftEdge);
|
||||
if (!l.Locked || !r.Locked) done = false;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a layout pass in the Y direction, resolving edges that can be resolved.
|
||||
/// </summary>
|
||||
/// <param name="children">The children to resolve.</param>
|
||||
/// <returns>true if all children have all Y edges constrained, or false otherwise.</returns>
|
||||
internal static bool RunPassY(this IEnumerable<RelativeLayoutResults> children) {
|
||||
bool done = true;
|
||||
foreach (var child in children) {
|
||||
float height = child.EffectiveHeight;
|
||||
EdgeStatus t = child.TopEdge, b = child.BottomEdge;
|
||||
if (LockEdgeAnchor(t, b))
|
||||
child.UseSizeDeltaY = true;
|
||||
LockEdgeAnchor(b);
|
||||
LockEdgeAnchor(t);
|
||||
LockEdgeRelative(b, -height, t);
|
||||
LockEdgeRelative(t, height, b);
|
||||
// Lock to other components
|
||||
if (child.BottomParams != null)
|
||||
LockEdgeComponent(b, child.BottomParams.TopEdge);
|
||||
if (child.TopParams != null)
|
||||
LockEdgeComponent(t, child.TopParams.BottomEdge);
|
||||
if (!t.Locked || !b.Locked) done = false;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an error when resolution fails.
|
||||
/// </summary>
|
||||
/// <param name="children">The children, some of which failed to resolve.</param>
|
||||
/// <param name="limit">The number of passes executed before failing.</param>
|
||||
/// <param name="direction">The direction that failed.</param>
|
||||
internal static void ThrowUnresolvable(this IEnumerable<RelativeLayoutResults> children,
|
||||
int limit, PanelDirection direction) {
|
||||
var message = new StringBuilder(256);
|
||||
message.Append("After ").Append(limit);
|
||||
message.Append(" passes, unable to complete resolution of RelativeLayout:");
|
||||
foreach (var child in children) {
|
||||
string name = child.Transform.gameObject.name;
|
||||
if (direction == PanelDirection.Horizontal) {
|
||||
if (!child.LeftEdge.Locked) {
|
||||
message.Append(' ');
|
||||
message.Append(name);
|
||||
message.Append(".Left");
|
||||
}
|
||||
if (!child.RightEdge.Locked) {
|
||||
message.Append(' ');
|
||||
message.Append(name);
|
||||
message.Append(".Right");
|
||||
}
|
||||
} else {
|
||||
if (!child.BottomEdge.Locked) {
|
||||
message.Append(' ');
|
||||
message.Append(name);
|
||||
message.Append(".Bottom");
|
||||
}
|
||||
if (!child.TopEdge.Locked) {
|
||||
message.Append(' ');
|
||||
message.Append(name);
|
||||
message.Append(".Top");
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException(message.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
173
mod/PLibUI/PButton.cs
Normal file
173
mod/PLibUI/PButton.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI button factory class.
|
||||
/// </summary>
|
||||
public class PButton : PTextComponent {
|
||||
/// <summary>
|
||||
/// The default margins around a button.
|
||||
/// </summary>
|
||||
internal static readonly RectOffset BUTTON_MARGIN = new RectOffset(7, 7, 5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the button to have the right sound and background image.
|
||||
/// </summary>
|
||||
/// <param name="button">The button to set up.</param>
|
||||
/// <param name="bgImage">The background image.</param>
|
||||
internal static void SetupButton(KButton button, KImage bgImage) {
|
||||
UIDetours.ADDITIONAL_K_IMAGES.Set(button, new KImage[0]);
|
||||
UIDetours.SOUND_PLAYER_BUTTON.Set(button, PUITuning.ButtonSounds);
|
||||
UIDetours.BG_IMAGE.Set(button, bgImage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the background image to have the right sprite and slice type.
|
||||
/// </summary>
|
||||
/// <param name="bgImage">The image that forms the button background.</param>
|
||||
internal static void SetupButtonBackground(KImage bgImage) {
|
||||
UIDetours.APPLY_COLOR_STYLE.Invoke(bgImage);
|
||||
bgImage.sprite = PUITuning.Images.ButtonBorder;
|
||||
bgImage.type = Image.Type.Sliced;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables a realized button.
|
||||
/// </summary>
|
||||
/// <param name="obj">The realized button object.</param>
|
||||
/// <param name="enabled">true to make it enabled, or false to make it disabled (greyed out).</param>
|
||||
public static void SetButtonEnabled(GameObject obj, bool enabled) {
|
||||
if (obj != null && obj.TryGetComponent(out KButton button))
|
||||
UIDetours.IS_INTERACTABLE.Set(button, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The button's background color.
|
||||
/// </summary>
|
||||
public ColorStyleSetting Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger on click. It is passed the realized source object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnButtonPressed OnClick { get; set; }
|
||||
|
||||
public PButton() : this(null) { }
|
||||
|
||||
public PButton(string name) : base(name ?? "Button") {
|
||||
Margin = BUTTON_MARGIN;
|
||||
Sprite = null;
|
||||
Text = null;
|
||||
ToolTip = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this button is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PButton AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
var button = PUIElements.CreateUI(null, Name);
|
||||
GameObject sprite = null, text = null;
|
||||
// Background
|
||||
var bgImage = button.AddComponent<KImage>();
|
||||
var bgColorStyle = Color ?? PUITuning.Colors.ButtonPinkStyle;
|
||||
UIDetours.COLOR_STYLE_SETTING.Set(bgImage, bgColorStyle);
|
||||
SetupButtonBackground(bgImage);
|
||||
// Set on click event
|
||||
var kButton = button.AddComponent<KButton>();
|
||||
var evt = OnClick;
|
||||
if (evt != null)
|
||||
// Detouring an Event is not worth the effort
|
||||
kButton.onClick += () => evt?.Invoke(button);
|
||||
SetupButton(kButton, bgImage);
|
||||
// Add foreground image since the background already has one
|
||||
if (Sprite != null) {
|
||||
var fgImage = ImageChildHelper(button, this);
|
||||
UIDetours.FG_IMAGE.Set(kButton, fgImage);
|
||||
sprite = fgImage.gameObject;
|
||||
}
|
||||
// Add text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
text = TextChildHelper(button, TextStyle ?? PUITuning.Fonts.UILightStyle,
|
||||
Text).gameObject;
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(button, ToolTip).SetActive(true);
|
||||
// Arrange the icon and text
|
||||
var layout = button.AddComponent<RelativeLayoutGroup>();
|
||||
layout.Margin = Margin;
|
||||
GameObject inner;
|
||||
ArrangeComponent(layout, inner = WrapTextAndSprite(text, sprite), TextAlignment);
|
||||
if (!DynamicSize) layout.LockLayout();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
DestroyLayoutIfPossible(button);
|
||||
InvokeRealize(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the sprite to a leftward facing arrow. Beware the size, scale the button down!
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PButton SetImageLeftArrow() {
|
||||
Sprite = PUITuning.Images.Arrow;
|
||||
SpriteTransform = ImageTransform.FlipHorizontal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the sprite to a rightward facing arrow. Beware the size, scale the button
|
||||
/// down!
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PButton SetImageRightArrow() {
|
||||
Sprite = PUITuning.Images.Arrow;
|
||||
SpriteTransform = ImageTransform.None;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink button style as this button's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PButton SetKleiPinkStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
Color = PUITuning.Colors.ButtonPinkStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue button style as this button's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PButton SetKleiBlueStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
Color = PUITuning.Colors.ButtonBlueStyle;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
270
mod/PLibUI/PCheckBox.cs
Normal file
270
mod/PLibUI/PCheckBox.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI button check box factory class.
|
||||
/// </summary>
|
||||
public class PCheckBox : PTextComponent {
|
||||
/// <summary>
|
||||
/// The border size between the checkbox border and icon.
|
||||
/// </summary>
|
||||
private const float CHECKBOX_MARGIN = 2.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The unchecked state.
|
||||
/// </summary>
|
||||
public const int STATE_UNCHECKED = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The checked state.
|
||||
/// </summary>
|
||||
public const int STATE_CHECKED = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The partially checked state.
|
||||
/// </summary>
|
||||
public const int STATE_PARTIAL = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Generates the checkbox image states.
|
||||
/// </summary>
|
||||
/// <param name="imageColor">The color style for the checked icon.</param>
|
||||
/// <returns>The states for this checkbox.</returns>
|
||||
private static ToggleState[] GenerateStates(ColorStyleSetting imageColor) {
|
||||
// Structs sadly do not work well with detours
|
||||
var sps = new StatePresentationSetting() {
|
||||
color = imageColor.activeColor, use_color_on_hover = true,
|
||||
color_on_hover = imageColor.hoverColor, image_target = null,
|
||||
name = "Partial"
|
||||
};
|
||||
return new ToggleState[] {
|
||||
new ToggleState() {
|
||||
// Unchecked
|
||||
color = PUITuning.Colors.Transparent, color_on_hover = PUITuning.Colors.
|
||||
Transparent, sprite = null, use_color_on_hover = false,
|
||||
additional_display_settings = new StatePresentationSetting[] {
|
||||
new StatePresentationSetting() {
|
||||
color = imageColor.activeColor, use_color_on_hover = false,
|
||||
image_target = null, name = "Unchecked"
|
||||
}
|
||||
}
|
||||
},
|
||||
new ToggleState() {
|
||||
// Checked
|
||||
color = imageColor.activeColor, color_on_hover = imageColor.hoverColor,
|
||||
sprite = PUITuning.Images.Checked, use_color_on_hover = true,
|
||||
additional_display_settings = new StatePresentationSetting[] { sps }
|
||||
},
|
||||
new ToggleState() {
|
||||
// Partial
|
||||
color = imageColor.activeColor, color_on_hover = imageColor.hoverColor,
|
||||
sprite = PUITuning.Images.Partial, use_color_on_hover = true,
|
||||
additional_display_settings = new StatePresentationSetting[] { sps }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a realized check box's state.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized check box.</param>
|
||||
/// <returns>The check box state.</returns>
|
||||
public static int GetCheckState(GameObject realized) {
|
||||
int state = STATE_UNCHECKED;
|
||||
if (realized == null)
|
||||
throw new ArgumentNullException(nameof(realized));
|
||||
var checkButton = realized.GetComponentInChildren<MultiToggle>();
|
||||
if (checkButton != null)
|
||||
state = UIDetours.CURRENT_STATE.Get(checkButton);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a realized check box's state.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized check box.</param>
|
||||
/// <param name="state">The new state to set.</param>
|
||||
public static void SetCheckState(GameObject realized, int state) {
|
||||
if (realized == null)
|
||||
throw new ArgumentNullException(nameof(realized));
|
||||
var checkButton = realized.GetComponentInChildren<MultiToggle>();
|
||||
if (checkButton != null && UIDetours.CURRENT_STATE.Get(checkButton) != state)
|
||||
UIDetours.CHANGE_STATE.Invoke(checkButton, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The check box color.
|
||||
/// </summary>
|
||||
public ColorStyleSetting CheckColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The check box's background color.
|
||||
///
|
||||
/// Unlike other components, this color applies only to the check box itself.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size to scale the check box. If 0x0, it will not be scaled.
|
||||
/// </summary>
|
||||
public Vector2 CheckSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The background color of everything that is not the check box.
|
||||
/// </summary>
|
||||
public Color ComponentBackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial check box state.
|
||||
/// </summary>
|
||||
public int InitialState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger on click. It is passed the realized source object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnChecked OnChecked { get; set; }
|
||||
|
||||
public PCheckBox() : this(null) { }
|
||||
|
||||
public PCheckBox(string name) : base(name ?? "CheckBox") {
|
||||
BackColor = PUITuning.Colors.BackgroundLight;
|
||||
CheckColor = null;
|
||||
CheckSize = new Vector2(16.0f, 16.0f);
|
||||
ComponentBackColor = PUITuning.Colors.Transparent;
|
||||
IconSpacing = 3;
|
||||
InitialState = STATE_UNCHECKED;
|
||||
Sprite = null;
|
||||
Text = null;
|
||||
ToolTip = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this check box is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This check box for call chaining.</returns>
|
||||
public PCheckBox AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
var checkbox = PUIElements.CreateUI(null, Name);
|
||||
var actualSize = CheckSize;
|
||||
GameObject sprite = null, text = null;
|
||||
// Background
|
||||
if (ComponentBackColor.a > 0)
|
||||
checkbox.AddComponent<Image>().color = ComponentBackColor;
|
||||
var trueColor = CheckColor ?? PUITuning.Colors.ComponentLightStyle;
|
||||
// Checkbox background
|
||||
var checkBG = PUIElements.CreateUI(checkbox, "CheckBox");
|
||||
checkBG.AddComponent<Image>().color = BackColor;
|
||||
var checkImage = CreateCheckImage(checkBG, trueColor, ref actualSize);
|
||||
checkBG.SetUISize(new Vector2(actualSize.x + 2.0f * CHECKBOX_MARGIN,
|
||||
actualSize.y + 2.0f * CHECKBOX_MARGIN), true);
|
||||
// Add foreground image
|
||||
if (Sprite != null)
|
||||
sprite = ImageChildHelper(checkbox, this).gameObject;
|
||||
// Add text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
text = TextChildHelper(checkbox, TextStyle ?? PUITuning.Fonts.UILightStyle,
|
||||
Text).gameObject;
|
||||
// Toggle
|
||||
var mToggle = checkbox.AddComponent<MultiToggle>();
|
||||
var evt = OnChecked;
|
||||
if (evt != null)
|
||||
mToggle.onClick += () => evt?.Invoke(checkbox, mToggle.CurrentState);
|
||||
UIDetours.PLAY_SOUND_CLICK.Set(mToggle, true);
|
||||
UIDetours.PLAY_SOUND_RELEASE.Set(mToggle, false);
|
||||
mToggle.states = GenerateStates(trueColor);
|
||||
mToggle.toggle_image = checkImage;
|
||||
UIDetours.CHANGE_STATE.Invoke(mToggle, InitialState);
|
||||
PUIElements.SetToolTip(checkbox, ToolTip).SetActive(true);
|
||||
// Faster than ever!
|
||||
var subLabel = WrapTextAndSprite(text, sprite);
|
||||
var layout = checkbox.AddComponent<RelativeLayoutGroup>();
|
||||
layout.Margin = Margin;
|
||||
ArrangeComponent(layout, WrapTextAndSprite(subLabel, checkBG), TextAlignment);
|
||||
if (!DynamicSize) layout.LockLayout();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
DestroyLayoutIfPossible(checkbox);
|
||||
InvokeRealize(checkbox);
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the actual image that shows the checkbox graphically.
|
||||
/// </summary>
|
||||
/// <param name="checkbox">The parent object to add the image.</param>
|
||||
/// <param name="color">The color style for the box border.</param>
|
||||
/// <param name="actualSize">The actual check mark size, which will be updated if it
|
||||
/// is 0x0 to the default size.</param>
|
||||
/// <returns>The image reference to the checkmark image itself.</returns>
|
||||
private Image CreateCheckImage(GameObject checkbox, ColorStyleSetting color,
|
||||
ref Vector2 actualSize) {
|
||||
// Checkbox border (grr rule of only one Graphics per GO...)
|
||||
var checkBorder = PUIElements.CreateUI(checkbox, "CheckBorder");
|
||||
var borderImg = checkBorder.AddComponent<Image>();
|
||||
borderImg.sprite = PUITuning.Images.CheckBorder;
|
||||
borderImg.color = color.activeColor;
|
||||
borderImg.type = Image.Type.Sliced;
|
||||
// Checkbox foreground
|
||||
var imageChild = PUIElements.CreateUI(checkbox, "CheckMark", true, PUIAnchoring.
|
||||
Center, PUIAnchoring.Center);
|
||||
var checkImage = imageChild.AddComponent<Image>();
|
||||
checkImage.sprite = PUITuning.Images.Checked;
|
||||
checkImage.preserveAspect = true;
|
||||
// Determine the checkbox size
|
||||
if (actualSize.x <= 0.0f || actualSize.y <= 0.0f) {
|
||||
var rt = imageChild.rectTransform();
|
||||
actualSize.x = LayoutUtility.GetPreferredWidth(rt);
|
||||
actualSize.y = LayoutUtility.GetPreferredHeight(rt);
|
||||
}
|
||||
imageChild.SetUISize(CheckSize, false);
|
||||
return checkImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink button style as this check box's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This check box for call chaining.</returns>
|
||||
public PCheckBox SetKleiPinkStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
CheckColor = PUITuning.Colors.ComponentDarkStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue button style as this check box's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This check box for call chaining.</returns>
|
||||
public PCheckBox SetKleiBlueStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
CheckColor = PUITuning.Colors.ComponentDarkStyle;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
315
mod/PLibUI/PComboBox.cs
Normal file
315
mod/PLibUI/PComboBox.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI combo box factory class.
|
||||
/// </summary>
|
||||
public sealed class PComboBox<T> : IUIComponent, IDynamicSizable where T : class,
|
||||
IListableOption {
|
||||
/// <summary>
|
||||
/// The default margin around items in the pulldown.
|
||||
/// </summary>
|
||||
private static readonly RectOffset DEFAULT_ITEM_MARGIN = new RectOffset(3, 3, 3, 3);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected option in a realized combo box.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized combo box.</param>
|
||||
/// <param name="option">The option to set.</param>
|
||||
/// <param name="fireListener">true to fire the on select listener, or false otherwise.</param>
|
||||
public static void SetSelectedItem(GameObject realized, IListableOption option,
|
||||
bool fireListener = false) {
|
||||
if (option != null && realized != null && realized.TryGetComponent(
|
||||
out PComboBoxComponent component))
|
||||
component.SetSelectedItem(option, fireListener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The size of the sprite used to expand/contract the options.
|
||||
/// </summary>
|
||||
public Vector2 ArrowSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo box's background color.
|
||||
/// </summary>
|
||||
public ColorStyleSetting BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the check mark sprite used on the selected option.
|
||||
/// </summary>
|
||||
public Vector2 CheckSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content of this combo box.
|
||||
/// </summary>
|
||||
public IEnumerable<T> Content { get; set; }
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The background color for each entry in the combo box pulldown.
|
||||
/// </summary>
|
||||
public ColorStyleSetting EntryColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this combo box.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initially selected item of the combo box.
|
||||
/// </summary>
|
||||
public T InitialItem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin around each item in the pulldown.
|
||||
/// </summary>
|
||||
public RectOffset ItemMargin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the component.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items to be shown at once before a scroll bar is added.
|
||||
/// </summary>
|
||||
public int MaxRowsShown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum width in units (not characters!) of this text field.
|
||||
/// </summary>
|
||||
public int MinWidth { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger when an item is selected. It is passed the realized source
|
||||
/// object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnDropdownChanged<T> OnOptionSelected { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
/// <summary>
|
||||
/// The text alignment in the combo box.
|
||||
/// </summary>
|
||||
public TextAnchor TextAlignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo box's text color, font, word wrap settings, and font size.
|
||||
/// </summary>
|
||||
public TextStyleSetting TextStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
public PComboBox() : this("Dropdown") { }
|
||||
|
||||
public PComboBox(string name) {
|
||||
ArrowSize = new Vector2(8.0f, 8.0f);
|
||||
BackColor = null;
|
||||
CheckSize = new Vector2(12.0f, 12.0f);
|
||||
Content = null;
|
||||
DynamicSize = false;
|
||||
FlexSize = Vector2.zero;
|
||||
InitialItem = null;
|
||||
ItemMargin = DEFAULT_ITEM_MARGIN;
|
||||
Margin = PButton.BUTTON_MARGIN;
|
||||
MaxRowsShown = 6;
|
||||
MinWidth = 0;
|
||||
Name = name;
|
||||
TextAlignment = TextAnchor.MiddleLeft;
|
||||
TextStyle = null;
|
||||
ToolTip = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this combo box is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This combo box for call chaining.</returns>
|
||||
public PComboBox<T> AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var combo = PUIElements.CreateUI(null, Name);
|
||||
var style = TextStyle ?? PUITuning.Fonts.UILightStyle;
|
||||
var entryColor = EntryColor ?? PUITuning.Colors.ButtonBlueStyle;
|
||||
RectOffset margin = Margin, im = ItemMargin;
|
||||
// Background color
|
||||
var bgImage = combo.AddComponent<KImage>();
|
||||
var backColorStyle = BackColor ?? PUITuning.Colors.ButtonBlueStyle;
|
||||
UIDetours.COLOR_STYLE_SETTING.Set(bgImage, backColorStyle);
|
||||
PButton.SetupButtonBackground(bgImage);
|
||||
// Need a LocText (selected item)
|
||||
var selection = PUIElements.CreateUI(combo, "SelectedItem");
|
||||
if (MinWidth > 0)
|
||||
selection.SetMinUISize(new Vector2(MinWidth, 0.0f));
|
||||
var selectedLabel = PUIElements.AddLocText(selection, style);
|
||||
// Vertical flow panel with the choices
|
||||
var contentContainer = PUIElements.CreateUI(null, "Content");
|
||||
contentContainer.AddComponent<VerticalLayoutGroup>().childForceExpandWidth = true;
|
||||
// Scroll pane with items is laid out below everything else
|
||||
var pullDown = new PScrollPane("PullDown") {
|
||||
ScrollHorizontal = false, ScrollVertical = true, AlwaysShowVertical = true,
|
||||
FlexSize = Vector2.right, TrackSize = 8.0f, BackColor = entryColor.
|
||||
inactiveColor
|
||||
}.BuildScrollPane(combo, contentContainer);
|
||||
// Add a black border (Does not work, covered by the combo box buttons...)
|
||||
#if false
|
||||
var pdImage = pullDown.GetComponent<Image>();
|
||||
pdImage.sprite = PUITuning.Images.BoxBorder;
|
||||
pdImage.type = Image.Type.Sliced;
|
||||
#endif
|
||||
pullDown.rectTransform().pivot = new Vector2(0.5f, 1.0f);
|
||||
// Initialize the drop down
|
||||
var comboBox = combo.AddComponent<PComboBoxComponent>();
|
||||
comboBox.CheckColor = style.textColor;
|
||||
comboBox.ContentContainer = contentContainer.rectTransform();
|
||||
comboBox.EntryPrefab = BuildRowPrefab(style, entryColor);
|
||||
comboBox.MaxRowsShown = MaxRowsShown;
|
||||
comboBox.Pulldown = pullDown;
|
||||
comboBox.SelectedLabel = selectedLabel;
|
||||
comboBox.SetItems(Content);
|
||||
comboBox.SetSelectedItem(InitialItem);
|
||||
comboBox.OnSelectionChanged = (obj, item) => OnOptionSelected?.Invoke(obj.
|
||||
gameObject, item as T);
|
||||
// Inner component with the pulldown image
|
||||
var image = PUIElements.CreateUI(combo, "OpenImage");
|
||||
var icon = image.AddComponent<Image>();
|
||||
icon.sprite = PUITuning.Images.Contract;
|
||||
icon.color = style.textColor;
|
||||
// Button component
|
||||
var dropButton = combo.AddComponent<KButton>();
|
||||
PButton.SetupButton(dropButton, bgImage);
|
||||
UIDetours.FG_IMAGE.Set(dropButton, icon);
|
||||
dropButton.onClick += comboBox.OnClick;
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(selection, ToolTip);
|
||||
combo.SetActive(true);
|
||||
// Button gets laid out on the right, rest of space goes to the label
|
||||
// Scroll pane is laid out on the bottom
|
||||
var layout = combo.AddComponent<RelativeLayoutGroup>();
|
||||
layout.AnchorYAxis(selection).SetLeftEdge(selection, fraction:
|
||||
0.0f).SetRightEdge(selection, toLeft: image).AnchorYAxis(image).SetRightEdge(
|
||||
image, fraction: 1.0f).SetMargin(selection, new RectOffset(margin.left,
|
||||
im.right, margin.top, margin.bottom)).SetMargin(image, new RectOffset(0,
|
||||
margin.right, margin.top, margin.bottom)).OverrideSize(image, ArrowSize).
|
||||
AnchorYAxis(pullDown, 0.0f).OverrideSize(pullDown, Vector2.up);
|
||||
layout.LockLayout();
|
||||
if (DynamicSize)
|
||||
layout.UnlockLayout();
|
||||
// Disable sizing on the pulldown
|
||||
pullDown.AddOrGet<LayoutElement>().ignoreLayout = true;
|
||||
// Scroll pane is hidden right away
|
||||
pullDown.SetActive(false);
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
OnRealize?.Invoke(combo);
|
||||
return combo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a row selection prefab object for this combo box.
|
||||
/// </summary>
|
||||
/// <param name="style">The text style for the entries.</param>
|
||||
/// <param name="entryColor">The color for the entry backgrounds.</param>
|
||||
/// <returns>A template for each row in the dropdown.</returns>
|
||||
private GameObject BuildRowPrefab(TextStyleSetting style, ColorStyleSetting entryColor)
|
||||
{
|
||||
var im = ItemMargin;
|
||||
var rowPrefab = PUIElements.CreateUI(null, "RowEntry");
|
||||
// Background of the entry
|
||||
var bgImage = rowPrefab.AddComponent<KImage>();
|
||||
UIDetours.COLOR_STYLE_SETTING.Set(bgImage, entryColor);
|
||||
UIDetours.APPLY_COLOR_STYLE.Invoke(bgImage);
|
||||
// Checkmark for the front of the entry
|
||||
var isSelected = PUIElements.CreateUI(rowPrefab, "Selected");
|
||||
var fgImage = isSelected.AddComponent<Image>();
|
||||
fgImage.color = style.textColor;
|
||||
fgImage.preserveAspect = true;
|
||||
fgImage.sprite = PUITuning.Images.Checked;
|
||||
// Button for the entry to select it
|
||||
var entryButton = rowPrefab.AddComponent<KButton>();
|
||||
PButton.SetupButton(entryButton, bgImage);
|
||||
UIDetours.FG_IMAGE.Set(entryButton, fgImage);
|
||||
// Tooltip for the entry
|
||||
rowPrefab.AddComponent<ToolTip>();
|
||||
// Text for the entry
|
||||
var textContainer = PUIElements.CreateUI(rowPrefab, "Text");
|
||||
PUIElements.AddLocText(textContainer, style).SetText(" ");
|
||||
// Configure the entire layout in 1 statement! (jk this is awful)
|
||||
var group = rowPrefab.AddComponent<RelativeLayoutGroup>();
|
||||
group.AnchorYAxis(isSelected).OverrideSize(isSelected, CheckSize).SetLeftEdge(
|
||||
isSelected, fraction: 0.0f).SetMargin(isSelected, im).AnchorYAxis(
|
||||
textContainer).SetLeftEdge(textContainer, toRight: isSelected).SetRightEdge(
|
||||
textContainer, 1.0f).SetMargin(textContainer, new RectOffset(0, im.right,
|
||||
im.top, im.bottom)).LockLayout();
|
||||
rowPrefab.SetActive(false);
|
||||
return rowPrefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink button style as this combo box's foreground color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This combo box for call chaining.</returns>
|
||||
public PComboBox<T> SetKleiPinkStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue button style as this combo box's foreground color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This combo box for call chaining.</returns>
|
||||
public PComboBox<T> SetKleiBlueStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the minimum (and preferred) width of this combo box in characters.
|
||||
///
|
||||
/// The width is computed using the currently selected text style.
|
||||
/// </summary>
|
||||
/// <param name="chars">The number of characters to be displayed.</param>
|
||||
/// <returns>This combo box for call chaining.</returns>
|
||||
public PComboBox<T> SetMinWidthInCharacters(int chars) {
|
||||
int width = Mathf.RoundToInt(chars * PUIUtils.GetEmWidth(TextStyle));
|
||||
if (width > 0)
|
||||
MinWidth = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PComboBox[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
260
mod/PLibUI/PComboBoxComponent.cs
Normal file
260
mod/PLibUI/PComboBoxComponent.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// An improved variant of DropDown/Dropdown.
|
||||
/// </summary>
|
||||
internal sealed class PComboBoxComponent : KMonoBehaviour {
|
||||
/// <summary>
|
||||
/// The container where the combo box items will be placed.
|
||||
/// </summary>
|
||||
internal RectTransform ContentContainer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color for the checkbox if it is selected.
|
||||
/// </summary>
|
||||
internal Color CheckColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The prefab used to display each row.
|
||||
/// </summary>
|
||||
internal GameObject EntryPrefab { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of rows to be shown.
|
||||
/// </summary>
|
||||
internal int MaxRowsShown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is selected.
|
||||
/// </summary>
|
||||
internal Action<PComboBoxComponent, IListableOption> OnSelectionChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The object which contains the pull down section of the combo box.
|
||||
/// </summary>
|
||||
internal GameObject Pulldown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected label.
|
||||
/// </summary>
|
||||
internal TMP_Text SelectedLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The items which are currently shown in this combo box.
|
||||
/// </summary>
|
||||
private readonly IList<ComboBoxItem> currentItems;
|
||||
|
||||
/// <summary>
|
||||
/// The currently active mouse event handler, or null if not yet configured.
|
||||
/// </summary>
|
||||
private MouseEventHandler handler;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the combo box is expanded.
|
||||
/// </summary>
|
||||
private bool open;
|
||||
|
||||
internal PComboBoxComponent() {
|
||||
CheckColor = Color.white;
|
||||
currentItems = new List<ComboBoxItem>(32);
|
||||
handler = null;
|
||||
MaxRowsShown = 8;
|
||||
open = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the pulldown. The selected choice is not saved.
|
||||
/// </summary>
|
||||
public void Close() {
|
||||
Pulldown?.SetActive(false);
|
||||
open = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the combo box is clicked.
|
||||
/// </summary>
|
||||
public void OnClick() {
|
||||
if (open)
|
||||
Close();
|
||||
else
|
||||
Open();
|
||||
}
|
||||
|
||||
protected override void OnPrefabInit() {
|
||||
base.OnPrefabInit();
|
||||
handler = gameObject.AddOrGet<MouseEventHandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the pulldown.
|
||||
/// </summary>
|
||||
public void Open() {
|
||||
var pdn = Pulldown;
|
||||
if (pdn != null) {
|
||||
float rowHeight = 0.0f;
|
||||
int itemCount = currentItems.Count, rows = Math.Min(MaxRowsShown, itemCount);
|
||||
var canvas = pdn.AddOrGet<Canvas>();
|
||||
pdn.SetActive(true);
|
||||
// Calculate desired height of scroll pane
|
||||
if (itemCount > 0) {
|
||||
var rt = currentItems[0].rowInstance.rectTransform();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
|
||||
rowHeight = LayoutUtility.GetPreferredHeight(rt);
|
||||
}
|
||||
// Update size and enable/disable scrolling if not needed
|
||||
pdn.rectTransform().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical,
|
||||
rows * rowHeight);
|
||||
if (pdn.TryGetComponent(out ScrollRect sp))
|
||||
sp.vertical = itemCount >= MaxRowsShown;
|
||||
// Move above normal UI elements but below the tooltips
|
||||
if (canvas != null) {
|
||||
canvas.overrideSorting = true;
|
||||
canvas.sortingOrder = 2;
|
||||
}
|
||||
}
|
||||
open = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the items which will be shown in this combo box.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to show.</param>
|
||||
public void SetItems(IEnumerable<IListableOption> items) {
|
||||
if (items != null) {
|
||||
var content = ContentContainer;
|
||||
var pdn = Pulldown;
|
||||
int n = content.childCount;
|
||||
var prefab = EntryPrefab;
|
||||
bool wasOpen = open;
|
||||
if (wasOpen)
|
||||
Close();
|
||||
// Destroy only destroys it at end of frame!
|
||||
for (int i = 0; i < n; i++)
|
||||
Destroy(content.GetChild(i));
|
||||
currentItems.Clear();
|
||||
foreach (var item in items) {
|
||||
string tooltip = "";
|
||||
var rowInstance = Util.KInstantiate(prefab, content.gameObject);
|
||||
// Update the text shown
|
||||
rowInstance.GetComponentInChildren<TextMeshProUGUI>().SetText(item.
|
||||
GetProperName());
|
||||
// Apply the listener for the button
|
||||
var button = rowInstance.GetComponentInChildren<KButton>();
|
||||
button.ClearOnClick();
|
||||
button.onClick += () => {
|
||||
SetSelectedItem(item, true);
|
||||
Close();
|
||||
};
|
||||
// Assign the tooltip if possible
|
||||
if (item is ITooltipListableOption extended)
|
||||
tooltip = extended.GetToolTipText();
|
||||
if (rowInstance.TryGetComponent(out ToolTip tt)) {
|
||||
if (string.IsNullOrEmpty(tooltip))
|
||||
tt.ClearMultiStringTooltip();
|
||||
else
|
||||
tt.SetSimpleTooltip(tooltip);
|
||||
}
|
||||
rowInstance.SetActive(true);
|
||||
currentItems.Add(new ComboBoxItem(item, rowInstance));
|
||||
}
|
||||
SelectedLabel?.SetText((currentItems.Count > 0) ? currentItems[0].data.
|
||||
GetProperName() : "");
|
||||
if (wasOpen)
|
||||
Open();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected item in the combo box.
|
||||
/// </summary>
|
||||
/// <param name="option">The option that was chosen.</param>
|
||||
/// <param name="fireListener">true to also fire the option selected listener, or false otherwise.</param>
|
||||
public void SetSelectedItem(IListableOption option, bool fireListener = false) {
|
||||
if (option != null) {
|
||||
SelectedLabel?.SetText(option.GetProperName());
|
||||
// No guarantee that the options are hashable
|
||||
foreach (var item in currentItems) {
|
||||
var data = item.data;
|
||||
// Show or hide the check mark next to the selected option
|
||||
item.rowImage.color = (data != null && data.Equals(option)) ? CheckColor :
|
||||
PUITuning.Colors.Transparent;
|
||||
}
|
||||
if (fireListener)
|
||||
OnSelectionChanged?.Invoke(this, option);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called each frame by Unity, checks to see if the user clicks/scrolls outside of
|
||||
/// the dropdown while open, and closes it if so.
|
||||
/// </summary>
|
||||
internal void Update() {
|
||||
if (open && handler != null && !handler.IsOver && (Input.GetMouseButton(0) ||
|
||||
Input.GetAxis("Mouse ScrollWheel") != 0.0f))
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The items in a combo box, paired with the game object owning that row and the
|
||||
/// object that goes there.
|
||||
/// </summary>
|
||||
private struct ComboBoxItem {
|
||||
public readonly IListableOption data;
|
||||
public readonly Image rowImage;
|
||||
public readonly GameObject rowInstance;
|
||||
|
||||
public ComboBoxItem(IListableOption data, GameObject rowInstance) {
|
||||
this.data = data;
|
||||
this.rowInstance = rowInstance;
|
||||
rowImage = rowInstance.GetComponentInChildrenOnly<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles mouse events on the pulldown.
|
||||
/// </summary>
|
||||
private sealed class MouseEventHandler : MonoBehaviour, IPointerEnterHandler,
|
||||
IPointerExitHandler {
|
||||
/// <summary>
|
||||
/// Whether the mouse is over this component.
|
||||
/// </summary>
|
||||
public bool IsOver { get; private set; }
|
||||
|
||||
internal MouseEventHandler() {
|
||||
IsOver = true;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData data) {
|
||||
IsOver = true;
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData data) {
|
||||
IsOver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
mod/PLibUI/PContainer.cs
Normal file
99
mod/PLibUI/PContainer.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// The abstract parent of PLib UI objects that are meant to contain other UI objects.
|
||||
/// </summary>
|
||||
public abstract class PContainer : IUIComponent {
|
||||
/// <summary>
|
||||
/// The background color of this panel.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The background image of this panel. Tinted by the background color, acts as all
|
||||
/// white if left null.
|
||||
///
|
||||
/// Note that the default background color is transparent, so unless it is set to
|
||||
/// some other color this image will be invisible!
|
||||
/// </summary>
|
||||
public Sprite BackImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mode to use when displaying the background image.
|
||||
/// </summary>
|
||||
public Image.Type ImageMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin left around the contained components in pixels. If null, no margin will
|
||||
/// be used.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
public string Name { get; protected set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
protected PContainer(string name) {
|
||||
BackColor = PUITuning.Colors.Transparent;
|
||||
BackImage = null;
|
||||
FlexSize = Vector2.zero;
|
||||
ImageMode = Image.Type.Simple;
|
||||
Margin = null;
|
||||
Name = name ?? "Container";
|
||||
}
|
||||
|
||||
public abstract GameObject Build();
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the OnRealize event.
|
||||
/// </summary>
|
||||
/// <param name="obj">The realized text component.</param>
|
||||
protected void InvokeRealize(GameObject obj) {
|
||||
OnRealize?.Invoke(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the background color and/or image for this panel.
|
||||
/// </summary>
|
||||
/// <param name="panel">The realized panel object.</param>
|
||||
protected void SetImage(GameObject panel) {
|
||||
if (BackColor.a > 0.0f || BackImage != null) {
|
||||
var img = panel.AddComponent<Image>();
|
||||
img.color = BackColor;
|
||||
if (BackImage != null) {
|
||||
img.sprite = BackImage;
|
||||
img.type = ImageMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PContainer[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
434
mod/PLibUI/PDialog.cs
Normal file
434
mod/PLibUI/PDialog.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A dialog root for UI components.
|
||||
/// </summary>
|
||||
public sealed class PDialog : IUIComponent {
|
||||
/// <summary>
|
||||
/// The margin around dialog buttons.
|
||||
/// </summary>
|
||||
public static readonly RectOffset BUTTON_MARGIN = new RectOffset(13, 13, 13, 13);
|
||||
|
||||
/// <summary>
|
||||
/// The margin inside the dialog close button.
|
||||
/// </summary>
|
||||
private static readonly RectOffset CLOSE_ICON_MARGIN = new RectOffset(4, 4, 4, 4);
|
||||
|
||||
/// <summary>
|
||||
/// The size of the dialog close button's icon.
|
||||
/// </summary>
|
||||
private static readonly Vector2 CLOSE_ICON_SIZE = new Vector2f(16.0f, 16.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The dialog key returned if the user closes the dialog with [ESC] or the X.
|
||||
/// </summary>
|
||||
public const string DIALOG_KEY_CLOSE = "close";
|
||||
|
||||
/// <summary>
|
||||
/// Returns a suitable parent object for a dialog.
|
||||
/// </summary>
|
||||
/// <returns>A game object that can be used as a dialog parent depending on the game
|
||||
/// stage, or null if none is available.</returns>
|
||||
public static GameObject GetParentObject() {
|
||||
GameObject parent = null;
|
||||
var fi = FrontEndManager.Instance;
|
||||
if (fi != null)
|
||||
parent = fi.gameObject;
|
||||
else {
|
||||
// Grr unity
|
||||
var gi = GameScreenManager.Instance;
|
||||
if (gi != null)
|
||||
parent = gi.ssOverlayCanvas;
|
||||
else
|
||||
PUIUtils.LogUIWarning("No dialog parent found!");
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds the size up to the nearest even integer.
|
||||
/// </summary>
|
||||
/// <param name="size">The current size.</param>
|
||||
/// <param name="maxSize">The maximum allowed size.</param>
|
||||
/// <returns>The rounded size.</returns>
|
||||
private static float RoundUpSize(float size, float maxSize) {
|
||||
int upOne = Mathf.CeilToInt(size);
|
||||
if (upOne % 2 == 1) upOne++;
|
||||
if (upOne > maxSize && maxSize > 0.0f)
|
||||
upOne -= 2;
|
||||
return upOne;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The dialog body panel. To add custom components to the dialog, use AddChild on
|
||||
/// this panel. Its direction, margin, and spacing can also be customized.
|
||||
/// </summary>
|
||||
public PPanel Body { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The background color of the dialog itself (including button panel).
|
||||
/// </summary>
|
||||
public Color DialogBackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dialog's maximum size. If the dialog preferred size is bigger than this size,
|
||||
/// the dialog will be decreased in size to fit. If either axis is zero, the dialog
|
||||
/// gets its preferred size in that axis, at least the value in Size.
|
||||
/// </summary>
|
||||
public Vector2 MaxSize { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The dialog's parent.
|
||||
/// </summary>
|
||||
public GameObject Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a dialog with an odd width/height is displayed, all offsets will end up on a
|
||||
/// half pixel offset, which may cause unusual display artifacts as Banker's Rounding
|
||||
/// will round values that are supposed to be 1.0 units apart into integer values 2
|
||||
/// units apart. If set, this flag will cause Build to round the dialog's size up to
|
||||
/// the nearest even integer. If the dialog is already at its maximum size and is still
|
||||
/// an odd integer in size, it is rounded down one instead.
|
||||
/// </summary>
|
||||
public bool RoundToNearestEven { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dialog's minimum size. If the dialog preferred size is bigger than this size,
|
||||
/// the dialog will be increased in size to fit. If either axis is zero, the dialog
|
||||
/// gets its preferred size in that axis, up until the value in MaxSize.
|
||||
/// </summary>
|
||||
public Vector2 Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dialog sort order which determines which other dialogs this one is on top of.
|
||||
/// </summary>
|
||||
public float SortKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The dialog's title.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The allowable button choices for the dialog.
|
||||
/// </summary>
|
||||
private readonly ICollection<DialogButton> buttons;
|
||||
|
||||
/// <summary>
|
||||
/// The events to invoke when the dialog is closed.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnDialogClosed DialogClosed { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
public PDialog(string name) {
|
||||
Body = new PPanel("Body") {
|
||||
Alignment = TextAnchor.UpperCenter, FlexSize = Vector2.one,
|
||||
Margin = new RectOffset(6, 6, 6, 6)
|
||||
};
|
||||
DialogBackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
buttons = new List<DialogButton>(4);
|
||||
MaxSize = Vector2.zero;
|
||||
Name = name ?? "Dialog";
|
||||
// First try the front end manager (menu), then the in-game (game)
|
||||
Parent = GetParentObject();
|
||||
RoundToNearestEven = false;
|
||||
Size = Vector2.zero;
|
||||
SortKey = 0.0f;
|
||||
Title = "Dialog";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a button to the dialog. The button will use a blue background with white text
|
||||
/// in the default UI font, except for the last button which will be pink.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to report if this button is selected.</param>
|
||||
/// <param name="text">The button text.</param>
|
||||
/// <param name="tooltip">The tooltip to display on the button (optional)</param>
|
||||
/// <returns>This dialog for call chaining.</returns>
|
||||
public PDialog AddButton(string key, string text, string tooltip = null) {
|
||||
buttons.Add(new DialogButton(key, text, tooltip, null, null));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a button to the dialog.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to report if this button is selected.</param>
|
||||
/// <param name="text">The button text.</param>
|
||||
/// <param name="tooltip">The tooltip to display on the button (optional)</param>
|
||||
/// <param name="backColor">The background color to use for the button. If null or
|
||||
/// omitted, the last button will be pink and all others will be blue.</param>
|
||||
/// <param name="foreColor">The foreground color to use for the button. If null or
|
||||
/// omitted, white text with the default game UI font will be used.</param>
|
||||
/// <returns>This dialog for call chaining.</returns>
|
||||
public PDialog AddButton(string key, string text, string tooltip = null,
|
||||
ColorStyleSetting backColor = null, TextStyleSetting foreColor = null) {
|
||||
buttons.Add(new DialogButton(key, text, tooltip, backColor, foreColor));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this dialog is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This dialog for call chaining.</returns>
|
||||
public PDialog AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
if (Parent == null)
|
||||
throw new InvalidOperationException("Parent for dialog may not be null");
|
||||
var dialog = PUIElements.CreateUI(Parent, Name);
|
||||
var dComponent = dialog.AddComponent<PDialogComp>();
|
||||
// Background
|
||||
dialog.AddComponent<Canvas>();
|
||||
dialog.AddComponent<GraphicRaycaster>();
|
||||
var bg = dialog.AddComponent<Image>();
|
||||
bg.color = DialogBackColor;
|
||||
bg.sprite = PUITuning.Images.BoxBorder;
|
||||
bg.type = Image.Type.Sliced;
|
||||
// Add each component
|
||||
var layout = dialog.AddComponent<PGridLayoutGroup>();
|
||||
layout.AddRow(new GridRowSpec());
|
||||
layout.AddRow(new GridRowSpec(flex: 1.0f));
|
||||
layout.AddRow(new GridRowSpec());
|
||||
layout.AddColumn(new GridColumnSpec(flex: 1.0f));
|
||||
layout.AddColumn(new GridColumnSpec());
|
||||
LayoutTitle(layout, dComponent.DoButton);
|
||||
layout.AddComponent(Body.Build(), new GridComponentSpec(1, 0) {
|
||||
ColumnSpan = 2, Margin = new RectOffset(10, 10, 10, 10)
|
||||
});
|
||||
CreateUserButtons(layout, dComponent.DoButton);
|
||||
// Configure body position
|
||||
SetDialogSize(dialog);
|
||||
// Dialog is realized
|
||||
dComponent.dialog = this;
|
||||
dComponent.sortKey = SortKey;
|
||||
OnRealize?.Invoke(dialog);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the user buttons.
|
||||
/// </summary>
|
||||
/// <param name="layout">The location to add the buttons.</param>
|
||||
/// <param name="onPressed">The handler to call when any button is pressed.</param>
|
||||
private void CreateUserButtons(PGridLayoutGroup layout,
|
||||
PUIDelegates.OnButtonPressed onPressed) {
|
||||
var buttonPanel = new PPanel("Buttons") {
|
||||
Alignment = TextAnchor.LowerCenter, Spacing = 7, Direction = PanelDirection.
|
||||
Horizontal, Margin = new RectOffset(5, 5, 0, 10)
|
||||
};
|
||||
int i = 0;
|
||||
// Add each user button
|
||||
foreach (var button in buttons) {
|
||||
string key = button.key;
|
||||
var bgColor = button.backColor;
|
||||
var fgColor = button.textColor ?? PUITuning.Fonts.UILightStyle;
|
||||
var db = new PButton(key) {
|
||||
Text = button.text, ToolTip = button.tooltip, Margin = BUTTON_MARGIN,
|
||||
OnClick = onPressed, Color = bgColor, TextStyle = fgColor
|
||||
};
|
||||
// Last button is special and gets a pink color
|
||||
if (bgColor == null) {
|
||||
if (++i >= buttons.Count)
|
||||
db.SetKleiPinkStyle();
|
||||
else
|
||||
db.SetKleiBlueStyle();
|
||||
}
|
||||
buttonPanel.AddChild(db);
|
||||
}
|
||||
layout.AddComponent(buttonPanel.Build(), new GridComponentSpec(2, 0) {
|
||||
ColumnSpan = 2
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lays out the dialog title bar and close button.
|
||||
/// </summary>
|
||||
/// <param name="layout">The layout manager for the dialog.</param>
|
||||
/// <param name="onClose">The action to invoke when close is pressed.</param>
|
||||
private void LayoutTitle(PGridLayoutGroup layout, PUIDelegates.OnButtonPressed
|
||||
onClose) {
|
||||
// Title text, expand to width
|
||||
var title = new PLabel("Title") {
|
||||
Margin = new RectOffset(3, 4, 0, 0), Text = Title, FlexSize = Vector2.one
|
||||
}.SetKleiPinkColor().Build();
|
||||
layout.AddComponent(title, new GridComponentSpec(0, 0) {
|
||||
Margin = new RectOffset(0, -2, 0, 0)
|
||||
});
|
||||
// Black border on the title bar overlaps the edge, but 0 margins so should be OK
|
||||
// Fixes the 2px bug handily!
|
||||
var titleBG = title.AddOrGet<Image>();
|
||||
titleBG.sprite = PUITuning.Images.BoxBorder;
|
||||
titleBG.type = Image.Type.Sliced;
|
||||
// Close button
|
||||
layout.AddComponent(new PButton(DIALOG_KEY_CLOSE) {
|
||||
Sprite = PUITuning.Images.Close, Margin = CLOSE_ICON_MARGIN, OnClick = onClose,
|
||||
SpriteSize = CLOSE_ICON_SIZE, ToolTip = STRINGS.UI.TOOLTIPS.CLOSETOOLTIP
|
||||
}.SetKleiBlueStyle().Build(), new GridComponentSpec(0, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the final size of the dialog using its current position.
|
||||
/// </summary>
|
||||
/// <param name="dialog">The realized dialog with all components populated.</param>
|
||||
private void SetDialogSize(GameObject dialog) {
|
||||
// Calculate the final dialog size
|
||||
var dialogRT = dialog.rectTransform();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(dialogRT);
|
||||
float bodyWidth = Math.Max(Size.x, LayoutUtility.GetPreferredWidth(dialogRT)),
|
||||
bodyHeight = Math.Max(Size.y, LayoutUtility.GetPreferredHeight(dialogRT)),
|
||||
maxX = MaxSize.x, maxY = MaxSize.y;
|
||||
// Maximum size constraint
|
||||
if (maxX > 0.0f)
|
||||
bodyWidth = Math.Min(bodyWidth, maxX);
|
||||
if (maxY > 0.0f)
|
||||
bodyHeight = Math.Min(bodyHeight, maxY);
|
||||
if (RoundToNearestEven) {
|
||||
// Round up the size to even integers, even if currently fractional
|
||||
bodyWidth = RoundUpSize(bodyWidth, maxX);
|
||||
bodyHeight = RoundUpSize(bodyHeight, maxY);
|
||||
}
|
||||
dialogRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, bodyWidth);
|
||||
dialogRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, bodyHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and shows this dialog.
|
||||
/// </summary>
|
||||
public void Show() {
|
||||
if (Build().TryGetComponent(out KScreen screen))
|
||||
UIDetours.ACTIVATE_KSCREEN.Invoke(screen);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PDialog[Name={0},Title={1}]", Name, Title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about a dialog button in this dialog.
|
||||
/// </summary>
|
||||
private sealed class DialogButton {
|
||||
/// <summary>
|
||||
/// The color to use when displaying the button. If null, the default color will
|
||||
/// be used.
|
||||
/// </summary>
|
||||
public readonly ColorStyleSetting backColor;
|
||||
|
||||
/// <summary>
|
||||
/// The button key used to indicate that it was selected.
|
||||
/// </summary>
|
||||
public readonly string key;
|
||||
|
||||
/// <summary>
|
||||
/// The text to display for the button.
|
||||
/// </summary>
|
||||
public readonly string text;
|
||||
|
||||
/// <summary>
|
||||
/// The color to use when displaying the button text. If null, the default color
|
||||
/// will be used.
|
||||
/// </summary>
|
||||
public readonly TextStyleSetting textColor;
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for this button.
|
||||
/// </summary>
|
||||
public readonly string tooltip;
|
||||
|
||||
internal DialogButton(string key, string text, string tooltip,
|
||||
ColorStyleSetting backColor, TextStyleSetting foreColor) {
|
||||
this.backColor = backColor;
|
||||
this.key = key;
|
||||
this.text = text;
|
||||
this.tooltip = tooltip;
|
||||
textColor = foreColor;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("DialogButton[key={0:D},text={1:D}]", key, text);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Klei component which backs the dialog.
|
||||
/// </summary>
|
||||
private sealed class PDialogComp : KScreen {
|
||||
/// <summary>
|
||||
/// The events to invoke when the dialog is closed.
|
||||
/// </summary>
|
||||
internal PDialog dialog;
|
||||
|
||||
/// <summary>
|
||||
/// The key selected by the user.
|
||||
/// </summary>
|
||||
internal string key;
|
||||
|
||||
/// <summary>
|
||||
/// The sort order of this dialog.
|
||||
/// </summary>
|
||||
internal float sortKey;
|
||||
|
||||
internal PDialogComp() {
|
||||
key = DIALOG_KEY_CLOSE;
|
||||
sortKey = 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate which closes the dialog on prompt.
|
||||
/// </summary>
|
||||
/// <param name="source">The button source.</param>
|
||||
internal void DoButton(GameObject source) {
|
||||
key = source.name;
|
||||
UIDetours.DEACTIVATE_KSCREEN.Invoke(this);
|
||||
}
|
||||
|
||||
public override float GetSortKey() {
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
protected override void OnDeactivate() {
|
||||
if (dialog != null)
|
||||
// Klei destroys the dialog GameObject for us
|
||||
dialog.DialogClosed?.Invoke(key);
|
||||
base.OnDeactivate();
|
||||
dialog = null;
|
||||
}
|
||||
|
||||
public override void OnKeyDown(KButtonEvent e) {
|
||||
if (e.TryConsume(Action.Escape))
|
||||
UIDetours.DEACTIVATE_KSCREEN.Invoke(this);
|
||||
else
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
mod/PLibUI/PGridPanel.cs
Normal file
144
mod/PLibUI/PGridPanel.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A panel which lays out its components using grid-type constraints.
|
||||
/// </summary>
|
||||
public class PGridPanel : PContainer, IDynamicSizable {
|
||||
/// <summary>
|
||||
/// The number of columns currently defined.
|
||||
/// </summary>
|
||||
public int Columns => columns.Count;
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of rows currently defined.
|
||||
/// </summary>
|
||||
public int Rows => rows.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The children of this panel.
|
||||
/// </summary>
|
||||
private readonly ICollection<GridComponent<IUIComponent>> children;
|
||||
|
||||
/// <summary>
|
||||
/// The columns in this panel.
|
||||
/// </summary>
|
||||
private readonly IList<GridColumnSpec> columns;
|
||||
|
||||
/// <summary>
|
||||
/// The rows in this panel.
|
||||
/// </summary>
|
||||
private readonly IList<GridRowSpec> rows;
|
||||
|
||||
public PGridPanel() : this(null) { }
|
||||
|
||||
public PGridPanel(string name) : base(name ?? "GridPanel") {
|
||||
children = new List<GridComponent<IUIComponent>>(16);
|
||||
columns = new List<GridColumnSpec>(16);
|
||||
rows = new List<GridRowSpec>(16);
|
||||
DynamicSize = true;
|
||||
Margin = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child to this panel.
|
||||
/// </summary>
|
||||
/// <param name="child">The child to add.</param>
|
||||
/// <param name="spec">The location where the child will be placed.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PGridPanel AddChild(IUIComponent child, GridComponentSpec spec) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
if (spec == null)
|
||||
throw new ArgumentNullException(nameof(spec));
|
||||
children.Add(new GridComponent<IUIComponent>(spec, child));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to this panel.
|
||||
/// </summary>
|
||||
/// <param name="column">The specification for that column.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PGridPanel AddColumn(GridColumnSpec column) {
|
||||
if (column == null)
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
columns.Add(column);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this panel is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PGridPanel AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to this panel.
|
||||
/// </summary>
|
||||
/// <param name="row">The specification for that row.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PGridPanel AddRow(GridRowSpec row) {
|
||||
if (row == null)
|
||||
throw new ArgumentNullException(nameof(row));
|
||||
rows.Add(row);
|
||||
return this;
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
if (Columns < 1)
|
||||
throw new InvalidOperationException("At least one column must be defined");
|
||||
if (Rows < 1)
|
||||
throw new InvalidOperationException("At least one row must be defined");
|
||||
var panel = PUIElements.CreateUI(null, Name);
|
||||
SetImage(panel);
|
||||
// Add layout component
|
||||
var layout = panel.AddComponent<PGridLayoutGroup>();
|
||||
layout.Margin = Margin;
|
||||
foreach (var column in columns)
|
||||
layout.AddColumn(column);
|
||||
foreach (var row in rows)
|
||||
layout.AddRow(row);
|
||||
// Add children
|
||||
foreach (var child in children)
|
||||
layout.AddComponent(child.Item.Build(), child);
|
||||
if (!DynamicSize)
|
||||
layout.LockLayout();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
InvokeRealize(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PGridPanel[Name={0},Rows={1:D},Columns={2:D}]", Name, Rows,
|
||||
Columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
mod/PLibUI/PLabel.cs
Normal file
93
mod/PLibUI/PLabel.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI label factory class.
|
||||
/// </summary>
|
||||
public class PLabel : PTextComponent {
|
||||
/// <summary>
|
||||
/// The label's background color.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
public PLabel() : this(null) { }
|
||||
|
||||
public PLabel(string name) : base(name ?? "Label") {
|
||||
BackColor = PUITuning.Colors.Transparent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this label is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This label for call chaining.</returns>
|
||||
public PLabel AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
var label = PUIElements.CreateUI(null, Name);
|
||||
GameObject sprite = null, text = null;
|
||||
// Background
|
||||
if (BackColor.a > 0)
|
||||
label.AddComponent<Image>().color = BackColor;
|
||||
// Add foreground image
|
||||
if (Sprite != null)
|
||||
sprite = ImageChildHelper(label, this).gameObject;
|
||||
// Add text
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
text = TextChildHelper(label, TextStyle ?? PUITuning.Fonts.UILightStyle,
|
||||
Text).gameObject;
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(label, ToolTip).SetActive(true);
|
||||
// Arrange the icon and text
|
||||
var layout = label.AddComponent<RelativeLayoutGroup>();
|
||||
layout.Margin = Margin;
|
||||
ArrangeComponent(layout, WrapTextAndSprite(text, sprite), TextAlignment);
|
||||
if (!DynamicSize) layout.LockLayout();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
DestroyLayoutIfPossible(label);
|
||||
InvokeRealize(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the default Klei dialog blue.
|
||||
/// </summary>
|
||||
/// <returns>This label for call chaining.</returns>
|
||||
public PLabel SetKleiBlueColor() {
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the Klei dialog header pink.
|
||||
/// </summary>
|
||||
/// <returns>This label for call chaining.</returns>
|
||||
public PLabel SetKleiPinkColor() {
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
mod/PLibUI/PLibUI.csproj
Normal file
20
mod/PLibUI/PLibUI.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>PLib User Interface</Title>
|
||||
<AssemblyTitle>PLib.UI</AssemblyTitle>
|
||||
<Version>4.11.0.0</Version>
|
||||
<UsesPLib>false</UsesPLib>
|
||||
<RootNamespace>PeterHan.PLib.UI</RootNamespace>
|
||||
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||
<DistributeMod>false</DistributeMod>
|
||||
<PLibCore>true</PLibCore>
|
||||
<Platforms>Vanilla;Mergedown</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\$(Platform)\Release\PLibUI.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
168
mod/PLibUI/PPanel.cs
Normal file
168
mod/PLibUI/PPanel.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI panel factory which can arrange its children horizontally or vertically.
|
||||
/// </summary>
|
||||
public class PPanel : PContainer, IDynamicSizable {
|
||||
/// <summary>
|
||||
/// The alignment position to use for child elements if they are smaller than the
|
||||
/// required size.
|
||||
/// </summary>
|
||||
public TextAnchor Alignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The direction in which components will be laid out.
|
||||
/// </summary>
|
||||
public PanelDirection Direction { get; set; }
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The spacing between components in pixels.
|
||||
/// </summary>
|
||||
public int Spacing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The children of this panel.
|
||||
/// </summary>
|
||||
protected readonly ICollection<IUIComponent> children;
|
||||
|
||||
public PPanel() : this(null) { }
|
||||
|
||||
public PPanel(string name) : base(name ?? "Panel") {
|
||||
Alignment = TextAnchor.MiddleCenter;
|
||||
children = new List<IUIComponent>();
|
||||
Direction = PanelDirection.Vertical;
|
||||
DynamicSize = true;
|
||||
Spacing = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child to this panel.
|
||||
/// </summary>
|
||||
/// <param name="child">The child to add.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PPanel AddChild(IUIComponent child) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
children.Add(child);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this panel is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PPanel AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
return Build(default, DynamicSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds this panel.
|
||||
/// </summary>
|
||||
/// <param name="size">The fixed size to use if dynamic is false.</param>
|
||||
/// <param name="dynamic">Whether to use dynamic sizing.</param>
|
||||
/// <returns>The realized panel.</returns>
|
||||
private GameObject Build(Vector2 size, bool dynamic) {
|
||||
var panel = PUIElements.CreateUI(null, Name);
|
||||
SetImage(panel);
|
||||
// Add children
|
||||
foreach (var child in children) {
|
||||
var obj = child.Build();
|
||||
obj.SetParent(panel);
|
||||
PUIElements.SetAnchors(obj, PUIAnchoring.Stretch, PUIAnchoring.Stretch);
|
||||
}
|
||||
var lg = panel.AddComponent<BoxLayoutGroup>();
|
||||
lg.Params = new BoxLayoutParams() {
|
||||
Direction = Direction, Alignment = Alignment, Spacing = Spacing,
|
||||
Margin = Margin
|
||||
};
|
||||
if (!dynamic) {
|
||||
lg.LockLayout();
|
||||
panel.SetMinUISize(size);
|
||||
}
|
||||
lg.flexibleWidth = FlexSize.x;
|
||||
lg.flexibleHeight = FlexSize.y;
|
||||
InvokeRealize(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds this panel with a given default size.
|
||||
/// </summary>
|
||||
/// <param name="size">The fixed size to use.</param>
|
||||
/// <returns>The realized panel.</returns>
|
||||
public GameObject BuildWithFixedSize(Vector2 size) {
|
||||
return Build(size, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a child from this panel.
|
||||
/// </summary>
|
||||
/// <param name="child">The child to remove.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PPanel RemoveChild(IUIComponent child) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
children.Remove(child);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the default Klei dialog blue.
|
||||
/// </summary>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PPanel SetKleiBlueColor() {
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the Klei dialog header pink.
|
||||
/// </summary>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PPanel SetKleiPinkColor() {
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PPanel[Name={0},Direction={1}]", Name, Direction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The direction in which PPanel lays out components.
|
||||
/// </summary>
|
||||
public enum PanelDirection {
|
||||
Horizontal, Vertical
|
||||
}
|
||||
}
|
||||
338
mod/PLibUI/PRelativePanel.cs
Normal file
338
mod/PLibUI/PRelativePanel.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
using RelativeConfig = PeterHan.PLib.UI.Layouts.RelativeLayoutParamsBase<PeterHan.PLib.UI.
|
||||
IUIComponent>;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A panel which lays out its components using relative constraints.
|
||||
///
|
||||
/// This layout manager is the fastest of all panels when laid out, especially since it
|
||||
/// can function properly when frozen even on dynamically sized items. However, it is also
|
||||
/// the most difficult to set up and cannot handle all layouts.
|
||||
/// </summary>
|
||||
public class PRelativePanel : PContainer, IDynamicSizable {
|
||||
/// <summary>
|
||||
/// Constraints for each object are stored here.
|
||||
/// </summary>
|
||||
private readonly IDictionary<IUIComponent, RelativeConfig> constraints;
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
public PRelativePanel() : this(null) {
|
||||
DynamicSize = true;
|
||||
}
|
||||
|
||||
public PRelativePanel(string name) : base(name ?? "RelativePanel") {
|
||||
constraints = new Dictionary<IUIComponent, RelativeConfig>(16);
|
||||
Margin = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child to this panel. Children must be added to the panel before they are
|
||||
/// referenced in a constraint.
|
||||
/// </summary>
|
||||
/// <param name="child">The child to add.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PRelativePanel AddChild(IUIComponent child) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
if (!constraints.ContainsKey(child))
|
||||
constraints.Add(child, new RelativeConfig());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this panel is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This panel for call chaining.</returns>
|
||||
public PRelativePanel AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anchors the component's pivot in the X axis to the specified anchor position.
|
||||
/// The component will be laid out at its preferred (or overridden) width with its
|
||||
/// pivot locked to the specified relative fraction of the parent component's width.
|
||||
///
|
||||
/// Any other existing left or right edge constraints will be overwritten. This method
|
||||
/// is equivalent to setting both the left and right edges to the same fraction.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="anchor">The fraction to which to align the pivot, with 0.0f
|
||||
/// being the left and 1.0f being the right.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel AnchorXAxis(IUIComponent item, float anchor = 0.5f) {
|
||||
SetLeftEdge(item, fraction: anchor);
|
||||
return SetRightEdge(item, fraction: anchor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anchors the component's pivot in the Y axis to the specified anchor position.
|
||||
/// The component will be laid out at its preferred (or overridden) height with its
|
||||
/// pivot locked to the specified relative fraction of the parent component's height.
|
||||
///
|
||||
/// Any other existing top or bottom edge constraints will be overwritten. This method
|
||||
/// is equivalent to setting both the top and bottom edges to the same fraction.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="anchor">The fraction to which to align the pivot, with 0.0f
|
||||
/// being the bottom and 1.0f being the top.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel AnchorYAxis(IUIComponent item, float anchor = 0.5f) {
|
||||
SetTopEdge(item, fraction: anchor);
|
||||
return SetBottomEdge(item, fraction: anchor);
|
||||
}
|
||||
|
||||
public override GameObject Build() {
|
||||
var panel = PUIElements.CreateUI(null, Name);
|
||||
var mapping = DictionaryPool<IUIComponent, GameObject, PRelativePanel>.Allocate();
|
||||
SetImage(panel);
|
||||
// Realize each component and add them to the panel
|
||||
foreach (var pair in constraints) {
|
||||
var component = pair.Key;
|
||||
var realized = component.Build();
|
||||
realized.SetParent(panel);
|
||||
// We were already guaranteed that there were no duplicate keys
|
||||
mapping[component] = realized;
|
||||
}
|
||||
// Add layout component
|
||||
var layout = panel.AddComponent<RelativeLayoutGroup>();
|
||||
layout.Margin = Margin;
|
||||
foreach (var pair in constraints) {
|
||||
var realized = mapping[pair.Key];
|
||||
var rawParams = pair.Value;
|
||||
var newParams = new RelativeLayoutParams();
|
||||
// Copy all of the settings
|
||||
Resolve(newParams.TopEdge, rawParams.TopEdge, mapping);
|
||||
Resolve(newParams.BottomEdge, rawParams.BottomEdge, mapping);
|
||||
Resolve(newParams.LeftEdge, rawParams.LeftEdge, mapping);
|
||||
Resolve(newParams.RightEdge, rawParams.RightEdge, mapping);
|
||||
newParams.OverrideSize = rawParams.OverrideSize;
|
||||
newParams.Insets = rawParams.Insets;
|
||||
layout.SetRaw(realized, newParams);
|
||||
}
|
||||
if (!DynamicSize)
|
||||
layout.LockLayout();
|
||||
mapping.Recycle();
|
||||
// Set flex size
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
InvokeRealize(panel);
|
||||
return panel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the constraints for a component, or throws an exception if the component
|
||||
/// has not yet been added.
|
||||
/// </summary>
|
||||
/// <param name="item">The unrealized component to look up.</param>
|
||||
/// <returns>The constraints for that component.</returns>
|
||||
/// <exception cref="ArgumentException">If the component has not yet been added to the panel.</exception>
|
||||
private RelativeConfig GetOrThrow(IUIComponent item) {
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
if (!constraints.TryGetValue(item, out RelativeConfig value))
|
||||
throw new ArgumentException("Components must be added to the panel before using them in a constraint");
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the preferred size of a component. If set, instead of looking at layout
|
||||
/// sizes of the component, the specified size will be used instead.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="size">The size to apply. Only dimensions greater than zero will be used.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel OverrideSize(IUIComponent item, Vector2 size) {
|
||||
if (item != null)
|
||||
GetOrThrow(item).OverrideSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the edge settings configured in this component to settings for the
|
||||
/// relative panel.
|
||||
/// </summary>
|
||||
/// <param name="dest">The location where the converted settings will be stored.</param>
|
||||
/// <param name="status">The original component edge configuration.</param>
|
||||
/// <param name="mapping">The mapping from PLib UI components to Unity objects.</param>
|
||||
private void Resolve(RelativeLayoutParams.EdgeStatus dest, RelativeConfig.EdgeStatus
|
||||
status, IDictionary<IUIComponent, GameObject> mapping) {
|
||||
var c = status.FromComponent;
|
||||
dest.FromAnchor = status.FromAnchor;
|
||||
if (c != null)
|
||||
dest.FromComponent = mapping[c];
|
||||
dest.Constraint = status.Constraint;
|
||||
dest.Offset = status.Offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the bottom edge of a game object. If the fraction is supplied, the component
|
||||
/// will be laid out with the bottom edge anchored to that fraction of the parent's
|
||||
/// height. If a component is specified and no fraction is specified, the component
|
||||
/// will be anchored with its bottom edge above the top edge of that component.
|
||||
/// If neither is specified, all bottom edge constraints will be removed.
|
||||
///
|
||||
/// Any other existing bottom edge constraint will be overwritten.
|
||||
///
|
||||
/// Remember that +Y is in the upwards direction.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="fraction">The fraction to which to align the bottom edge, with 0.0f
|
||||
/// being the bottom and 1.0f being the top.</param>
|
||||
/// <param name="above">The game object which this component must be above.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel SetBottomEdge(IUIComponent item, float fraction = -1.0f,
|
||||
IUIComponent above = null) {
|
||||
if (item != null) {
|
||||
if (above == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(GetOrThrow(item).BottomEdge, fraction, above);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a component's edge constraint.
|
||||
/// </summary>
|
||||
/// <param name="edge">The edge to set.</param>
|
||||
/// <param name="fraction">The fraction of the parent to anchor.</param>
|
||||
/// <param name="child">The other component to anchor.</param>
|
||||
private void SetEdge(RelativeConfig.EdgeStatus edge, float fraction,
|
||||
IUIComponent child) {
|
||||
if (fraction >= 0.0f && fraction <= 1.0f) {
|
||||
edge.Constraint = RelativeConstraintType.ToAnchor;
|
||||
edge.FromAnchor = fraction;
|
||||
edge.FromComponent = null;
|
||||
} else if (child != null) {
|
||||
edge.Constraint = RelativeConstraintType.ToComponent;
|
||||
edge.FromComponent = child;
|
||||
} else {
|
||||
edge.Constraint = RelativeConstraintType.Unconstrained;
|
||||
edge.FromComponent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the left edge of a game object. If the fraction is supplied, the component
|
||||
/// will be laid out with the left edge anchored to that fraction of the parent's
|
||||
/// width. If a component is specified and no fraction is specified, the component
|
||||
/// will be anchored with its left edge to the right of that component.
|
||||
/// If neither is specified, all left edge constraints will be removed.
|
||||
///
|
||||
/// Any other existing left edge constraint will be overwritten.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="fraction">The fraction to which to align the left edge, with 0.0f
|
||||
/// being the left and 1.0f being the right.</param>
|
||||
/// <param name="toLeft">The game object which this component must be to the right of.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel SetLeftEdge(IUIComponent item, float fraction = -1.0f,
|
||||
IUIComponent toRight = null) {
|
||||
if (item != null) {
|
||||
if (toRight == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(GetOrThrow(item).LeftEdge, fraction, toRight);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the insets of a component from its anchor points. A positive number insets the
|
||||
/// component away from the edge, whereas a negative number out-sets the component
|
||||
/// across the edge.
|
||||
///
|
||||
/// All components default to no insets.
|
||||
///
|
||||
/// Any reference to a component's edge using other constraints always refers to its
|
||||
/// edge <b>before</b> insets are applied.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="insets">The insets to apply. If null, the insets will be set to zero.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel SetMargin(IUIComponent item, RectOffset insets) {
|
||||
if (item != null)
|
||||
GetOrThrow(item).Insets = insets;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the right edge of a game object. If the fraction is supplied, the component
|
||||
/// will be laid out with the right edge anchored to that fraction of the parent's
|
||||
/// width. If a component is specified and no fraction is specified, the component
|
||||
/// will be anchored with its right edge to the left of that component.
|
||||
/// If neither is specified, all right edge constraints will be removed.
|
||||
///
|
||||
/// Any other existing right edge constraint will be overwritten.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="fraction">The fraction to which to align the right edge, with 0.0f
|
||||
/// being the left and 1.0f being the right.</param>
|
||||
/// <param name="toLeft">The game object which this component must be to the left of.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel SetRightEdge(IUIComponent item, float fraction = -1.0f,
|
||||
IUIComponent toLeft = null) {
|
||||
if (item != null) {
|
||||
if (toLeft == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(GetOrThrow(item).RightEdge, fraction, toLeft);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the top edge of a game object. If the fraction is supplied, the component
|
||||
/// will be laid out with the top edge anchored to that fraction of the parent's
|
||||
/// height. If a component is specified and no fraction is specified, the component
|
||||
/// will be anchored with its top edge above the bottom edge of that component.
|
||||
/// If neither is specified, all top edge constraints will be removed.
|
||||
///
|
||||
/// Any other existing top edge constraint will be overwritten.
|
||||
///
|
||||
/// Remember that +Y is in the upwards direction.
|
||||
/// </summary>
|
||||
/// <param name="item">The component to adjust.</param>
|
||||
/// <param name="fraction">The fraction to which to align the top edge, with 0.0f
|
||||
/// being the bottom and 1.0f being the top.</param>
|
||||
/// <param name="below">The game object which this component must be below.</param>
|
||||
/// <returns>This object, for call chaining.</returns>
|
||||
public PRelativePanel SetTopEdge(IUIComponent item, float fraction = -1.0f,
|
||||
IUIComponent below = null) {
|
||||
if (item != null) {
|
||||
if (below == item)
|
||||
throw new ArgumentException("Component cannot refer directly to itself");
|
||||
SetEdge(GetOrThrow(item).TopEdge, fraction, below);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PRelativePanel[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
376
mod/PLibUI/PScrollPane.cs
Normal file
376
mod/PLibUI/PScrollPane.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A factory for scrollable panes.
|
||||
/// </summary>
|
||||
public sealed class PScrollPane : IUIComponent {
|
||||
/// <summary>
|
||||
/// The track size of scrollbars is based on the sprite.
|
||||
/// </summary>
|
||||
private const float DEFAULT_TRACK_SIZE = 16.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the horizontal scrollbar is always visible.
|
||||
/// </summary>
|
||||
public bool AlwaysShowHorizontal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vertical scrollbar is always visible.
|
||||
/// </summary>
|
||||
public bool AlwaysShowVertical { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The background color of this scroll pane.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The child of this scroll pane.
|
||||
/// </summary>
|
||||
public IUIComponent Child { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether horizontal scrolling is allowed.
|
||||
/// </summary>
|
||||
public bool ScrollHorizontal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scrolling is allowed.
|
||||
/// </summary>
|
||||
public bool ScrollVertical { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the scrollbar track.
|
||||
/// </summary>
|
||||
public float TrackSize { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
public PScrollPane() : this(null) { }
|
||||
|
||||
public PScrollPane(string name) {
|
||||
AlwaysShowHorizontal = AlwaysShowVertical = true;
|
||||
BackColor = PUITuning.Colors.Transparent;
|
||||
Child = null;
|
||||
FlexSize = Vector2.zero;
|
||||
Name = name ?? "Scroll";
|
||||
ScrollHorizontal = false;
|
||||
ScrollVertical = false;
|
||||
TrackSize = DEFAULT_TRACK_SIZE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this scroll pane is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This scroll pane for call chaining.</returns>
|
||||
public PScrollPane AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
if (Child == null)
|
||||
throw new InvalidOperationException("No child component");
|
||||
var pane = BuildScrollPane(null, Child.Build());
|
||||
OnRealize?.Invoke(pane);
|
||||
return pane;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the actual scroll pane object.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent of this scroll pane.</param>
|
||||
/// <param name="child">The child element of this scroll pane.</param>
|
||||
/// <returns>The realized scroll pane.</returns>
|
||||
internal GameObject BuildScrollPane(GameObject parent, GameObject child) {
|
||||
var pane = PUIElements.CreateUI(parent, Name);
|
||||
if (BackColor.a > 0.0f)
|
||||
pane.AddComponent<Image>().color = BackColor;
|
||||
pane.SetActive(false);
|
||||
// Scroll pane itself
|
||||
var scroll = pane.AddComponent<KScrollRect>();
|
||||
scroll.horizontal = ScrollHorizontal;
|
||||
scroll.vertical = ScrollVertical;
|
||||
// Viewport
|
||||
var viewport = PUIElements.CreateUI(pane, "Viewport");
|
||||
viewport.rectTransform().pivot = Vector2.up;
|
||||
viewport.AddComponent<RectMask2D>().enabled = true;
|
||||
viewport.AddComponent<ViewportLayoutGroup>();
|
||||
scroll.viewport = viewport.rectTransform();
|
||||
// Give the Child a separate Canvas to reduce layout rebuilds
|
||||
child.AddOrGet<Canvas>().pixelPerfect = false;
|
||||
child.AddOrGet<GraphicRaycaster>();
|
||||
PUIElements.SetAnchors(child.SetParent(viewport), PUIAnchoring.Beginning,
|
||||
PUIAnchoring.End);
|
||||
scroll.content = child.rectTransform();
|
||||
// Vertical scrollbar
|
||||
if (ScrollVertical) {
|
||||
scroll.verticalScrollbar = CreateScrollVert(pane);
|
||||
scroll.verticalScrollbarVisibility = AlwaysShowVertical ? ScrollRect.
|
||||
ScrollbarVisibility.Permanent : ScrollRect.ScrollbarVisibility.
|
||||
AutoHideAndExpandViewport;
|
||||
}
|
||||
if (ScrollHorizontal) {
|
||||
scroll.horizontalScrollbar = CreateScrollHoriz(pane);
|
||||
scroll.horizontalScrollbarVisibility = AlwaysShowHorizontal ? ScrollRect.
|
||||
ScrollbarVisibility.Permanent : ScrollRect.ScrollbarVisibility.
|
||||
AutoHideAndExpandViewport;
|
||||
}
|
||||
pane.SetActive(true);
|
||||
// Custom layout to pass child sizes to the scroll pane
|
||||
var layout = pane.AddComponent<PScrollPaneLayout>();
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
return pane;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a horizontal scroll bar.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent component.</param>
|
||||
/// <returns>The scroll bar component.</returns>
|
||||
private Scrollbar CreateScrollHoriz(GameObject parent) {
|
||||
// Outer scrollbar
|
||||
var track = PUIElements.CreateUI(parent, "Scrollbar H", true, PUIAnchoring.Stretch,
|
||||
PUIAnchoring.Beginning);
|
||||
var bg = track.AddComponent<Image>();
|
||||
bg.sprite = PUITuning.Images.ScrollBorderHorizontal;
|
||||
bg.type = Image.Type.Sliced;
|
||||
// Scroll track
|
||||
var sb = track.AddComponent<Scrollbar>();
|
||||
sb.interactable = true;
|
||||
sb.transition = Selectable.Transition.ColorTint;
|
||||
sb.colors = PUITuning.Colors.ScrollbarColors;
|
||||
sb.SetDirection(Scrollbar.Direction.RightToLeft, true);
|
||||
// Handle
|
||||
var handle = PUIElements.CreateUI(track, "Handle", true, PUIAnchoring.Stretch,
|
||||
PUIAnchoring.End);
|
||||
PUIElements.SetAnchorOffsets(handle, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
sb.handleRect = handle.rectTransform();
|
||||
var hImg = handle.AddComponent<Image>();
|
||||
hImg.sprite = PUITuning.Images.ScrollHandleHorizontal;
|
||||
hImg.type = Image.Type.Sliced;
|
||||
sb.targetGraphic = hImg;
|
||||
track.SetActive(true);
|
||||
PUIElements.SetAnchorOffsets(track, 2.0f, 2.0f, -TrackSize, 0.0f);
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a vertical scroll bar.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent component.</param>
|
||||
/// <returns>The scroll bar component.</returns>
|
||||
private Scrollbar CreateScrollVert(GameObject parent) {
|
||||
// Outer scrollbar
|
||||
var track = PUIElements.CreateUI(parent, "Scrollbar V", true, PUIAnchoring.End,
|
||||
PUIAnchoring.Stretch);
|
||||
var bg = track.AddComponent<Image>();
|
||||
bg.sprite = PUITuning.Images.ScrollBorderVertical;
|
||||
bg.type = Image.Type.Sliced;
|
||||
// Scroll track
|
||||
var sb = track.AddComponent<Scrollbar>();
|
||||
sb.interactable = true;
|
||||
sb.transition = Selectable.Transition.ColorTint;
|
||||
sb.colors = PUITuning.Colors.ScrollbarColors;
|
||||
sb.SetDirection(Scrollbar.Direction.BottomToTop, true);
|
||||
// Handle
|
||||
var handle = PUIElements.CreateUI(track, "Handle", true, PUIAnchoring.Stretch,
|
||||
PUIAnchoring.Beginning);
|
||||
PUIElements.SetAnchorOffsets(handle, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
sb.handleRect = handle.rectTransform();
|
||||
var hImg = handle.AddComponent<Image>();
|
||||
hImg.sprite = PUITuning.Images.ScrollHandleVertical;
|
||||
hImg.type = Image.Type.Sliced;
|
||||
sb.targetGraphic = hImg;
|
||||
track.SetActive(true);
|
||||
PUIElements.SetAnchorOffsets(track, -TrackSize, 0.0f, 2.0f, 2.0f);
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the default Klei dialog blue.
|
||||
/// </summary>
|
||||
/// <returns>This scroll pane for call chaining.</returns>
|
||||
public PScrollPane SetKleiBlueColor() {
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the background color to the Klei dialog header pink.
|
||||
/// </summary>
|
||||
/// <returns>This scroll pane for call chaining.</returns>
|
||||
public PScrollPane SetKleiPinkColor() {
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PScrollPane[Name={0},Child={1}]", Name, Child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles layout for scroll panes. Not freezable.
|
||||
/// </summary>
|
||||
private sealed class PScrollPaneLayout : AbstractLayoutGroup {
|
||||
/// <summary>
|
||||
/// Caches elements when calculating layout to improve performance.
|
||||
/// </summary>
|
||||
private Component[] calcElements;
|
||||
|
||||
/// <summary>
|
||||
/// The calculated horizontal size of the child element.
|
||||
/// </summary>
|
||||
private LayoutSizes childHorizontal;
|
||||
|
||||
/// <summary>
|
||||
/// The calculated vertical size of the child element.
|
||||
/// </summary>
|
||||
private LayoutSizes childVertical;
|
||||
|
||||
/// <summary>
|
||||
/// The child object inside the scroll rect.
|
||||
/// </summary>
|
||||
private GameObject child;
|
||||
|
||||
/// <summary>
|
||||
/// Caches elements when setting layout to improve performance.
|
||||
/// </summary>
|
||||
private ILayoutController[] setElements;
|
||||
|
||||
/// <summary>
|
||||
/// The viewport which clips the child rectangle.
|
||||
/// </summary>
|
||||
private GameObject viewport;
|
||||
|
||||
internal PScrollPaneLayout() {
|
||||
minHeight = minWidth = 0.0f;
|
||||
child = viewport = null;
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputHorizontal() {
|
||||
if (child == null)
|
||||
UpdateComponents();
|
||||
if (child != null) {
|
||||
calcElements = child.GetComponents<Component>();
|
||||
// Lay out children
|
||||
childHorizontal = PUIUtils.CalcSizes(child, PanelDirection.Horizontal,
|
||||
calcElements);
|
||||
if (childHorizontal.ignore)
|
||||
throw new InvalidOperationException("ScrollPane child ignores layout!");
|
||||
preferredWidth = childHorizontal.preferred;
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateLayoutInputVertical() {
|
||||
if (child == null)
|
||||
UpdateComponents();
|
||||
if (child != null && calcElements != null) {
|
||||
// Lay out children
|
||||
childVertical = PUIUtils.CalcSizes(child, PanelDirection.Vertical,
|
||||
calcElements);
|
||||
preferredHeight = childVertical.preferred;
|
||||
calcElements = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEnable() {
|
||||
base.OnEnable();
|
||||
UpdateComponents();
|
||||
}
|
||||
|
||||
public override void SetLayoutHorizontal() {
|
||||
if (viewport != null && child != null) {
|
||||
// Observe the flex width
|
||||
float prefWidth = childHorizontal.preferred;
|
||||
float actualWidth = viewport.rectTransform().rect.width;
|
||||
if (prefWidth < actualWidth && childHorizontal.flexible > 0.0f)
|
||||
prefWidth = actualWidth;
|
||||
// Resize child
|
||||
setElements = child.GetComponents<ILayoutController>();
|
||||
child.rectTransform().SetSizeWithCurrentAnchors(RectTransform.Axis.
|
||||
Horizontal, prefWidth);
|
||||
// ScrollRect does not rebuild the child's layout
|
||||
foreach (var component in setElements)
|
||||
component.SetLayoutHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayoutVertical() {
|
||||
if (viewport != null && child != null && setElements != null) {
|
||||
// Observe the flex height
|
||||
float prefHeight = childVertical.preferred;
|
||||
float actualHeight = viewport.rectTransform().rect.height;
|
||||
if (prefHeight < actualHeight && childVertical.flexible > 0.0f)
|
||||
prefHeight = actualHeight;
|
||||
// Resize child
|
||||
child.rectTransform().SetSizeWithCurrentAnchors(RectTransform.Axis.
|
||||
Vertical, prefHeight);
|
||||
// ScrollRect does not rebuild the child's layout
|
||||
foreach (var component in setElements)
|
||||
component.SetLayoutVertical();
|
||||
setElements = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches the child component for performance reasons at runtime.
|
||||
/// </summary>
|
||||
private void UpdateComponents() {
|
||||
var obj = gameObject;
|
||||
if (obj != null && obj.TryGetComponent(out ScrollRect sr)) {
|
||||
child = sr.content?.gameObject;
|
||||
viewport = sr.viewport?.gameObject;
|
||||
} else
|
||||
child = viewport = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A layout group object that does nothing. While it seems completely pointless,
|
||||
/// it allows LayoutRebuilder to pass by the viewport on Scroll Rects on its way up
|
||||
/// the tree, thus ensuring that the scroll rect gets rebuilt.
|
||||
///
|
||||
/// On the way back down, this component gets skipped over by PScrollPaneLayout to
|
||||
/// save on processing, and the child layout is built directly.
|
||||
/// </summary>
|
||||
private sealed class ViewportLayoutGroup : UIBehaviour, ILayoutGroup {
|
||||
public void SetLayoutHorizontal() { }
|
||||
|
||||
public void SetLayoutVertical() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
268
mod/PLibUI/PSlider.cs
Normal file
268
mod/PLibUI/PSlider.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI slider factory class with one handle. Does not include a text field to set
|
||||
/// the value.
|
||||
/// </summary>
|
||||
public class PSliderSingle : IUIComponent {
|
||||
/// <summary>
|
||||
/// Sets the current value of a realized slider.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized slider.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
public static void SetCurrentValue(GameObject realized, float value) {
|
||||
if (realized != null && realized.TryGetComponent(out KSlider slider) && !value.
|
||||
IsNaNOrInfinity())
|
||||
slider.value = value.InRange(slider.minValue, slider.maxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, the default Klei track and fill will be skipped; only the handle will be
|
||||
/// shown.
|
||||
/// </summary>
|
||||
public bool CustomTrack { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The direction of the slider. The slider goes from minimum to maximum value in the
|
||||
/// direction indicated, i.e. LeftToRight is minimum left, maximum right.
|
||||
/// </summary>
|
||||
public Slider.Direction Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The slider's handle color.
|
||||
/// </summary>
|
||||
public ColorStyleSetting HandleColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the slider handle.
|
||||
/// </summary>
|
||||
public float HandleSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial slider value.
|
||||
/// </summary>
|
||||
public float InitialValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// true to make the slider snap to integers, or false to allow any representable
|
||||
/// floating point number in the range.
|
||||
/// </summary>
|
||||
public bool IntegersOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value that can be set by this slider. The slider is a linear scale, but
|
||||
/// can be post-scaled by the user to nonlinear if necessary.
|
||||
/// </summary>
|
||||
public float MaxValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum value that can be set by this slider.
|
||||
/// </summary>
|
||||
public float MinValue { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger during slider dragging.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnSliderDrag OnDrag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred length of the scrollbar. If vertical, this is the height, otherwise
|
||||
/// it is the width.
|
||||
/// </summary>
|
||||
public float PreferredLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger after the slider is changed. It is passed the realized source
|
||||
/// object and new value.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnSliderChanged OnValueChanged { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text. If {0} is present, it will be formatted with the slider's
|
||||
/// current value.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the slider track.
|
||||
/// </summary>
|
||||
public float TrackSize { get; set; }
|
||||
|
||||
public PSliderSingle() : this("SliderSingle") { }
|
||||
|
||||
public PSliderSingle(string name) {
|
||||
CustomTrack = false;
|
||||
Direction = Slider.Direction.LeftToRight;
|
||||
HandleColor = PUITuning.Colors.ButtonPinkStyle;
|
||||
HandleSize = 16.0f;
|
||||
InitialValue = 0.5f;
|
||||
IntegersOnly = false;
|
||||
MaxValue = 1.0f;
|
||||
MinValue = 0.0f;
|
||||
Name = name;
|
||||
PreferredLength = 100.0f;
|
||||
TrackSize = 12.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this slider is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This slider for call chaining.</returns>
|
||||
public PSliderSingle AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
// Bounds must be valid
|
||||
if (MaxValue.IsNaNOrInfinity())
|
||||
throw new ArgumentException(nameof(MaxValue));
|
||||
if (MinValue.IsNaNOrInfinity())
|
||||
throw new ArgumentException(nameof(MinValue));
|
||||
// max > min
|
||||
if (MaxValue <= MinValue)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxValue));
|
||||
// Initial value must be in range
|
||||
var slider = PUIElements.CreateUI(null, Name);
|
||||
bool isVertical = Direction == Slider.Direction.BottomToTop || Direction ==
|
||||
Slider.Direction.TopToBottom;
|
||||
var trueColor = HandleColor ?? PUITuning.Colors.ButtonBlueStyle;
|
||||
slider.SetActive(false);
|
||||
// Track (visual)
|
||||
if (!CustomTrack) {
|
||||
var trackImg = slider.AddComponent<Image>();
|
||||
trackImg.sprite = isVertical ? PUITuning.Images.ScrollBorderVertical :
|
||||
PUITuning.Images.ScrollBorderHorizontal;
|
||||
trackImg.type = Image.Type.Sliced;
|
||||
}
|
||||
// Fill
|
||||
var fill = PUIElements.CreateUI(slider, "Fill", true);
|
||||
if (!CustomTrack) {
|
||||
var fillImg = fill.AddComponent<Image>();
|
||||
fillImg.sprite = isVertical ? PUITuning.Images.ScrollHandleVertical :
|
||||
PUITuning.Images.ScrollHandleHorizontal;
|
||||
fillImg.color = trueColor.inactiveColor;
|
||||
fillImg.type = Image.Type.Sliced;
|
||||
}
|
||||
PUIElements.SetAnchorOffsets(fill, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
// Slider component itself
|
||||
var ks = slider.AddComponent<KSlider>();
|
||||
ks.maxValue = MaxValue;
|
||||
ks.minValue = MinValue;
|
||||
ks.value = InitialValue.IsNaNOrInfinity() ? MinValue : InitialValue.InRange(
|
||||
MinValue, MaxValue);
|
||||
ks.wholeNumbers = IntegersOnly;
|
||||
ks.handleRect = CreateHandle(slider).rectTransform();
|
||||
ks.fillRect = fill.rectTransform();
|
||||
ks.SetDirection(Direction, true);
|
||||
if (OnValueChanged != null)
|
||||
ks.onValueChanged.AddListener((value) => OnValueChanged(slider, value));
|
||||
if (OnDrag != null)
|
||||
ks.onDrag += () => OnDrag(slider, ks.value);
|
||||
// Manually add tooltip with slider link
|
||||
string tt = ToolTip;
|
||||
if (!string.IsNullOrEmpty(tt)) {
|
||||
var toolTip = slider.AddComponent<ToolTip>();
|
||||
toolTip.OnToolTip = () => string.Format(tt, ks.value);
|
||||
// Tooltip can be dynamically updated
|
||||
toolTip.refreshWhileHovering = true;
|
||||
}
|
||||
slider.SetActive(true);
|
||||
// Static layout!
|
||||
slider.SetMinUISize(isVertical ? new Vector2(TrackSize, PreferredLength) :
|
||||
new Vector2(PreferredLength, TrackSize));
|
||||
slider.SetFlexUISize(FlexSize);
|
||||
OnRealize?.Invoke(slider);
|
||||
return slider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handle component.
|
||||
/// </summary>
|
||||
/// <param name="slider">The parent component.</param>
|
||||
/// <returns>The sliding handle object.</returns>
|
||||
private GameObject CreateHandle(GameObject slider) {
|
||||
// Handle
|
||||
var handle = PUIElements.CreateUI(slider, "Handle", true, PUIAnchoring.Center,
|
||||
PUIAnchoring.Center);
|
||||
var handleImg = handle.AddComponent<Image>();
|
||||
handleImg.sprite = PUITuning.Images.SliderHandle;
|
||||
handleImg.preserveAspect = true;
|
||||
handle.SetUISize(new Vector2(HandleSize, HandleSize));
|
||||
// Rotate the handle if needed (CCW)
|
||||
float rot = 0.0f;
|
||||
switch (Direction) {
|
||||
case Slider.Direction.TopToBottom:
|
||||
rot = 90.0f;
|
||||
break;
|
||||
case Slider.Direction.RightToLeft:
|
||||
rot = 180.0f;
|
||||
break;
|
||||
case Slider.Direction.BottomToTop:
|
||||
rot = 270.0f;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (rot != 0.0f)
|
||||
handle.transform.Rotate(new Vector3(0.0f, 0.0f, rot));
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink button style as this slider's foreground color style.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PSliderSingle SetKleiPinkStyle() {
|
||||
HandleColor = PUITuning.Colors.ButtonPinkStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue button style as this slider's foreground color style.
|
||||
///
|
||||
/// Note that the default slider handle has a hard coded pink color.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PSliderSingle SetKleiBlueStyle() {
|
||||
HandleColor = PUITuning.Colors.ButtonBlueStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PSliderSingle[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
mod/PLibUI/PSpacer.cs
Normal file
66
mod/PLibUI/PSpacer.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A spacer to add into layouts. Has a large flexible width/height by default to eat all
|
||||
/// the extra space.
|
||||
/// </summary>
|
||||
public class PSpacer : IUIComponent {
|
||||
/// <summary>
|
||||
/// The flexible size of this spacer. Defaults to (1, 1) but can be set to (0, 0) to
|
||||
/// make this spacer a fixed size area instead.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred size of this spacer.
|
||||
/// </summary>
|
||||
public Vector2 PreferredSize { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public PSpacer() {
|
||||
Name = "Spacer";
|
||||
FlexSize = Vector2.one;
|
||||
PreferredSize = Vector2.zero;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var spacer = new GameObject(Name);
|
||||
var le = spacer.AddComponent<LayoutElement>();
|
||||
le.flexibleHeight = FlexSize.y;
|
||||
le.flexibleWidth = FlexSize.x;
|
||||
le.minHeight = 0.0f;
|
||||
le.minWidth = 0.0f;
|
||||
le.preferredHeight = PreferredSize.y;
|
||||
le.preferredWidth = PreferredSize.x;
|
||||
OnRealize?.Invoke(spacer);
|
||||
return spacer;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PSpacer";
|
||||
}
|
||||
}
|
||||
}
|
||||
219
mod/PLibUI/PTextArea.cs
Normal file
219
mod/PLibUI/PTextArea.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI text area (multi-line text field) factory class. This class should
|
||||
/// probably be wrapped in a scroll pane.
|
||||
/// </summary>
|
||||
public sealed class PTextArea : IUIComponent {
|
||||
/// <summary>
|
||||
/// The text area's background color.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The preferred number of text lines to be displayed. If the component is made
|
||||
/// bigger, the number of text lines (and size) can increase.
|
||||
/// </summary>
|
||||
public int LineCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of characters in this text area.
|
||||
/// </summary>
|
||||
public int MaxLength { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum width in units (not characters!) of this text area.
|
||||
/// </summary>
|
||||
public int MinWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text alignment in the text area.
|
||||
/// </summary>
|
||||
public TextAlignmentOptions TextAlignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial text in the text field.
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text field's text color, font, word wrap settings, and font size.
|
||||
/// </summary>
|
||||
public TextStyleSetting TextStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger on text change. It is passed the realized source object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnTextChanged OnTextChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The callback to invoke when validating input.
|
||||
/// </summary>
|
||||
public TMP_InputField.OnValidateInput OnValidate { get; set; }
|
||||
|
||||
public PTextArea() : this(null) { }
|
||||
|
||||
public PTextArea(string name) {
|
||||
BackColor = PUITuning.Colors.BackgroundLight;
|
||||
FlexSize = Vector2.one;
|
||||
LineCount = 4;
|
||||
MaxLength = 1024;
|
||||
MinWidth = 64;
|
||||
Name = name ?? "TextArea";
|
||||
Text = null;
|
||||
TextAlignment = TextAlignmentOptions.TopLeft;
|
||||
TextStyle = PUITuning.Fonts.TextDarkStyle;
|
||||
ToolTip = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this text area is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This text area for call chaining.</returns>
|
||||
public PTextArea AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var textField = PUIElements.CreateUI(null, Name);
|
||||
var style = TextStyle ?? PUITuning.Fonts.TextLightStyle;
|
||||
// Background
|
||||
var border = textField.AddComponent<Image>();
|
||||
border.sprite = PUITuning.Images.BoxBorderWhite;
|
||||
border.type = Image.Type.Sliced;
|
||||
border.color = style.textColor;
|
||||
// Text box with rectangular clipping area; put pivot in upper left
|
||||
var textArea = PUIElements.CreateUI(textField, "Text Area", false);
|
||||
textArea.AddComponent<Image>().color = BackColor;
|
||||
var mask = textArea.AddComponent<RectMask2D>();
|
||||
// Scrollable text
|
||||
var textBox = PUIElements.CreateUI(textArea, "Text");
|
||||
// Text to display
|
||||
var textDisplay = PTextField.ConfigureField(textBox.AddComponent<TextMeshProUGUI>(),
|
||||
style, TextAlignment);
|
||||
textDisplay.enableWordWrapping = true;
|
||||
textDisplay.raycastTarget = true;
|
||||
// Text field itself
|
||||
textField.SetActive(false);
|
||||
var textEntry = textField.AddComponent<TMP_InputField>();
|
||||
textEntry.textComponent = textDisplay;
|
||||
textEntry.textViewport = textArea.rectTransform();
|
||||
textEntry.text = Text ?? "";
|
||||
textDisplay.text = Text ?? "";
|
||||
// Events!
|
||||
ConfigureTextEntry(textEntry);
|
||||
var events = textField.AddComponent<PTextFieldEvents>();
|
||||
events.OnTextChanged = OnTextChanged;
|
||||
events.OnValidate = OnValidate;
|
||||
events.TextObject = textBox;
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(textField, ToolTip);
|
||||
mask.enabled = true;
|
||||
PUIElements.SetAnchorOffsets(textBox, new RectOffset());
|
||||
textField.SetActive(true);
|
||||
// Lay out
|
||||
var layout = PUIUtils.InsetChild(textField, textArea, Vector2.one, new Vector2(
|
||||
MinWidth, Math.Max(LineCount, 1) * PUIUtils.GetLineHeight(style))).
|
||||
AddOrGet<LayoutElement>();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
OnRealize?.Invoke(textField);
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the text entry field.
|
||||
/// </summary>
|
||||
/// <param name="textEntry">The input field to configure.</param>
|
||||
private void ConfigureTextEntry(TMP_InputField textEntry) {
|
||||
textEntry.characterLimit = Math.Max(1, MaxLength);
|
||||
textEntry.enabled = true;
|
||||
textEntry.inputType = TMP_InputField.InputType.Standard;
|
||||
textEntry.interactable = true;
|
||||
textEntry.isRichTextEditingAllowed = false;
|
||||
textEntry.keyboardType = TouchScreenKeyboardType.Default;
|
||||
textEntry.lineType = TMP_InputField.LineType.MultiLineNewline;
|
||||
textEntry.navigation = Navigation.defaultNavigation;
|
||||
textEntry.richText = false;
|
||||
textEntry.selectionColor = PUITuning.Colors.SelectionBackground;
|
||||
textEntry.transition = Selectable.Transition.None;
|
||||
textEntry.restoreOriginalTextOnEscape = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink style as this text area's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PTextArea SetKleiPinkStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue style as this text area's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PTextArea SetKleiBlueStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the minimum (and preferred) width of this text area in characters.
|
||||
///
|
||||
/// The width is computed using the currently selected text style.
|
||||
/// </summary>
|
||||
/// <param name="chars">The number of characters to be displayed.</param>
|
||||
/// <returns>This button for call chaining.</returns>
|
||||
public PTextArea SetMinWidthInCharacters(int chars) {
|
||||
int width = Mathf.RoundToInt(chars * PUIUtils.GetEmWidth(TextStyle));
|
||||
if (width > 0)
|
||||
MinWidth = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PTextArea[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
332
mod/PLibUI/PTextComponent.cs
Normal file
332
mod/PLibUI/PTextComponent.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI.Layouts;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// The abstract parent of PLib UI components which display text and/or images.
|
||||
/// </summary>
|
||||
public abstract class PTextComponent : IDynamicSizable {
|
||||
/// <summary>
|
||||
/// The center of an object for pivoting.
|
||||
/// </summary>
|
||||
private static readonly Vector2 CENTER = new Vector2(0.5f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// Arranges a component in the parent layout in both directions.
|
||||
/// </summary>
|
||||
/// <param name="layout">The layout to modify.</param>
|
||||
/// <param name="target">The target object to arrange.</param>
|
||||
/// <param name="alignment">The object alignment to use.</param>
|
||||
protected static void ArrangeComponent(RelativeLayoutGroup layout, GameObject target,
|
||||
TextAnchor alignment) {
|
||||
// X
|
||||
switch (alignment) {
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.MiddleLeft:
|
||||
case TextAnchor.UpperLeft:
|
||||
layout.SetLeftEdge(target, fraction: 0.0f);
|
||||
break;
|
||||
case TextAnchor.LowerRight:
|
||||
case TextAnchor.MiddleRight:
|
||||
case TextAnchor.UpperRight:
|
||||
layout.SetRightEdge(target, fraction: 1.0f);
|
||||
break;
|
||||
default:
|
||||
// MiddleCenter, LowerCenter, UpperCenter
|
||||
layout.AnchorXAxis(target, 0.5f);
|
||||
break;
|
||||
}
|
||||
// Y
|
||||
switch (alignment) {
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.LowerRight:
|
||||
layout.SetBottomEdge(target, fraction: 0.0f);
|
||||
break;
|
||||
case TextAnchor.UpperLeft:
|
||||
case TextAnchor.UpperCenter:
|
||||
case TextAnchor.UpperRight:
|
||||
layout.SetTopEdge(target, fraction: 1.0f);
|
||||
break;
|
||||
default:
|
||||
// MiddleCenter, LowerCenter, UpperCenter
|
||||
layout.AnchorYAxis(target, 0.5f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared routine to spawn UI image objects.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent object for the image.</param>
|
||||
/// <param name="settings">The settings to use for displaying the image.</param>
|
||||
/// <returns>The child image object.</returns>
|
||||
protected static Image ImageChildHelper(GameObject parent, PTextComponent settings) {
|
||||
var imageChild = PUIElements.CreateUI(parent, "Image", true,
|
||||
PUIAnchoring.Beginning, PUIAnchoring.Beginning);
|
||||
var rt = imageChild.rectTransform();
|
||||
// The pivot is important here
|
||||
rt.pivot = CENTER;
|
||||
var img = imageChild.AddComponent<Image>();
|
||||
img.color = settings.SpriteTint;
|
||||
img.sprite = settings.Sprite;
|
||||
img.type = settings.SpriteMode;
|
||||
img.preserveAspect = settings.MaintainSpriteAspect;
|
||||
// Set up transform
|
||||
var scale = Vector3.one;
|
||||
float rot = 0.0f;
|
||||
var rotate = settings.SpriteTransform;
|
||||
if ((rotate & ImageTransform.FlipHorizontal) != ImageTransform.None)
|
||||
scale.x = -1.0f;
|
||||
if ((rotate & ImageTransform.FlipVertical) != ImageTransform.None)
|
||||
scale.y = -1.0f;
|
||||
if ((rotate & ImageTransform.Rotate90) != ImageTransform.None)
|
||||
rot = 90.0f;
|
||||
if ((rotate & ImageTransform.Rotate180) != ImageTransform.None)
|
||||
rot += 180.0f;
|
||||
// Update transform
|
||||
var transform = imageChild.rectTransform();
|
||||
transform.localScale = scale;
|
||||
transform.Rotate(new Vector3(0.0f, 0.0f, rot));
|
||||
// Limit size if needed
|
||||
var imageSize = settings.SpriteSize;
|
||||
if (imageSize.x > 0.0f && imageSize.y > 0.0f)
|
||||
imageChild.SetUISize(imageSize, true);
|
||||
return img;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared routine to spawn UI text objects.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent object for the text.</param>
|
||||
/// <param name="style">The text style to use.</param>
|
||||
/// <param name="contents">The default text.</param>
|
||||
/// <returns>The child text object.</returns>
|
||||
protected static LocText TextChildHelper(GameObject parent, TextStyleSetting style,
|
||||
string contents = "") {
|
||||
var textChild = PUIElements.CreateUI(parent, "Text");
|
||||
var locText = PUIElements.AddLocText(textChild, style);
|
||||
// Font needs to be set before the text
|
||||
locText.alignment = TMPro.TextAlignmentOptions.Center;
|
||||
locText.text = contents;
|
||||
return locText;
|
||||
}
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The spacing between text and icon.
|
||||
/// </summary>
|
||||
public int IconSpacing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the sprite aspect ratio will be maintained even if it is resized.
|
||||
/// </summary>
|
||||
public bool MaintainSpriteAspect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the component.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display, or null to display no sprite.
|
||||
/// </summary>
|
||||
public Sprite Sprite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The image mode to use for the sprite.
|
||||
/// </summary>
|
||||
public Image.Type SpriteMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position to use for the sprite relative to the text.
|
||||
///
|
||||
/// If TextAnchor.MiddleCenter is used, the image will directly overlap the text.
|
||||
/// Otherwise, it will be placed in the specified location relative to the text.
|
||||
/// </summary>
|
||||
public TextAnchor SpritePosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size to scale the sprite. If 0x0, it will not be scaled.
|
||||
/// </summary>
|
||||
public Vector2 SpriteSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color to tint the sprite. For no tint, use Color.white.
|
||||
/// </summary>
|
||||
public Color SpriteTint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How to rotate or flip the sprite.
|
||||
/// </summary>
|
||||
public ImageTransform SpriteTransform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The component's text.
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text alignment in the component. Controls the placement of the text and sprite
|
||||
/// combination relative to the component's overall outline if the component is
|
||||
/// expanded from its default size.
|
||||
///
|
||||
/// The text and sprite will move as a unit to follow this text alignment. Note that
|
||||
/// incorrect positions will result if this alignment is centered in the same direction
|
||||
/// as the sprite position offset, if both a sprite and text are defined.
|
||||
///
|
||||
/// If the SpritePosition uses any variant of Left or Right, using UpperCenter,
|
||||
/// MiddleCenter, or LowerCenter for TextAlignment would result in undefined text and
|
||||
/// sprite positioning. Likewise, a SpritePosition using any variant of Lower or Upper
|
||||
/// would cause undefined positioning if TextAlignment was MiddleLeft, MiddleCenter,
|
||||
/// or MiddleRight.
|
||||
/// </summary>
|
||||
public TextAnchor TextAlignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The component's text color, font, word wrap settings, and font size.
|
||||
/// </summary>
|
||||
public TextStyleSetting TextStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
protected PTextComponent(string name) {
|
||||
DynamicSize = false;
|
||||
FlexSize = Vector2.zero;
|
||||
IconSpacing = 0;
|
||||
MaintainSpriteAspect = true;
|
||||
Margin = null;
|
||||
Name = name;
|
||||
Sprite = null;
|
||||
SpriteMode = Image.Type.Simple;
|
||||
SpritePosition = TextAnchor.MiddleLeft;
|
||||
SpriteSize = Vector2.zero;
|
||||
SpriteTint = Color.white;
|
||||
SpriteTransform = ImageTransform.None;
|
||||
Text = null;
|
||||
TextAlignment = TextAnchor.MiddleCenter;
|
||||
TextStyle = null;
|
||||
ToolTip = "";
|
||||
}
|
||||
|
||||
public abstract GameObject Build();
|
||||
|
||||
/// <summary>
|
||||
/// If the flex size is zero and dynamic size is false, the layout group can be
|
||||
/// completely destroyed on a text component after the layout is locked.
|
||||
/// </summary>
|
||||
/// <param name="component">The realized text component.</param>
|
||||
protected void DestroyLayoutIfPossible(GameObject component) {
|
||||
if (FlexSize.x == 0.0f && FlexSize.y == 0.0f && !DynamicSize)
|
||||
AbstractLayoutGroup.DestroyAndReplaceLayout(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the OnRealize event.
|
||||
/// </summary>
|
||||
/// <param name="obj">The realized text component.</param>
|
||||
protected void InvokeRealize(GameObject obj) {
|
||||
OnRealize?.Invoke(obj);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("{3}[Name={0},Text={1},Sprite={2}]", Name, Text, Sprite,
|
||||
GetType().Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the text and sprite into a single GameObject that properly positions them
|
||||
/// relative to each other, if necessary.
|
||||
/// </summary>
|
||||
/// <param name="text">The text component.</param>
|
||||
/// <param name="sprite">The sprite component.</param>
|
||||
/// <returns>A game object that contains both of them, or null if both are null.</returns>
|
||||
protected GameObject WrapTextAndSprite(GameObject text, GameObject sprite) {
|
||||
GameObject result = null;
|
||||
if (text != null && sprite != null) {
|
||||
// Automatically hoist them into a new game object
|
||||
result = PUIElements.CreateUI(text.GetParent(), "AlignmentWrapper");
|
||||
text.SetParent(result);
|
||||
sprite.SetParent(result);
|
||||
var layout = result.AddOrGet<RelativeLayoutGroup>();
|
||||
// X
|
||||
switch (SpritePosition) {
|
||||
case TextAnchor.MiddleLeft:
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.UpperLeft:
|
||||
layout.SetLeftEdge(sprite, fraction: 0.0f).SetLeftEdge(text, toRight:
|
||||
sprite).SetMargin(sprite, new RectOffset(0, IconSpacing, 0, 0));
|
||||
break;
|
||||
case TextAnchor.MiddleRight:
|
||||
case TextAnchor.LowerRight:
|
||||
case TextAnchor.UpperRight:
|
||||
layout.SetRightEdge(sprite, fraction: 1.0f).SetRightEdge(text, toLeft:
|
||||
sprite).SetMargin(sprite, new RectOffset(IconSpacing, 0, 0, 0));
|
||||
break;
|
||||
default:
|
||||
// MiddleCenter, UpperCenter, LowerCenter
|
||||
layout.AnchorXAxis(text).AnchorXAxis(sprite);
|
||||
break;
|
||||
}
|
||||
// Y
|
||||
switch (SpritePosition) {
|
||||
case TextAnchor.UpperCenter:
|
||||
case TextAnchor.UpperLeft:
|
||||
case TextAnchor.UpperRight:
|
||||
layout.SetTopEdge(sprite, fraction: 1.0f).SetTopEdge(text, below:
|
||||
sprite).SetMargin(sprite, new RectOffset(0, 0, 0, IconSpacing));
|
||||
break;
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.LowerRight:
|
||||
layout.SetBottomEdge(sprite, fraction: 0.0f).SetBottomEdge(text, above:
|
||||
sprite).SetMargin(sprite, new RectOffset(0, 0, IconSpacing, 0));
|
||||
break;
|
||||
default:
|
||||
// MiddleCenter, MiddleLeft, MiddleRight
|
||||
layout.AnchorYAxis(text).AnchorYAxis(sprite);
|
||||
break;
|
||||
}
|
||||
if (!DynamicSize)
|
||||
layout.LockLayout();
|
||||
} else if (text != null)
|
||||
result = text;
|
||||
else if (sprite != null)
|
||||
result = sprite;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
302
mod/PLibUI/PTextField.cs
Normal file
302
mod/PLibUI/PTextField.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI text field factory class.
|
||||
/// </summary>
|
||||
public sealed class PTextField : IUIComponent {
|
||||
/// <summary>
|
||||
/// Configures a Text Mesh Pro field.
|
||||
/// </summary>
|
||||
/// <param name="component">The text component to configure.</param>
|
||||
/// <param name="style">The desired text color, font, and style.</param>
|
||||
/// <param name="alignment">The text alignment.</param>
|
||||
/// <returns>The component, for call chaining.</returns>
|
||||
internal static TextMeshProUGUI ConfigureField(TextMeshProUGUI component,
|
||||
TextStyleSetting style, TextAlignmentOptions alignment) {
|
||||
component.alignment = alignment;
|
||||
component.autoSizeTextContainer = false;
|
||||
component.enabled = true;
|
||||
component.color = style.textColor;
|
||||
component.font = style.sdfFont;
|
||||
component.fontSize = style.fontSize;
|
||||
component.fontStyle = style.style;
|
||||
component.overflowMode = TextOverflowModes.Overflow;
|
||||
return component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a text field's text.
|
||||
/// </summary>
|
||||
/// <param name="textField">The UI element to retrieve.</param>
|
||||
/// <returns>The current text in the field.</returns>
|
||||
public static string GetText(GameObject textField) {
|
||||
if (textField == null)
|
||||
throw new ArgumentNullException(nameof(textField));
|
||||
return textField.TryGetComponent(out TMP_InputField field) ? field.text : "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text field's background color.
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the built-in field type used for Text Mesh Pro.
|
||||
/// </summary>
|
||||
private TMP_InputField.ContentType ContentType {
|
||||
get {
|
||||
TMP_InputField.ContentType cType;
|
||||
switch (Type) {
|
||||
case FieldType.Float:
|
||||
cType = TMP_InputField.ContentType.DecimalNumber;
|
||||
break;
|
||||
case FieldType.Integer:
|
||||
cType = TMP_InputField.ContentType.IntegerNumber;
|
||||
break;
|
||||
case FieldType.Text:
|
||||
default:
|
||||
cType = TMP_InputField.ContentType.Standard;
|
||||
break;
|
||||
}
|
||||
return cType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of characters in this text field.
|
||||
/// </summary>
|
||||
public int MaxLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum width in units (not characters!) of this text field.
|
||||
/// </summary>
|
||||
public int MinWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The placeholder text style (including color, font, and word wrap settings) if the
|
||||
/// field is empty.
|
||||
/// </summary>
|
||||
public TextStyleSetting PlaceholderStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The placeholder text if the field is empty.
|
||||
/// </summary>
|
||||
public string PlaceholderText { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The text alignment in the text field.
|
||||
/// </summary>
|
||||
public TextAlignmentOptions TextAlignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial text in the text field.
|
||||
/// </summary>
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text field's text color, font, word wrap settings, and font size.
|
||||
/// </summary>
|
||||
public TextStyleSetting TextStyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The field type.
|
||||
/// </summary>
|
||||
public FieldType Type { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger on text change. It is passed the realized source object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnTextChanged OnTextChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The callback to invoke when validating input.
|
||||
/// </summary>
|
||||
public TMP_InputField.OnValidateInput OnValidate { get; set; }
|
||||
|
||||
public PTextField() : this(null) { }
|
||||
|
||||
public PTextField(string name) {
|
||||
BackColor = PUITuning.Colors.BackgroundLight;
|
||||
FlexSize = Vector2.zero;
|
||||
MaxLength = 256;
|
||||
MinWidth = 32;
|
||||
Name = name ?? "TextField";
|
||||
PlaceholderText = null;
|
||||
Text = null;
|
||||
TextAlignment = TextAlignmentOptions.Center;
|
||||
TextStyle = PUITuning.Fonts.TextDarkStyle;
|
||||
PlaceholderStyle = TextStyle;
|
||||
ToolTip = "";
|
||||
Type = FieldType.Text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this text field is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This text field for call chaining.</returns>
|
||||
public PTextField AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var textField = PUIElements.CreateUI(null, Name);
|
||||
var style = TextStyle ?? PUITuning.Fonts.TextLightStyle;
|
||||
// Background
|
||||
var border = textField.AddComponent<Image>();
|
||||
border.sprite = PUITuning.Images.BoxBorderWhite;
|
||||
border.type = Image.Type.Sliced;
|
||||
border.color = style.textColor;
|
||||
// Text box with rectangular clipping area
|
||||
var textArea = PUIElements.CreateUI(textField, "Text Area", false);
|
||||
textArea.AddComponent<Image>().color = BackColor;
|
||||
var mask = textArea.AddComponent<RectMask2D>();
|
||||
// Scrollable text
|
||||
var textBox = PUIElements.CreateUI(textArea, "Text");
|
||||
// Text to display
|
||||
var textDisplay = ConfigureField(textBox.AddComponent<TextMeshProUGUI>(), style,
|
||||
TextAlignment);
|
||||
textDisplay.enableWordWrapping = false;
|
||||
textDisplay.maxVisibleLines = 1;
|
||||
textDisplay.raycastTarget = true;
|
||||
// Text field itself
|
||||
textField.SetActive(false);
|
||||
var textEntry = textField.AddComponent<TMP_InputField>();
|
||||
textEntry.textComponent = textDisplay;
|
||||
textEntry.textViewport = textArea.rectTransform();
|
||||
textEntry.text = Text ?? "";
|
||||
textDisplay.text = Text ?? "";
|
||||
// Placeholder
|
||||
if (PlaceholderText != null) {
|
||||
var placeholder = PUIElements.CreateUI(textArea, "Placeholder Text");
|
||||
var textPlace = ConfigureField(placeholder.AddComponent<TextMeshProUGUI>(),
|
||||
PlaceholderStyle ?? style, TextAlignment);
|
||||
textPlace.maxVisibleLines = 1;
|
||||
textPlace.text = PlaceholderText;
|
||||
textEntry.placeholder = textPlace;
|
||||
}
|
||||
// Events!
|
||||
ConfigureTextEntry(textEntry);
|
||||
var events = textField.AddComponent<PTextFieldEvents>();
|
||||
events.OnTextChanged = OnTextChanged;
|
||||
events.OnValidate = OnValidate;
|
||||
events.TextObject = textBox;
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(textField, ToolTip);
|
||||
mask.enabled = true;
|
||||
PUIElements.SetAnchorOffsets(textBox, new RectOffset());
|
||||
textField.SetActive(true);
|
||||
// Lay out
|
||||
var rt = textBox.rectTransform();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
|
||||
var layout = PUIUtils.InsetChild(textField, textArea, Vector2.one, new Vector2(
|
||||
MinWidth, LayoutUtility.GetPreferredHeight(rt))).AddOrGet<LayoutElement>();
|
||||
layout.flexibleWidth = FlexSize.x;
|
||||
layout.flexibleHeight = FlexSize.y;
|
||||
OnRealize?.Invoke(textField);
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the text entry field.
|
||||
/// </summary>
|
||||
/// <param name="textEntry">The input field to configure.</param>
|
||||
private void ConfigureTextEntry(TMP_InputField textEntry) {
|
||||
textEntry.characterLimit = Math.Max(1, MaxLength);
|
||||
textEntry.contentType = ContentType;
|
||||
textEntry.enabled = true;
|
||||
textEntry.inputType = TMP_InputField.InputType.Standard;
|
||||
textEntry.interactable = true;
|
||||
textEntry.isRichTextEditingAllowed = false;
|
||||
textEntry.keyboardType = TouchScreenKeyboardType.Default;
|
||||
textEntry.lineType = TMP_InputField.LineType.SingleLine;
|
||||
textEntry.navigation = Navigation.defaultNavigation;
|
||||
textEntry.richText = false;
|
||||
textEntry.selectionColor = PUITuning.Colors.SelectionBackground;
|
||||
textEntry.transition = Selectable.Transition.None;
|
||||
textEntry.restoreOriginalTextOnEscape = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei pink style as this text field's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This text field for call chaining.</returns>
|
||||
public PTextField SetKleiPinkStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default Klei blue style as this text field's color and text style.
|
||||
/// </summary>
|
||||
/// <returns>This text field for call chaining.</returns>
|
||||
public PTextField SetKleiBlueStyle() {
|
||||
TextStyle = PUITuning.Fonts.UILightStyle;
|
||||
BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the minimum (and preferred) width of this text field in characters.
|
||||
///
|
||||
/// The width is computed using the currently selected text style.
|
||||
/// </summary>
|
||||
/// <param name="chars">The number of characters to be displayed.</param>
|
||||
/// <returns>This text field for call chaining.</returns>
|
||||
public PTextField SetMinWidthInCharacters(int chars) {
|
||||
int width = Mathf.RoundToInt(chars * PUIUtils.GetEmWidth(TextStyle));
|
||||
if (width > 0)
|
||||
MinWidth = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PTextField[Name={0},Type={1}]", Name, Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The valid text field types supported by this class.
|
||||
/// </summary>
|
||||
public enum FieldType {
|
||||
Text, Integer, Float
|
||||
}
|
||||
}
|
||||
}
|
||||
159
mod/PLibUI/PTextFieldEvents.cs
Normal file
159
mod/PLibUI/PTextFieldEvents.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A class instance that handles events for text fields.
|
||||
/// </summary>
|
||||
internal sealed class PTextFieldEvents : KScreen {
|
||||
/// <summary>
|
||||
/// The action to trigger on text change. It is passed the realized source object.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
internal PUIDelegates.OnTextChanged OnTextChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The callback to invoke when validating input.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
internal TMP_InputField.OnValidateInput OnValidate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The object to resize on text change.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
internal GameObject TextObject { get; set; }
|
||||
|
||||
[MyCmpReq]
|
||||
#pragma warning disable IDE0044 // Add readonly modifier
|
||||
#pragma warning disable CS0649
|
||||
private TMP_InputField textEntry;
|
||||
#pragma warning restore CS0649
|
||||
#pragma warning restore IDE0044 // Add readonly modifier
|
||||
|
||||
/// <summary>
|
||||
/// Whether editing is in progress.
|
||||
/// </summary>
|
||||
private bool editing;
|
||||
|
||||
internal PTextFieldEvents() {
|
||||
activateOnSpawn = true;
|
||||
editing = false;
|
||||
TextObject = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the edit process one frame after the data is entered.
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator DelayEndEdit() {
|
||||
yield return new WaitForEndOfFrame();
|
||||
StopEditing();
|
||||
}
|
||||
|
||||
public override float GetSortKey() {
|
||||
return editing ? 99.0f : base.GetSortKey();
|
||||
}
|
||||
|
||||
protected override void OnCleanUp() {
|
||||
textEntry.onFocus -= OnFocus;
|
||||
textEntry.onValueChanged.RemoveListener(OnValueChanged);
|
||||
textEntry.onEndEdit.RemoveListener(OnEndEdit);
|
||||
base.OnCleanUp();
|
||||
}
|
||||
|
||||
protected override void OnSpawn() {
|
||||
base.OnSpawn();
|
||||
textEntry.onFocus += OnFocus;
|
||||
textEntry.onValueChanged.AddListener(OnValueChanged);
|
||||
textEntry.onEndEdit.AddListener(OnEndEdit);
|
||||
if (OnValidate != null)
|
||||
textEntry.onValidateInput = OnValidate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when editing of the text ends (field loses focus).
|
||||
/// </summary>
|
||||
/// <param name="text">The text entered.</param>
|
||||
private void OnEndEdit(string text) {
|
||||
var obj = gameObject;
|
||||
if (obj != null) {
|
||||
OnTextChanged?.Invoke(obj, text);
|
||||
if (obj.activeInHierarchy)
|
||||
StartCoroutine(DelayEndEdit());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the text field gains focus.
|
||||
/// </summary>
|
||||
private void OnFocus() {
|
||||
editing = true;
|
||||
textEntry.Select();
|
||||
textEntry.ActivateInputField();
|
||||
KScreenManager.Instance.RefreshStack();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys events if editing is in progress to prevent bubbling through to the
|
||||
/// game UI.
|
||||
/// </summary>
|
||||
public override void OnKeyDown(KButtonEvent e) {
|
||||
if (editing)
|
||||
e.Consumed = true;
|
||||
else
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys events if editing is in progress to prevent bubbling through to the
|
||||
/// game UI.
|
||||
/// </summary>
|
||||
public override void OnKeyUp(KButtonEvent e) {
|
||||
if (editing)
|
||||
e.Consumed = true;
|
||||
else
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the text box value changes.
|
||||
/// </summary>
|
||||
/// <param name="text">The text entered.</param>
|
||||
private void OnValueChanged(string text) {
|
||||
var obj = gameObject;
|
||||
if (obj != null && TextObject != null) {
|
||||
var rt = TextObject.rectTransform();
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, LayoutUtility.
|
||||
GetPreferredHeight(rt));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the edit process.
|
||||
/// </summary>
|
||||
private void StopEditing() {
|
||||
if (textEntry != null && textEntry.gameObject.activeInHierarchy)
|
||||
textEntry.DeactivateInputField();
|
||||
editing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
mod/PLibUI/PToggle.cs
Normal file
174
mod/PLibUI/PToggle.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// A custom UI toggled button factory class.
|
||||
/// </summary>
|
||||
public sealed class PToggle : IDynamicSizable {
|
||||
/// <summary>
|
||||
/// The default margins around a toggle.
|
||||
/// </summary>
|
||||
internal static readonly RectOffset TOGGLE_MARGIN = new RectOffset(1, 1, 1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a realized toggle button's state.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized toggle button.</param>
|
||||
/// <returns>The toggle button state.</returns>
|
||||
public static bool GetToggleState(GameObject realized) {
|
||||
return realized != null && realized.TryGetComponent(out KToggle toggle) &&
|
||||
UIDetours.IS_ON.Get(toggle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a realized toggle button's state.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized toggle button.</param>
|
||||
/// <param name="on">Whether the button should be on or off.</param>
|
||||
public static void SetToggleState(GameObject realized, bool on) {
|
||||
if (realized != null && realized.TryGetComponent(out KToggle toggle))
|
||||
UIDetours.IS_ON.Set(toggle, on);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display when active.
|
||||
/// </summary>
|
||||
public Sprite ActiveSprite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The toggle's color.
|
||||
/// </summary>
|
||||
public ColorStyleSetting Color { get; set; }
|
||||
|
||||
public bool DynamicSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The flexible size bounds of this component.
|
||||
/// </summary>
|
||||
public Vector2 FlexSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display when inactive.
|
||||
/// </summary>
|
||||
public Sprite InactiveSprite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial state of the toggle button.
|
||||
/// </summary>
|
||||
public bool InitialState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The margin around the component.
|
||||
/// </summary>
|
||||
public RectOffset Margin { get; set; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The action to trigger when the state changes. It is passed the realized source
|
||||
/// object.
|
||||
/// </summary>
|
||||
public PUIDelegates.OnToggleButton OnStateChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size to scale the toggle images. If 0x0, it will not be scaled.
|
||||
/// </summary>
|
||||
public Vector2 Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip text.
|
||||
/// </summary>
|
||||
public string ToolTip { get; set; }
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
public PToggle() : this(null) { }
|
||||
|
||||
public PToggle(string name) {
|
||||
ActiveSprite = PUITuning.Images.Contract;
|
||||
Color = PUITuning.Colors.ComponentDarkStyle;
|
||||
InitialState = false;
|
||||
Margin = TOGGLE_MARGIN;
|
||||
Name = name ?? "Toggle";
|
||||
InactiveSprite = PUITuning.Images.Expand;
|
||||
ToolTip = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a handler when this toggle button is realized.
|
||||
/// </summary>
|
||||
/// <param name="onRealize">The handler to invoke on realization.</param>
|
||||
/// <returns>This toggle button for call chaining.</returns>
|
||||
public PToggle AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
||||
OnRealize += onRealize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var toggle = PUIElements.CreateUI(null, Name);
|
||||
// Set on click event
|
||||
var kToggle = toggle.AddComponent<KToggle>();
|
||||
var evt = OnStateChanged;
|
||||
if (evt != null)
|
||||
kToggle.onValueChanged += (on) => {
|
||||
evt?.Invoke(toggle, on);
|
||||
};
|
||||
UIDetours.ART_EXTENSION.Set(kToggle, new KToggleArtExtensions());
|
||||
UIDetours.SOUND_PLAYER_TOGGLE.Set(kToggle, PUITuning.ToggleSounds);
|
||||
// Background image
|
||||
var fgImage = toggle.AddComponent<Image>();
|
||||
fgImage.color = Color.activeColor;
|
||||
fgImage.sprite = InactiveSprite;
|
||||
toggle.SetActive(false);
|
||||
// Toggled images
|
||||
var toggleImage = toggle.AddComponent<ImageToggleState>();
|
||||
toggleImage.TargetImage = fgImage;
|
||||
toggleImage.useSprites = true;
|
||||
toggleImage.InactiveSprite = InactiveSprite;
|
||||
toggleImage.ActiveSprite = ActiveSprite;
|
||||
toggleImage.startingState = InitialState ? ImageToggleState.State.Active :
|
||||
ImageToggleState.State.Inactive;
|
||||
toggleImage.useStartingState = true;
|
||||
toggleImage.ActiveColour = Color.activeColor;
|
||||
toggleImage.DisabledActiveColour = Color.disabledActiveColor;
|
||||
toggleImage.InactiveColour = Color.inactiveColor;
|
||||
toggleImage.DisabledColour = Color.disabledColor;
|
||||
toggleImage.HoverColour = Color.hoverColor;
|
||||
toggleImage.DisabledHoverColor = Color.disabledhoverColor;
|
||||
UIDetours.IS_ON.Set(kToggle, InitialState);
|
||||
toggle.SetActive(true);
|
||||
// Set size
|
||||
if (Size.x > 0.0f && Size.y > 0.0f)
|
||||
toggle.SetUISize(Size, true);
|
||||
else
|
||||
PUIElements.AddSizeFitter(toggle, DynamicSize);
|
||||
// Add tooltip
|
||||
PUIElements.SetToolTip(toggle, ToolTip).SetFlexUISize(FlexSize).SetActive(true);
|
||||
OnRealize?.Invoke(toggle);
|
||||
return toggle;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("PToggle[Name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
mod/PLibUI/PUIDelegates.cs
Normal file
89
mod/PLibUI/PUIDelegates.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Delegate types used in the UI event system.
|
||||
/// </summary>
|
||||
public sealed class PUIDelegates {
|
||||
/// <summary>
|
||||
/// The delegate type invoked when a dialog is closed.
|
||||
/// </summary>
|
||||
/// <param name="option">The key of the chosen option, or PDialog.DIALOG_CLOSE_KEY if
|
||||
/// the dialog was closed with ESC or the X button.</param>
|
||||
public delegate void OnDialogClosed(string option);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when a button is pressed.
|
||||
/// </summary>
|
||||
/// <param name="source">The source button.</param>
|
||||
public delegate void OnButtonPressed(GameObject source);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when a checkbox is clicked.
|
||||
/// </summary>
|
||||
/// <param name="source">The source button.</param>
|
||||
/// <param name="state">The checkbox state.</param>
|
||||
public delegate void OnChecked(GameObject source, int state);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when an option is selected from a combo box.
|
||||
/// </summary>
|
||||
/// <param name="source">The source dropdown.</param>
|
||||
/// <param name="choice">The option chosen.</param>
|
||||
/// <typeparam name="T">The type of the objects in the drop down.</typeparam>
|
||||
public delegate void OnDropdownChanged<T>(GameObject source, T choice) where T :
|
||||
class, IListableOption;
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when components are converted into Unity game objects.
|
||||
/// </summary>
|
||||
/// <param name="realized">The realized object.</param>
|
||||
public delegate void OnRealize(GameObject realized);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked once after a slider is changed and released.
|
||||
/// </summary>
|
||||
/// <param name="source">The source slider.</param>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
public delegate void OnSliderChanged(GameObject source, float newValue);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked while a slider is being changed.
|
||||
/// </summary>
|
||||
/// <param name="source">The source slider.</param>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
public delegate void OnSliderDrag(GameObject source, float newValue);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when text in a text field is changed.
|
||||
/// </summary>
|
||||
/// <param name="source">The source text field.</param>
|
||||
/// <param name="text">The new text.</param>
|
||||
public delegate void OnTextChanged(GameObject source, string text);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type invoked when a toggle button is swapped between states.
|
||||
/// </summary>
|
||||
/// <param name="source">The source button.</param>
|
||||
/// <param name="on">true if the button is toggled on, or false otherwise.</param>
|
||||
public delegate void OnToggleButton(GameObject source, bool on);
|
||||
}
|
||||
}
|
||||
357
mod/PLibUI/PUIElements.cs
Normal file
357
mod/PLibUI/PUIElements.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.Detours;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using ContentFitMode = UnityEngine.UI.ContentSizeFitter.FitMode;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Used for creating and managing UI elements.
|
||||
/// </summary>
|
||||
public sealed class PUIElements {
|
||||
/// <summary>
|
||||
/// Safely adds a LocText to a game object without throwing an NRE on construction.
|
||||
/// </summary>
|
||||
/// <param name="parent">The game object to add the LocText.</param>
|
||||
/// <param name="setting">The text style.</param>
|
||||
/// <returns>The added LocText object.</returns>
|
||||
internal static LocText AddLocText(GameObject parent, TextStyleSetting setting = null)
|
||||
{
|
||||
if (parent == null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
bool active = parent.activeSelf;
|
||||
parent.SetActive(false);
|
||||
var text = parent.AddComponent<LocText>();
|
||||
// This is enough to let it activate
|
||||
UIDetours.LOCTEXT_KEY.Set(text, string.Empty);
|
||||
UIDetours.LOCTEXT_STYLE.Set(text, setting ?? PUITuning.Fonts.UIDarkStyle);
|
||||
parent.SetActive(active);
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an auto-fit resizer to a UI element.
|
||||
///
|
||||
/// UI elements should be active before any layouts are added, especially if they are
|
||||
/// to be frozen.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The element to resize.</param>
|
||||
/// <param name="dynamic">true to use the Unity content size fitter which adjusts to
|
||||
/// content changes, or false to set the size only once.</param>
|
||||
/// <param name="modeHoriz">The sizing mode to use in the horizontal direction.</param>
|
||||
/// <param name="modeVert">The sizing mode to use in the vertical direction.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject AddSizeFitter(GameObject uiElement, bool dynamic = false,
|
||||
ContentFitMode modeHoriz = ContentFitMode.PreferredSize,
|
||||
ContentFitMode modeVert = ContentFitMode.PreferredSize) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
if (dynamic) {
|
||||
var fitter = uiElement.AddOrGet<ContentSizeFitter>();
|
||||
fitter.horizontalFit = modeHoriz;
|
||||
fitter.verticalFit = modeVert;
|
||||
fitter.enabled = true;
|
||||
} else
|
||||
FitSizeNow(uiElement, modeHoriz, modeVert);
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a UI game object.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent of the UI object. If not set now, or added/changed
|
||||
/// later, the anchors must be redefined.</param>
|
||||
/// <param name="name">The object name.</param>
|
||||
/// <param name="canvas">true to add a canvas renderer, or false otherwise.</param>
|
||||
/// <param name="horizAnchor">How to anchor the object horizontally.</param>
|
||||
/// <param name="vertAnchor">How to anchor the object vertically.</param>
|
||||
/// <returns>The UI object with transform and canvas initialized.</returns>
|
||||
public static GameObject CreateUI(GameObject parent, string name, bool canvas = true,
|
||||
PUIAnchoring horizAnchor = PUIAnchoring.Stretch,
|
||||
PUIAnchoring vertAnchor = PUIAnchoring.Stretch) {
|
||||
var element = new GameObject(name);
|
||||
if (parent != null)
|
||||
element.SetParent(parent);
|
||||
// Size and position
|
||||
var transform = element.AddOrGet<RectTransform>();
|
||||
transform.localScale = Vector3.one;
|
||||
SetAnchors(element, horizAnchor, vertAnchor);
|
||||
// Almost all UI components need a canvas renderer for some reason
|
||||
if (canvas)
|
||||
element.AddComponent<CanvasRenderer>();
|
||||
element.layer = LayerMask.NameToLayer("UI");
|
||||
return element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does nothing, to make the buttons appear.
|
||||
/// </summary>
|
||||
private static void DoNothing() { }
|
||||
|
||||
/// <summary>
|
||||
/// Fits the UI element's size immediately, as if ContentSizeFitter was created on it,
|
||||
/// but does not create a component and only affects the size once.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The element to resize.</param>
|
||||
/// <param name="modeHoriz">The sizing mode to use in the horizontal direction.</param>
|
||||
/// <param name="modeVert">The sizing mode to use in the vertical direction.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
private static void FitSizeNow(GameObject uiElement, ContentFitMode modeHoriz,
|
||||
ContentFitMode modeVert) {
|
||||
float width = 0.0f, height = 0.0f;
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
// Follow order in https://docs.unity3d.com/Manual/UIAutoLayout.html
|
||||
var elements = uiElement.GetComponents<ILayoutElement>();
|
||||
var constraints = uiElement.AddOrGet<LayoutElement>();
|
||||
var rt = uiElement.AddOrGet<RectTransform>();
|
||||
// Calculate horizontal
|
||||
foreach (var layoutElement in elements)
|
||||
layoutElement.CalculateLayoutInputHorizontal();
|
||||
if (modeHoriz != ContentFitMode.Unconstrained) {
|
||||
// Layout horizontal
|
||||
foreach (var layoutElement in elements)
|
||||
switch (modeHoriz) {
|
||||
case ContentFitMode.MinSize:
|
||||
width = Math.Max(width, layoutElement.minWidth);
|
||||
break;
|
||||
case ContentFitMode.PreferredSize:
|
||||
width = Math.Max(width, layoutElement.preferredWidth);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
width = Math.Max(width, constraints.minWidth);
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
|
||||
constraints.minWidth = width;
|
||||
constraints.flexibleWidth = 0.0f;
|
||||
}
|
||||
// Calculate vertical
|
||||
foreach (var layoutElement in elements)
|
||||
layoutElement.CalculateLayoutInputVertical();
|
||||
if (modeVert != ContentFitMode.Unconstrained) {
|
||||
// Layout vertical
|
||||
foreach (var layoutElement in elements)
|
||||
switch (modeVert) {
|
||||
case ContentFitMode.MinSize:
|
||||
height = Math.Max(height, layoutElement.minHeight);
|
||||
break;
|
||||
case ContentFitMode.PreferredSize:
|
||||
height = Math.Max(height, layoutElement.preferredHeight);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
height = Math.Max(height, constraints.minHeight);
|
||||
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
constraints.minHeight = height;
|
||||
constraints.flexibleHeight = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the anchor location of a UI element. The offsets will be reset, use
|
||||
/// SetAnchorOffsets to adjust the offset from the new anchor locations.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="horizAnchor">The horizontal anchor mode.</param>
|
||||
/// <param name="vertAnchor">The vertical anchor mode.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetAnchors(GameObject uiElement, PUIAnchoring horizAnchor,
|
||||
PUIAnchoring vertAnchor) {
|
||||
Vector2 aMax = new Vector2(), aMin = new Vector2(), pivot = new Vector2();
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var transform = uiElement.rectTransform();
|
||||
// Anchor: horizontal
|
||||
switch (horizAnchor) {
|
||||
case PUIAnchoring.Center:
|
||||
aMin.x = 0.5f;
|
||||
aMax.x = 0.5f;
|
||||
pivot.x = 0.5f;
|
||||
break;
|
||||
case PUIAnchoring.End:
|
||||
aMin.x = 1.0f;
|
||||
aMax.x = 1.0f;
|
||||
pivot.x = 1.0f;
|
||||
break;
|
||||
case PUIAnchoring.Stretch:
|
||||
aMin.x = 0.0f;
|
||||
aMax.x = 1.0f;
|
||||
pivot.x = 0.5f;
|
||||
break;
|
||||
default:
|
||||
aMin.x = 0.0f;
|
||||
aMax.x = 0.0f;
|
||||
pivot.x = 0.0f;
|
||||
break;
|
||||
}
|
||||
// Anchor: vertical
|
||||
switch (vertAnchor) {
|
||||
case PUIAnchoring.Center:
|
||||
aMin.y = 0.5f;
|
||||
aMax.y = 0.5f;
|
||||
pivot.y = 0.5f;
|
||||
break;
|
||||
case PUIAnchoring.End:
|
||||
aMin.y = 1.0f;
|
||||
aMax.y = 1.0f;
|
||||
pivot.y = 1.0f;
|
||||
break;
|
||||
case PUIAnchoring.Stretch:
|
||||
aMin.y = 0.0f;
|
||||
aMax.y = 1.0f;
|
||||
pivot.y = 0.5f;
|
||||
break;
|
||||
default:
|
||||
aMin.y = 0.0f;
|
||||
aMax.y = 0.0f;
|
||||
pivot.y = 0.0f;
|
||||
break;
|
||||
}
|
||||
transform.anchorMax = aMax;
|
||||
transform.anchorMin = aMin;
|
||||
transform.pivot = pivot;
|
||||
transform.anchoredPosition = Vector2.zero;
|
||||
transform.offsetMax = Vector2.zero;
|
||||
transform.offsetMin = Vector2.zero;
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the offsets of the UI component from its anchors. Positive for each value
|
||||
/// denotes towards the component center, and negative away from the component center.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="border">The offset of each corner from the anchors.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetAnchorOffsets(GameObject uiElement, RectOffset border) {
|
||||
return SetAnchorOffsets(uiElement, border.left, border.right, border.top, border.
|
||||
bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the offsets of the UI component from its anchors. Positive for each value
|
||||
/// denotes towards the component center, and negative away from the component center.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="left">The left border in pixels.</param>
|
||||
/// <param name="right">The right border in pixels.</param>
|
||||
/// <param name="top">The top border in pixels.</param>
|
||||
/// <param name="bottom">The bottom border in pixels.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetAnchorOffsets(GameObject uiElement, float left,
|
||||
float right, float top, float bottom) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var transform = uiElement.rectTransform();
|
||||
transform.offsetMin = new Vector2(left, bottom);
|
||||
transform.offsetMax = new Vector2(-right, -top);
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's text.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="text">The text to display on the element.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetText(GameObject uiElement, string text) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var lt = uiElement.GetComponentInChildren<TMPro.TextMeshProUGUI>();
|
||||
if (lt != null)
|
||||
lt.SetText(text ?? string.Empty);
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's tool tip.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="tooltip">The tool tip text to display when hovered.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetToolTip(GameObject uiElement, string tooltip) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
if (!string.IsNullOrEmpty(tooltip))
|
||||
uiElement.AddOrGet<ToolTip>().toolTip = tooltip;
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a confirmation dialog.
|
||||
/// </summary>
|
||||
/// <param name="parent">The dialog's parent.</param>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="onConfirm">The action to invoke if Yes or OK is selected.</param>
|
||||
/// <param name="onCancel">The action to invoke if No or Cancel is selected.</param>
|
||||
/// <param name="confirmText">The text for the OK/Yes button.</param>
|
||||
/// <param name="cancelText">The text for the Cancel/No button.</param>
|
||||
/// <returns>The dialog created.</returns>
|
||||
public static ConfirmDialogScreen ShowConfirmDialog(GameObject parent, string message,
|
||||
System.Action onConfirm, System.Action onCancel = null,
|
||||
string confirmText = null, string cancelText = null) {
|
||||
if (parent == null)
|
||||
parent = PDialog.GetParentObject();
|
||||
var obj = Util.KInstantiateUI(ScreenPrefabs.Instance.ConfirmDialogScreen.
|
||||
gameObject, parent, false);
|
||||
if (obj.TryGetComponent(out ConfirmDialogScreen confirmDialog)) {
|
||||
UIDetours.POPUP_CONFIRM.Invoke(confirmDialog, message, onConfirm, onCancel ??
|
||||
DoNothing, null, null, null, confirmText, cancelText);
|
||||
obj.SetActive(true);
|
||||
} else
|
||||
confirmDialog = null;
|
||||
return confirmDialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a message dialog.
|
||||
/// </summary>
|
||||
/// <param name="parent">The dialog's parent.</param>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <returns>The dialog created.</returns>
|
||||
public static ConfirmDialogScreen ShowMessageDialog(GameObject parent, string message)
|
||||
{
|
||||
return ShowConfirmDialog(parent, message, DoNothing);
|
||||
}
|
||||
|
||||
// This class should probably be static, but that might be binary compatibility
|
||||
// breaking
|
||||
private PUIElements() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The anchor mode to set a UI component.
|
||||
/// </summary>
|
||||
public enum PUIAnchoring {
|
||||
// Stretch to all
|
||||
Stretch,
|
||||
// Relative to beginning
|
||||
Beginning,
|
||||
// Relative to center
|
||||
Center,
|
||||
// Relative to end
|
||||
End
|
||||
}
|
||||
}
|
||||
432
mod/PLibUI/PUITuning.cs
Normal file
432
mod/PLibUI/PUITuning.cs
Normal file
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Sets up common parameters for the UI in PLib based mods. Note that this class is still
|
||||
/// specific to individual mods so the values in the latest PLib will not supersede them.
|
||||
/// </summary>
|
||||
public static class PUITuning {
|
||||
/// <summary>
|
||||
/// UI images.
|
||||
/// </summary>
|
||||
public static class Images {
|
||||
/// <summary>
|
||||
/// The right arrow image. Rotate it in the Image to get more directions.
|
||||
/// </summary>
|
||||
public static Sprite Arrow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image used to make a 1px solid black border.
|
||||
/// </summary>
|
||||
public static Sprite BoxBorder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image used to make a 1px solid white border.
|
||||
/// </summary>
|
||||
public static Sprite BoxBorderWhite { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default image used for button appearance.
|
||||
/// </summary>
|
||||
public static Sprite ButtonBorder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The border image around a checkbox.
|
||||
/// </summary>
|
||||
public static Sprite CheckBorder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image for a check box which is checked.
|
||||
/// </summary>
|
||||
public static Sprite Checked { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image used for dialog close buttons.
|
||||
/// </summary>
|
||||
public static Sprite Close { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image for contracting a category.
|
||||
/// </summary>
|
||||
public static Sprite Contract { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image for expanding a category.
|
||||
/// </summary>
|
||||
public static Sprite Expand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The image for a check box which is neither checked nor unchecked.
|
||||
/// </summary>
|
||||
public static Sprite Partial { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The border of a horizontal scroll bar.
|
||||
/// </summary>
|
||||
public static Sprite ScrollBorderHorizontal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The handle of a horizontal scroll bar.
|
||||
/// </summary>
|
||||
public static Sprite ScrollHandleHorizontal { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The border of a vertical scroll bar.
|
||||
/// </summary>
|
||||
public static Sprite ScrollBorderVertical { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The handle of a vertical scroll bar.
|
||||
/// </summary>
|
||||
public static Sprite ScrollHandleVertical { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The handle of a horizontal slider.
|
||||
/// </summary>
|
||||
public static Sprite SliderHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The sprite dictionary.
|
||||
/// </summary>
|
||||
private static readonly IDictionary<string, Sprite> SPRITES;
|
||||
|
||||
static Images() {
|
||||
SPRITES = new Dictionary<string, Sprite>(512);
|
||||
// List out all sprites shipped with the game
|
||||
foreach (var img in Resources.FindObjectsOfTypeAll<Sprite>()) {
|
||||
string name = img?.name;
|
||||
if (!string.IsNullOrEmpty(name) && !SPRITES.ContainsKey(name))
|
||||
SPRITES.Add(name, img);
|
||||
}
|
||||
|
||||
Arrow = GetSpriteByName("game_speed_play");
|
||||
BoxBorder = GetSpriteByName("web_box");
|
||||
BoxBorderWhite = GetSpriteByName("web_border");
|
||||
ButtonBorder = GetSpriteByName("web_button");
|
||||
CheckBorder = GetSpriteByName("overview_jobs_skill_box");
|
||||
Checked = GetSpriteByName("overview_jobs_icon_checkmark");
|
||||
Close = GetSpriteByName("cancel");
|
||||
Contract = GetSpriteByName("iconDown");
|
||||
Expand = GetSpriteByName("iconRight");
|
||||
Partial = GetSpriteByName("overview_jobs_icon_mixed");
|
||||
ScrollBorderHorizontal = GetSpriteByName("build_menu_scrollbar_frame_horizontal");
|
||||
ScrollHandleHorizontal = GetSpriteByName("build_menu_scrollbar_inner_horizontal");
|
||||
ScrollBorderVertical = GetSpriteByName("build_menu_scrollbar_frame");
|
||||
ScrollHandleVertical = GetSpriteByName("build_menu_scrollbar_inner");
|
||||
SliderHandle = GetSpriteByName("game_speed_selected_med");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sprite by its name.
|
||||
/// </summary>
|
||||
/// <param name="name">The sprite name.</param>
|
||||
/// <returns>The matching sprite, or null if no sprite found in the resources has that name.</returns>
|
||||
public static Sprite GetSpriteByName(string name) {
|
||||
if (!SPRITES.TryGetValue(name, out Sprite sprite))
|
||||
sprite = null;
|
||||
return sprite;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI colors.
|
||||
/// </summary>
|
||||
public static class Colors {
|
||||
/// <summary>
|
||||
/// A white color used for default backgrounds.
|
||||
/// </summary>
|
||||
public static Color BackgroundLight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color styles used on pink buttons.
|
||||
/// </summary>
|
||||
public static ColorStyleSetting ButtonPinkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color styles used on blue buttons.
|
||||
/// </summary>
|
||||
public static ColorStyleSetting ButtonBlueStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default colors used on check boxes / toggles with dark backgrounds.
|
||||
/// </summary>
|
||||
public static ColorStyleSetting ComponentDarkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default colors used on check boxes / toggles with white backgrounds.
|
||||
/// </summary>
|
||||
public static ColorStyleSetting ComponentLightStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color displayed on dialog backgrounds.
|
||||
/// </summary>
|
||||
public static Color DialogBackground { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color displayed in the large border around the outsides of options dialogs.
|
||||
/// </summary>
|
||||
public static Color DialogDarkBackground { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color displayed on options dialog backgrounds.
|
||||
/// </summary>
|
||||
public static Color OptionsBackground { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color displayed on scrollbar handles.
|
||||
/// </summary>
|
||||
public static ColorBlock ScrollbarColors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The background color for selections.
|
||||
/// </summary>
|
||||
public static Color SelectionBackground { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The foreground color for selections.
|
||||
/// </summary>
|
||||
public static Color SelectionForeground { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A completely transparent color.
|
||||
/// </summary>
|
||||
public static Color Transparent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used for dark-colored UI text.
|
||||
/// </summary>
|
||||
public static Color UITextDark { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used for light-colored UI text.
|
||||
/// </summary>
|
||||
public static Color UITextLight { get; }
|
||||
|
||||
static Colors() {
|
||||
BackgroundLight = new Color32(255, 255, 255, 255);
|
||||
DialogBackground = new Color32(0, 0, 0, 255);
|
||||
DialogDarkBackground = new Color32(48, 52, 67, 255);
|
||||
OptionsBackground = new Color32(31, 34, 43, 255);
|
||||
SelectionBackground = new Color32(189, 218, 255, 255);
|
||||
SelectionForeground = new Color32(0, 0, 0, 255);
|
||||
Transparent = new Color32(255, 255, 255, 0);
|
||||
UITextLight = new Color32(255, 255, 255, 255);
|
||||
UITextDark = new Color32(0, 0, 0, 255);
|
||||
|
||||
// Check boxes
|
||||
Color active = new Color(0.0f, 0.0f, 0.0f), disabled = new Color(0.784f,
|
||||
0.784f, 0.784f, 1.0f);
|
||||
ComponentLightStyle = ScriptableObject.CreateInstance<ColorStyleSetting>();
|
||||
ComponentLightStyle.activeColor = active;
|
||||
ComponentLightStyle.inactiveColor = active;
|
||||
ComponentLightStyle.hoverColor = active;
|
||||
ComponentLightStyle.disabledActiveColor = disabled;
|
||||
ComponentLightStyle.disabledColor = disabled;
|
||||
ComponentLightStyle.disabledhoverColor = disabled;
|
||||
|
||||
active = new Color(1.0f, 1.0f, 1.0f);
|
||||
ComponentDarkStyle = ScriptableObject.CreateInstance<ColorStyleSetting>();
|
||||
ComponentDarkStyle.activeColor = active;
|
||||
ComponentDarkStyle.inactiveColor = active;
|
||||
ComponentDarkStyle.hoverColor = active;
|
||||
ComponentDarkStyle.disabledActiveColor = disabled;
|
||||
ComponentDarkStyle.disabledColor = disabled;
|
||||
ComponentDarkStyle.disabledhoverColor = disabled;
|
||||
|
||||
// Buttons: pink
|
||||
ButtonPinkStyle = ScriptableObject.CreateInstance<ColorStyleSetting>();
|
||||
ButtonPinkStyle.activeColor = new Color(0.7941176f, 0.4496107f, 0.6242238f);
|
||||
ButtonPinkStyle.inactiveColor = new Color(0.5294118f, 0.2724914f, 0.4009516f);
|
||||
ButtonPinkStyle.disabledColor = new Color(0.4156863f, 0.4117647f, 0.4f);
|
||||
ButtonPinkStyle.disabledActiveColor = Transparent;
|
||||
ButtonPinkStyle.hoverColor = new Color(0.6176471f, 0.3315311f, 0.4745891f);
|
||||
ButtonPinkStyle.disabledhoverColor = new Color(0.5f, 0.5f, 0.5f);
|
||||
|
||||
// Buttons: blue
|
||||
ButtonBlueStyle = ScriptableObject.CreateInstance<ColorStyleSetting>();
|
||||
ButtonBlueStyle.activeColor = new Color(0.5033521f, 0.5444419f, 0.6985294f);
|
||||
ButtonBlueStyle.inactiveColor = new Color(0.2431373f, 0.2627451f, 0.3411765f);
|
||||
ButtonBlueStyle.disabledColor = new Color(0.4156863f, 0.4117647f, 0.4f);
|
||||
ButtonBlueStyle.disabledActiveColor = new Color(0.625f, 0.6158088f, 0.5882353f);
|
||||
ButtonBlueStyle.hoverColor = new Color(0.3461289f, 0.3739619f, 0.4852941f);
|
||||
ButtonBlueStyle.disabledhoverColor = new Color(0.5f, 0.4898898f, 0.4595588f);
|
||||
|
||||
// Scrollbars
|
||||
ScrollbarColors = new ColorBlock {
|
||||
colorMultiplier = 1.0f,
|
||||
fadeDuration = 0.1f,
|
||||
disabledColor = new Color(0.392f, 0.392f, 0.392f),
|
||||
highlightedColor = new Color32(161, 163, 174, 255),
|
||||
normalColor = new Color32(161, 163, 174, 255),
|
||||
pressedColor = BackgroundLight
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects references to fonts in the game.
|
||||
/// </summary>
|
||||
public static class Fonts {
|
||||
/// <summary>
|
||||
/// The text font name.
|
||||
/// </summary>
|
||||
private const string DEFAULT_FONT_TEXT = "NotoSans-Regular";
|
||||
|
||||
/// <summary>
|
||||
/// The UI font name.
|
||||
/// </summary>
|
||||
private const string DEFAULT_FONT_UI = "GRAYSTROKE REGULAR SDF";
|
||||
|
||||
/// <summary>
|
||||
/// The default font size.
|
||||
/// </summary>
|
||||
public static int DefaultSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default font asset for text strings.
|
||||
/// </summary>
|
||||
private static readonly TMP_FontAsset DefaultTextFont;
|
||||
|
||||
/// <summary>
|
||||
/// The default font asset for UI titles and buttons.
|
||||
/// </summary>
|
||||
private static readonly TMP_FontAsset DefaultUIFont;
|
||||
|
||||
/// <summary>
|
||||
/// The font used on text.
|
||||
/// </summary>
|
||||
internal static TMP_FontAsset Text {
|
||||
get {
|
||||
TMP_FontAsset font = null;
|
||||
if (Localization.GetSelectedLanguageType() != Localization.
|
||||
SelectedLanguageType.None)
|
||||
font = Localization.FontAsset;
|
||||
return font ?? DefaultTextFont;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text styles used on all items with a light background.
|
||||
/// </summary>
|
||||
public static TextStyleSetting TextDarkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The text styles used on all items with a dark background.
|
||||
/// </summary>
|
||||
public static TextStyleSetting TextLightStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The font used on UI elements.
|
||||
/// </summary>
|
||||
internal static TMP_FontAsset UI {
|
||||
get {
|
||||
TMP_FontAsset font = null;
|
||||
if (Localization.GetSelectedLanguageType() != Localization.
|
||||
SelectedLanguageType.None)
|
||||
font = Localization.FontAsset;
|
||||
return font ?? DefaultUIFont;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text styles used on all UI items with a light background.
|
||||
/// </summary>
|
||||
public static TextStyleSetting UIDarkStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The text styles used on all UI items with a dark background.
|
||||
/// </summary>
|
||||
public static TextStyleSetting UILightStyle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The font dictionary.
|
||||
/// </summary>
|
||||
private static readonly IDictionary<string, TMP_FontAsset> FONTS;
|
||||
|
||||
static Fonts() {
|
||||
FONTS = new Dictionary<string, TMP_FontAsset>(16);
|
||||
// List out all fonts shipped with the game
|
||||
foreach (var newFont in Resources.FindObjectsOfTypeAll<TMP_FontAsset>()) {
|
||||
string name = newFont?.name;
|
||||
if (!string.IsNullOrEmpty(name) && !FONTS.ContainsKey(name))
|
||||
FONTS.Add(name, newFont);
|
||||
}
|
||||
|
||||
// Initialization: UI fonts
|
||||
if ((DefaultTextFont = GetFontByName(DEFAULT_FONT_TEXT)) == null)
|
||||
PUIUtils.LogUIWarning("Unable to find font " + DEFAULT_FONT_TEXT);
|
||||
if ((DefaultUIFont = GetFontByName(DEFAULT_FONT_UI)) == null)
|
||||
PUIUtils.LogUIWarning("Unable to find font " + DEFAULT_FONT_UI);
|
||||
|
||||
// Initialization: Text style
|
||||
DefaultSize = 14;
|
||||
TextDarkStyle = ScriptableObject.CreateInstance<TextStyleSetting>();
|
||||
TextDarkStyle.enableWordWrapping = false;
|
||||
TextDarkStyle.fontSize = DefaultSize;
|
||||
TextDarkStyle.sdfFont = Text;
|
||||
TextDarkStyle.style = FontStyles.Normal;
|
||||
TextDarkStyle.textColor = Colors.UITextDark;
|
||||
TextLightStyle = TextDarkStyle.DeriveStyle(newColor: Colors.UITextLight);
|
||||
UIDarkStyle = ScriptableObject.CreateInstance<TextStyleSetting>();
|
||||
UIDarkStyle.enableWordWrapping = false;
|
||||
UIDarkStyle.fontSize = DefaultSize;
|
||||
UIDarkStyle.sdfFont = UI;
|
||||
UIDarkStyle.style = FontStyles.Normal;
|
||||
UIDarkStyle.textColor = Colors.UITextDark;
|
||||
UILightStyle = UIDarkStyle.DeriveStyle(newColor: Colors.UITextLight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a font by its name.
|
||||
/// </summary>
|
||||
/// <param name="name">The font name.</param>
|
||||
/// <returns>The matching font, or null if no font found in the resources has that name.</returns>
|
||||
internal static TMP_FontAsset GetFontByName(string name) {
|
||||
if (!FONTS.TryGetValue(name, out TMP_FontAsset font))
|
||||
font = null;
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The sounds played by the button.
|
||||
/// </summary>
|
||||
internal static ButtonSoundPlayer ButtonSounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The sounds played by the toggle.
|
||||
/// </summary>
|
||||
internal static ToggleSoundPlayer ToggleSounds { get; }
|
||||
|
||||
|
||||
static PUITuning() {
|
||||
// Initialization: Button sounds
|
||||
ButtonSounds = new ButtonSoundPlayer() {
|
||||
Enabled = true
|
||||
};
|
||||
ToggleSounds = new ToggleSoundPlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
801
mod/PLibUI/PUIUtils.cs
Normal file
801
mod/PLibUI/PUIUtils.cs
Normal file
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using SideScreenRef = DetailsScreen.SideScreenRef;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Utility functions for dealing with Unity UIs.
|
||||
/// </summary>
|
||||
public static class PUIUtils {
|
||||
/// <summary>
|
||||
/// Adds text describing a particular component if available.
|
||||
/// </summary>
|
||||
/// <param name="result">The location to append the text.</param>
|
||||
/// <param name="component">The component to describe.</param>
|
||||
private static void AddComponentText(StringBuilder result, Component component) {
|
||||
// Include all fields
|
||||
var fields = component.GetType().GetFields(BindingFlags.DeclaredOnly |
|
||||
BindingFlags.Instance | PPatchTools.BASE_FLAGS);
|
||||
// Class specific
|
||||
if (component is TMP_Text lt)
|
||||
result.AppendFormat(", Text={0}, Color={1}, Font={2}", lt.text, lt.color,
|
||||
lt.font);
|
||||
else if (component is Image im) {
|
||||
result.AppendFormat(", Color={0}", im.color);
|
||||
if (im.sprite != null)
|
||||
result.AppendFormat(", Sprite={0}", im.sprite);
|
||||
} else if (component is HorizontalOrVerticalLayoutGroup lg)
|
||||
result.AppendFormat(", Child Align={0}, Control W={1}, Control H={2}",
|
||||
lg.childAlignment, lg.childControlWidth, lg.childControlHeight);
|
||||
foreach (var field in fields) {
|
||||
object value = field.GetValue(component) ?? "null";
|
||||
// Value type specific
|
||||
if (value is LayerMask lm)
|
||||
value = "Layer #" + lm.value;
|
||||
else if (value is System.Collections.ICollection ic)
|
||||
value = "[" + ic.Join() + "]";
|
||||
else if (value is Array ar)
|
||||
value = "[" + ar.Join() + "]";
|
||||
result.AppendFormat(", {0}={1}", field.Name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hot pink rectangle over the target matching its size, to help identify it
|
||||
/// better.
|
||||
/// </summary>
|
||||
/// <param name="parent">The target UI component.</param>
|
||||
public static void AddPinkOverlay(GameObject parent) {
|
||||
var child = PUIElements.CreateUI(parent, "Overlay");
|
||||
var img = child.AddComponent<Image>();
|
||||
img.color = new Color(1.0f, 0.0f, 1.0f, 0.2f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified side screen content to the side screen list. The side screen
|
||||
/// behavior should be defined in a class inherited from SideScreenContent.
|
||||
///
|
||||
/// The side screen will be added at the end of the list, which will cause it to
|
||||
/// appear above previous side screens in the details panel.
|
||||
///
|
||||
/// This method should be used in a postfix on DetailsScreen.OnPrefabInit.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the controller that will determine how the side
|
||||
/// screen works. A new instance will be created and added as a component to the new
|
||||
/// side screen.</typeparam>
|
||||
/// <param name="uiPrefab">The UI prefab to use. If null is passed, the UI should
|
||||
/// be created and added to the GameObject hosting the controller object in its
|
||||
/// constructor.</param>
|
||||
public static void AddSideScreenContent<T>(GameObject uiPrefab = null)
|
||||
where T : SideScreenContent {
|
||||
AddSideScreenContentWithOrdering<T>(null, true, uiPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified side screen content to the side screen list. The side screen
|
||||
/// behavior should be defined in a class inherited from SideScreenContent.
|
||||
///
|
||||
/// This method should be used in a postfix on DetailsScreen.OnPrefabInit.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the controller that will determine how the side
|
||||
/// screen works. A new instance will be created and added as a component to the new
|
||||
/// side screen.</typeparam>
|
||||
/// <param name="targetClassName">The full name of the type of side screen to based to ordering
|
||||
/// around. An example of how this method can be used is:
|
||||
/// `AddSideScreenContentWithOrdering<MySideScreen>(typeof(CapacityControlSideScreen).FullName);`
|
||||
/// `typeof(TargetedSideScreen).FullName` is the suggested value of this parameter.
|
||||
/// Side screens from other mods can be used with their qualified names, even if no
|
||||
/// reference to their type is available, but the target mod must have added their
|
||||
/// custom side screen to the list first.</param>
|
||||
/// <param name="insertBefore">Whether to insert the new screen before or after the
|
||||
/// target side screen in the list. Defaults to before (true).
|
||||
/// When inserting before the screen, if both are valid for a building then the side
|
||||
/// screen of type "T" will show below the one of type "fullName". When inserting after
|
||||
/// the screen, the reverse is true.</param>
|
||||
/// <param name="uiPrefab">The UI prefab to use. If null is passed, the UI should
|
||||
/// be created and added to the GameObject hosting the controller object in its
|
||||
/// constructor.</param>
|
||||
public static void AddSideScreenContentWithOrdering<T>(string targetClassName,
|
||||
bool insertBefore = true, GameObject uiPrefab = null)
|
||||
where T : SideScreenContent {
|
||||
var inst = DetailsScreen.Instance;
|
||||
if (inst == null)
|
||||
LogUIWarning("DetailsScreen is not yet initialized, try a postfix on DetailsScreen.OnPrefabInit");
|
||||
else {
|
||||
var screens = UIDetours.SIDE_SCREENS.Get(inst);
|
||||
var body = UIDetours.SS_CONTENT_BODY.Get(inst);
|
||||
string name = typeof(T).Name;
|
||||
if (body != null && screens != null) {
|
||||
// The ref normally contains a prefab which is instantiated
|
||||
var newScreen = new DetailsScreen.SideScreenRef();
|
||||
// Mimic the basic screens
|
||||
var rootObject = PUIElements.CreateUI(body, name);
|
||||
// Preserve the border by fitting the child
|
||||
rootObject.AddComponent<BoxLayoutGroup>().Params = new BoxLayoutParams() {
|
||||
Direction = PanelDirection.Vertical, Alignment = TextAnchor.
|
||||
UpperCenter, Margin = new RectOffset(1, 1, 0, 1)
|
||||
};
|
||||
var controller = rootObject.AddComponent<T>();
|
||||
if (uiPrefab != null) {
|
||||
// Add prefab if supplied
|
||||
UIDetours.SS_CONTENT_CONTAINER.Set(controller, uiPrefab);
|
||||
uiPrefab.transform.SetParent(rootObject.transform);
|
||||
}
|
||||
newScreen.name = name;
|
||||
// Offset is never used
|
||||
UIDetours.SS_OFFSET.Set(newScreen, Vector2.zero);
|
||||
UIDetours.SS_PREFAB.Set(newScreen, controller);
|
||||
UIDetours.SS_INSTANCE.Set(newScreen, controller);
|
||||
InsertSideScreenContent(screens, newScreen, targetClassName, insertBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a PLib UI object and adds it to an existing UI object.
|
||||
/// </summary>
|
||||
/// <param name="component">The UI object to add.</param>
|
||||
/// <param name="parent">The parent of the new object.</param>
|
||||
/// <param name="index">The sibling index to insert the element at, if provided.</param>
|
||||
/// <returns>The built version of the UI object.</returns>
|
||||
public static GameObject AddTo(this IUIComponent component, GameObject parent,
|
||||
int index = -2) {
|
||||
if (component == null)
|
||||
throw new ArgumentNullException(nameof(component));
|
||||
if (parent == null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
var child = component.Build();
|
||||
child.SetParent(parent);
|
||||
if (index == -1)
|
||||
child.transform.SetAsLastSibling();
|
||||
else if (index >= 0)
|
||||
child.transform.SetSiblingIndex(index);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the size of a single game object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to calculate.</param>
|
||||
/// <param name="direction">The direction to calculate.</param>
|
||||
/// <param name="components">The components of this game object.</param>
|
||||
/// <returns>The object's minimum and preferred size.</returns>
|
||||
internal static LayoutSizes CalcSizes(GameObject obj, PanelDirection direction,
|
||||
IEnumerable<Component> components) {
|
||||
float min = 0.0f, preferred = 0.0f, flexible = 0.0f, scaleFactor;
|
||||
int minPri = int.MinValue, prefPri = int.MinValue, flexPri = int.MinValue;
|
||||
var scale = obj.transform.localScale;
|
||||
// Find the correct scale direction
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
scaleFactor = Math.Abs(scale.x);
|
||||
else
|
||||
scaleFactor = Math.Abs(scale.y);
|
||||
bool ignore = false;
|
||||
foreach (var component in components) {
|
||||
if ((component as ILayoutIgnorer)?.ignoreLayout == true) {
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
if ((component as Behaviour)?.isActiveAndEnabled != false && component is
|
||||
ILayoutElement le) {
|
||||
int lp = le.layoutPriority;
|
||||
// Calculate must come first
|
||||
if (direction == PanelDirection.Horizontal) {
|
||||
le.CalculateLayoutInputHorizontal();
|
||||
PriValue(ref min, le.minWidth, lp, ref minPri);
|
||||
PriValue(ref preferred, le.preferredWidth, lp, ref prefPri);
|
||||
PriValue(ref flexible, le.flexibleWidth, lp, ref flexPri);
|
||||
} else { // if (direction == PanelDirection.Vertical)
|
||||
le.CalculateLayoutInputVertical();
|
||||
PriValue(ref min, le.minHeight, lp, ref minPri);
|
||||
PriValue(ref preferred, le.preferredHeight, lp, ref prefPri);
|
||||
PriValue(ref flexible, le.flexibleHeight, lp, ref flexPri);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new LayoutSizes(obj, min * scaleFactor, Math.Max(min, preferred) *
|
||||
scaleFactor, flexible) { ignore = ignore };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dumps information about the parent tree of the specified GameObject to the debug
|
||||
/// log.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to determine hierarchy.</param>
|
||||
public static void DebugObjectHierarchy(this GameObject item) {
|
||||
string info = "null";
|
||||
if (item != null) {
|
||||
var result = new StringBuilder(256);
|
||||
do {
|
||||
result.Append("- ");
|
||||
result.Append(item.name ?? "Unnamed");
|
||||
item = item.transform?.parent?.gameObject;
|
||||
if (item != null)
|
||||
result.AppendLine();
|
||||
} while (item != null);
|
||||
info = result.ToString();
|
||||
}
|
||||
LogUIDebug("Object Tree:" + Environment.NewLine + info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dumps information about the specified GameObject to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="root">The root hierarchy to dump.</param>
|
||||
public static void DebugObjectTree(this GameObject root) {
|
||||
string info = "null";
|
||||
if (root != null)
|
||||
info = GetObjectTree(root, 0);
|
||||
LogUIDebug("Object Dump:" + Environment.NewLine + info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives a font style from an existing style. The font face is copied unchanged,
|
||||
/// but the other settings can be optionally modified.
|
||||
/// </summary>
|
||||
/// <param name="root">The style to use as a template.</param>
|
||||
/// <param name="size">The font size, or 0 to use the template size.</param>
|
||||
/// <param name="newColor">The font color, or null to use the template color.</param>
|
||||
/// <param name="style">The font style, or null to use the template style.</param>
|
||||
/// <returns>A copy of the root style with the specified parameters altered.</returns>
|
||||
public static TextStyleSetting DeriveStyle(this TextStyleSetting root, int size = 0,
|
||||
Color? newColor = null, FontStyles? style = null) {
|
||||
if (root == null)
|
||||
throw new ArgumentNullException(nameof(root));
|
||||
var newStyle = ScriptableObject.CreateInstance<TextStyleSetting>();
|
||||
newStyle.enableWordWrapping = root.enableWordWrapping;
|
||||
newStyle.style = (style == null) ? root.style : (FontStyles)style;
|
||||
newStyle.fontSize = (size > 0) ? size : root.fontSize;
|
||||
newStyle.sdfFont = root.sdfFont;
|
||||
newStyle.textColor = (newColor == null) ? root.textColor : (Color)newColor;
|
||||
return newStyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A debug function used to forcefully re-layout a UI.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI to layout</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject ForceLayoutRebuild(GameObject uiElement) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var rt = uiElement.rectTransform();
|
||||
if (rt != null)
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the estimated width of a single string character (uses 'm' as the
|
||||
/// standard estimation character) in the given text style.
|
||||
/// </summary>
|
||||
/// <param name="style">The text style to use.</param>
|
||||
/// <returns>The width in pixels that should be allocated.</returns>
|
||||
public static float GetEmWidth(TextStyleSetting style) {
|
||||
float width = 0.0f;
|
||||
if (style == null)
|
||||
throw new ArgumentNullException(nameof(style));
|
||||
var font = style.sdfFont;
|
||||
// Use the em width
|
||||
if (font != null && font.characterDictionary.TryGetValue('m', out TMP_Glyph em)) {
|
||||
var info = font.fontInfo;
|
||||
float ptSize = style.fontSize / (info.PointSize * info.Scale);
|
||||
width = em.width * ptSize + style.fontSize * 0.01f * font.normalSpacingOffset;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the estimated height of one line of text in the given text style.
|
||||
/// </summary>
|
||||
/// <param name="style">The text style to use.</param>
|
||||
/// <returns>The height in pixels that should be allocated.</returns>
|
||||
public static float GetLineHeight(TextStyleSetting style) {
|
||||
float height = 0.0f;
|
||||
if (style == null)
|
||||
throw new ArgumentNullException(nameof(style));
|
||||
var font = style.sdfFont;
|
||||
if (font != null) {
|
||||
var info = font.fontInfo;
|
||||
height = info.LineHeight * style.fontSize / (info.Scale * info.PointSize);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string recursively describing the specified GameObject.
|
||||
/// </summary>
|
||||
/// <param name="root">The root GameObject hierarchy.</param>
|
||||
/// <param name="indent">The indentation to use.</param>
|
||||
/// <returns>A string describing this game object.</returns>
|
||||
private static string GetObjectTree(GameObject root, int indent) {
|
||||
var result = new StringBuilder(1024);
|
||||
// Calculate indent to make nested reading easier
|
||||
var solBuilder = new StringBuilder(indent);
|
||||
for (int i = 0; i < indent; i++)
|
||||
solBuilder.Append(' ');
|
||||
string sol = solBuilder.ToString();
|
||||
var transform = root.transform;
|
||||
int n = transform.childCount;
|
||||
// Basic information
|
||||
result.Append(sol).AppendFormat("GameObject[{0}, {1:D} child(ren), Layer {2:D}, " +
|
||||
"Active={3}]", root.name, n, root.layer, root.activeInHierarchy).AppendLine();
|
||||
// Transformation
|
||||
result.Append(sol).AppendFormat(" Translation={0} [{3}] Rotation={1} [{4}] " +
|
||||
"Scale={2}", transform.position, transform.rotation, transform.
|
||||
localScale, transform.localPosition, transform.localRotation).AppendLine();
|
||||
// Components
|
||||
foreach (var component in root.GetComponents<Component>()) {
|
||||
if (component is RectTransform rt) {
|
||||
// UI rectangle
|
||||
Vector2 size = rt.rect.size, aMin = rt.anchorMin, aMax = rt.anchorMax,
|
||||
oMin = rt.offsetMin, oMax = rt.offsetMax, pivot = rt.pivot;
|
||||
result.Append(sol).AppendFormat(" Rect[Size=({0:F2},{1:F2}) Min=" +
|
||||
"({2:F2},{3:F2}) ", size.x, size.y, LayoutUtility.GetMinWidth(rt),
|
||||
LayoutUtility.GetMinHeight(rt));
|
||||
result.AppendFormat("Preferred=({0:F2},{1:F2}) Flexible=({2:F2}," +
|
||||
"{3:F2}) ", LayoutUtility.GetPreferredWidth(rt), LayoutUtility.
|
||||
GetPreferredHeight(rt), LayoutUtility.GetFlexibleWidth(rt),
|
||||
LayoutUtility.GetFlexibleHeight(rt));
|
||||
result.AppendFormat("Pivot=({4:F2},{5:F2}) AnchorMin=({0:F2},{1:F2}) " +
|
||||
"AnchorMax=({2:F2},{3:F2}) ", aMin.x, aMin.y, aMax.x, aMax.y, pivot.x,
|
||||
pivot.y);
|
||||
result.AppendFormat("OffsetMin=({0:F2},{1:F2}) OffsetMax=({2:F2}," +
|
||||
"{3:F2})]", oMin.x, oMin.y, oMax.x, oMax.y).AppendLine();
|
||||
} else if (component != null && !(component is Transform)) {
|
||||
// Exclude destroyed components and Transform objects
|
||||
result.Append(sol).Append(" Component[").Append(component.GetType().
|
||||
FullName);
|
||||
AddComponentText(result, component);
|
||||
result.AppendLine("]");
|
||||
}
|
||||
}
|
||||
// Children
|
||||
if (n > 0)
|
||||
result.Append(sol).AppendLine(" Children:");
|
||||
for (int i = 0; i < n; i++) {
|
||||
var child = transform.GetChild(i).gameObject;
|
||||
if (child != null)
|
||||
// Exclude destroyed objects
|
||||
result.AppendLine(GetObjectTree(child, indent + 2));
|
||||
}
|
||||
return result.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the size for a component on a particular axis.
|
||||
/// </summary>
|
||||
/// <param name="sizes">The declared sizes.</param>
|
||||
/// <param name="allocated">The space allocated.</param>
|
||||
/// <returns>The size that the component should be.</returns>
|
||||
internal static float GetProperSize(LayoutSizes sizes, float allocated) {
|
||||
float size = sizes.min, preferred = Math.Max(sizes.preferred, size);
|
||||
// Compute size: minimum guaranteed, then preferred, then flexible
|
||||
if (allocated > size)
|
||||
size = Math.Min(preferred, allocated);
|
||||
if (allocated > preferred && sizes.flexible > 0.0f)
|
||||
size = allocated;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset required for a component in its box.
|
||||
/// </summary>
|
||||
/// <param name="alignment">The alignment to use.</param>
|
||||
/// <param name="direction">The direction of layout.</param>
|
||||
/// <param name="delta">The remaining space.</param>
|
||||
/// <returns>The offset from the edge.</returns>
|
||||
internal static float GetOffset(TextAnchor alignment, PanelDirection direction,
|
||||
float delta) {
|
||||
float offset = 0.0f;
|
||||
// Based on alignment, offset component
|
||||
if (direction == PanelDirection.Horizontal)
|
||||
switch (alignment) {
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.MiddleCenter:
|
||||
case TextAnchor.UpperCenter:
|
||||
offset = delta * 0.5f;
|
||||
break;
|
||||
case TextAnchor.LowerRight:
|
||||
case TextAnchor.MiddleRight:
|
||||
case TextAnchor.UpperRight:
|
||||
offset = delta;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
} else
|
||||
switch (alignment) {
|
||||
case TextAnchor.MiddleLeft:
|
||||
case TextAnchor.MiddleCenter:
|
||||
case TextAnchor.MiddleRight:
|
||||
offset = delta * 0.5f;
|
||||
break;
|
||||
case TextAnchor.LowerLeft:
|
||||
case TextAnchor.LowerCenter:
|
||||
case TextAnchor.LowerRight:
|
||||
offset = delta;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the parent of the GameObject, or null if it does not have a parent.
|
||||
/// </summary>
|
||||
/// <param name="child">The child object.</param>
|
||||
/// <returns>The parent of that object, or null if it does not have a parent.</returns>
|
||||
public static GameObject GetParent(this GameObject child) {
|
||||
GameObject parent = null;
|
||||
if (child != null) {
|
||||
var newParent = child.transform.parent?.gameObject;
|
||||
// If parent is disposed, prevent crash
|
||||
if (newParent != null)
|
||||
parent = newParent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insets a child component from its parent, and assigns a fixed size to the parent
|
||||
/// equal to the provided size plus the insets.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent component.</param>
|
||||
/// <param name="child">The child to inset.</param>
|
||||
/// <param name="vertical">The vertical inset on each side.</param>
|
||||
/// <param name="horizontal">The horizontal inset on each side.</param>
|
||||
/// <param name="prefSize">The minimum component size.</param>
|
||||
/// <returns>The parent component.</returns>
|
||||
internal static GameObject InsetChild(GameObject parent, GameObject child,
|
||||
Vector2 insets, Vector2 prefSize = default(Vector2)) {
|
||||
var rt = child.rectTransform();
|
||||
float horizontal = insets.x, vertical = insets.y, width = prefSize.x, height =
|
||||
prefSize.y;
|
||||
rt.offsetMax = new Vector2(-horizontal, -vertical);
|
||||
rt.offsetMin = insets;
|
||||
var layout = parent.AddOrGet<LayoutElement>();
|
||||
layout.minWidth = layout.preferredWidth = (width <= 0.0f ? LayoutUtility.
|
||||
GetPreferredWidth(rt) : width) + horizontal * 2.0f;
|
||||
layout.minHeight = layout.preferredHeight = (height <= 0.0f ? LayoutUtility.
|
||||
GetPreferredHeight(rt) : height) + vertical * 2.0f;
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the side screen at the target location.
|
||||
/// </summary>
|
||||
/// <param name="screens">The current list of side screens.</param>
|
||||
/// <param name="newScreen">The screen to insert.</param>
|
||||
/// <param name="targetClassName">The target class name for locating the screen. If this
|
||||
/// class is not found, it will be added at the end regardless of insertBefore.</param>
|
||||
/// <param name="insertBefore">true to insert before that class, or false to insert after.</param>
|
||||
private static void InsertSideScreenContent(IList<SideScreenRef> screens,
|
||||
SideScreenRef newScreen, string targetClassName, bool insertBefore) {
|
||||
if (screens == null)
|
||||
throw new ArgumentNullException(nameof(screens));
|
||||
if (newScreen == null)
|
||||
throw new ArgumentNullException(nameof(newScreen));
|
||||
if (string.IsNullOrEmpty(targetClassName))
|
||||
// Add to end by default
|
||||
screens.Add(newScreen);
|
||||
else {
|
||||
int n = screens.Count;
|
||||
bool found = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
var screen = screens[i];
|
||||
var sideScreenPrefab = UIDetours.SS_PREFAB.Get(screen);
|
||||
if (sideScreenPrefab != null) {
|
||||
var contents = sideScreenPrefab.
|
||||
GetComponentsInChildren<SideScreenContent>();
|
||||
if (contents == null || contents.Length < 1)
|
||||
// Some naughty mod added a prefab with no side screen content!
|
||||
LogUIWarning("Could not find SideScreenContent on side screen: " +
|
||||
screen.name);
|
||||
else if (contents[0].GetType().FullName == targetClassName) {
|
||||
// Once the first matching screen is found, perform insertion
|
||||
if (insertBefore)
|
||||
screens.Insert(i, newScreen);
|
||||
else if (i >= n - 1)
|
||||
screens.Add(newScreen);
|
||||
else
|
||||
screens.Insert(i + 1, newScreen);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Warn if no match found
|
||||
if (!found) {
|
||||
LogUIWarning("No side screen with class name {0} found!".F(
|
||||
targetClassName));
|
||||
screens.Add(newScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite embedded in the calling assembly.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="path">The fully qualified path to the image to load.</param>
|
||||
/// <param name="border">The sprite border. If there is no 9-patch border, use default(Vector4).</param>
|
||||
/// <param name="log">true to log the sprite load, or false to load silently.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
public static Sprite LoadSprite(string path, Vector4 border = default, bool log = true)
|
||||
{
|
||||
return LoadSprite(Assembly.GetCallingAssembly() ?? Assembly.GetExecutingAssembly(),
|
||||
path, border, log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite embedded in the specified assembly as a 9-slice sprite.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the image.</param>
|
||||
/// <param name="path">The fully qualified path to the image to load.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
internal static Sprite LoadSprite(Assembly assembly, string path, Vector4 border =
|
||||
default, bool log = false) {
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = assembly.GetManifestResourceStream(path)) {
|
||||
if (stream == null)
|
||||
throw new ArgumentException("Could not load image: " + path);
|
||||
// If len > int.MaxValue we will not go to space today
|
||||
int len = (int)stream.Length;
|
||||
byte[] buffer = new byte[len];
|
||||
var texture = new Texture2D(2, 2);
|
||||
// Load the texture from the stream
|
||||
stream.Read(buffer, 0, len);
|
||||
ImageConversion.LoadImage(texture, buffer, false);
|
||||
// Create a sprite centered on the texture
|
||||
int width = texture.width, height = texture.height;
|
||||
#if DEBUG
|
||||
log = true;
|
||||
#endif
|
||||
if (log)
|
||||
LogUIDebug("Loaded sprite: {0} ({1:D}x{2:D}, {3:D} bytes)".F(path,
|
||||
width, height, len));
|
||||
// pivot is in RELATIVE coordinates!
|
||||
return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ArgumentException("Could not load image: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a sprite from the file system as a 9-slice sprite.
|
||||
///
|
||||
/// It may be encoded using PNG, DXT5, or JPG format.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the image to load.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded, or null if it could not be loaded.</returns>
|
||||
public static Sprite LoadSpriteFile(string path, Vector4 border = default) {
|
||||
Sprite sprite = null;
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = new FileStream(path, FileMode.Open)) {
|
||||
// If len > int.MaxValue we will not go to space today
|
||||
int len = (int)stream.Length;
|
||||
byte[] buffer = new byte[len];
|
||||
var texture = new Texture2D(2, 2);
|
||||
// Load the texture from the stream
|
||||
stream.Read(buffer, 0, len);
|
||||
ImageConversion.LoadImage(texture, buffer, false);
|
||||
// Create a sprite centered on the texture
|
||||
int width = texture.width, height = texture.height;
|
||||
sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
#if DEBUG
|
||||
PUtil.LogExcWarn(e);
|
||||
#endif
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a DDS sprite embedded in the specified assembly as a 9-slice sprite.
|
||||
///
|
||||
/// It must be encoded using the DXT5 format.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly containing the image.</param>
|
||||
/// <param name="path">The fully qualified path to the DDS image to load.</param>
|
||||
/// <param name="width">The desired width.</param>
|
||||
/// <param name="height">The desired height.</param>
|
||||
/// <param name="border">The sprite border.</param>
|
||||
/// <param name="log">true to log the load, or false otherwise.</param>
|
||||
/// <returns>The sprite thus loaded.</returns>
|
||||
/// <exception cref="ArgumentException">If the image could not be loaded.</exception>
|
||||
internal static Sprite LoadSpriteLegacy(Assembly assembly, string path, int width,
|
||||
int height, Vector4 border = default) {
|
||||
// Open a stream to the image
|
||||
try {
|
||||
using (var stream = assembly.GetManifestResourceStream(path)) {
|
||||
const int SKIP = 128;
|
||||
if (stream == null)
|
||||
throw new ArgumentException("Could not load image: " + path);
|
||||
// If len > int.MaxValue we will not go to space today, skip first 128
|
||||
// bytes of stream
|
||||
int len = (int)stream.Length - SKIP;
|
||||
if (len < 0)
|
||||
throw new ArgumentException("Image is too small: " + path);
|
||||
byte[] buffer = new byte[len];
|
||||
stream.Seek(SKIP, SeekOrigin.Begin);
|
||||
stream.Read(buffer, 0, len);
|
||||
// Load the texture from the stream
|
||||
var texture = new Texture2D(width, height, TextureFormat.DXT5, false);
|
||||
texture.LoadRawTextureData(buffer);
|
||||
texture.Apply(true, true);
|
||||
// Create a sprite centered on the texture
|
||||
LogUIDebug("Loaded sprite: {0} ({1:D}x{2:D}, {3:D} bytes)".F(path,
|
||||
width, height, len));
|
||||
// pivot is in RELATIVE coordinates!
|
||||
return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(
|
||||
0.5f, 0.5f), 100.0f, 0, SpriteMeshType.FullRect, border);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ArgumentException("Could not load image: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message encountered in PLib UI functions.
|
||||
/// </summary>
|
||||
/// <param name="message">The debug message.</param>
|
||||
internal static void LogUIDebug(string message) {
|
||||
Debug.LogFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?.GetName()?.
|
||||
Name ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning encountered in PLib UI functions.
|
||||
/// </summary>
|
||||
/// <param name="message">The warning message.</param>
|
||||
internal static void LogUIWarning(string message) {
|
||||
Debug.LogWarningFormat("[PLib/UI/{0}] {1}", Assembly.GetCallingAssembly()?.
|
||||
GetName()?.Name ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates layout values, replacing the value if a higher priority value is given
|
||||
/// and otherwise taking the largest value.
|
||||
/// </summary>
|
||||
/// <param name="value">The current value.</param>
|
||||
/// <param name="newValue">The candidate new value. No operation if this is less than zero.</param>
|
||||
/// <param name="newPri">The new value's layout priority.</param>
|
||||
/// <param name="pri">The current value's priority</param>
|
||||
private static void PriValue(ref float value, float newValue, int newPri, ref int pri)
|
||||
{
|
||||
int thisPri = pri;
|
||||
if (newValue >= 0.0f) {
|
||||
if (newPri > thisPri) {
|
||||
// Priority override?
|
||||
pri = newPri;
|
||||
value = newValue;
|
||||
} else if (newValue > value && newPri == thisPri)
|
||||
// Same priority and higher value?
|
||||
value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's flexible size.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="flexSize">The flexible size as a ratio.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetFlexUISize(this GameObject uiElement, Vector2 flexSize) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
if (uiElement.TryGetComponent(out ISettableFlexSize fs)) {
|
||||
// Avoid duplicate LayoutElement on layouts
|
||||
fs.flexibleWidth = flexSize.x;
|
||||
fs.flexibleHeight = flexSize.y;
|
||||
} else {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
le.flexibleWidth = flexSize.x;
|
||||
le.flexibleHeight = flexSize.y;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a UI element's minimum size.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="minSize">The minimum size in units.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetMinUISize(this GameObject uiElement, Vector2 minSize) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
float minX = minSize.x, minY = minSize.y;
|
||||
if (minX > 0.0f || minY > 0.0f) {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
if (minX > 0.0f)
|
||||
le.minWidth = minX;
|
||||
if (minY > 0.0f)
|
||||
le.minHeight = minY;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately resizes a UI element. Uses the element's current anchors. If a
|
||||
/// dimension of the size is negative, the component will not be resized in that
|
||||
/// dimension.
|
||||
///
|
||||
/// If addLayout is true, a layout element is also added so that future auto layout
|
||||
/// calls will try to maintain that size. Do not set addLayout to true if either of
|
||||
/// the size dimensions are negative, as laying out components with a negative
|
||||
/// preferred size may cause unexpected behavior.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">The UI element to modify.</param>
|
||||
/// <param name="size">The new element size.</param>
|
||||
/// <param name="addLayout">true to add a layout element with that size, or false
|
||||
/// otherwise.</param>
|
||||
/// <returns>The UI element, for call chaining.</returns>
|
||||
public static GameObject SetUISize(this GameObject uiElement, Vector2 size,
|
||||
bool addLayout = false) {
|
||||
if (uiElement == null)
|
||||
throw new ArgumentNullException(nameof(uiElement));
|
||||
var transform = uiElement.rectTransform();
|
||||
float width = size.x, height = size.y;
|
||||
if (transform != null) {
|
||||
if (width >= 0.0f)
|
||||
transform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
|
||||
if (height >= 0.0f)
|
||||
transform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
||||
}
|
||||
if (addLayout) {
|
||||
var le = uiElement.AddOrGet<LayoutElement>();
|
||||
// Set minimum and preferred size
|
||||
le.minWidth = width;
|
||||
le.minHeight = height;
|
||||
le.preferredWidth = width;
|
||||
le.preferredHeight = height;
|
||||
le.flexibleHeight = 0.0f;
|
||||
le.flexibleWidth = 0.0f;
|
||||
}
|
||||
return uiElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
mod/PLibUI/TextAnchorUtils.cs
Normal file
92
mod/PLibUI/TextAnchorUtils.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Extension methods to deal with TextAnchor alignments.
|
||||
/// </summary>
|
||||
public static class TextAnchorUtils {
|
||||
/// <summary>
|
||||
/// Returns true if this text alignment is at the left.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to check.</param>
|
||||
/// <returns>true if it denotes a Left alignment, or false otherwise.</returns>
|
||||
public static bool IsLeft(this TextAnchor anchor) {
|
||||
return anchor == TextAnchor.UpperLeft || anchor == TextAnchor.MiddleLeft ||
|
||||
anchor == TextAnchor.LowerLeft;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this text alignment is at the bottom.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to check.</param>
|
||||
/// <returns>true if it denotes a Lower alignment, or false otherwise.</returns>
|
||||
public static bool IsLower(this TextAnchor anchor) {
|
||||
return anchor == TextAnchor.LowerCenter || anchor == TextAnchor.LowerLeft ||
|
||||
anchor == TextAnchor.LowerRight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this text alignment is at the right.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to check.</param>
|
||||
/// <returns>true if it denotes a Right alignment, or false otherwise.</returns>
|
||||
public static bool IsRight(this TextAnchor anchor) {
|
||||
return anchor == TextAnchor.UpperRight || anchor == TextAnchor.MiddleRight ||
|
||||
anchor == TextAnchor.LowerRight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this text alignment is at the top.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to check.</param>
|
||||
/// <returns>true if it denotes an Upper alignment, or false otherwise.</returns>
|
||||
public static bool IsUpper(this TextAnchor anchor) {
|
||||
return anchor == TextAnchor.UpperCenter || anchor == TextAnchor.UpperLeft ||
|
||||
anchor == TextAnchor.UpperRight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a text alignment horizontally. UpperLeft becomes UpperRight, MiddleLeft
|
||||
/// becomes MiddleRight, and so forth.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to mirror.</param>
|
||||
/// <returns>The horizontally reflected version of that mirror.</returns>
|
||||
public static TextAnchor MirrorHorizontal(this TextAnchor anchor) {
|
||||
int newAnchor = (int)anchor;
|
||||
// UL UC UR ML MC MR LL LC LR
|
||||
// Danger will robinson!
|
||||
newAnchor = 3 * (newAnchor / 3) + 2 - newAnchor % 3;
|
||||
return (TextAnchor)newAnchor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors a text alignment vertically. UpperLeft becomes LowerLeft, LowerCenter
|
||||
/// becomes UpperCenter, and so forth.
|
||||
/// </summary>
|
||||
/// <param name="anchor">The anchoring to mirror.</param>
|
||||
/// <returns>The vertically reflected version of that mirror.</returns>
|
||||
public static TextAnchor MirrorVertical(this TextAnchor anchor) {
|
||||
int newAnchor = (int)anchor;
|
||||
newAnchor = 6 - 3 * (newAnchor / 3) + newAnchor % 3;
|
||||
return (TextAnchor)newAnchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
mod/PLibUI/UIDetours.cs
Normal file
97
mod/PLibUI/UIDetours.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Detours;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using SideScreenRef = DetailsScreen.SideScreenRef;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Stores detours used for Klei UI components. Klei loves adding optional parameters and
|
||||
/// changing fields to/from properties, which while source compatible is binary
|
||||
/// incompatible. These lazy detours (resolved on first use) can bridge over a variety of
|
||||
/// such differences with minimal overhead and no recompilation.
|
||||
/// </summary>
|
||||
internal static class UIDetours {
|
||||
#region ConfirmDialogScreen
|
||||
public delegate void PCD(ConfirmDialogScreen dialog, string text,
|
||||
System.Action on_confirm, System.Action on_cancel, string configurable_text,
|
||||
System.Action on_configurable_clicked, string title_text, string confirm_text,
|
||||
string cancel_text);
|
||||
|
||||
public static readonly DetouredMethod<PCD> POPUP_CONFIRM = typeof(ConfirmDialogScreen).DetourLazy<PCD>(nameof(ConfirmDialogScreen.PopupConfirmDialog));
|
||||
#endregion
|
||||
|
||||
#region DetailsScreen
|
||||
public static readonly IDetouredField<DetailsScreen, List<SideScreenRef>> SIDE_SCREENS = PDetours.DetourFieldLazy<DetailsScreen, List<SideScreenRef>>("sideScreens");
|
||||
public static readonly IDetouredField<DetailsScreen, GameObject> SS_CONTENT_BODY = PDetours.DetourFieldLazy<DetailsScreen, GameObject>("sideScreenContentBody");
|
||||
#endregion
|
||||
|
||||
#region KButton
|
||||
public static readonly IDetouredField<KButton, KImage[]> ADDITIONAL_K_IMAGES = PDetours.DetourFieldLazy<KButton, KImage[]>(nameof(KButton.additionalKImages));
|
||||
public static readonly IDetouredField<KButton, KImage> BG_IMAGE = PDetours.DetourFieldLazy<KButton, KImage>(nameof(KButton.bgImage));
|
||||
public static readonly IDetouredField<KButton, Image> FG_IMAGE = PDetours.DetourFieldLazy<KButton, Image>(nameof(KButton.fgImage));
|
||||
public static readonly IDetouredField<KButton, bool> IS_INTERACTABLE = PDetours.DetourFieldLazy<KButton, bool>(nameof(KButton.isInteractable));
|
||||
public static readonly IDetouredField<KButton, ButtonSoundPlayer> SOUND_PLAYER_BUTTON = PDetours.DetourFieldLazy<KButton, ButtonSoundPlayer>(nameof(KButton.soundPlayer));
|
||||
#endregion
|
||||
|
||||
#region KImage
|
||||
public static readonly IDetouredField<KImage, ColorStyleSetting> COLOR_STYLE_SETTING = PDetours.DetourFieldLazy<KImage, ColorStyleSetting>(nameof(KImage.colorStyleSetting));
|
||||
|
||||
public static readonly DetouredMethod<Action<KImage>> APPLY_COLOR_STYLE = typeof(KImage).DetourLazy<Action<KImage>>(nameof(KImage.ApplyColorStyleSetting));
|
||||
#endregion
|
||||
|
||||
#region KScreen
|
||||
public static readonly DetouredMethod<Action<KScreen>> ACTIVATE_KSCREEN = typeof(KScreen).DetourLazy<Action<KScreen>>(nameof(KScreen.Activate));
|
||||
public static readonly DetouredMethod<Action<KScreen>> DEACTIVATE_KSCREEN = typeof(KScreen).DetourLazy<Action<KScreen>>(nameof(KScreen.Deactivate));
|
||||
#endregion
|
||||
|
||||
#region KToggle
|
||||
public static readonly IDetouredField<KToggle, KToggleArtExtensions> ART_EXTENSION = PDetours.DetourFieldLazy<KToggle, KToggleArtExtensions>(nameof(KToggle.artExtension));
|
||||
public static readonly IDetouredField<KToggle, bool> IS_ON = PDetours.DetourFieldLazy<KToggle, bool>(nameof(KToggle.isOn));
|
||||
public static readonly IDetouredField<KToggle, ToggleSoundPlayer> SOUND_PLAYER_TOGGLE = PDetours.DetourFieldLazy<KToggle, ToggleSoundPlayer>(nameof(KToggle.soundPlayer));
|
||||
#endregion
|
||||
|
||||
#region LocText
|
||||
public static readonly IDetouredField<LocText, string> LOCTEXT_KEY = PDetours.DetourFieldLazy<LocText, string>(nameof(LocText.key));
|
||||
public static readonly IDetouredField<LocText, TextStyleSetting> LOCTEXT_STYLE = PDetours.DetourFieldLazy<LocText, TextStyleSetting>(nameof(LocText.textStyleSetting));
|
||||
#endregion
|
||||
|
||||
#region MultiToggle
|
||||
public static readonly IDetouredField<MultiToggle, int> CURRENT_STATE = PDetours.DetourFieldLazy<MultiToggle, int>(nameof(MultiToggle.CurrentState));
|
||||
public static readonly IDetouredField<MultiToggle, bool> PLAY_SOUND_CLICK = PDetours.DetourFieldLazy<MultiToggle, bool>(nameof(MultiToggle.play_sound_on_click));
|
||||
public static readonly IDetouredField<MultiToggle, bool> PLAY_SOUND_RELEASE = PDetours.DetourFieldLazy<MultiToggle, bool>(nameof(MultiToggle.play_sound_on_release));
|
||||
|
||||
public static readonly DetouredMethod<Action<MultiToggle, int>> CHANGE_STATE = typeof(MultiToggle).DetourLazy<Action<MultiToggle, int>>(nameof(MultiToggle.ChangeState));
|
||||
#endregion
|
||||
|
||||
#region SideScreenContent
|
||||
public static readonly IDetouredField<SideScreenContent, GameObject> SS_CONTENT_CONTAINER = PDetours.DetourFieldLazy<SideScreenContent, GameObject>(nameof(SideScreenContent.ContentContainer));
|
||||
#endregion
|
||||
|
||||
#region SideScreenRef
|
||||
public static readonly IDetouredField<SideScreenRef, Vector2> SS_OFFSET = PDetours.DetourFieldLazy<SideScreenRef, Vector2>(nameof(SideScreenRef.offset));
|
||||
public static readonly IDetouredField<SideScreenRef, SideScreenContent> SS_PREFAB = PDetours.DetourFieldLazy<SideScreenRef, SideScreenContent>(nameof(SideScreenRef.screenPrefab));
|
||||
public static readonly IDetouredField<SideScreenRef, SideScreenContent> SS_INSTANCE = PDetours.DetourFieldLazy<SideScreenRef, SideScreenContent>(nameof(SideScreenRef.screenInstance));
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user