/* * 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 { /// /// A factory for scrollable panes. /// public sealed class PScrollPane : IUIComponent { /// /// The track size of scrollbars is based on the sprite. /// private const float DEFAULT_TRACK_SIZE = 16.0f; /// /// Whether the horizontal scrollbar is always visible. /// public bool AlwaysShowHorizontal { get; set; } /// /// Whether the vertical scrollbar is always visible. /// public bool AlwaysShowVertical { get; set; } /// /// The background color of this scroll pane. /// public Color BackColor { get; set; } /// /// The child of this scroll pane. /// public IUIComponent Child { get; set; } /// /// The flexible size bounds of this component. /// public Vector2 FlexSize { get; set; } public string Name { get; } /// /// Whether horizontal scrolling is allowed. /// public bool ScrollHorizontal { get; set; } /// /// Whether vertical scrolling is allowed. /// public bool ScrollVertical { get; set; } /// /// The size of the scrollbar track. /// 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; } /// /// Adds a handler when this scroll pane is realized. /// /// The handler to invoke on realization. /// This scroll pane for call chaining. 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; } /// /// Builds the actual scroll pane object. /// /// The parent of this scroll pane. /// The child element of this scroll pane. /// The realized scroll pane. internal GameObject BuildScrollPane(GameObject parent, GameObject child) { var pane = PUIElements.CreateUI(parent, Name); if (BackColor.a > 0.0f) pane.AddComponent().color = BackColor; pane.SetActive(false); // Scroll pane itself var scroll = pane.AddComponent(); scroll.horizontal = ScrollHorizontal; scroll.vertical = ScrollVertical; // Viewport var viewport = PUIElements.CreateUI(pane, "Viewport"); viewport.rectTransform().pivot = Vector2.up; viewport.AddComponent().enabled = true; viewport.AddComponent(); scroll.viewport = viewport.rectTransform(); // Give the Child a separate Canvas to reduce layout rebuilds child.AddOrGet().pixelPerfect = false; child.AddOrGet(); 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(); layout.flexibleHeight = FlexSize.y; layout.flexibleWidth = FlexSize.x; return pane; } /// /// Creates a horizontal scroll bar. /// /// The parent component. /// The scroll bar component. private Scrollbar CreateScrollHoriz(GameObject parent) { // Outer scrollbar var track = PUIElements.CreateUI(parent, "Scrollbar H", true, PUIAnchoring.Stretch, PUIAnchoring.Beginning); var bg = track.AddComponent(); bg.sprite = PUITuning.Images.ScrollBorderHorizontal; bg.type = Image.Type.Sliced; // Scroll track var sb = track.AddComponent(); 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(); 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; } /// /// Creates a vertical scroll bar. /// /// The parent component. /// The scroll bar component. private Scrollbar CreateScrollVert(GameObject parent) { // Outer scrollbar var track = PUIElements.CreateUI(parent, "Scrollbar V", true, PUIAnchoring.End, PUIAnchoring.Stretch); var bg = track.AddComponent(); bg.sprite = PUITuning.Images.ScrollBorderVertical; bg.type = Image.Type.Sliced; // Scroll track var sb = track.AddComponent(); 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(); 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; } /// /// Sets the background color to the default Klei dialog blue. /// /// This scroll pane for call chaining. public PScrollPane SetKleiBlueColor() { BackColor = PUITuning.Colors.ButtonBlueStyle.inactiveColor; return this; } /// /// Sets the background color to the Klei dialog header pink. /// /// This scroll pane for call chaining. public PScrollPane SetKleiPinkColor() { BackColor = PUITuning.Colors.ButtonPinkStyle.inactiveColor; return this; } public override string ToString() { return string.Format("PScrollPane[Name={0},Child={1}]", Name, Child); } /// /// Handles layout for scroll panes. Not freezable. /// private sealed class PScrollPaneLayout : AbstractLayoutGroup { /// /// Caches elements when calculating layout to improve performance. /// private Component[] calcElements; /// /// The calculated horizontal size of the child element. /// private LayoutSizes childHorizontal; /// /// The calculated vertical size of the child element. /// private LayoutSizes childVertical; /// /// The child object inside the scroll rect. /// private GameObject child; /// /// Caches elements when setting layout to improve performance. /// private ILayoutController[] setElements; /// /// The viewport which clips the child rectangle. /// 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(); // 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(); 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; } } /// /// Caches the child component for performance reasons at runtime. /// 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; } } /// /// 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. /// private sealed class ViewportLayoutGroup : UIBehaviour, ILayoutGroup { public void SetLayoutHorizontal() { } public void SetLayoutVertical() { } } } }