context: #region Copyright & License Information
  /*
   * Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
   * This file is part of OpenRA, which is free software. It is made
   * available to you under the terms of the GNU General Public License
   * as published by the Free Software Foundation, either version 3 of
   * the License, or (at your option) any later version. For more
   * information, see COPYING.
   */
  #endregion
  
  using System;
  using System.Linq;
  using OpenRA.Graphics;
  using OpenRA.Primitives;
  using OpenRA.Widgets;
  
  namespace OpenRA.Mods.Common.Widgets
  {
  	public interface ILayout
  	{
  		void AdjustChild(Widget w);
  		void AdjustChildren();
  	}
  
  	public enum ScrollPanelAlign
  	{
  		Bottom,
  		Top
  	}
  
  	public enum ScrollBar
  	{
  		Left,
  		Right,
  		Hidden
  	}
  
  	public class ScrollPanelWidget : Widget
  	{
  		readonly Ruleset modRules;
  		public int ScrollbarWidth = 24;
  		public int BorderWidth = 1;
  		public int TopBottomSpacing = 2;
  		public int ItemSpacing = 0;
  		public int ButtonDepth = ChromeMetrics.Get<int>("ButtonDepth");
  		public string ClickSound = ChromeMetrics.Get<string>("ClickSound");
  		public string ClickDisabledSound = ChromeMetrics.Get<string>("ClickDisabledSound");
  		public string Background = "scrollpanel-bg";
  		public string ScrollBarBackground = "scrollpanel-bg";
  		public string Button = "scrollpanel-button";
  		public string Decorations = "scrollpanel-decorations";
  		public readonly string DecorationScrollUp = "up";
  		public readonly string DecorationScrollDown = "down";
  		readonly CachedTransform<(bool Disabled, bool Pressed, bool Hover, bool Focused), Sprite> getUpArrowImage;
  		readonly CachedTransform<(bool Disabled, bool Pressed, bool Hover, bool Focused), Sprite> getDownArrowImage;
  		public int ContentHeight;
  		public ILayout Layout;
  		public int MinimumThumbSize = 10;
  		public ScrollPanelAlign Align = ScrollPanelAlign.Top;
  		public ScrollBar ScrollBar = ScrollBar.Right;
  		public bool CollapseHiddenChildren;
  
  		// Fraction of the remaining scroll-delta to move in 40ms
  		public float SmoothScrollSpeed = 0.333f;
  
  		protected bool upPressed;
  		protected bool downPressed;
  		protected bool upDisabled;
  		protected bool downDisabled;
  		protected bool thumbPressed;
  		protected Rectangle upButtonRect;
  		protected Rectangle downButtonRect;
  		protected Rectangle backgroundRect;
  		protected Rectangle scrollbarRect;
  		protected Rectangle thumbRect;
  
  		// The target value is the list offset we're trying to reach
  		float targetListOffset;
  
  		// The current value is the actual list offset at the moment
  		float currentListOffset;
  
  		// The Game.Runtime value when UpdateSmoothScrolling was last called
  		// Used for calculating the per-frame smooth-scrolling delta
  		long lastSmoothScrollTime = 0;
  
  		// Setting "smooth" to true will only update the target list offset.
  		// Setting "smooth" to false will also set the current list offset,
  		// i.e. it will scroll immediately.
  		//
  		// For example, scrolling with the mouse wheel will use smooth
  		// scrolling to give a nice visual effect that makes it easier
  		// for the user to follow. Dragging the scrollbar's thumb, however,
  		// will scroll to the desired position immediately.
  		protected void SetListOffset(float value, bool smooth)
  		{
  			targetListOffset = value;
  			if (!smooth)
  			{
  				var oldListOffset = currentListOffset;
  				currentListOffset = value;
  
  				// Update mouseover
  				if (oldListOffset != currentListOffset)
  					Ui.ResetTooltips();
  			}
  		}
  
  		[ObjectCreator.UseCtor]
  		public ScrollPanelWidget(ModData modData)
  		{
  			modRules = modData.DefaultRules;
  
  			Layout = new ListLayout(this);
  
  			getUpArrowImage = WidgetUtils.GetCachedStatefulImage(Decorations, DecorationScrollUp);
  			getDownArrowImage = WidgetUtils.GetCachedStatefulImage(Decorations, DecorationScrollDown);
  		}
  
  		public override void RemoveChildren()
  		{
  			ContentHeight = 0;
  			base.RemoveChildren();
  		}
  
  		public override void AddChild(Widget child)
  		{
  			// Initial setup of margins/height
  			Layout.AdjustChild(child);
  			base.AddChild(child);
  		}
  
  		public override void RemoveChild(Widget child)
  		{
  			base.RemoveChild(child);
  			Layout.AdjustChildren();
  			Scroll(0);
  		}
  
  		public void ReplaceChild(Widget oldChild, Widget newChild)
  		{
  			oldChild.Removed();
  			newChild.Parent = this;
  			Children[Children.IndexOf(oldChild)] = newChild;
  			Layout.AdjustChildren();
  			Scroll(0);
  		}
  
  		public override void DrawOuter()
  		{
  			if (!IsVisible())
  				return;
  
  			UpdateSmoothScrolling();
  
  			var rb = RenderBounds;
  			var scrollbarHeight = rb.Height - 2 * ScrollbarWidth;
  
  			// Scroll thumb is only visible if the content does not fit within the panel bounds
  			var thumbHeight = 0;
  			var thumbOrigin = rb.Y + ScrollbarWidth;
  			if (ContentHeight > rb.Height)
  			{
  				thumbHeight = Math.Max(MinimumThumbSize, scrollbarHeight * rb.Height / ContentHeight);
  				thumbOrigin += (int)((scrollbarHeight - thumbHeight) * currentListOffset / (rb.Height - ContentHeight));
  			}
  
  			switch (ScrollBar)
  			{
  				case ScrollBar.Left:
  					backgroundRect = new Rectangle(rb.X + ScrollbarWidth, rb.Y, rb.Width + 1, rb.Height);
  					upButtonRect = new Rectangle(rb.X, rb.Y, ScrollbarWidth, ScrollbarWidth);
  					downButtonRect = new Rectangle(rb.X, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth);
  					scrollbarRect = new Rectangle(rb.X, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2);
  					thumbRect = new Rectangle(rb.X, thumbOrigin, ScrollbarWidth, thumbHeight);
  					break;
  				case ScrollBar.Right:
  					backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width - ScrollbarWidth + 1, rb.Height);
  					upButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y, ScrollbarWidth, ScrollbarWidth);
  					downButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth);
  					scrollbarRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2);
  					thumbRect = new Rectangle(rb.Right - ScrollbarWidth, thumbOrigin, ScrollbarWidth, thumbHeight);
  					break;
  				case ScrollBar.Hidden:
  					backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width + 1, rb.Height);
  					break;
  				default:
  					throw new ArgumentOutOfRangeException();
  			}
  
  			WidgetUtils.DrawPanel(Background, backgroundRect);
  
  			if (ScrollBar != ScrollBar.Hidden)
  			{
  				var upHover = Ui.MouseOverWidget == this && upButtonRect.Contains(Viewport.LastMousePos);
  				upDisabled = thumbHeight == 0 || currentListOffset >= 0;
  
  				var downHover = Ui.MouseOverWidget == this && downButtonRect.Contains(Viewport.LastMousePos);
  				downDisabled = thumbHeight == 0 || currentListOffset <= Bounds.Height - ContentHeight;
  
  				var thumbHover = Ui.MouseOverWidget == this && thumbRect.Contains(Viewport.LastMousePos);
  				WidgetUtils.DrawPanel(ScrollBarBackground, scrollbarRect);
  				ButtonWidget.DrawBackground(Button, upButtonRect, upDisabled, upPressed, upHover, false);
  				ButtonWidget.DrawBackground(Button, downButtonRect, downDisabled, downPressed, downHover, false);
  
  				if (thumbHeight > 0)
  					ButtonWidget.DrawBackground(Button, thumbRect, false, HasMouseFocus && thumbHover, thumbHover, false);
  
  				var upOffset = !upPressed || upDisabled ? 4 : 4 + ButtonDepth;
  				var downOffset = !downPressed || downDisabled ? 4 : 4 + ButtonDepth;
  
  				var upArrowImage = getUpArrowImage.Update((upDisabled, upPressed, upHover, false));
  				WidgetUtils.DrawSprite(upArrowImage,
  					new float2(upButtonRect.Left + upOffset, upButtonRect.Top + upOffset));
  
  				var downArrowImage = getDownArrowImage.Update((downDisabled, downPressed, downHover, false));
  				WidgetUtils.DrawSprite(downArrowImage,
  					new float2(downButtonRect.Left + downOffset, downButtonRect.Top + downOffset));
  			}
  
  			var drawBounds = backgroundRect.InflateBy(-BorderWidth, -BorderWidth, -BorderWidth, -BorderWidth);
  			Game.Renderer.EnableScissor(drawBounds);
  
  			// ChildOrigin enumerates the widget tree, so only evaluate it once
  			var co = ChildOrigin;
  			drawBounds.X -= co.X;
  			drawBounds.Y -= co.Y;
  
  			foreach (var child in Children)
  				if (child.Bounds.IntersectsWith(drawBounds))
  					child.DrawOuter();
  
  			Game.Renderer.DisableScissor();
  		}
  
  		public override int2 ChildOrigin => RenderOrigin + new int2(ScrollBar == ScrollBar.Left ? ScrollbarWidth : 0, (int)currentListOffset);
  
  		public override bool EventBoundsContains(int2 location)
  		{
  			return EventBounds.Contains(location);
  		}
  
  		void Scroll(int amount, bool smooth = false)
  		{
  			var newTarget = targetListOffset + amount * Game.Settings.Game.UIScrollSpeed;
  			newTarget = Math.Min(0, Math.Max(Bounds.Height - ContentHeight, newTarget));
  
  			SetListOffset(newTarget, smooth);
  		}
  
  		public void ScrollToBottom(bool smooth = false)
  		{
  			var value = Align == ScrollPanelAlign.Top ?
  				Math.Min(0, Bounds.Height - ContentHeight) :
  				Bounds.Height - ContentHeight;
  
  			SetListOffset(value, smooth);
  		}
  
  		public void ScrollToTop(bool smooth = false)
  		{
  			var value = Align == ScrollPanelAlign.Top ? 0 :
  				Math.Max(0, Bounds.Height - ContentHeight);
  
  			SetListOffset(value, smooth);
  		}
  
  		public bool ScrolledToBottom => targetListOffset == Math.Min(0, Bounds.Height - ContentHeight) || ContentHeight <= Bounds.Height;
  
  		void ScrollToItem(Widget item, bool smooth = false)
  		{
  			// Scroll the item to be visible
  			float? newOffset = null;
  			if (item.Bounds.Top + currentListOffset < 0)
  				newOffset = ItemSpacing - item.Bounds.Top;
  
  			if (item.Bounds.Bottom + currentListOffset > RenderBounds.Height)
  				newOffset = RenderBounds.Height - item.Bounds.Bottom - ItemSpacing;
  
  			if (newOffset.HasValue)
  				SetListOffset(newOffset.Value, smooth);
  		}
  
  		public void ScrollToItem(string itemKey, bool smooth = false)
  		{
  			var item = Children.FirstOrDefault(c =>
  			{
  				return c is ScrollItemWidget si && si.ItemKey == itemKey;
  			});
  
  			if (item != null)
  				ScrollToItem(item, smooth);
  		}
  
  		public void ScrollToSelectedItem()
  		{
  			var item = Children.FirstOrDefault(c =>
  			{
  				return c is ScrollItemWidget si && si.IsSelected();
  			});
  
  			if (item != null)
  				ScrollToItem(item);
  		}
  
  		void UpdateSmoothScrolling()
  		{
  			if (lastSmoothScrollTime == 0)
  			{
  				lastSmoothScrollTime = Game.RunTime;
  				return;
  			}
  
  			var offsetDiff = targetListOffset - currentListOffset;
  			var absOffsetDiff = Math.Abs(offsetDiff);
  			if (absOffsetDiff > 1f)
  			{
  				var dt = Game.RunTime - lastSmoothScrollTime;
  				currentListOffset += offsetDiff * SmoothScrollSpeed.Clamp(0.1f, 1.0f) * dt / 40;
  
  				Ui.ResetTooltips();
  			}
  			else
  				SetListOffset(targetListOffset, false);
  
  			lastSmoothScrollTime = Game.RunTime;
  		}
  
  		public override void Tick()
  		{
  			if (upPressed)
  				Scroll(1);
  
  			if (downPressed)
  				Scroll(-1);
  		}
  
  		public override bool YieldMouseFocus(MouseInput mi)
  		{
  			upPressed = downPressed = thumbPressed = false;
  			return base.YieldMouseFocus(mi);
  		}
  
  		int2 lastMouseLocation;
  
  		public override bool HandleMouseInput(MouseInput mi)
  		{
  			if (mi.Event == MouseInputEvent.Scroll)
  			{
  				Scroll(mi.Delta.Y, true);
  				return true;
  			}
  
  			if (mi.Button != MouseButton.Left)
  				return false;
  
  			if (mi.Event == MouseInputEvent.Down && !TakeMouseFocus(mi))
  				return false;
  
  			if (!HasMouseFocus)
  				return false;
  
  			if (HasMouseFocus && mi.Event == MouseInputEvent.Up)
  				return YieldMouseFocus(mi);
  
  			if (thumbPressed && mi.Event == MouseInputEvent.Move)
  			{
  				var rb = RenderBounds;
  
gt: 				var scrollbarHeight = rb.Height - 2 * ScrollbarWidth;