377 lines
13 KiB
C#
377 lines
13 KiB
C#
/*
|
|
* 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() { }
|
|
}
|
|
}
|
|
}
|