#region Copyright & License Information /* * Copyright 2007-2020 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("ButtonDepth"); public string ClickSound = ChromeMetrics.Get("ClickSound"); public string Background = "scrollpanel-bg"; public string ScrollBarBackground = "scrollpanel-bg"; public string Button = "scrollpanel-button"; 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); } 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; var thumbHeight = ContentHeight == 0 ? 0 : Math.Max(MinimumThumbSize, (int)(scrollbarHeight * Math.Min(rb.Height * 1f / ContentHeight, 1f))); var thumbOrigin = rb.Y + ScrollbarWidth + (int)((scrollbarHeight - thumbHeight) * (-1f * currentListOffset / (ContentHeight - rb.Height))); if (thumbHeight == scrollbarHeight) thumbHeight = 0; 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; WidgetUtils.DrawRGBA(ChromeProvider.GetImage("scrollbar", upPressed || upDisabled ? "up_pressed" : "up_arrow"), new float2(upButtonRect.Left + upOffset, upButtonRect.Top + upOffset)); WidgetUtils.DrawRGBA(ChromeProvider.GetImage("scrollbar", downPressed || downDisabled ? "down_pressed" : "down_arrow"), 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 { get { return RenderOrigin + new int2(ScrollBar == ScrollBar.Left ? ScrollbarWidth : 0, (int)currentListOffset); } } public override Rectangle GetEventBounds() { return EventBounds; } 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 { get { return 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 => { var si = c as ScrollItemWidget; return si != null && si.ItemKey == itemKey; }); if (item != null) ScrollToItem(item, smooth); } public void ScrollToSelectedItem() { var item = Children.FirstOrDefault(c => { var si = c as ScrollItemWidget; return si != null && 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; var scrollbarHeight = rb.Height - 2 * ScrollbarWidth; var thumbHeight = ContentHeight == 0 ? 0 : Math.Max(MinimumThumbSize, (int)(scrollbarHeight * Math.Min(rb.Height * 1f / ContentHeight, 1f))); var oldOffset = currentListOffset; var newOffset = currentListOffset + ((int)((lastMouseLocation.Y - mi.Location.Y) * (ContentHeight - rb.Height) * 1f / (scrollbarHeight - thumbHeight))); newOffset = Math.Min(0, Math.Max(rb.Height - ContentHeight, newOffset)); SetListOffset(newOffset, false); if (oldOffset != newOffset) lastMouseLocation = mi.Location; } else { upPressed = upButtonRect.Contains(mi.Location); downPressed = downButtonRect.Contains(mi.Location); thumbPressed = thumbRect.Contains(mi.Location); if (thumbPressed) lastMouseLocation = mi.Location; if (mi.Event == MouseInputEvent.Down && ((upPressed && !upDisabled) || (downPressed && !downDisabled) || thumbPressed)) Game.Sound.PlayNotification(modRules, null, "Sounds", ClickSound, null); } return upPressed || downPressed || thumbPressed; } IObservableCollection collection; Func makeWidget; Func widgetItemEquals; bool autoScroll; public void Unbind() { Bind(null, null, null, false); } public void Bind(IObservableCollection c, Func makeWidget, Func widgetItemEquals, bool autoScroll) { this.autoScroll = autoScroll; Game.RunAfterTick(() => { if (collection != null) { collection.OnAdd -= BindingAdd; collection.OnRemove -= BindingRemove; collection.OnRemoveAt -= BindingRemoveAt; collection.OnSet -= BindingSet; collection.OnRefresh -= BindingRefresh; } this.makeWidget = makeWidget; this.widgetItemEquals = widgetItemEquals; RemoveChildren(); collection = c; if (c != null) { foreach (var item in c.ObservedItems) BindingAddImpl(item); c.OnAdd += BindingAdd; c.OnRemove += BindingRemove; c.OnRemoveAt += BindingRemoveAt; c.OnSet += BindingSet; c.OnRefresh += BindingRefresh; } }); } void BindingAdd(IObservableCollection col, object item) { Game.RunAfterTick(() => { if (collection != col) return; BindingAddImpl(item); }); } void BindingAddImpl(object item) { if (makeWidget == null) return; var widget = makeWidget(item); var scrollToBottom = autoScroll && ScrolledToBottom; AddChild(widget); if (scrollToBottom) ScrollToBottom(); } void BindingRemove(IObservableCollection col, object item) { Game.RunAfterTick(() => { if (collection != col) return; var widget = Children.FirstOrDefault(w => widgetItemEquals(w, item)); if (widget != null) RemoveChild(widget); }); } void BindingRemoveAt(IObservableCollection col, int index) { Game.RunAfterTick(() => { if (collection != col) return; if (index < 0 || index >= Children.Count) return; RemoveChild(Children[index]); }); } void BindingSet(IObservableCollection col, object oldItem, object newItem) { Game.RunAfterTick(() => { if (collection != col) return; var newWidget = makeWidget(newItem); newWidget.Parent = this; var i = Children.FindIndex(w => widgetItemEquals(w, oldItem)); if (i >= 0) { var oldWidget = Children[i]; oldWidget.Removed(); Children[i] = newWidget; Layout.AdjustChildren(); } else AddChild(newWidget); }); } void BindingRefresh(IObservableCollection col) { Game.RunAfterTick(() => { if (collection != col) return; RemoveChildren(); foreach (var item in collection.ObservedItems) BindingAddImpl(item); }); } } }